diff --git a/core/Api/User/Fetch.class.php b/core/Api/User/Fetch.class.php index ab75e47..e81c2fa 100644 --- a/core/Api/User/Fetch.class.php +++ b/core/Api/User/Fetch.class.php @@ -2,14 +2,38 @@ namespace Api\User; +use Api\Parameter\Parameter; use \Api\Request; class Fetch extends Request { + const SELECT_SIZE = 20; + + private int $userCount; + public function __construct($user, $externalCall = false) { - parent::__construct($user, $externalCall, array()); + + parent::__construct($user, $externalCall, array( + 'page' => new Parameter('page', Parameter::TYPE_INT, true, 1) + )); + $this->loginRequired = true; $this->requiredGroup = USER_GROUP_ADMIN; + $this->userCount = 0; + } + + private function getUserCount() { + + $sql = $this->user->getSQL(); + $res = $sql->select($sql->count())->from("User")->execute(); + $this->success = ($res !== FALSE); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + $this->userCount = $res[0]["count"]; + } + + return $this->success; } public function execute($values = array()) { @@ -17,11 +41,25 @@ class Fetch extends Request { return false; } + $page = $this->getParam("page"); + if($page < 1) { + return $this->createError("Invalid page count"); + } + + if (!$this->getUserCount()) { + return false; + } + $sql = $this->user->getSQL(); - $res = $sql->select("User.uid as userId", "User.name", "User.email", "Group.uid as groupId", "Group.name as groupName") + $res = $sql->select("User.uid as userId", "User.name", "User.email", "User.created_at", + "Group.uid as groupId", "Group.name as groupName") ->from("User") ->leftJoin("UserGroup", "User.uid", "UserGroup.user_id") ->leftJoin("Group", "Group.uid", "UserGroup.group_id") + ->orderBy("User.uid") + ->ascending() + ->limit(Fetch::SELECT_SIZE) + ->offset(($page - 1) * Fetch::SELECT_SIZE) ->execute(); $this->success = ($res !== FALSE); @@ -30,14 +68,15 @@ class Fetch extends Request { if($this->success) { $this->result["users"] = array(); foreach($res as $row) { - $userId = $row["userId"]; - $groupId = $row["groupId"]; + $userId = intval($row["userId"]); + $groupId = intval($row["groupId"]); $groupName = $row["groupName"]; if (!isset($this->result["users"][$userId])) { $this->result["users"][$userId] = array( "uid" => $userId, "name" => $row["name"], "email" => $row["email"], + "created_at" => $row["created_at"], "groups" => array(), ); } @@ -46,6 +85,7 @@ class Fetch extends Request { $this->result["users"][$userId]["groups"][$groupId] = $groupName; } } + $this->result["pages"] = intval(ceil($this->userCount / Fetch::SELECT_SIZE)); } return $this->success; diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index d3c66b7..463ed8e 100755 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -31,6 +31,7 @@ class CreateDatabase { ->addString("salt", 16) ->addString("password", 64) ->addInt("language_id", true, 1) + ->addDateTime("registered_at", false, $sql->currentTimestamp()) ->primaryKey("uid") ->unique("email") ->unique("name") diff --git a/core/Documents/Admin.class.php b/core/Documents/Admin.class.php index e34601c..6386b41 100644 --- a/core/Documents/Admin.class.php +++ b/core/Documents/Admin.class.php @@ -5,12 +5,12 @@ namespace Documents { use Documents\Admin\AdminHead; use Elements\Document; use Objects\User; - use Views\AdminDashboard; + use Views\Admin\AdminDashboardBody; use Views\LoginBody; class Admin extends Document { public function __construct(User $user) { - $body = $user->isLoggedIn() ? AdminDashboard::class : LoginBody::class; + $body = $user->isLoggedIn() ? AdminDashboardBody::class : LoginBody::class; parent::__construct($user, AdminHead::class, $body); } } diff --git a/core/Documents/Document404.class.php b/core/Documents/Document404.class.php index 80a0950..298b7f1 100644 --- a/core/Documents/Document404.class.php +++ b/core/Documents/Document404.class.php @@ -17,6 +17,7 @@ namespace Documents\Document404 { use Elements\Body; use Elements\Head; + use Views\View404; class Head404 extends Head { @@ -25,11 +26,6 @@ namespace Documents\Document404 { } protected function initSources() { - // $this->loadJQuery(); - // $this->loadBootstrap(); - // $this->loadFontawesome(); - // $this->addJS(\Elements\Script::CORE); - // $this->addCSS(\Elements\Link::CORE); } protected function initMetas() { @@ -59,7 +55,7 @@ namespace Documents\Document404 { public function getCode() { $html = parent::getCode(); - $html .= "404 Not Found"; + $html .= "
" . (new View404($this->getDocument())) . ""; return $html; } } diff --git a/core/Documents/Install.class.php b/core/Documents/Install.class.php index e1d0db6..a693c80 100644 --- a/core/Documents/Install.class.php +++ b/core/Documents/Install.class.php @@ -17,6 +17,7 @@ namespace Documents { namespace Documents\Install { use Api\Notifications\Create; + use Api\Parameter\Parameter; use Configuration\CreateDatabase; use Driver\SQL\SQL; use Elements\Body; @@ -289,8 +290,8 @@ namespace Documents\Install { $username = $this->getParameter("username"); $password = $this->getParameter("password"); $confirmPassword = $this->getParameter("confirmPassword"); + $email = $this->getParameter("email") ?? ""; - $msg = $this->errorString; $success = true; $missingInputs = array(); @@ -321,13 +322,16 @@ namespace Documents\Install { } else if(strlen($password) < 6) { $msg = "The password should be at least 6 characters long"; $success = false; + } else if($email && Parameter::parseType($email) !== Parameter::TYPE_EMAIL) { + $msg = "Invalid email address"; + $success = false; } else { $salt = generateRandomString(16); $hash = hash('sha256', $password . $salt); $sql = $user->getSQL(); - $success = $sql->insert("User", array("name", "salt", "password")) - ->addRow($username, $salt, $hash) + $success = $sql->insert("User", array("name", "salt", "password", "email")) + ->addRow($username, $salt, $hash, $email) ->returning("uid") ->execute() && $sql->insert("UserGroup", array("group_id", "user_id")) @@ -606,6 +610,7 @@ namespace Documents\Install { "title" => "Create a User", "form" => array( array("title" => "Username", "name" => "username", "type" => "text", "required" => true), + array("title" => "Email", "name" => "email", "type" => "text"), array("title" => "Password", "name" => "password", "type" => "password", "required" => true), array("title" => "Confirm Password", "name" => "confirmPassword", "type" => "password", "required" => true), ), @@ -704,7 +709,7 @@ namespace Documents\Install { $id = $button["id"]; $float = $button["float"]; $disabled = (isset($button["disabled"]) && $button["disabled"]) ? " disabled" : ""; - $button = ""; + $button = ""; if($float === "left") { $buttonsLeft .= $button; diff --git a/core/Driver/SQL/Condition/CondOr.class.php b/core/Driver/SQL/Condition/CondOr.class.php index 8d83b34..b92ab6b 100644 --- a/core/Driver/SQL/Condition/CondOr.class.php +++ b/core/Driver/SQL/Condition/CondOr.class.php @@ -7,7 +7,7 @@ class CondOr extends Condition { private array $conditions; public function __construct(...$conditions) { - $this->conditions = $conditions; + $this->conditions = (!empty($conditions) && is_array($conditions[0])) ? $conditions[0] : $conditions; } public function getConditions() { return $this->conditions; } diff --git a/core/Driver/SQL/Query/Delete.class.php b/core/Driver/SQL/Query/Delete.class.php index 98d5232..705fbfe 100644 --- a/core/Driver/SQL/Query/Delete.class.php +++ b/core/Driver/SQL/Query/Delete.class.php @@ -2,6 +2,8 @@ namespace Driver\SQL\Query; +use Driver\SQL\Condition\CondOr; + class Delete extends Query { private string $table; @@ -14,7 +16,7 @@ class Delete extends Query { } public function where(...$conditions) { - $this->conditions = array_merge($this->conditions, $conditions); + $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); return $this; } diff --git a/core/Driver/SQL/Query/Query.class.php b/core/Driver/SQL/Query/Query.class.php index 7d410e6..b58a3b7 100644 --- a/core/Driver/SQL/Query/Query.class.php +++ b/core/Driver/SQL/Query/Query.class.php @@ -7,9 +7,16 @@ use Driver\SQL\SQL; abstract class Query { protected SQL $sql; + public bool $dump; public function __construct($sql) { $this->sql = $sql; + $this->dump = false; + } + + public function dump() { + $this->dump = true; + return $this; } public abstract function execute(); diff --git a/core/Driver/SQL/Query/Select.class.php b/core/Driver/SQL/Query/Select.class.php index e3255eb..116fca7 100644 --- a/core/Driver/SQL/Query/Select.class.php +++ b/core/Driver/SQL/Query/Select.class.php @@ -2,6 +2,7 @@ namespace Driver\SQL\Query; +use Driver\SQL\Condition\CondOr; use Driver\SQL\Join; class Select extends Query { @@ -33,7 +34,7 @@ class Select extends Query { } public function where(...$conditions) { - $this->conditions = array_merge($this->conditions, $conditions); + $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); return $this; } diff --git a/core/Driver/SQL/Query/Update.class.php b/core/Driver/SQL/Query/Update.class.php index 898f56f..e69a4f3 100644 --- a/core/Driver/SQL/Query/Update.class.php +++ b/core/Driver/SQL/Query/Update.class.php @@ -2,6 +2,8 @@ namespace Driver\SQL\Query; +use Driver\SQL\Condition\CondOr; + class Update extends Query { private array $values; @@ -16,7 +18,7 @@ class Update extends Query { } public function where(...$conditions) { - $this->conditions = array_merge($this->conditions, $conditions); + $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); return $this; } diff --git a/core/Driver/SQL/SQL.class.php b/core/Driver/SQL/SQL.class.php index d6edac9..5854df8 100644 --- a/core/Driver/SQL/SQL.class.php +++ b/core/Driver/SQL/SQL.class.php @@ -144,6 +144,7 @@ abstract class SQL { $returning = $this->getReturning($returningCol); $query = "INSERT INTO $tableName$columnStr VALUES$values$onDuplicateKey$returning"; + if($insert->dump) { var_dump($query); var_dump($parameters); } $res = $this->execute($query, $parameters, !empty($returning)); $success = ($res !== FALSE); @@ -189,6 +190,7 @@ abstract class SQL { $limit = ($select->getLimit() > 0 ? (" LIMIT " . $select->getLimit()) : ""); $offset = ($select->getOffset() > 0 ? (" OFFSET " . $select->getOffset()) : ""); $query = "SELECT $columns FROM $tables$joinStr$where$orderBy$limit$offset"; + if($select->dump) { var_dump($query); var_dump($params); } return $this->execute($query, $params, true); } @@ -198,6 +200,7 @@ abstract class SQL { $where = $this->getWhereClause($delete->getConditions(), $params); $query = "DELETE FROM $table$where"; + if($delete->dump) { var_dump($query); } return $this->execute($query); } @@ -218,6 +221,7 @@ abstract class SQL { $where = $this->getWhereClause($update->getConditions(), $params); $query = "UPDATE $table SET $valueStr$where"; + if($update->dump) { var_dump($query); var_dump($params); } return $this->execute($query, $params); } @@ -290,6 +294,7 @@ abstract class SQL { protected abstract function execute($query, $values=NULL, $returnValues=false); protected function buildCondition($condition, &$params) { + if ($condition instanceof CondOr) { $conditions = array(); foreach($condition->getConditions() as $cond) { @@ -304,7 +309,7 @@ abstract class SQL { } else if ($condition instanceof CondBool) { return $this->columnName($condition->getValue()); } else if (is_array($condition)) { - if (count($condition) == 1) { + if (count($condition) === 1) { return $this->buildCondition($condition[0], $params); } else { $conditions = array(); diff --git a/core/Elements/Body.class.php b/core/Elements/Body.class.php index 3e1f66a..196961a 100644 --- a/core/Elements/Body.class.php +++ b/core/Elements/Body.class.php @@ -2,8 +2,6 @@ namespace Elements; -use View; - abstract class Body extends View { public function __construct($document) { parent::__construct($document); diff --git a/core/Elements/Head.class.php b/core/Elements/Head.class.php index 0ac7391..ac071ab 100644 --- a/core/Elements/Head.class.php +++ b/core/Elements/Head.class.php @@ -2,8 +2,6 @@ namespace Elements; -use View; - abstract class Head extends View { protected array $sources; @@ -47,7 +45,7 @@ abstract class Head extends View { public function addKeywords($keywords) { array_merge($this->keywords, $keywords); } public function getTitle() { return $this->title; } - public function addCSS($href, $type = Link::MIME_TEXT_CSS) { $this->sources[] = new Link("stylesheet", $href, $type); } + public function addCSS($href, $type = Link::MIME_TEXT_CSS) { $this->sources[] = new Link(Link::STYLESHEET, $href, $type); } public function addStyle($style) { $this->sources[] = new Style($style); } public function addJS($url) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, $url, ""); } public function addJSCode($code) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, "", $code); } diff --git a/core/Elements/Link.class.php b/core/Elements/Link.class.php index 25a8105..3e06cce 100644 --- a/core/Elements/Link.class.php +++ b/core/Elements/Link.class.php @@ -2,29 +2,16 @@ namespace Elements; -use View; - -class Link extends View { +class Link extends StaticView { const STYLESHEET = "stylesheet"; const MIME_TEXT_CSS = "text/css"; - const FONTAWESOME = '/css/fontawesome.min.css'; - // const JQUERY_UI = '/css/jquery-ui.css'; - // const JQUERY_TERMINAL = '/css/jquery.terminal.min.css'; - const BOOTSTRAP = '/css/bootstrap.min.css'; - // const BOOTSTRAP_THEME = '/css/bootstrap-theme.min.css'; - // const BOOTSTRAP_DATEPICKER_CSS = '/css/bootstrap-datepicker.standalone.min.css'; - // const BOOTSTRAP_DATEPICKER3_CSS = '/css/bootstrap-datepicker.standalone.min.css'; - // const HIGHLIGHT = '/css/highlight.css'; - // const HIGHLIGHT_THEME = '/css/theme.css'; - const CORE = "/css/style.css"; - const ADMIN = "/css/admin.css"; - // const HOME = "/css/home.css"; - // const REVEALJS = "/css/reveal.css"; - // const REVEALJS_THEME_MOON = "/css/reveal_moon.css"; - // const REVEALJS_THEME_BLACK = "/css/reveal_black.css"; - const ADMINLTE = "/css/adminlte.min.css"; + const FONTAWESOME = "/css/fontawesome.min.css"; + const BOOTSTRAP = "/css/bootstrap.min.css"; + const CORE = "/css/style.css"; + const ADMIN = "/css/admin.css"; + const ADMINLTE = "/css/adminlte.min.css"; private string $type; private string $rel; diff --git a/core/Elements/Script.class.php b/core/Elements/Script.class.php index 5e9224a..b924f7a 100644 --- a/core/Elements/Script.class.php +++ b/core/Elements/Script.class.php @@ -2,37 +2,16 @@ namespace Elements; -class Script extends \View { +class Script extends StaticView { const MIME_TEXT_JAVASCRIPT = "text/javascript"; - const CORE = "/js/script.js"; - // const HOME = "/js/home.js"; - const ADMIN = "/js/admin.js"; - // const SORTTABLE = "/js/sorttable.js"; - const JQUERY = "/js/jquery.min.js"; - // const JQUERY_UI = "/js/jquery-ui.js"; - // const JQUERY_MASKED_INPUT = "/js/jquery.maskedinput.min.js"; - // const JQUERY_CONTEXT_MENU = "/js/jquery.contextmenu.min.js"; - // const JQUERY_TERMINAL = "/js/jquery.terminal.min.js"; - // const JQUERY_TERMINAL_UNIX = "/js/unix_formatting.js"; - // const JSCOLOR = "/js/jscolor.min.js"; - // const SYNTAX_HIGHLIGHTER = "/js/syntaxhighlighter.js"; - // const HIGHLIGHT = "/js/highlight.pack.js"; - // const GOOGLE_CHARTS = "/js/loader.js"; - // const BOOTSTRAP = "/js/bootstrap.min.js"; - // const BOOTSTRAP_DATEPICKER_JS = "/js/bootstrap-datepicker.min.js"; - // const POPPER = "/js/popper.min.js"; - // const JSMPEG = "/js/jsmpeg.min.js"; - // const MOMENT = "/js/moment.min.js"; - // const CHART = "/js/chart.js"; - // const REVEALJS = "/js/reveal.js"; - // const REVEALJS_PLUGIN_NOTES = "/js/reveal_notes.js"; - const INSTALL = "/js/install.js"; + const CORE = "/js/script.js"; + const ADMIN = "/js/admin.js"; + const JQUERY = "/js/jquery.min.js"; + const INSTALL = "/js/install.js"; const BOOTSTRAP = "/js/bootstrap.bundle.min.js"; - - const HIGHLIGHT_JS_LOADER = "\$(document).ready(function(){\$('code').each(function(i, block) { hljs.highlightBlock(block); }); })"; - const ADMINLTE = "/js/adminlte.min.js"; + const ADMINLTE = "/js/adminlte.min.js"; private string $type; private string $content; diff --git a/core/Elements/StaticView.class.php b/core/Elements/StaticView.class.php new file mode 100644 index 0000000..c207f50 --- /dev/null +++ b/core/Elements/StaticView.class.php @@ -0,0 +1,9 @@ +$text"; } + + protected function createBadge($type, $text) { + $text = htmlspecialchars($text); + return "$text"; + } } \ No newline at end of file diff --git a/core/Objects/Session.class.php b/core/Objects/Session.class.php index 45cbc86..4081293 100644 --- a/core/Objects/Session.class.php +++ b/core/Objects/Session.class.php @@ -9,7 +9,8 @@ use External\JWT; class Session extends ApiObject { - const DURATION = 120; + # in minutes + const DURATION = 60*24; private ?int $sessionId; private User $user; @@ -91,13 +92,13 @@ class Session extends ApiObject { $this->updateMetaData(); $sql = $this->user->getSQL(); - $hours = Session::DURATION; + $minutes = Session::DURATION; $columns = array("expires", "user_id", "ipAddress", "os", "browser", "data", "stay_logged_in"); $success = $sql ->insert("Session", $columns) ->addRow( - (new DateTime())->modify("+$hours hour"), + (new DateTime())->modify("+$minutes minute"), $this->user->getId(), $this->ipAddress, $this->os, @@ -125,11 +126,11 @@ class Session extends ApiObject { public function update() { $this->updateMetaData(); - $hours = Session::DURATION; + $minutes = Session::DURATION; $sql = $this->user->getSQL(); return $sql->update("Session") - ->set("Session.expires", (new DateTime())->modify("+$hours hour")) + ->set("Session.expires", (new DateTime())->modify("+$minutes minute")) ->set("Session.ipAddress", $this->ipAddress) ->set("Session.os", $this->os) ->set("Session.browser", $this->browser) diff --git a/core/Views/AdminDashboard.class.php b/core/Views/Admin/AdminDashboardBody.class.php similarity index 78% rename from core/Views/AdminDashboard.class.php rename to core/Views/Admin/AdminDashboardBody.class.php index 24843cd..c705149 100644 --- a/core/Views/AdminDashboard.class.php +++ b/core/Views/Admin/AdminDashboardBody.class.php @@ -1,19 +1,24 @@ errorMessages = array(); + $this->notifications = array(); } private function getNotifications() : array { @@ -49,8 +54,7 @@ class AdminDashboard extends Body { $iconMail = $this->createIcon("envelope", "fas"); // Notifications - $notifications = $this->getNotifications(); - $numNotifications = count($notifications); + $numNotifications = count($this->notifications); if ($numNotifications === 0) { $notificationText = L("No new notifications"); } else if($numNotifications === 1) { @@ -98,7 +102,7 @@ class AdminDashboard extends Body { // Notifications $i = 0; - foreach($notifications as $notification) { + foreach($this->notifications as $notification) { $title = $notification["title"]; $notificationId = $notification["uid"]; @@ -150,12 +154,17 @@ class AdminDashboard extends Body { ), ); + $notificationCount = count($this->notifications); + if ($notificationCount > 0) { + $menuEntries["dashboard"]["badge"] = array("type" => "warning", "value" => $notificationCount); + } + $currentView = $_GET["view"] ?? "dashboard"; $html = ""; @@ -190,32 +205,64 @@ class AdminDashboard extends Body { return $html; } + private function getView() { + + $views = array( + "dashboard" => Dashboard::class, + "users" => UserOverview::class, + "404" => View404::class, + ); + + $currentView = $_GET["view"] ?? "dashboard"; + if (!isset($views[$currentView])) { + $currentView = "404"; + } + + $view = new $views[$currentView]($this->getDocument()); + assert($view instanceof View); + $code = $view->getCode(); + + if ($view instanceof AdminView) { + $this->errorMessages = array_merge($this->errorMessages, $view->getErrorMessages()); + } + + return $code; + } + + public function loadView() { + parent::loadView(); + + $head = $this->getDocument()->getHead(); + $head->addJS(Script::BOOTSTRAP); + $head->loadAdminlte(); + + $this->notifications = $this->getNotifications(); + } + private function getContent() { $this->getUsers(); + $view = $this->getView(); $html = "$name | +$groups | +$registeredAt | +
---|