diff --git a/Core/API/ContactAPI.class.php b/Core/API/ContactAPI.class.php deleted file mode 100644 index 59d863a..0000000 --- a/Core/API/ContactAPI.class.php +++ /dev/null @@ -1,291 +0,0 @@ -messageId = null; - $this->csrfTokenRequired = false; - } - - protected function sendMail(string $name, ?string $fromEmail, string $subject, string $message, ?string $to = null): bool { - $request = new \Core\API\Mail\Send($this->context); - $this->success = $request->execute(array( - "subject" => $subject, - "body" => $message, - "replyTo" => $fromEmail, - "replyName" => $name, - "to" => $to - )); - - $this->lastError = $request->getLastError(); - if ($this->success) { - $this->messageId = $request->getResult()["messageId"]; - } - - return $this->success; - } - } -} - -namespace Core\API\Contact { - - use Core\API\ContactAPI; - use Core\API\Parameter\Parameter; - use Core\API\Parameter\StringType; - use Core\API\VerifyCaptcha; - use Core\Driver\SQL\Condition\Compare; - use Core\Driver\SQL\Condition\CondNot; - use Core\Driver\SQL\Expression\CaseWhen; - use Core\Driver\SQL\Expression\Sum; - use Core\Objects\Context; - - class Request extends ContactAPI { - - public function __construct(Context $context, bool $externalCall = false) { - $parameters = array( - 'fromName' => new StringType('fromName', 32), - 'fromEmail' => new Parameter('fromEmail', Parameter::TYPE_EMAIL), - 'message' => new StringType('message', 512), - ); - - $settings = $context->getSettings(); - if ($settings->isRecaptchaEnabled()) { - $parameters["captcha"] = new StringType("captcha"); - } - - parent::__construct($context, $externalCall, $parameters); - } - - public function _execute(): bool { - $settings = $this->context->getSettings(); - if ($settings->isRecaptchaEnabled()) { - $captcha = $this->getParam("captcha"); - $req = new VerifyCaptcha($this->context); - if (!$req->execute(array("captcha" => $captcha, "action" => "contact"))) { - return $this->createError($req->getLastError()); - } - } - - // parameter - $message = $this->getParam("message"); - $name = $this->getParam("fromName"); - $email = $this->getParam("fromEmail"); - - $sendMail = $this->sendMail($name, $email, "Contact Request", $message); - $insertDB = $this->insertContactRequest(); - if (!$sendMail && !$insertDB) { - return $this->createError("The contact request could not be sent. The Administrator is already informed. Please try again later."); - } - - return $this->success; - } - - private function insertContactRequest(): bool { - $sql = $this->context->getSQL(); - $name = $this->getParam("fromName"); - $email = $this->getParam("fromEmail"); - $message = $this->getParam("message"); - $messageId = $this->messageId ?? null; - - $res = $sql->insert("ContactRequest", array("from_name", "from_email", "message", "messageId")) - ->addRow($name, $email, $message, $messageId) - ->returning("id") - ->execute(); - - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - return $this->success; - } - } - - class Respond extends ContactAPI { - - public function __construct(Context $context, bool $externalCall = false) { - parent::__construct($context, $externalCall, array( - "requestId" => new Parameter("requestId", Parameter::TYPE_INT), - 'message' => new StringType('message', 512), - )); - $this->loginRequired = true; - } - - private function getSenderMail(): ?string { - $requestId = $this->getParam("requestId"); - $sql = $this->context->getSQL(); - $res = $sql->select("from_email") - ->from("ContactRequest") - ->where(new Compare("id", $requestId)) - ->execute(); - - $this->success = ($res !== false); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - if (empty($res)) { - return $this->createError("Request does not exist"); - } else { - return $res[0]["from_email"]; - } - } - - return null; - } - - private function insertResponseMessage(): bool { - $sql = $this->context->getSQL(); - $message = $this->getParam("message"); - $requestId = $this->getParam("requestId"); - - $this->success = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId", "read"]) - ->addRow($requestId, $this->context->getUser()->getId(), $message, $this->messageId, true) - ->execute(); - - $this->lastError = $sql->getLastError(); - return $this->success; - } - - private function updateEntity() { - $sql = $this->context->getSQL(); - $requestId = $this->getParam("requestId"); - - $sql->update("EntityLog") - ->set("modified", $sql->now()) - ->where(new Compare("entityId", $requestId)) - ->execute(); - } - - public function _execute(): bool { - $message = $this->getParam("message"); - $senderMail = $this->getSenderMail(); - if (!$this->success) { - return false; - } - - $user = $this->context->getUser(); - $fromName = $user->getUsername(); - $fromEmail = $user->getEmail(); - - if (!$this->sendMail($fromName, $fromEmail, "Re: Contact Request", $message, $senderMail)) { - return false; - } - - if (!$this->insertResponseMessage()) { - return false; - } - - $this->updateEntity(); - return $this->success; - } - } - - class Fetch extends ContactAPI { - - public function __construct(Context $context, bool $externalCall = false) { - parent::__construct($context, $externalCall, array()); - $this->loginRequired = true; - $this->csrfTokenRequired = false; - } - - public function _execute(): bool { - $sql = $this->context->getSQL(); - $res = $sql->select("ContactRequest.id", "from_name", "from_email", "from_name", - new Sum(new CaseWhen(new CondNot("ContactMessage.read"), 1, 0), "unread")) - ->from("ContactRequest") - ->groupBy("ContactRequest.id") - ->leftJoin("ContactMessage", "ContactRequest.id", "ContactMessage.request_id") - ->execute(); - - $this->success = ($res !== false); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - $this->result["contactRequests"] = []; - foreach ($res as $row) { - $this->result["contactRequests"][] = array( - "id" => intval($row["id"]), - "from_name" => $row["from_name"], - "from_email" => $row["from_email"], - "unread" => intval($row["unread"]), - ); - } - } - - return $this->success; - } - } - - class Get extends ContactAPI { - - public function __construct(Context $context, bool $externalCall = false) { - parent::__construct($context, $externalCall, array( - "requestId" => new Parameter("requestId", Parameter::TYPE_INT), - )); - $this->loginRequired = true; - $this->csrfTokenRequired = false; - } - - private function updateRead() { - $requestId = $this->getParam("requestId"); - $sql = $this->context->getSQL(); - $sql->update("ContactMessage") - ->set("read", 1) - ->where(new Compare("request_id", $requestId)) - ->execute(); - } - - public function _execute(): bool { - $requestId = $this->getParam("requestId"); - $sql = $this->context->getSQL(); - - $res = $sql->select("from_name", "from_email", "message", "created_at") - ->from("ContactRequest") - ->where(new Compare("id", $requestId)) - ->execute(); - - $this->success = ($res !== false); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - if (empty($res)) { - return $this->createError("Request does not exist"); - } else { - $row = $res[0]; - $this->result["request"] = array( - "from_name" => $row["from_name"], - "from_email" => $row["from_email"], - "messages" => array( - ["sender_id" => null, "message" => $row["message"], "timestamp" => $row["created_at"]] - ) - ); - - $res = $sql->select("user_id", "message", "created_at") - ->from("ContactMessage") - ->where(new Compare("request_id", $requestId)) - ->orderBy("created_at") - ->execute(); - - $this->success = ($res !== false); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - foreach ($res as $row) { - $this->result["request"]["messages"][] = array( - "sender_id" => $row["user_id"], "message" => $row["message"], "timestamp" => $row["created_at"] - ); - } - - $this->updateRead(); - } - } - } - - return $this->success; - } - } -} \ No newline at end of file diff --git a/Core/API/GroupsAPI.class.php b/Core/API/GroupsAPI.class.php index 7823482..2fa975a 100644 --- a/Core/API/GroupsAPI.class.php +++ b/Core/API/GroupsAPI.class.php @@ -31,6 +31,7 @@ namespace Core\API\Groups { use Core\API\Parameter\Parameter; use Core\API\Parameter\StringType; use Core\Objects\Context; + use Core\Objects\DatabaseEntity\Controller\NMRelation; use Core\Objects\DatabaseEntity\Group; class Fetch extends GroupsAPI { @@ -46,20 +47,6 @@ namespace Core\API\Groups { $this->groupCount = 0; } - private function fetchGroupCount(): bool { - - $sql = $this->context->getSQL(); - $res = $sql->select($sql->count())->from("Group")->execute(); - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - $this->groupCount = $res[0]["count"]; - } - - return $this->success; - } - public function _execute(): bool { $page = $this->getParam("page"); if($page < 1) { @@ -71,39 +58,42 @@ namespace Core\API\Groups { return $this->createError("Invalid fetch count"); } - if (!$this->fetchGroupCount()) { - return false; + $sql = $this->context->getSQL(); + $groupCount = Group::count($sql); + if ($groupCount === false) { + return $this->createError("Error fetching group count: " . $sql->getLastError()); } - $sql = $this->context->getSQL(); - $res = $sql->select("Group.id as groupId", "Group.name as groupName", "Group.color as groupColor", $sql->count("UserGroup.user_id")) - ->from("Group") - ->leftJoin("UserGroup", "UserGroup.group_id", "Group.id") - ->groupBy("Group.id") - ->orderBy("Group.id") + $groups = Group::findBy(Group::createBuilder($sql, false) + ->orderBy("id") ->ascending() ->limit($count) - ->offset(($page - 1) * $count) - ->execute(); + ->offset(($page - 1) * $count)); - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - - if($this->success) { - $this->result["groups"] = array(); - foreach($res as $row) { - $groupId = intval($row["groupId"]); - $groupName = $row["groupName"]; - $groupColor = $row["groupColor"]; - $memberCount = $row["usergroup_user_id_count"]; - $this->result["groups"][$groupId] = array( - "name" => $groupName, - "memberCount" => $memberCount, - "color" => $groupColor, - ); - } + if ($groups !== false) { + $this->result["groups"] = []; $this->result["pageCount"] = intval(ceil($this->groupCount / $count)); $this->result["totalCount"] = $this->groupCount; + + foreach ($groups as $groupId => $group) { + $this->result["groups"][$groupId] = $group->jsonSerialize(); + $this->result["groups"][$groupId]["memberCount"] = 0; + } + + $nmTable = NMRelation::buildTableName("User", "Group"); + $res = $sql->select("group_id", $sql->count("user_id")) + ->from($nmTable) + ->groupBy("group_id") + ->execute(); + + if (is_array($res)) { + foreach ($res as $row) { + list ($groupId, $memberCount) = [$row["group_id"], $row["user_id_count"]]; + if (isset($this->result["groups"][$groupId])) { + $this->result["groups"][$groupId]["memberCount"] = $memberCount; + } + } + } } return $this->success; @@ -162,7 +152,7 @@ namespace Core\API\Groups { public function _execute(): bool { $id = $this->getParam("id"); - if (in_array($id, DEFAULT_GROUPS)) { + if (in_array($id, array_keys(Group::GROUPS))) { return $this->createError("You cannot delete a default group."); } diff --git a/Core/API/LogsAPI.class.php b/Core/API/LogsAPI.class.php index afa05a2..3d58dd9 100644 --- a/Core/API/LogsAPI.class.php +++ b/Core/API/LogsAPI.class.php @@ -47,7 +47,8 @@ namespace Core\API\Logs { $shownLogLevels = array_slice(Logger::LOG_LEVELS, $logLevel); } - $query = SystemLog::findAllBuilder($sql) + + $query = SystemLog::createBuilder($sql, false) ->orderBy("timestamp") ->descending(); @@ -59,7 +60,7 @@ namespace Core\API\Logs { $query->where(new CondIn(new Column("severity"), $shownLogLevels)); } - $logEntries = $query->execute(); + $logEntries = SystemLog::findBy($query); $this->success = $logEntries !== false; $this->lastError = $sql->getLastError(); diff --git a/Core/API/MailAPI.class.php b/Core/API/MailAPI.class.php index 19365f3..c88c68c 100644 --- a/Core/API/MailAPI.class.php +++ b/Core/API/MailAPI.class.php @@ -46,6 +46,7 @@ namespace Core\API\Mail { use Core\API\MailAPI; use Core\API\Parameter\Parameter; use Core\API\Parameter\StringType; + use Core\Objects\DatabaseEntity\MailQueueItem; use DateTimeInterface; use Core\Driver\SQL\Column\Column; use Core\Driver\SQL\Condition\Compare; @@ -84,7 +85,7 @@ namespace Core\API\Mail { class Send extends MailAPI { public function __construct(Context $context, $externalCall = false) { parent::__construct($context, $externalCall, array( - 'to' => new Parameter('to', Parameter::TYPE_EMAIL, true, null), + 'to' => new Parameter('to', Parameter::TYPE_EMAIL), 'subject' => new StringType('subject', -1), 'body' => new StringType('body', -1), 'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null), @@ -104,7 +105,7 @@ namespace Core\API\Mail { $fromMail = $mailConfig->getProperty('from'); $mailFooter = $mailConfig->getProperty('mail_footer'); - $toMail = $this->getParam('to') ?? $fromMail; + $toMail = $this->getParam('to'); $subject = $this->getParam('subject'); $replyTo = $this->getParam('replyTo'); $replyName = $this->getParam('replyName'); @@ -119,10 +120,8 @@ namespace Core\API\Mail { if ($mailAsync) { $sql = $this->context->getSQL(); - $this->success = $sql->insert("MailQueue", ["from", "to", "subject", "body", - "replyTo", "replyName", "gpgFingerprint"]) - ->addRow($fromMail, $toMail, $subject, $body, $replyTo, $replyName, $gpgFingerprint) - ->execute() !== false; + $mailQueueItem = new MailQueueItem($fromMail, $toMail, $subject, $body, $replyTo, $replyName, $gpgFingerprint); + $this->success = $mailQueueItem->save($sql); $this->lastError = $sql->getLastError(); return $this->success; } @@ -223,77 +222,37 @@ namespace Core\API\Mail { } $sql = $this->context->getSQL(); - $res = $sql->select("id", "from", "to", "subject", "body", - "replyTo", "replyName", "gpgFingerprint", "retryCount") - ->from("MailQueue") + $mailQueueItems = MailQueueItem::findBy(MailQueueItem::createBuilder($sql, false) ->where(new Compare("retryCount", 0, ">")) ->where(new Compare("status", "waiting")) - ->where(new Compare("nextTry", $sql->now(), "<=")) - ->execute(); + ->where(new Compare("nextTry", $sql->now(), "<="))); - $this->success = ($res !== false); + $this->success = ($mailQueueItems !== false); $this->lastError = $sql->getLastError(); - if ($this->success && is_array($res)) { + if ($this->success && is_array($mailQueueItems)) { if ($debug) { - echo "Found " . count($res) . " mails to send" . PHP_EOL; + echo "Found " . count($mailQueueItems) . " mails to send" . PHP_EOL; } - $successfulMails = []; - foreach ($res as $row) { + $successfulMails = 0; + foreach ($mailQueueItems as $mailQueueItem) { if (time() - $startTime >= 45) { $this->lastError = "Not able to process whole mail queue within 45 seconds, will continue on next time"; break; } - $to = $row["to"]; - $subject = $row["subject"]; - if ($debug) { - echo "Sending subject=$subject to=$to" . PHP_EOL; + echo "Sending subject=$mailQueueItem->subject to=$mailQueueItem->to" . PHP_EOL; } - $mailId = intval($row["id"]); - $retryCount = intval($row["retryCount"]); - $req = new Send($this->context); - $args = [ - "to" => $to, - "subject" => $subject, - "body" => $row["body"], - "replyTo" => $row["replyTo"], - "replyName" => $row["replyName"], - "gpgFingerprint" => $row["gpgFingerprint"], - "async" => false - ]; - $success = $req->execute($args); - $error = $req->getLastError(); - - if (!$success) { - $delay = [0, 720, 360, 60, 30, 1]; - $minutes = $delay[max(0, min(count($delay) - 1, $retryCount))]; - $nextTry = (new \DateTime())->modify("+$minutes minute"); - $sql->update("MailQueue") - ->set("retryCount", $retryCount - 1) - ->set("status", "error") - ->set("errorMessage", $error) - ->set("nextTry", $nextTry) - ->where(new Compare("id", $mailId)) - ->execute(); - } else { - $successfulMails[] = $mailId; + if ($mailQueueItem->send($this->context)) { + $successfulMails++; } } - $this->success = count($successfulMails) === count($res); - if (!empty($successfulMails)) { - $res = $sql->update("MailQueue") - ->set("status", "success") - ->where(new CondIn(new Column("id"), $successfulMails)) - ->execute(); - $this->success = $res !== false; - $this->lastError = $sql->getLastError(); - } + $this->success = $successfulMails === count($mailQueueItems); } return $this->success; diff --git a/Core/API/NewsAPI.class.php b/Core/API/NewsAPI.class.php index 9d889fd..5332176 100644 --- a/Core/API/NewsAPI.class.php +++ b/Core/API/NewsAPI.class.php @@ -19,6 +19,7 @@ namespace Core\API\News { use Core\API\Parameter\StringType; use Core\Driver\SQL\Condition\Compare; use Core\Objects\Context; + use Core\Objects\DatabaseEntity\Group; use Core\Objects\DatabaseEntity\News; class Get extends NewsAPI { @@ -40,7 +41,7 @@ namespace Core\API\News { } $sql = $this->context->getSQL(); - $newsQuery = News::findAllBuilder($sql) + $newsQuery = News::createBuilder($sql, false) ->limit($limit) ->orderBy("published_at") ->descending() @@ -50,7 +51,7 @@ namespace Core\API\News { $newsQuery->where(new Compare("published_at", $since, ">=")); } - $newsArray = $newsQuery->execute(); + $newsArray = News::findBy($newsQuery); $this->success = $newsArray !== null; $this->lastError = $sql->getLastError(); @@ -113,7 +114,7 @@ namespace Core\API\News { return false; } else if ($news === null) { return $this->createError("News Post not found"); - } else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(USER_GROUP_ADMIN)) { + } else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(Group::ADMIN)) { return $this->createError("You do not have permissions to delete news post of other users."); } @@ -144,7 +145,7 @@ namespace Core\API\News { return false; } else if ($news === null) { return $this->createError("News Post not found"); - } else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(USER_GROUP_ADMIN)) { + } else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(Group::ADMIN)) { return $this->createError("You do not have permissions to edit news post of other users."); } diff --git a/Core/API/NotificationsAPI.class.php b/Core/API/NotificationsAPI.class.php deleted file mode 100644 index 4135401..0000000 --- a/Core/API/NotificationsAPI.class.php +++ /dev/null @@ -1,231 +0,0 @@ - new Parameter('groupId', Parameter::TYPE_INT, true), - 'userId' => new Parameter('userId', Parameter::TYPE_INT, true), - 'title' => new StringType('title', 32), - 'message' => new StringType('message', 256), - )); - $this->isPublic = false; - } - - private function insertUserNotification($userId, $notificationId): bool { - $sql = $this->context->getSQL(); - $res = $sql->insert("UserNotification", array("user_id", "notification_id")) - ->addRow($userId, $notificationId) - ->execute(); - - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - return $this->success; - } - - private function insertGroupNotification($groupId, $notificationId): bool { - $sql = $this->context->getSQL(); - $res = $sql->insert("GroupNotification", array("group_id", "notification_id")) - ->addRow($groupId, $notificationId) - ->execute(); - - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - return $this->success; - } - - private function createNotification($title, $message): bool|int { - $sql = $this->context->getSQL(); - $notification = new Notification(); - $notification->title = $title; - $notification->message = $message; - - $this->success = ($notification->save($sql) !== FALSE); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - return $notification->getId(); - } - - return $this->success; - } - - public function _execute(): bool { - $sql = $this->context->getSQL(); - $userId = $this->getParam("userId"); - $groupId = $this->getParam("groupId"); - $title = $this->getParam("title"); - $message = $this->getParam("message"); - - if (is_null($userId) && is_null($groupId)) { - return $this->createError("Either userId or groupId must be specified."); - } else if(!is_null($userId) && !is_null($groupId)) { - return $this->createError("Only one of userId and groupId must be specified."); - } else if(!is_null($userId)) { - if (User::exists($sql, $userId)) { - $id = $this->createNotification($title, $message); - if ($this->success) { - return $this->insertUserNotification($userId, $id); - } - } else { - return $this->createError("User not found: $userId"); - } - } else if(!is_null($groupId)) { - if (Group::exists($sql, $groupId)) { - $id = $this->createNotification($title, $message); - if ($this->success) { - return $this->insertGroupNotification($groupId, $id); - } - } else { - return $this->createError("Group not found: $groupId"); - } - } - - return $this->success; - } - } - - class Fetch extends NotificationsAPI { - - private array $notifications; - private array $notificationIds; - - public function __construct(Context $context, $externalCall = false) { - parent::__construct($context, $externalCall, array( - 'new' => new Parameter('new', Parameter::TYPE_BOOLEAN, true, true) - )); - $this->loginRequired = true; - } - - private function fetchUserNotifications(): bool { - $onlyNew = $this->getParam('new'); - $userId = $this->context->getUser()->getId(); - $sql = $this->context->getSQL(); - $query = $sql->select($sql->distinct("Notification.id"), "created_at", "title", "message", "type") - ->from("Notification") - ->innerJoin("UserNotification", "UserNotification.notification_id", "Notification.id") - ->where(new Compare("UserNotification.user_id", $userId)) - ->orderBy("created_at")->descending(); - - if ($onlyNew) { - $query->where(new Compare("UserNotification.seen", false)); - } - - return $this->fetchNotifications($query); - } - - private function fetchGroupNotifications(): bool { - $onlyNew = $this->getParam('new'); - $userId = $this->context->getUser()->getId(); - $sql = $this->context->getSQL(); - $query = $sql->select($sql->distinct("Notification.id"), "created_at", "title", "message", "type") - ->from("Notification") - ->innerJoin("GroupNotification", "GroupNotification.notification_id", "Notification.id") - ->innerJoin("UserGroup", "GroupNotification.group_id", "UserGroup.group_id") - ->where(new Compare("UserGroup.user_id", $userId)) - ->orderBy("created_at")->descending(); - - if ($onlyNew) { - $query->where(new Compare("GroupNotification.seen", false)); - } - - return $this->fetchNotifications($query); - } - - private function fetchNotifications(Select $query): bool { - $sql = $this->context->getSQL(); - $res = $query->execute(); - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - foreach($res as $row) { - $id = $row["id"]; - if (!in_array($id, $this->notificationIds)) { - $this->notificationIds[] = $id; - $this->notifications[] = array( - "id" => $id, - "title" => $row["title"], - "message" => $row["message"], - "created_at" => $row["created_at"], - "type" => $row["type"] - ); - } - } - } - - return $this->success; - } - - public function _execute(): bool { - $this->notifications = array(); - $this->notificationIds = array(); - if ($this->fetchUserNotifications() && $this->fetchGroupNotifications()) { - $this->result["notifications"] = $this->notifications; - } - - return $this->success; - } - } - - class Seen extends NotificationsAPI { - - public function __construct(Context $context, bool $externalCall = false) { - parent::__construct($context, $externalCall, array()); - $this->loginRequired = true; - } - - public function _execute(): bool { - - $currentUser = $this->context->getUser(); - $sql = $this->context->getSQL(); - $res = $sql->update("UserNotification") - ->set("seen", true) - ->where(new Compare("user_id", $currentUser->getId())) - ->execute(); - - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - - if ($this->success) { - $res = $sql->update("GroupNotification") - ->set("seen", true) - ->where(new CondIn(new Column("group_id"), - $sql->select("group_id") - ->from("UserGroup") - ->where(new Compare("user_id", $currentUser->getId())))) - ->execute(); - - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - } - - return $this->success; - } - } -} \ No newline at end of file diff --git a/Core/API/PermissionAPI.class.php b/Core/API/PermissionAPI.class.php index 6821bb1..d6ae29f 100644 --- a/Core/API/PermissionAPI.class.php +++ b/Core/API/PermissionAPI.class.php @@ -3,6 +3,7 @@ namespace Core\API { use Core\Objects\Context; + use Core\Objects\DatabaseEntity\Group; abstract class PermissionAPI extends Request { @@ -12,7 +13,7 @@ namespace Core\API { protected function checkStaticPermission(): bool { $user = $this->context->getUser(); - if (!$user || !$user->hasGroup(USER_GROUP_ADMIN)) { + if (!$user || !$user->hasGroup(Group::ADMIN)) { return $this->createError("Permission denied."); } @@ -34,7 +35,6 @@ namespace Core\API\Permission { use Core\Driver\SQL\Strategy\UpdateStrategy; use Core\Objects\Context; use Core\Objects\DatabaseEntity\Group; - use Core\Objects\DatabaseEntity\User; class Check extends PermissionAPI { diff --git a/Core/API/Swagger.class.php b/Core/API/Swagger.class.php index 03ccc2b..b423714 100644 --- a/Core/API/Swagger.class.php +++ b/Core/API/Swagger.class.php @@ -4,6 +4,7 @@ namespace Core\API; use Core\API\Parameter\StringType; use Core\Objects\Context; +use Core\Objects\DatabaseEntity\Group; use Core\Objects\DatabaseEntity\User; class Swagger extends Request { @@ -90,7 +91,7 @@ class Swagger extends Request { } // special case: hardcoded permission - if ($request instanceof Permission\Save && (!$currentUser || !$currentUser->hasGroup(USER_GROUP_ADMIN))) { + if ($request instanceof Permission\Save && (!$currentUser || !$currentUser->hasGroup(Group::ADMIN))) { return false; } diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php index 4051414..9841dc4 100644 --- a/Core/API/UserAPI.class.php +++ b/Core/API/UserAPI.class.php @@ -75,7 +75,7 @@ namespace Core\API { $this->checkPasswordRequirements($password, $confirmPassword); } - protected function insertUser($username, $email, $password, $confirmed, $fullName = ""): bool|User { + protected function insertUser(string $username, ?string $email, string $password, bool $confirmed, string $fullName = "", array $groups = []): bool|User { $sql = $this->context->getSQL(); $user = new User(); @@ -86,6 +86,7 @@ namespace Core\API { $user->email = $email; $user->confirmed = $confirmed; $user->fullName = $fullName ?? ""; + $user->groups = $groups; $this->success = ($user->save($sql) !== FALSE); $this->lastError = $sql->getLastError(); @@ -107,12 +108,11 @@ namespace Core\API { protected function checkToken(string $token) : UserToken|bool { $sql = $this->context->getSQL(); - $userToken = UserToken::findBuilder($sql) + $userToken = UserToken::findBy(UserToken::createBuilder($sql, true) ->where(new Compare("UserToken.token", $token)) ->where(new Compare("UserToken.valid_until", $sql->now(), ">")) ->where(new Compare("UserToken.used", 0)) - ->fetchEntities() - ->execute(); + ->fetchEntities()); if ($userToken === false) { return $this->createError("Error verifying token: " . $sql->getLastError()); @@ -128,11 +128,16 @@ namespace Core\API { namespace Core\API\User { + use Core\API\Parameter\ArrayType; use Core\API\Parameter\Parameter; use Core\API\Parameter\StringType; use Core\API\Template\Render; use Core\API\UserAPI; use Core\API\VerifyCaptcha; + use Core\Driver\SQL\Condition\CondBool; + use Core\Driver\SQL\Condition\CondNot; + use Core\Driver\SQL\Condition\CondOr; + use Core\Objects\DatabaseEntity\Group; use Core\Objects\DatabaseEntity\UserToken; use Core\Driver\SQL\Column\Column; use Core\Driver\SQL\Condition\Compare; @@ -146,12 +151,15 @@ namespace Core\API\User { class Create extends UserAPI { + private User $user; + public function __construct(Context $context, $externalCall = false) { parent::__construct($context, $externalCall, array( 'username' => new StringType('username', 32), 'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL), 'password' => new StringType('password'), 'confirmPassword' => new StringType('confirmPassword'), + 'groups' => new ArrayType("groups", Parameter::TYPE_INT, true, true, []) )); $this->loginRequired = true; @@ -171,21 +179,36 @@ namespace Core\API\User { return false; } + $groups = []; + $sql = $this->context->getSQL(); + $requestedGroups = array_unique($this->getParam("groups")); + if (!empty($requestedGroups)) { + $groups = Group::findAll($sql, new CondIn(new Column("id"), $requestedGroups)); + foreach ($requestedGroups as $groupId) { + if (!isset($groups[$groupId])) { + return $this->createError("Group with id=$groupId does not exist."); + } + } + } + // prevent duplicate keys $email = (!is_null($email) && empty($email)) ? null : $email; - $user = $this->insertUser($username, $email, $password, true); + $user = $this->insertUser($username, $email, $password, true, "", $groups); if ($user !== false) { + $this->user = $user; $this->result["userId"] = $user->getId(); } return $this->success; } + + public function getUser(): User { + return $this->user; + } } class Fetch extends UserAPI { - private int $userCount; - public function __construct(Context $context, $externalCall = false) { parent::__construct($context, $externalCall, array( 'page' => new Parameter('page', Parameter::TYPE_INT, true, 1), @@ -193,21 +216,7 @@ namespace Core\API\User { )); } - private function getUserCount(): bool { - - $sql = $this->context->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; - } - - private function selectIds($page, $count) { + private function selectIds($page, $count): array|bool { $sql = $this->context->getSQL(); $res = $sql->select("User.id") ->from("User") @@ -241,72 +250,57 @@ namespace Core\API\User { return $this->createError("Invalid fetch count"); } - if (!$this->getUserCount()) { - return false; - } - - $userIds = $this->selectIds($page, $count); - if ($userIds === false) { - return false; + $condition = null; + $currentUser = $this->context->getUser(); + $fullInfo = ($currentUser->hasGroup(Group::ADMIN) || + $currentUser->hasGroup(Group::SUPPORT)); + if (!$fullInfo) { + $condition = new CondOr( + new Compare("User.id", $currentUser->getId()), + new CondBool("User.confirmed") + ); } $sql = $this->context->getSQL(); - $res = $sql->select("User.id as userId", "User.name", "User.email", "User.registered_at", "User.confirmed", - "User.profile_picture", "User.full_name", "Group.id as groupId", "User.last_online", - "Group.name as groupName", "Group.color as groupColor") - ->from("User") - ->leftJoin("UserGroup", "User.id", "UserGroup.user_id") - ->leftJoin("Group", "Group.id", "UserGroup.group_id") - ->where(new CondIn(new Column("User.id"), $userIds)) - ->execute(); + $userCount = User::count($sql, $condition); + if ($userCount === false) { + return $this->createError("Error fetching user count: " . $sql->getLastError()); + } - $this->success = ($res !== FALSE); - $this->lastError = $sql->getLastError(); - $currentUser = $this->context->getUser(); + $userQuery = User::createBuilder($sql, false) + ->orderBy("id") + ->ascending() + ->limit($count) + ->offset(($page - 1) * $count) + ->fetchEntities(); - if ($this->success) { - $this->result["users"] = array(); - foreach ($res as $row) { - $userId = intval($row["userId"]); - $groupId = $row["groupId"]; - $groupName = $row["groupName"]; - $groupColor = $row["groupColor"]; + if ($condition) { + $userQuery->where($condition); + } - $fullInfo = ($userId === $currentUser->getId() || - $currentUser->hasGroup(USER_GROUP_ADMIN) || - $currentUser->hasGroup(USER_GROUP_SUPPORT)); + $users = User::findBy($userQuery); + if ($users !== false) { + $this->result["users"] = []; - if (!isset($this->result["users"][$userId])) { - $user = array( - "id" => $userId, - "name" => $row["name"], - "fullName" => $row["full_name"], - "profilePicture" => $row["profile_picture"], - "email" => $row["email"], - "confirmed" => $sql->parseBool($row["confirmed"]), - "groups" => array(), - ); + foreach ($users as $userId => $user) { + $serialized = $user->jsonSerialize(); - if ($fullInfo) { - $user["registered_at"] = $row["registered_at"]; - $user["last_online"] = $row["last_online"]; - } else if (!$sql->parseBool($row["confirmed"])) { - continue; + if (!$fullInfo && $userId !== $currentUser->getId()) { + $publicAttributes = ["id", "name", "fullName", "profilePicture", "email", "groups"]; + foreach (array_keys($serialized) as $attr) { + if (!in_array($attr, $publicAttributes)) { + unset($serialized[$attr]); + } } - - $this->result["users"][$userId] = $user; } - if (!is_null($groupId)) { - $this->result["users"][$userId]["groups"][intval($groupId)] = array( - "id" => intval($groupId), - "name" => $groupName, - "color" => $groupColor - ); - } + $this->result["users"][$userId] = $serialized; } + $this->result["pageCount"] = intval(ceil($this->userCount / $count)); $this->result["totalCount"] = $this->userCount; + } else { + return $this->createError("Error fetching users: " . $sql->getLastError()); } return $this->success; @@ -338,13 +332,13 @@ namespace Core\API\User { // either we are querying own info or we are support / admin $currentUser = $this->context->getUser(); $canView = ($userId === $currentUser->getId() || - $currentUser->hasGroup(USER_GROUP_ADMIN) || - $currentUser->hasGroup(USER_GROUP_SUPPORT)); + $currentUser->hasGroup(Group::ADMIN) || + $currentUser->hasGroup(Group::SUPPORT)); // full info only when we have administrative privileges, or we are querying ourselves $fullInfo = ($userId === $currentUser->getId() || - $currentUser->hasGroup(USER_GROUP_ADMIN) || - $currentUser->hasGroup(USER_GROUP_SUPPORT)); + $currentUser->hasGroup(Group::ADMIN) || + $currentUser->hasGroup(Group::SUPPORT)); if (!$canView) { @@ -617,10 +611,9 @@ namespace Core\API\User { $stayLoggedIn = $this->getParam('stayLoggedIn'); $sql = $this->context->getSQL(); - $user = User::findBuilder($sql) + $user = User::findBy(User::createBuilder($sql, true) ->where(new Compare("User.name", $username), new Compare("User.email", $username)) - ->fetchEntities() - ->execute(); + ->fetchEntities()); if ($user !== false) { if ($user === null) { @@ -842,7 +835,7 @@ namespace Core\API\User { $groupIds[] = $param->value; } - if ($id === $currentUser->getId() && !in_array(USER_GROUP_ADMIN, $groupIds)) { + if ($id === $currentUser->getId() && !in_array(Group::ADMIN, $groupIds)) { return $this->createError("Cannot remove Administrator group from own user."); } } @@ -958,10 +951,9 @@ namespace Core\API\User { $sql = $this->context->getSQL(); $email = $this->getParam("email"); - $user = User::findBuilder($sql) + $user = User::findBy(User::createBuilder($sql, true) ->where(new Compare("email", $email)) - ->fetchEntities() - ->execute(); + ->fetchEntities()); if ($user === false) { return $this->createError("Could not fetch user details: " . $sql->getLastError()); } else if ($user !== null) { @@ -1040,10 +1032,9 @@ namespace Core\API\User { $email = $this->getParam("email"); $sql = $this->context->getSQL(); - $user = User::findBuilder($sql) + $user = User::findBy(User::createBuilder($sql, true) ->where(new Compare("User.email", $email)) - ->where(new Compare("User.confirmed", false)) - ->execute(); + ->where(new Compare("User.confirmed", false))); if ($user === false) { return $this->createError("Error retrieving user details: " . $sql->getLastError()); @@ -1052,11 +1043,10 @@ namespace Core\API\User { return true; } - $userToken = UserToken::findBuilder($sql) + $userToken = UserToken::findBy(UserToken::createBuilder($sql, true) ->where(new Compare("used", false)) ->where(new Compare("tokenType", UserToken::TYPE_EMAIL_CONFIRM)) - ->where(new Compare("user_id", $user->getId())) - ->execute(); + ->where(new Compare("user_id", $user->getId()))); $validHours = 48; if ($userToken === false) { @@ -1366,12 +1356,11 @@ namespace Core\API\User { $token = $this->getParam("token"); $sql = $this->context->getSQL(); - $userToken = UserToken::findBuilder($sql) + $userToken = UserToken::findBy(UserToken::createBuilder($sql, true) ->where(new Compare("token", $token)) ->where(new Compare("valid_until", $sql->now(), ">=")) ->where(new Compare("user_id", $currentUser->getId())) - ->where(new Compare("token_type", UserToken::TYPE_GPG_CONFIRM)) - ->execute(); + ->where(new Compare("token_type", UserToken::TYPE_GPG_CONFIRM))); if ($userToken !== false) { if ($userToken === null) { diff --git a/Core/Configuration/CreateDatabase.class.php b/Core/Configuration/CreateDatabase.class.php index d91390b..83cc889 100644 --- a/Core/Configuration/CreateDatabase.class.php +++ b/Core/Configuration/CreateDatabase.class.php @@ -3,9 +3,9 @@ namespace Core\Configuration; use Core\Driver\SQL\SQL; -use Core\Driver\SQL\Strategy\SetNullStrategy; -use Core\Driver\SQL\Strategy\CascadeStrategy; -use Core\Objects\DatabaseEntity\DatabaseEntity; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; +use Core\Objects\DatabaseEntity\Group; +use Core\Objects\DatabaseEntity\Language; use PHPUnit\Util\Exception; class CreateDatabase extends DatabaseScript { @@ -15,37 +15,16 @@ class CreateDatabase extends DatabaseScript { self::loadEntities($queries, $sql); - $queries[] = $sql->insert("Language", array("code", "name")) - ->addRow("en_US", 'American English') - ->addRow("de_DE", 'Deutsch Standard'); + $queries[] = Language::getHandler($sql)->getInsertQuery([ + new Language(Language::AMERICAN_ENGLISH, "en_US", 'American English'), + new Language(Language::AMERICAN_ENGLISH, "de_DE", 'Deutsch Standard'), + ]); - $queries[] = $sql->insert("Group", array("name", "color")) - ->addRow(USER_GROUP_MODERATOR_NAME, "#007bff") - ->addRow(USER_GROUP_SUPPORT_NAME, "#28a745") - ->addRow(USER_GROUP_ADMIN_NAME, "#dc3545"); - - $queries[] = $sql->createTable("UserGroup") - ->addInt("user_id") - ->addInt("group_id") - ->unique("user_id", "group_id") - ->foreignKey("user_id", "User", "id", new CascadeStrategy()) - ->foreignKey("group_id", "Group", "id", new CascadeStrategy()); - - $queries[] = $sql->createTable("UserNotification") - ->addInt("user_id") - ->addInt("notification_id") - ->addBool("seen", false) - ->foreignKey("user_id", "User", "id") - ->foreignKey("notification_id", "Notification", "id") - ->unique("user_id", "notification_id"); - - $queries[] = $sql->createTable("GroupNotification") - ->addInt("group_id") - ->addInt("notification_id") - ->addBool("seen", false) - ->foreignKey("group_id", "Group", "id") - ->foreignKey("notification_id", "Notification", "id") - ->unique("group_id", "notification_id"); + $queries[] = Group::getHandler($sql)->getInsertQuery([ + new Group(Group::ADMIN, Group::GROUPS[Group::ADMIN], "#007bff"), + new Group(Group::MODERATOR, Group::GROUPS[Group::MODERATOR], "#28a745"), + new Group(Group::SUPPORT, Group::GROUPS[Group::SUPPORT], "#dc3545"), + ]); $queries[] = $sql->createTable("Visitor") ->addInt("day") @@ -81,92 +60,43 @@ class CreateDatabase extends DatabaseScript { ->addBool("private", false) // these values are not returned from '/api/settings/get', but can be changed ->addBool("readonly", false) // these values are neither returned, nor can be changed from outside ->primaryKey("name"); - - $settingsQuery = $sql->insert("Settings", array("name", "value", "private", "readonly")) - // ->addRow("mail_enabled", "0") # this key will be set during installation - ->addRow("mail_host", "", false, false) - ->addRow("mail_port", "", false, false) - ->addRow("mail_username", "", false, false) - ->addRow("mail_password", "", true, false) - ->addRow("mail_from", "", false, false) - ->addRow("mail_last_sync", "", false, false) - ->addRow("mail_footer", "", false, false); - + $settingsQuery = $sql->insert("Settings", array("name", "value", "private", "readonly")); (Settings::loadDefaults())->addRows($settingsQuery); $queries[] = $settingsQuery; - $queries[] = $sql->createTable("ContactRequest") - ->addSerial("id") - ->addString("from_name", 32) - ->addString("from_email", 64) - ->addString("message", 512) - ->addString("messageId", 78, true) # null = don't sync with mails (usually if mail could not be sent) - ->addDateTime("created_at", false, $sql->currentTimestamp()) - ->unique("messageId") - ->primaryKey("id"); - - $queries[] = $sql->createTable("ContactMessage") - ->addSerial("id") - ->addInt("request_id") - ->addInt("user_id", true) # null = customer has sent this message - ->addString("message", 512) - ->addString("messageId", 78) - ->addDateTime("created_at", false, $sql->currentTimestamp()) - ->addBool("read", false) - ->unique("messageId") - ->primaryKey("id") - ->foreignKey("request_id", "ContactRequest", "id", new CascadeStrategy()) - ->foreignKey("user_id", "User", "id", new SetNullStrategy()); - $queries[] = $sql->createTable("ApiPermission") ->addString("method", 32) ->addJson("groups", true, '[]') ->addString("description", 128, false, "") ->primaryKey("method"); - $queries[] = $sql->createTable("MailQueue") - ->addSerial("id") - ->addString("from", 64) - ->addString("to", 64) - ->addString("subject") - ->addString("body") - ->addString("replyTo", 64, true) - ->addString("replyName", 32, true) - ->addString("gpgFingerprint", 64, true) - ->addEnum("status", ["waiting", "success", "error"], false, 'waiting') - ->addInt("retryCount", false, 5) - ->addDateTime("nextTry", false, $sql->now()) - ->addString("errorMessage", NULL, true) - ->primaryKey("id"); - - $queries = array_merge($queries, \Core\Configuration\Patch\EntityLog_2021_04_08::createTableLog($sql, "MailQueue", 30)); - $queries[] = $sql->insert("ApiPermission", array("method", "groups", "description")) ->addRow("ApiKey/create", array(), "Allows users to create API-Keys for themselves") ->addRow("ApiKey/fetch", array(), "Allows users to list their API-Keys") ->addRow("ApiKey/refresh", array(), "Allows users to refresh their API-Keys") ->addRow("ApiKey/revoke", array(), "Allows users to revoke their API-Keys") - ->addRow("Groups/fetch", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to list all available groups") - ->addRow("Groups/create", array(USER_GROUP_ADMIN), "Allows users to create a new groups") - ->addRow("Groups/delete", array(USER_GROUP_ADMIN), "Allows users to delete a group") - ->addRow("Routes/fetch", array(USER_GROUP_ADMIN), "Allows users to list all configured routes") - ->addRow("Routes/save", array(USER_GROUP_ADMIN), "Allows users to create, delete and modify routes") - ->addRow("Mail/test", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to send a test email to a given address") - ->addRow("Mail/Sync", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to synchronize mails with the database") - ->addRow("Settings/get", array(USER_GROUP_ADMIN), "Allows users to fetch server settings") - ->addRow("Settings/set", array(USER_GROUP_ADMIN), "Allows users create, delete or modify server settings") - ->addRow("Stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to fetch server stats") - ->addRow("User/create", array(USER_GROUP_ADMIN), "Allows users to create a new user, email address does not need to be confirmed") - ->addRow("User/fetch", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to list all registered users") - ->addRow("User/get", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to get information about a single user") - ->addRow("User/invite", array(USER_GROUP_ADMIN), "Allows users to create a new user and send them an invitation link") - ->addRow("User/edit", array(USER_GROUP_ADMIN), "Allows users to edit details and group memberships of any user") - ->addRow("User/delete", array(USER_GROUP_ADMIN), "Allows users to delete any other user") - ->addRow("Permission/fetch", array(USER_GROUP_ADMIN), "Allows users to list all API permissions") - ->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics") - ->addRow("Contact/respond", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to respond to contact requests") - ->addRow("Contact/fetch", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to fetch all contact requests") - ->addRow("Contact/get", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see messages within a contact request"); + ->addRow("Groups/fetch", array(Group::SUPPORT, Group::ADMIN), "Allows users to list all available groups") + ->addRow("Groups/create", array(Group::ADMIN), "Allows users to create a new groups") + ->addRow("Groups/delete", array(Group::ADMIN), "Allows users to delete a group") + ->addRow("Routes/fetch", array(Group::ADMIN), "Allows users to list all configured routes") + ->addRow("Routes/save", array(Group::ADMIN), "Allows users to create, delete and modify routes") + ->addRow("Mail/test", array(Group::SUPPORT, Group::ADMIN), "Allows users to send a test email to a given address") + ->addRow("Mail/Sync", array(Group::SUPPORT, Group::ADMIN), "Allows users to synchronize mails with the database") + ->addRow("Settings/get", array(Group::ADMIN), "Allows users to fetch server settings") + ->addRow("Settings/set", array(Group::ADMIN), "Allows users create, delete or modify server settings") + ->addRow("Stats", array(Group::ADMIN, Group::SUPPORT), "Allows users to fetch server stats") + ->addRow("User/create", array(Group::ADMIN), "Allows users to create a new user, email address does not need to be confirmed") + ->addRow("User/fetch", array(Group::ADMIN, Group::SUPPORT), "Allows users to list all registered users") + ->addRow("User/get", array(Group::ADMIN, Group::SUPPORT), "Allows users to get information about a single user") + ->addRow("User/invite", array(Group::ADMIN), "Allows users to create a new user and send them an invitation link") + ->addRow("User/edit", array(Group::ADMIN), "Allows users to edit details and group memberships of any user") + ->addRow("User/delete", array(Group::ADMIN), "Allows users to delete any other user") + ->addRow("Permission/fetch", array(Group::ADMIN), "Allows users to list all API permissions") + ->addRow("Visitors/stats", array(Group::ADMIN, Group::SUPPORT), "Allows users to see visitor statistics") + ->addRow("Contact/respond", array(Group::ADMIN, Group::SUPPORT), "Allows users to respond to contact requests") + ->addRow("Contact/fetch", array(Group::ADMIN, Group::SUPPORT), "Allows users to fetch all contact requests") + ->addRow("Contact/get", array(Group::ADMIN, Group::SUPPORT), "Allows users to see messages within a contact request") + ->addRow("Logs/get", [Group::ADMIN], "Allows users to fetch system logs"); self::loadPatches($queries, $sql); @@ -195,9 +125,10 @@ class CreateDatabase extends DatabaseScript { } public static function loadEntities(&$queries, $sql) { - $handlers = []; + $persistables = []; $baseDirs = ["Core", "Site"]; foreach ($baseDirs as $baseDir) { + $entityDirectory = "./$baseDir/Objects/DatabaseEntity/"; if (file_exists($entityDirectory) && is_dir($entityDirectory)) { $scan_arr = scandir($entityDirectory); @@ -206,38 +137,40 @@ class CreateDatabase extends DatabaseScript { $suffix = ".class.php"; if (endsWith($file, $suffix)) { $className = substr($file, 0, strlen($file) - strlen($suffix)); - if (!in_array($className, ["DatabaseEntity", "DatabaseEntityQuery", "DatabaseEntityHandler"])) { - $className = "\\$baseDir\\Objects\\DatabaseEntity\\$className"; - $reflectionClass = new \ReflectionClass($className); - if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) { - $method = "$className::getHandler"; - $handler = call_user_func($method, $sql); - $handlers[$handler->getTableName()] = $handler; + $className = "\\$baseDir\\Objects\\DatabaseEntity\\$className"; + $reflectionClass = new \ReflectionClass($className); + if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) { + $method = "$className::getHandler"; + $handler = call_user_func($method, $sql); + $persistables[$handler->getTableName()] = $handler; + + foreach ($handler->getNMRelations() as $nmTableName => $nmRelation) { + $persistables[$nmTableName] = $nmRelation; } } } } } - $tableCount = count($handlers); + $tableCount = count($persistables); $createdTables = []; - while (!empty($handlers)) { + while (!empty($persistables)) { $prevCount = $tableCount; $unmetDependenciesTotal = []; - foreach ($handlers as $tableName => $handler) { - $dependsOn = $handler->dependsOn(); + foreach ($persistables as $tableName => $persistable) { + $dependsOn = $persistable->dependsOn(); $unmetDependencies = array_diff($dependsOn, $createdTables); if (empty($unmetDependencies)) { - $queries[] = $handler->getTableQuery(); + $queries = array_merge($queries, $persistable->getCreateQueries($sql)); $createdTables[] = $tableName; - unset($handlers[$tableName]); + unset($persistables[$tableName]); } else { $unmetDependenciesTotal = array_merge($unmetDependenciesTotal, $unmetDependencies); } } - $tableCount = count($handlers); + $tableCount = count($persistables); if ($tableCount === $prevCount) { throw new Exception("Circular or unmet table dependency detected. Unmet dependencies: " . implode(", ", $unmetDependenciesTotal)); diff --git a/Core/Configuration/Patch/EntityLog_2021_04_08.class.php b/Core/Configuration/Patch/EntityLog_2021_04_08.class.php index 1c23d4d..61f8c4c 100644 --- a/Core/Configuration/Patch/EntityLog_2021_04_08.class.php +++ b/Core/Configuration/Patch/EntityLog_2021_04_08.class.php @@ -5,7 +5,6 @@ namespace Core\Configuration\Patch; use Core\Configuration\DatabaseScript; use Core\Driver\SQL\Column\IntColumn; use Core\Driver\SQL\Condition\Compare; -use Core\Driver\SQL\Query\CreateProcedure; use Core\Driver\SQL\SQL; use Core\Driver\SQL\Type\CurrentColumn; use Core\Driver\SQL\Type\CurrentTable; @@ -13,32 +12,6 @@ use Core\Driver\SQL\Type\Trigger; class EntityLog_2021_04_08 extends DatabaseScript { - public static function createTableLog(SQL $sql, string $table, int $lifetime = 90): array { - return [ - $sql->createTrigger("${table}_trg_insert") - ->after()->insert($table) - ->exec(new CreateProcedure($sql, "InsertEntityLog"), [ - "tableName" => new CurrentTable(), - "entityId" => new CurrentColumn("id"), - "lifetime" => $lifetime, - ]), - - $sql->createTrigger("${table}_trg_update") - ->after()->update($table) - ->exec(new CreateProcedure($sql, "UpdateEntityLog"), [ - "tableName" => new CurrentTable(), - "entityId" => new CurrentColumn("id"), - ]), - - $sql->createTrigger("${table}_trg_delete") - ->after()->delete($table) - ->exec(new CreateProcedure($sql, "DeleteEntityLog"), [ - "tableName" => new CurrentTable(), - "entityId" => new CurrentColumn("id"), - ]) - ]; - } - public static function createQueries(SQL $sql): array { $queries = array(); @@ -84,11 +57,6 @@ class EntityLog_2021_04_08 extends DatabaseScript { $queries[] = $updateProcedure; $queries[] = $deleteProcedure; - $tables = ["ContactRequest"]; - foreach ($tables as $table) { - $queries = array_merge($queries, self::createTableLog($sql, $table)); - } - return $queries; } diff --git a/Core/Configuration/Patch/SystemLog_2022_03_30.class.php b/Core/Configuration/Patch/SystemLog_2022_03_30.class.php deleted file mode 100644 index ecc7ec9..0000000 --- a/Core/Configuration/Patch/SystemLog_2022_03_30.class.php +++ /dev/null @@ -1,16 +0,0 @@ -insert("ApiPermission", ["method", "groups", "description"]) - ->addRow("Logs/get", [USER_GROUP_ADMIN], "Allows users to fetch system logs") - ]; - } -} \ No newline at end of file diff --git a/Core/Configuration/Settings.class.php b/Core/Configuration/Settings.class.php index 7d42e53..675b441 100644 --- a/Core/Configuration/Settings.class.php +++ b/Core/Configuration/Settings.class.php @@ -63,7 +63,7 @@ class Settings { } public static function loadDefaults(): Settings { - $hostname = $_SERVER["SERVER_NAME"]; + $hostname = $_SERVER["SERVER_NAME"] ?? null; if (empty($hostname)) { $hostname = "localhost"; } @@ -190,7 +190,14 @@ class Settings { ->addRow("recaptcha_enabled", $this->recaptchaEnabled ? "1" : "0", false, false) ->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false) ->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false) - ->addRow("allowed_extensions", implode(",", $this->allowedExtensions), true, false); + ->addRow("allowed_extensions", implode(",", $this->allowedExtensions), true, false) + ->addRow("mail_host", "", false, false) + ->addRow("mail_port", "", false, false) + ->addRow("mail_username", "", false, false) + ->addRow("mail_password", "", true, false) + ->addRow("mail_from", "", false, false) + ->addRow("mail_last_sync", "", false, false) + ->addRow("mail_footer", "", false, false); } public function getSiteName(): string { diff --git a/Core/Documents/Info.class.php b/Core/Documents/Info.class.php index aeef703..ec3fcd3 100644 --- a/Core/Documents/Info.class.php +++ b/Core/Documents/Info.class.php @@ -17,7 +17,7 @@ class Info extends HtmlDocument { class InfoBody extends SimpleBody { protected function getContent(): string { $user = $this->getDocument()->getUser(); - if ($user && $user->hasGroup(USER_GROUP_ADMIN)) { + if ($user && $user->hasGroup(Group::ADMIN)) { phpinfo(); return ""; } else { diff --git a/Core/Documents/Install.class.php b/Core/Documents/Install.class.php index 47925c9..afd1721 100644 --- a/Core/Documents/Install.class.php +++ b/Core/Documents/Install.class.php @@ -30,6 +30,7 @@ namespace Documents\Install { use Core\External\PHPMailer\Exception; use Core\External\PHPMailer\PHPMailer; use Core\Objects\ConnectionData; + use Core\Objects\DatabaseEntity\Group; class InstallHead extends Head { @@ -202,7 +203,7 @@ namespace Documents\Install { $req->execute(array( "title" => "Welcome", "message" => "Your Web-base was successfully installed. Check out the admin dashboard. Have fun!", - "groupId" => USER_GROUP_ADMIN + "groupId" => Group::ADMIN ) ); $this->errorString = $req->getLastError(); @@ -436,16 +437,10 @@ namespace Documents\Install { 'email' => $email, 'password' => $password, 'confirmPassword' => $confirmPassword, + 'groups' => [Group::ADMIN] )); $msg = $req->getLastError(); - if ($success) { - $sql = $context->getSQL(); - $success = $sql->insert("UserGroup", array("group_id", "user_id")) - ->addRow(USER_GROUP_ADMIN, $req->getResult()["userId"]) - ->execute(); - $msg = $sql->getLastError(); - } } return array("msg" => $msg, "success" => $success); diff --git a/Core/Driver/SQL/Query/Select.class.php b/Core/Driver/SQL/Query/Select.class.php index 942b1d6..a77f0ac 100644 --- a/Core/Driver/SQL/Query/Select.class.php +++ b/Core/Driver/SQL/Query/Select.class.php @@ -38,6 +38,11 @@ class Select extends Query { $this->fetchType = SQL::FETCH_ALL; } + public function addColumn(string $columnName): Select { + $this->selectValues[] = $columnName; + return $this; + } + public function from(...$tables): Select { $this->tables = array_merge($this->tables, $tables); return $this; diff --git a/Core/Objects/Context.class.php b/Core/Objects/Context.class.php index a3c415d..d227106 100644 --- a/Core/Objects/Context.class.php +++ b/Core/Objects/Context.class.php @@ -90,7 +90,7 @@ class Context { session_write_close(); } - private function loadSession(int $userId, int $sessionId) { + private function loadSession(int $userId, int $sessionId): void { $this->session = Session::init($this, $userId, $sessionId); $this->user = $this->session?->getUser(); if ($this->user) { @@ -128,12 +128,13 @@ class Context { public function updateLanguage(string $lang): bool { if ($this->sql) { - $language = Language::findBuilder($this->sql) + $language = Language::findBy(Language::createBuilder($this->sql, true) ->where(new CondOr( new CondLike("name", "%$lang%"), // english new Compare("code", $lang), // de_DE - new CondLike("code", $lang . "_%"))) // de -> de_% - ->execute(); + new CondLike("code", "${lang}_%") // de -> de_% + )) + ); if ($language) { $this->setLanguage($language); return true; @@ -176,14 +177,13 @@ class Context { } public function loadApiKey(string $apiKey): bool { - $this->user = User::findBuilder($this->sql) + $this->user = User::findBy(User::createBuilder($this->sql, true) ->addJoin(new Join("INNER","ApiKey", "ApiKey.user_id", "User.id")) ->where(new Compare("ApiKey.api_key", $apiKey)) ->where(new Compare("valid_until", $this->sql->currentTimestamp(), ">")) ->where(new Compare("ApiKey.active", true)) ->where(new Compare("User.confirmed", true)) - ->fetchEntities() - ->execute(); + ->fetchEntities()); return $this->user !== null; } diff --git a/Core/Objects/DatabaseEntity/ApiKey.class.php b/Core/Objects/DatabaseEntity/ApiKey.class.php index 7a99ff6..d9e0719 100644 --- a/Core/Objects/DatabaseEntity/ApiKey.class.php +++ b/Core/Objects/DatabaseEntity/ApiKey.class.php @@ -3,6 +3,7 @@ namespace Core\Objects\DatabaseEntity; use Core\Objects\DatabaseEntity\Attribute\MaxLength; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class ApiKey extends DatabaseEntity { diff --git a/Core/Objects/DatabaseEntity/Attribute/Multiple.php b/Core/Objects/DatabaseEntity/Attribute/Multiple.php new file mode 100644 index 0000000..ec62963 --- /dev/null +++ b/Core/Objects/DatabaseEntity/Attribute/Multiple.php @@ -0,0 +1,18 @@ +className = $className; + } + + public function getClassName(): string { + return $this->className; + } + +} \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/DatabaseEntity.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php similarity index 73% rename from Core/Objects/DatabaseEntity/DatabaseEntity.class.php rename to Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php index 58a406f..3814efb 100644 --- a/Core/Objects/DatabaseEntity/DatabaseEntity.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php @@ -1,6 +1,6 @@ false, + "update" => false, + "delete" => false, + "lifetime" => null, + ]; + private static array $handlers = []; protected ?int $id; @@ -51,8 +58,8 @@ abstract class DatabaseEntity { return $res !== false && $res[0]["count"] !== 0; } - public static function findBuilder(SQL $sql): DatabaseEntityQuery { - return DatabaseEntityQuery::fetchOne(self::getHandler($sql)); + public static function findBy(DatabaseEntityQuery $dbQuery): static|array|bool|null { + return $dbQuery->execute(); } public static function findAll(SQL $sql, ?Condition $condition = null): ?array { @@ -60,17 +67,22 @@ abstract class DatabaseEntity { return $handler->fetchMultiple($condition); } - public static function findAllBuilder(SQL $sql): DatabaseEntityQuery { - return DatabaseEntityQuery::fetchAll(self::getHandler($sql)); + public static function createBuilder(SQL $sql, bool $one): DatabaseEntityQuery { + if ($one) { + return DatabaseEntityQuery::fetchOne(self::getHandler($sql)); + } else { + return DatabaseEntityQuery::fetchAll(self::getHandler($sql)); + } } - public function save(SQL $sql, ?array $columns = null): bool { + public function save(SQL $sql, ?array $columns = null, bool $saveNM = false): bool { $handler = self::getHandler($sql); - $res = $handler->insertOrUpdate($this, $columns); + $res = $handler->insertOrUpdate($this, $columns, $saveNM); if ($res === false) { return false; } else if ($this->id === null) { $this->id = $res; + $handler->insertNM($this); } return true; @@ -127,4 +139,22 @@ abstract class DatabaseEntity { public function getId(): ?int { return $this->id; } + + public static function count(SQL $sql, ?Condition $condition = null): int|bool { + $handler = self::getHandler($sql); + $query = $sql->select($sql->count()) + ->from($handler->getTableName()); + + if ($condition) { + $query->where($condition); + } + + $res = $query->execute(); + + if (!empty($res)) { + return $res[0]["count"]; + } + + return false; + } } \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/DatabaseEntityHandler.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php similarity index 51% rename from Core/Objects/DatabaseEntity/DatabaseEntityHandler.php rename to Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php index 6c201c1..180afca 100644 --- a/Core/Objects/DatabaseEntity/DatabaseEntityHandler.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php @@ -1,33 +1,46 @@ tableName = $this->entityClass->getShortName(); $this->columns = []; // property name => database column name $this->properties = []; // property name => \ReflectionProperty - $this->relations = []; // property name => referenced table name + $this->relations = []; // property name => DatabaseEntityHandler $this->constraints = []; // \Driver\SQL\Constraint\Constraint + $this->nmRelations = []; // table name => NMRelation foreach ($this->entityClass->getProperties() as $property) { $propertyName = $property->getName(); @@ -60,6 +75,10 @@ class DatabaseEntityHandler { continue; } + if ($property->isStatic()) { + continue; + } + $propertyType = $property->getType(); $columnName = self::getColumnName($propertyName); if (!($propertyType instanceof \ReflectionNamedType)) { @@ -93,18 +112,46 @@ class DatabaseEntityHandler { $this->columns[$propertyName] = new BoolColumn($columnName, $defaultValue ?? false); } else if ($propertyTypeName === 'DateTime') { $this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable, $defaultValue); - /*} else if ($propertyName === 'array') { - $many = self::getAttribute($property, Many::class); - if ($many) { - $requestedType = $many->getValue(); - if (isClass($requestedType)) { - $requestedClass = new \ReflectionClass($requestedType); + /*} else if ($propertyName === 'array') { + $many = self::getAttribute($property, Many::class); + if ($many) { + $requestedType = $many->getValue(); + if (isClass($requestedType)) { + $requestedClass = new \ReflectionClass($requestedType); + } else { + $this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $requestedType"); + } } else { - $this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $requestedType"); + $this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName"); + }*/ + } else if ($propertyTypeName === "array") { + $multiple = self::getAttribute($property, Multiple::class); + if (!$multiple) { + $this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName. " . + "Is the 'Multiple' attribute missing?"); + } + + try { + $refClass = $multiple->getClassName(); + $requestedClass = new \ReflectionClass($refClass); + if ($requestedClass->isSubclassOf(DatabaseEntity::class)) { + $nmTableName = NMRelation::buildTableName($this->getTableName(), $requestedClass->getShortName()); + $nmRelation = $this->nmRelations[$nmTableName] ?? null; + if (!$nmRelation) { + $otherHandler = DatabaseEntity::getHandler($this->sql, $requestedClass); + $otherNM = $otherHandler->getNMRelations(); + $nmRelation = $otherNM[$nmTableName] ?? (new NMRelation($this, $otherHandler)); + $this->nmRelations[$nmTableName] = $nmRelation; + } + + $this->nmRelations[$nmTableName]->addProperty($this, $property); + } else { + $this->raiseError("Cannot persist class '$className': Property '$propertyName' of type multiple can " . + "only reference DatabaseEntity types, but got: $refClass"); } - } else { - $this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName"); - }*/ + } catch (\Exception $ex) { + $this->raiseError("Cannot persist class '$className' property '$propertyTypeName': " . $ex->getMessage()); + } } else if ($propertyTypeName !== "mixed") { try { $requestedClass = new \ReflectionClass($propertyTypeName); @@ -167,6 +214,10 @@ class DatabaseEntityHandler { return $this->relations; } + public function getNMRelations(): array { + return $this->nmRelations; + } + public function getColumnNames(): array { $columns = ["$this->tableName.id"]; foreach ($this->columns as $column) { @@ -238,6 +289,14 @@ class DatabaseEntityHandler { } } + // init n:m / 1:n properties with empty arrays + foreach ($this->nmRelations as $nmRelation) { + foreach ($nmRelation->getProperties($this) as $property) { + $property->setAccessible(true); + $property->setValue($entity, []); + } + } + $this->properties["id"]->setAccessible(true); $this->properties["id"]->setValue($entity, $row["id"]); $entity->postFetch($this->sql, $row); @@ -248,6 +307,190 @@ class DatabaseEntityHandler { } } + public function updateNM(DatabaseEntity $entity): bool { + if (empty($this->nmRelations)) { + return true; + } + + foreach ($this->nmRelations as $nmTable => $nmRelation) { + + $thisIdColumn = $nmRelation->getIdColumn($this); + $thisTableName = $this->getTableName(); + $dataColumns = $nmRelation->getDataColumns(); + $otherHandler = $nmRelation->getOtherHandler($this); + $refIdColumn = $nmRelation->getIdColumn($otherHandler); + + + // delete from n:m table if no longer exists + $deleteStatement = $this->sql->delete($nmTable) + ->where(new Compare($thisIdColumn, $entity->getId())); + + if (!empty($dataColumns)) { + $conditions = []; + foreach ($dataColumns[$thisTableName] as $propertyName => $columnName) { + $property = $this->properties[$propertyName]; + $entityIds = array_keys($property->getValue($entity)); + if (!empty($entityIds)) { + $conditions[] = new CondAnd( + new CondBool($columnName), + new CondNot(new CondIn(new Column($refIdColumn), $entityIds)), + ); + } + } + + if (!empty($conditions)) { + $deleteStatement->where(new CondOr(...$conditions)); + } + } else { + $property = next($nmRelation->getProperties($this)); + $entityIds = array_keys($property->getValue($entity)); + if (!empty($entityIds)) { + $deleteStatement->where( + new CondNot(new CondIn(new Column($refIdColumn), $entityIds)) + ); + } + } + + $deleteStatement->execute(); + } + + return $this->insertNM($entity, true); + } + + public function insertNM(DatabaseEntity $entity, bool $ignoreExisting = true): bool { + + if (empty($this->nmRelations)) { + return true; + } + + $success = true; + foreach ($this->nmRelations as $nmTable => $nmRelation) { + $otherHandler = $nmRelation->getOtherHandler($this); + $thisIdColumn = $nmRelation->getIdColumn($this); + $thisTableName = $this->getTableName(); + $refIdColumn = $nmRelation->getIdColumn($otherHandler); + $dataColumns = $nmRelation->getDataColumns(); + + $columns = [ + $thisIdColumn, + $refIdColumn, + ]; + + if (!empty($dataColumns)) { + $columns = array_merge($columns, array_values($dataColumns[$thisTableName])); + } + + $statement = $this->sql->insert($nmTable, $columns); + if ($ignoreExisting) { + $statement->onDuplicateKeyStrategy(new UpdateStrategy($nmRelation->getAllColumns(), [ + $thisIdColumn => $entity->getId() + ])); + } + + foreach ($nmRelation->getProperties($this) as $property) { + $property->setAccessible(true); + $relEntities = $property->getValue($entity); + foreach ($relEntities as $relEntity) { + $nmRow = [$entity->getId(), $relEntity->getId()]; + if (!empty($dataColumns)) { + foreach (array_keys($dataColumns[$thisTableName]) as $propertyName) { + $nmRow[] = $property->getName() === $propertyName; + } + } + $statement->addRow(...$nmRow); + } + } + + $success = $statement->execute() && $success; + } + + return $success; + } + + public function fetchNMRelations(array $entities, bool $recursive = false) { + + if ($recursive) { + foreach ($entities as $entity) { + foreach ($this->relations as $propertyName => $relHandler) { + $relEntity = $this->properties[$propertyName]->getValue($entity); + if ($relEntity) { + $relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true); + } + } + } + } + + if (empty($this->nmRelations)) { + return; + } + + $entityIds = array_keys($entities); + foreach ($this->nmRelations as $nmTable => $nmRelation) { + $otherHandler = $nmRelation->getOtherHandler($this); + + $thisIdColumn = $nmRelation->getIdColumn($this); + $thisProperties = $nmRelation->getProperties($this); + $thisTableName = $this->getTableName(); + + $refIdColumn = $nmRelation->getIdColumn($otherHandler); + $refProperties = $nmRelation->getProperties($otherHandler); + $refTableName = $otherHandler->getTableName(); + + $dataColumns = $nmRelation->getDataColumns(); + + $relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler) + ->addJoin(new Join("INNER", $nmTable, "$nmTable.$refIdColumn", "$refTableName.id")) + ->where(new CondIn(new Column($thisIdColumn), $entityIds)) + ->getQuery(); + + $relEntityQuery->addColumn($thisIdColumn); + foreach ($dataColumns as $tableDataColumns) { + foreach ($tableDataColumns as $columnName) { + $relEntityQuery->addColumn($columnName); + } + } + + $rows = $relEntityQuery->execute(); + if (!is_array($rows)) { + $this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError()); + return; + } + + $relEntities = []; + foreach ($rows as $row) { + $relId = $row["id"]; + if (!isset($relEntities[$relId])) { + $relEntity = $otherHandler->entityFromRow($row); + $relEntities[$relId] = $relEntity; + } + + $thisEntity = $entities[$row[$thisIdColumn]]; + $relEntity = $relEntities[$relId]; + $mappings = [ + [$refProperties, $refTableName, $relEntity, $thisEntity], + [$thisProperties, $thisTableName, $thisEntity, $relEntity], + ]; + + foreach ($mappings as $mapping) { + list($properties, $tableName, $targetEntity, $entityToAdd) = $mapping; + foreach ($properties as $propertyName => $property) { + $addToProperty = empty($dataColumns); + if (!$addToProperty) { + $columnName = $dataColumns[$tableName][$propertyName] ?? null; + $addToProperty = ($columnName && $this->sql->parseBool($row[$columnName])); + } + + if ($addToProperty) { + $targetArray = $property->getValue($targetEntity); + $targetArray[$entityToAdd->getId()] = $entityToAdd; + $property->setValue($targetEntity, $targetArray); + } + } + } + } + } + } + public function getSelectQuery(): Select { return $this->sql->select(...$this->getColumnNames()) ->from($this->tableName); @@ -259,11 +502,14 @@ class DatabaseEntityHandler { ->first() ->execute(); - if ($res === false || $res === null) { - return $res; - } else { - return $this->entityFromRow($res); + if ($res !== false && $res !== null) { + $res = $this->entityFromRow($res); + if ($res instanceof DatabaseEntity) { + $this->fetchNMRelations([$res->getId() => $res]); + } } + + return $res; } public function fetchMultiple(?Condition $condition = null): ?array { @@ -284,12 +530,56 @@ class DatabaseEntityHandler { $entities[$entity->getId()] = $entity; } } + + $this->fetchNMRelations($entities); return $entities; } } - public function getTableQuery(): CreateTable { - $query = $this->sql->createTable($this->tableName) + public function getCreateQueries(SQL $sql): array { + + $queries = []; + $queries[] = $this->getTableQuery($sql); + + $table = $this->getTableName(); + $entityLogConfig = $this->entityClass->getProperty("entityLogConfig"); + $entityLogConfig->setAccessible(true); + $entityLogConfig = $entityLogConfig->getValue(); + + if (isset($entityLogConfig["insert"]) && $entityLogConfig["insert"] === true) { + $queries[] = $sql->createTrigger("${table}_trg_insert") + ->after()->insert($table) + ->exec(new CreateProcedure($sql, "InsertEntityLog"), [ + "tableName" => new CurrentTable(), + "entityId" => new CurrentColumn("id"), + "lifetime" => $entityLogConfig["lifetime"] ?? 90, + ]); + } + + if (isset($entityLogConfig["update"]) && $entityLogConfig["update"] === true) { + $queries[] = $sql->createTrigger("${table}_trg_update") + ->after()->update($table) + ->exec(new CreateProcedure($sql, "UpdateEntityLog"), [ + "tableName" => new CurrentTable(), + "entityId" => new CurrentColumn("id"), + ]); + } + + if (isset($entityLogConfig["delete"]) && $entityLogConfig["delete"] === true) { + $queries[] = $sql->createTrigger("${table}_trg_delete") + ->after()->delete($table) + ->exec(new CreateProcedure($sql, "DeleteEntityLog"), [ + "tableName" => new CurrentTable(), + "entityId" => new CurrentColumn("id"), + ]); + } + + + return $queries; + } + + public function getTableQuery(SQL $sql): CreateTable { + $query = $sql->createTable($this->tableName) ->onlyIfNotExists() ->addSerial("id") ->primaryKey("id"); @@ -305,12 +595,7 @@ class DatabaseEntityHandler { return $query; } - public function createTable(): bool { - $query = $this->getTableQuery(); - return $query->execute(); - } - - private function prepareRow(DatabaseEntity $entity, string $action, ?array $columns = null) { + private function prepareRow(DatabaseEntity $entity, string $action, ?array $columns = null): bool|array { $row = []; foreach ($this->columns as $propertyName => $column) { if ($columns && !in_array($column->getName(), $columns)) { @@ -344,7 +629,7 @@ class DatabaseEntityHandler { return $row; } - public function update(DatabaseEntity $entity, ?array $columns = null) { + public function update(DatabaseEntity $entity, ?array $columns = null, bool $saveNM = false) { $row = $this->prepareRow($entity, "update", $columns); if ($row === false) { return false; @@ -358,10 +643,15 @@ class DatabaseEntityHandler { $query->set($columnName, $value); } - return $query->execute(); + $res = $query->execute(); + if ($res && $saveNM) { + $res = $this->updateNM($entity); + } + + return $res; } - public function insert(DatabaseEntity $entity) { + public function insert(DatabaseEntity $entity): bool|int { $row = $this->prepareRow($entity, "insert"); if ($row === false) { return false; @@ -391,12 +681,12 @@ class DatabaseEntityHandler { } } - public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) { + public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null, bool $saveNM = false) { $id = $entity->getId(); if ($id === null) { return $this->insert($entity); } else { - return $this->update($entity, $columns); + return $this->update($entity, $columns, $saveNM); } } @@ -415,4 +705,30 @@ class DatabaseEntityHandler { public function getSQL(): SQL { return $this->sql; } + + public function getInsertQuery(DatabaseEntity|array $entities): ?Insert { + + if (empty($entities)) { + return null; + } + + $firstEntity = (is_array($entities) ? current($entities) : $entities); + $firstRow = $this->prepareRow($firstEntity, "insert"); + + $statement = $this->sql->insert($this->tableName, array_keys($firstRow)) + ->addRow(...array_values($firstRow)); + + if (is_array($entities)) { + foreach ($entities as $entity) { + if ($entity === $firstEntity) { + continue; + } + + $row = $this->prepareRow($entity, "insert"); + $statement->addRow(...array_values($row)); + } + } + + return $statement; + } } \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php similarity index 83% rename from Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php rename to Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php index 842372c..add000e 100644 --- a/Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php @@ -1,6 +1,6 @@ handler = $handler; $this->selectQuery = $handler->getSelectQuery(); $this->logger = new Logger("DB-EntityQuery", $handler->getSQL()); $this->resultType = $resultType; $this->logVerbose = false; + $this->fetchSubEntities = self::FETCH_NONE; if ($this->resultType === SQL::FETCH_ONE) { $this->selectQuery->first(); @@ -50,6 +57,11 @@ class DatabaseEntityQuery { return $this; } + public function offset(int $offset): static { + $this->selectQuery->offset($offset); + return $this; + } + public function where(Condition ...$condition): DatabaseEntityQuery { $this->selectQuery->where(...$condition); return $this; @@ -74,6 +86,7 @@ class DatabaseEntityQuery { public function fetchEntities(bool $recursive = false): DatabaseEntityQuery { // $this->selectQuery->dump(); + $this->fetchSubEntities = ($recursive ? self::FETCH_RECURSIVE : self::FETCH_DIRECT); $relIndex = 1; foreach ($this->handler->getRelations() as $propertyName => $relationHandler) { @@ -136,9 +149,19 @@ class DatabaseEntityQuery { $entities[$entity->getId()] = $entity; } } + + if ($this->fetchSubEntities !== self::FETCH_NONE) { + $this->handler->fetchNMRelations($entities, $this->fetchSubEntities === self::FETCH_RECURSIVE); + } + return $entities; } else if ($this->resultType === SQL::FETCH_ONE) { - return $this->handler->entityFromRow($res); + $entity = $this->handler->entityFromRow($res); + if ($entity instanceof DatabaseEntity && $this->fetchSubEntities !== self::FETCH_NONE) { + $this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities === self::FETCH_RECURSIVE); + } + + return $entity; } else { $this->handler->getLogger()->error("Invalid result type for query builder, must be FETCH_ALL or FETCH_ONE"); return null; @@ -149,4 +172,8 @@ class DatabaseEntityQuery { $this->selectQuery->addJoin($join); return $this; } + + public function getQuery(): Select { + return $this->selectQuery; + } } \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php b/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php new file mode 100644 index 0000000..d85a188 --- /dev/null +++ b/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php @@ -0,0 +1,134 @@ +handlerA = $handlerA; + $this->handlerB = $handlerB; + $tableNameA = $handlerA->getTableName(); + $tableNameB = $handlerB->getTableName(); + if ($tableNameA === $tableNameB) { + throw new \Exception("Cannot create N:M Relation with only one table"); + } + + $this->properties = [ + $tableNameA => [], + $tableNameB => [], + ]; + } + + public function addProperty(DatabaseEntityHandler $src, \ReflectionProperty $property): void { + $this->properties[$src->getTableName()][$property->getName()] = $property; + } + + public function getIdColumn(DatabaseEntityHandler $handler): string { + return DatabaseEntityHandler::getColumnName($handler->getTableName()) . "_id"; + } + + public function getDataColumns(): array { + + $referenceCount = 0; + $columnsNeeded = false; + + // if in one of the relations we have multiple references, we need to differentiate + foreach ($this->properties as $refProperties) { + $referenceCount += count($refProperties); + if ($referenceCount > 1) { + $columnsNeeded = true; + break; + } + } + + $columns = []; + if ($columnsNeeded) { + foreach ($this->properties as $tableName => $properties) { + $columns[$tableName] = []; + foreach ($properties as $property) { + $columnName = DatabaseEntityHandler::getColumnName($tableName) . "_" . + DatabaseEntityHandler::getColumnName($property->getName()); + $columns[$tableName][$property->getName()] = $columnName; + } + } + } + + return $columns; + } + + public function getAllColumns(): array { + $relIdA = $this->getIdColumn($this->handlerA); + $relIdB = $this->getIdColumn($this->handlerB); + + $columns = [$relIdA, $relIdB]; + + foreach ($this->getDataColumns() as $dataColumns) { + foreach ($dataColumns as $columnName) { + $columns[] = $columnName; + } + } + + return $columns; + } + + public function getTableQuery(SQL $sql): CreateTable { + + $tableNameA = $this->handlerA->getTableName(); + $tableNameB = $this->handlerB->getTableName(); + + $columns = $this->getAllColumns(); + list ($relIdA, $relIdB) = $columns; + $dataColumns = array_slice($columns, 2); + $query = $sql->createTable(self::buildTableName($tableNameA, $tableNameB)) + ->addInt($relIdA) + ->addInt($relIdB) + ->foreignKey($relIdA, $tableNameA, "id", new CascadeStrategy()) + ->foreignKey($relIdB, $tableNameB, "id", new CascadeStrategy()); + + foreach ($dataColumns as $dataColumn) { + $query->addBool($dataColumn, false); + } + + $query->unique(...$columns); + return $query; + } + + public static function buildTableName(string ...$tables): string { + sort($tables); + return "NM_" . implode("_", $tables); + } + + public function dependsOn(): array { + return [$this->handlerA->getTableName(), $this->handlerB->getTableName()]; + } + + public function getTableName(): string { + return self::buildTableName(...$this->dependsOn()); + } + + public function getCreateQueries(SQL $sql): array { + return [$this->getTableQuery($sql)]; + } + + public function getProperties(DatabaseEntityHandler $handler): array { + return $this->properties[$handler->getTableName()]; + } + + public function getOtherHandler(DatabaseEntityHandler $handler): DatabaseEntityHandler { + if ($handler === $this->handlerA) { + return $this->handlerB; + } else { + return $this->handlerA; + } + } +} \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Controller/Persistable.interface.php b/Core/Objects/DatabaseEntity/Controller/Persistable.interface.php new file mode 100644 index 0000000..39394f7 --- /dev/null +++ b/Core/Objects/DatabaseEntity/Controller/Persistable.interface.php @@ -0,0 +1,13 @@ + "Administrator", + self::MODERATOR => "Moderator", + self::SUPPORT => "Support", + ]; + #[MaxLength(32)] public string $name; #[MaxLength(10)] public string $color; - public function __construct(?int $id = null) { + public function __construct(?int $id, string $name, string $color) { parent::__construct($id); + $this->name = $name; + $this->color = $color; } public function jsonSerialize(): array { diff --git a/Core/Objects/DatabaseEntity/Language.class.php b/Core/Objects/DatabaseEntity/Language.class.php index 23a7a4f..d8cc7e2 100644 --- a/Core/Objects/DatabaseEntity/Language.class.php +++ b/Core/Objects/DatabaseEntity/Language.class.php @@ -2,14 +2,17 @@ namespace Core\Objects\DatabaseEntity { - use Core\Driver\SQL\SQL; use Core\Objects\DatabaseEntity\Attribute\MaxLength; use Core\Objects\DatabaseEntity\Attribute\Transient; use Core\Objects\lang\LanguageModule; + use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; // TODO: language from cookie? class Language extends DatabaseEntity { + const AMERICAN_ENGLISH = 1; + const GERMAN_STANDARD = 2; + const LANG_CODE_PATTERN = "/^[a-zA-Z]{2}_[a-zA-Z]{2}$/"; #[MaxLength(5)] private string $code; diff --git a/Core/Objects/DatabaseEntity/MailQueueItem.class.php b/Core/Objects/DatabaseEntity/MailQueueItem.class.php new file mode 100644 index 0000000..5abc827 --- /dev/null +++ b/Core/Objects/DatabaseEntity/MailQueueItem.class.php @@ -0,0 +1,124 @@ + true, + "delete" => true, + "insert" => true, + "lifetime" => 30 + ]; + + const STATUS_WAITING = "waiting"; + const STATUS_SUCCESS = "success"; + const STATUS_ERROR = "error"; + const STATUS_ITEMS = [self::STATUS_WAITING, self::STATUS_SUCCESS, self::STATUS_ERROR]; + + #[MaxLength(64)] + private string $from; + + #[MaxLength(64)] + private string $to; + + private string $subject; + private string $body; + + #[MaxLength(64)] + private ?string $replyTo; + + #[MaxLength(64)] + private ?string $replyName; + + #[MaxLength(64)] + private ?string $gpgFingerprint; + + #[EnumArr(self::STATUS_ITEMS)] + #[DefaultValue(self::STATUS_WAITING)] + private string $status; + + #[DefaultValue(5)] + private int $retryCount; + + #[DefaultValue(CurrentTimeStamp::class)] + private \DateTime $nextTry; + + private ?string $errorMessage; + + public function __construct(string $fromMail, string $toMail, string $subject, string $body, + ?string $replyTo, ?string $replyName, ?string $gpgFingerprint) { + parent::__construct(); + $this->from = $fromMail; + $this->to = $toMail; + $this->subject = $subject; + $this->body = $body; + $this->replyTo = $replyTo; + $this->replyName = $replyName; + $this->gpgFingerprint = $gpgFingerprint; + $this->retryCount = 5; + $this->nextTry = new \DateTime(); + $this->errorMessage = null; + $this->status = self::STATUS_WAITING; + } + + public function jsonSerialize(): array { + return [ + "id" => $this->getId(), + "from" => $this->from, + "to" => $this->to, + "gpgFingerprint" => $this->gpgFingerprint, + "subject" => $this->subject, + "message" => $this->body, + "status" => $this->status, + "reply" => [ + "to" => $this->replyTo, + "name" => $this->replyName, + ], + "retryCount" => $this->retryCount, + "nextTry" => $this->nextTry->getTimestamp(), + "errorMessage" => $this->errorMessage, + ]; + } + + public function send(Context $context): bool { + + $args = [ + "to" => $this->to, + "subject" => $this->subject, + "body" => $this->body, + "replyTo" => $this->replyTo, + "replyName" => $this->replyName, + "gpgFingerprint" => $this->gpgFingerprint, + "async" => false + ]; + + $req = new Send($context); + $success = $req->execute($args); + $this->errorMessage = $req->getLastError(); + + $delay = [0, 720, 360, 60, 30, 1]; + $minutes = $delay[max(0, min(count($delay) - 1, $this->retryCount))]; + if ($this->retryCount > 0) { + $this->retryCount--; + $this->nextTry = (new \DateTime())->modify("+$minutes minute"); + } else if (!$success) { + $this->status = self::STATUS_ERROR; + } + + if ($success) { + $this->status = self::STATUS_SUCCESS; + } + + $this->save($context->getSQL()); + return $success; + } +} \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/News.class.php b/Core/Objects/DatabaseEntity/News.class.php index b0c5f07..cf738b7 100644 --- a/Core/Objects/DatabaseEntity/News.class.php +++ b/Core/Objects/DatabaseEntity/News.class.php @@ -6,6 +6,7 @@ use Core\API\Parameter\Parameter; use Core\Driver\SQL\Expression\CurrentTimeStamp; use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\MaxLength; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class News extends DatabaseEntity { @@ -16,6 +17,7 @@ class News extends DatabaseEntity { public function __construct(?int $id = null) { parent::__construct($id); + $this->publishedAt = new \DateTime(); } public function jsonSerialize(): array { diff --git a/Core/Objects/DatabaseEntity/Notification.class.php b/Core/Objects/DatabaseEntity/Notification.class.php deleted file mode 100644 index 563226b..0000000 --- a/Core/Objects/DatabaseEntity/Notification.class.php +++ /dev/null @@ -1,30 +0,0 @@ - $this->getId(), - "createdAt" => $this->createdAt->format(Parameter::DATE_TIME_FORMAT), - "title" => $this->title, - "message" => $this->message - ]; - } -} \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Session.class.php b/Core/Objects/DatabaseEntity/Session.class.php index 4fae092..663f8db 100644 --- a/Core/Objects/DatabaseEntity/Session.class.php +++ b/Core/Objects/DatabaseEntity/Session.class.php @@ -10,6 +10,7 @@ use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\Json; use Core\Objects\DatabaseEntity\Attribute\MaxLength; use Core\Objects\DatabaseEntity\Attribute\Transient; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class Session extends DatabaseEntity { diff --git a/Core/Objects/DatabaseEntity/SystemLog.class.php b/Core/Objects/DatabaseEntity/SystemLog.class.php index 8e14645..fffb258 100644 --- a/Core/Objects/DatabaseEntity/SystemLog.class.php +++ b/Core/Objects/DatabaseEntity/SystemLog.class.php @@ -7,6 +7,7 @@ use Core\Driver\SQL\Expression\CurrentTimeStamp; use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\Enum; use Core\Objects\DatabaseEntity\Attribute\MaxLength; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class SystemLog extends DatabaseEntity { diff --git a/Core/Objects/DatabaseEntity/TwoFactorToken.class.php b/Core/Objects/DatabaseEntity/TwoFactorToken.class.php index 1351d7a..77c7049 100644 --- a/Core/Objects/DatabaseEntity/TwoFactorToken.class.php +++ b/Core/Objects/DatabaseEntity/TwoFactorToken.class.php @@ -7,6 +7,7 @@ use Core\Objects\DatabaseEntity\Attribute\Enum; use Core\Objects\DatabaseEntity\Attribute\MaxLength; use Core\Objects\TwoFactor\KeyBasedTwoFactorToken; use Core\Objects\TwoFactor\TimeBasedTwoFactorToken; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; abstract class TwoFactorToken extends DatabaseEntity { diff --git a/Core/Objects/DatabaseEntity/User.class.php b/Core/Objects/DatabaseEntity/User.class.php index 4b21130..fd7ff0c 100644 --- a/Core/Objects/DatabaseEntity/User.class.php +++ b/Core/Objects/DatabaseEntity/User.class.php @@ -2,14 +2,13 @@ namespace Core\Objects\DatabaseEntity; -use Core\Driver\SQL\Condition\Compare; use Core\Driver\SQL\Expression\CurrentTimeStamp; -use Core\Driver\SQL\Join; use Core\Driver\SQL\SQL; use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\MaxLength; -use Core\Objects\DatabaseEntity\Attribute\Transient; +use Core\Objects\DatabaseEntity\Attribute\Multiple; use Core\Objects\DatabaseEntity\Attribute\Unique; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class User extends DatabaseEntity { @@ -20,30 +19,16 @@ class User extends DatabaseEntity { #[MaxLength(64)] public ?string $profilePicture; private ?\DateTime $lastOnline; #[DefaultValue(CurrentTimeStamp::class)] public \DateTime $registeredAt; - public bool $confirmed; - #[DefaultValue(1)] public Language $language; + #[DefaultValue(false)] public bool $confirmed; + #[DefaultValue(Language::AMERICAN_ENGLISH)] public Language $language; public ?GpgKey $gpgKey; private ?TwoFactorToken $twoFactorToken; - #[Transient] private array $groups; + #[Multiple(Group::class)] + public array $groups; public function __construct(?int $id = null) { parent::__construct($id); - $this->groups = []; - } - - public function postFetch(SQL $sql, array $row) { - parent::postFetch($sql, $row); - $this->groups = []; - - $groups = Group::findAllBuilder($sql) - ->addJoin(new Join("INNER", "UserGroup", "UserGroup.group_id", "Group.id")) - ->where(new Compare("UserGroup.user_id", $this->id)) - ->execute(); - - if ($groups) { - $this->groups = $groups; - } } public function getUsername(): string { diff --git a/Core/Objects/DatabaseEntity/UserToken.class.php b/Core/Objects/DatabaseEntity/UserToken.class.php index 0c41fff..5afbfaa 100644 --- a/Core/Objects/DatabaseEntity/UserToken.class.php +++ b/Core/Objects/DatabaseEntity/UserToken.class.php @@ -6,6 +6,7 @@ use Core\Driver\SQL\SQL; use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\EnumArr; use Core\Objects\DatabaseEntity\Attribute\MaxLength; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class UserToken extends DatabaseEntity { diff --git a/Core/constants.php b/Core/constants.php index fa49bcf..4de7987 100644 --- a/Core/constants.php +++ b/Core/constants.php @@ -1,26 +1,5 @@ USER_GROUP_MODERATOR_NAME, - USER_GROUP_SUPPORT => USER_GROUP_SUPPORT_NAME, - USER_GROUP_ADMIN => USER_GROUP_ADMIN_NAME, - ); - - return ($groupNames[$index] ?? "Unknown Group"); -} - // adapted from https://www.php.net/manual/en/function.http-response-code.php const HTTP_STATUS_DESCRIPTIONS = [ 100 => 'Continue', diff --git a/Core/core.php b/Core/core.php index 474881a..92b2377 100644 --- a/Core/core.php +++ b/Core/core.php @@ -10,12 +10,12 @@ if (is_file($autoLoad)) { require_once $autoLoad; } -define("WEBBASE_VERSION", "2.2.0"); +define("WEBBASE_VERSION", "2.3.0"); spl_autoload_extensions(".php"); spl_autoload_register(function ($class) { if (!class_exists($class)) { - $suffixes = ["", ".class", ".trait"]; + $suffixes = ["", ".class", ".trait", ".interface"]; foreach ($suffixes as $suffix) { $full_path = WEBROOT . "/" . getClassPath($class, $suffix); if (file_exists($full_path)) { diff --git a/cli.php b/cli.php index fb5cc70..6d5f6c0 100644 --- a/cli.php +++ b/cli.php @@ -94,13 +94,8 @@ function handleDatabase(array $argv) { $action = $argv[2] ?? ""; if ($action === "migrate") { - $class = $argv[3] ?? null; - if (!$class) { - _exit("Usage: cli.php db migrate "); - } - $sql = connectSQL() or die(); - applyPatch($sql, $class); + } else if (in_array($action, ["export", "import", "shell"])) { // database config diff --git a/test/DatabaseEntity.test.php b/test/DatabaseEntity.test.php index 6f68789..5875650 100644 --- a/test/DatabaseEntity.test.php +++ b/test/DatabaseEntity.test.php @@ -4,8 +4,8 @@ use Core\API\Parameter\Parameter; use Core\Driver\SQL\Query\CreateTable; use Core\Driver\SQL\SQL; use Core\Objects\Context; -use Core\Objects\DatabaseEntity\DatabaseEntity; -use Core\Objects\DatabaseEntity\DatabaseEntityHandler; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; +use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler; use Core\Objects\DatabaseEntity\User; class DatabaseEntityTest extends \PHPUnit\Framework\TestCase { @@ -28,8 +28,9 @@ class DatabaseEntityTest extends \PHPUnit\Framework\TestCase { } public function testCreateTable() { - $this->assertInstanceOf(CreateTable::class, self::$HANDLER->getTableQuery()); - $this->assertTrue(self::$HANDLER->createTable()); + $query = self::$HANDLER->getTableQuery(self::$CONTEXT->getSQL()); + $this->assertInstanceOf(CreateTable::class, $query); + $this->assertTrue($query->execute()); } public function testInsertEntity() { diff --git a/test/Parameter.test.php b/test/Parameter.test.php index 269469a..61c165b 100644 --- a/test/Parameter.test.php +++ b/test/Parameter.test.php @@ -56,6 +56,7 @@ class ParameterTest extends \PHPUnit\Framework\TestCase { // optional single value $arrayType = new ArrayType("int_array_single", Parameter::TYPE_INT, true); $this->assertTrue($arrayType->parseParam(1)); + $this->assertEquals([1], $arrayType->value); // mixed values $arrayType = new ArrayType("mixed_array", Parameter::TYPE_MIXED); diff --git a/test/TimeBasedTwoFactorToken.test.php b/test/TimeBasedTwoFactorToken.test.php index d81a067..a137530 100644 --- a/test/TimeBasedTwoFactorToken.test.php +++ b/test/TimeBasedTwoFactorToken.test.php @@ -27,17 +27,4 @@ class TimeBasedTwoFactorTokenTest extends PHPUnit\Framework\TestCase { $this->assertEquals($code, $generated, "$code != $generated, at=$seed"); } } - - public function testURL() { - $secret = Base32::encode("12345678901234567890"); - $context = new Context(); - - // $context-> - - $token = new TimeBasedTwoFactorToken($secret); - $siteName = $context->getSettings()->getSiteName(); - $username = $context->getUser()->getUsername(); - $url = $token->getUrl($context); - $this->assertEquals("otpauth://totp/$username?secret=$secret&issuer=$siteName", $url); - } } \ No newline at end of file