From 76c9cc50f4af01f669f9a23515b11af92a1bbc22 Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 15 Jul 2023 02:05:49 +0000 Subject: [PATCH] Rewrote the Changelog code. --- public/_github-callback.php | 29 +- public/manage/changelog/change.php | 153 +++++---- public/manage/changelog/index.php | 29 +- public/manage/changelog/tag.php | 65 ++-- public/manage/changelog/tags.php | 3 +- public/manage/general/emoticon.php | 23 +- public/manage/general/emoticons.php | 6 +- src/AuditLog.php | 14 + src/Changelog/ChangeInfo.php | 88 +++++ src/Changelog/ChangeTagInfo.php | 64 ++++ src/Changelog/Changelog.php | 426 +++++++++++++++++++++++++ src/Changelog/ChangelogChange.php | 256 --------------- src/Changelog/ChangelogChangeTag.php | 88 ----- src/Changelog/ChangelogException.php | 6 - src/Changelog/ChangelogTag.php | 129 -------- src/Emoticons/Emotes.php | 2 +- src/Http/Handlers/AssetsHandler.php | 5 +- src/Http/Handlers/ChangelogHandler.php | 100 ++++-- src/Http/Handlers/Handler.php | 7 +- src/Http/Handlers/HomeHandler.php | 3 +- src/MisuzuContext.php | 50 +-- src/url.php | 10 +- templates/changelog/change.twig | 30 +- templates/changelog/index.twig | 15 +- templates/changelog/macros.twig | 27 +- templates/manage/changelog/change.twig | 23 +- templates/manage/changelog/tag.twig | 17 +- templates/manage/changelog/tags.twig | 2 +- 28 files changed, 963 insertions(+), 707 deletions(-) create mode 100644 src/Changelog/ChangeInfo.php create mode 100644 src/Changelog/ChangeTagInfo.php create mode 100644 src/Changelog/Changelog.php delete mode 100644 src/Changelog/ChangelogChangeTag.php delete mode 100644 src/Changelog/ChangelogException.php delete mode 100644 src/Changelog/ChangelogTag.php diff --git a/public/_github-callback.php b/public/_github-callback.php index 5b05082..140cfa3 100644 --- a/public/_github-callback.php +++ b/public/_github-callback.php @@ -107,9 +107,7 @@ if(!empty($repoInfo['master'])) if($data->ref !== $repoMaster) die('only the master branch is tracked'); -// the actual changelog api sucks ass -$changeCreate = DB::prepare('INSERT INTO `msz_changelog_changes` (`change_log`, `change_text`, `change_action`, `user_id`, `change_created`) VALUES (:log, :text, :action, :user, FROM_UNIXTIME(:created))'); -$changeTag = DB::prepare('REPLACE INTO `msz_changelog_change_tags` VALUES (:change_id, :tag_id)'); +$changelog = $msz->getChangelog(); $tags = $repoInfo['tags'] ?? []; $addresses = $config['addresses'] ?? []; @@ -126,21 +124,14 @@ foreach($data->commits as $commit) { $line = $index === false ? $message : mb_substr($message, 0, $index); $body = trim($index === false ? '' : mb_substr($message, $index + 1)); - $changeCreate->bind('user', $addresses[$commit->author->email] ?? null); - $changeCreate->bind('action', ghcb_changelog_action($line)); - $changeCreate->bind('log', $line); - $changeCreate->bind('text', empty($body) ? null : $body); - $changeCreate->bind('created', max(1, strtotime($commit->timestamp))); - $changeId = $changeCreate->executeGetId(); + $changeInfo = $changelog->createChange( + ghcb_changelog_action($line), + $line, $body, + $addresses[$commit->author->email] ?? null, + max(1, strtotime($commit->timestamp)) + ); - if(!empty($tags) && !empty($changeId)) { - $changeTag->bind('change_id', $changeId); - - foreach($tags as $tag) { - $changeTag->bind('tag_id', $tag); - $changeTag->execute(); - } - } - - unset($changeId, $tag); + if(!empty($tags)) + foreach($tags as $tag) + $changelog->addTagToChange($changeInfo, $tag); } diff --git a/public/manage/changelog/change.php b/public/manage/changelog/change.php index 69f2299..70eb134 100644 --- a/public/manage/changelog/change.php +++ b/public/manage/changelog/change.php @@ -1,11 +1,11 @@ ChangelogChange::ACTION_ADD, 'action_name' => 'Added'], - ['action_id' => ChangelogChange::ACTION_REMOVE, 'action_name' => 'Removed'], - ['action_id' => ChangelogChange::ACTION_UPDATE, 'action_name' => 'Updated'], - ['action_id' => ChangelogChange::ACTION_FIX, 'action_name' => 'Fixed'], - ['action_id' => ChangelogChange::ACTION_IMPORT, 'action_name' => 'Imported'], - ['action_id' => ChangelogChange::ACTION_REVERT, 'action_name' => 'Reverted'], -]); +$changeActions = []; +foreach(Changelog::ACTIONS as $action) + $changeActions[$action] = Changelog::actionText($action); -$changeId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); -$tags = ChangelogTag::all(); +$changelog = $msz->getChangelog(); +$changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); +$loadChangeInfo = fn() => $changelog->getChangeById($changeId, withTags: true); +$changeTags = $changelog->getAllTags(); -if($changeId > 0) +if(empty($changeId)) + $isNew = true; +else try { - $change = ChangelogChange::byId($changeId); - } catch(ChangelogChangeNotFoundException $ex) { + $isNew = false; + $changeInfo = $loadChangeInfo(); + } catch(RuntimeException $ex) { + echo render_error(404); + return; + } + +if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { + if(CSRF::validateRequest()) { + $changelog->deleteChange($changeInfo); + AuditLog::create(AuditLog::CHANGELOG_ENTRY_DELETE, [$changeInfo->getId()]); url_redirect('manage-changelog-changes'); - return; + } else render_error(403); + return; +} + +// make errors not echos lol +while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { + $action = trim((string)filter_input(INPUT_POST, 'cl_action')); + $summary = trim((string)filter_input(INPUT_POST, 'cl_summary')); + $body = trim((string)filter_input(INPUT_POST, 'cl_body')); + $userId = (int)filter_input(INPUT_POST, 'cl_user', FILTER_SANITIZE_NUMBER_INT); + $createdAt = trim((string)filter_input(INPUT_POST, 'cl_created')); + $tags = filter_input(INPUT_POST, 'cl_tags', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); + + if($userId < 1) $userId = null; + else $userId = (string)$userId; + + if(empty($createdAt)) + $createdAt = null; + else { + $createdAt = DateTime::createFromFormat(DateTimeInterface::ATOM, $createdAt . ':00Z'); + if($createdAt->getUnixTimeSeconds() < 0) + $createdAt = null; } -if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - if(!empty($_POST['change']) && is_array($_POST['change'])) { - if(!isset($change)) { - $change = new ChangelogChange; - $isNew = true; + if($isNew) { + $changeInfo = $changelog->createChange($action, $summary, $body, $userId, $createdAt); + } else { + if($action === $changeInfo->getAction()) + $action = null; + if($summary === $changeInfo->getSummary()) + $summary = null; + if($body === $changeInfo->getBody()) + $body = null; + if($createdAt !== null && $createdAt->equals($changeInfo->getCreatedAt())) + $createdAt = null; + $updateUserInfo = $userId !== $changeInfo->getUserId(); + + if($action !== null || $summary !== null || $body !== null || $createdAt !== null || $updateUserInfo) + $changelog->updateChange($changeInfo, $action, $summary, $body, $updateUserInfo, $userId, $createdAt); + } + + $tCurrent = $changeInfo->getTagIds(); + $tApply = $tags; + $tRemove = []; + + foreach($tCurrent as $tag) + if(!in_array($tag, $tApply)) { + $tRemove[] = $tag; + $changelog->removeTagFromChange($changeInfo, $tag); } - $changeUserId = filter_var($_POST['change']['user'], FILTER_SANITIZE_NUMBER_INT); - if($changeUserId === 0) - $changeUser = null; - else - try { - $changeUser = User::byId($changeUserId); - } catch(UserNotFoundException $ex) { - $changeUser = User::getCurrent(); - } + $tCurrent = array_diff($tCurrent, $tRemove); - $change->setHeader($_POST['change']['log']) - ->setBody($_POST['change']['text']) - ->setAction($_POST['change']['action']) - ->setUser($changeUser) - ->save(); - - AuditLog::create( - empty($isNew) - ? AuditLog::CHANGELOG_ENTRY_EDIT - : AuditLog::CHANGELOG_ENTRY_CREATE, - [$change->getId()] - ); - } - - if(isset($change) && !empty($_POST['tags']) && is_array($_POST['tags'])) { - $applyTags = []; - foreach($_POST['tags'] as $tagId) { - if(!ctype_digit($tagId)) - die('Invalid item encountered in roles list.'); - - try { - $applyTags[] = ChangelogTag::byId((int)filter_var($tagId, FILTER_SANITIZE_NUMBER_INT)); - } catch(ChangelogTagNotFoundException $ex) {} + foreach($tApply as $tag) + if(!in_array($tag, $tCurrent)) { + $changelog->addTagToChange($changeInfo, $tag); + $tCurrent[] = $tag; } - $change->setTags($applyTags); - } - if(!empty($isNew)) { - url_redirect('manage-changelog-change', ['change' => $change->getId()]); + AuditLog::create( + $isNew ? AuditLog::CHANGELOG_ENTRY_CREATE : AuditLog::CHANGELOG_ENTRY_EDIT, + [$changeInfo->getId()] + ); + + if($isNew) { + url_redirect('manage-changelog-change', ['change' => $changeInfo->getId()]); return; - } + } else $changeInfo = $loadChangeInfo(); + break; } Template::render('manage.changelog.change', [ - 'change' => $change ?? null, - 'change_tags' => $tags, - 'change_actions' => MANAGE_ACTIONS, + 'change_new' => $isNew, + 'change_info' => $changeInfo ?? null, + 'change_tags' => $changeTags, + 'change_actions' => $changeActions, ]); diff --git a/public/manage/changelog/index.php b/public/manage/changelog/index.php index 022a507..d1aa340 100644 --- a/public/manage/changelog/index.php +++ b/public/manage/changelog/index.php @@ -1,7 +1,6 @@ getChangelog(); +$changelogPagination = new Pagination($changelog->countAllChanges(), 30); if(!$changelogPagination->hasValidOffset()) { echo render_error(404); return; } -$changes = ChangelogChange::all($changelogPagination); +$changeInfos = $changelog->getAllChanges(withTags: true, pagination: $changelogPagination); +$changes = []; +$userInfos = []; + +foreach($changeInfos as $changeInfo) { + $userId = $changeInfo->getUserId(); + + if(array_key_exists($userId, $userInfos)) { + $userInfo = $userInfos[$userId]; + } else { + try { + $userInfo = User::byId($userId); + } catch(UserNotFoundException $ex) { + $userInfo = null; + } + + $userInfos[$userId] = $userInfo; + } + + $changes[] = [ + 'change' => $changeInfo, + 'user' => $userInfo, + ]; +} Template::render('manage.changelog.changes', [ 'changelog_changes' => $changes, diff --git a/public/manage/changelog/tag.php b/public/manage/changelog/tag.php index 497cba0..8375820 100644 --- a/public/manage/changelog/tag.php +++ b/public/manage/changelog/tag.php @@ -1,9 +1,8 @@ getChangelog(); +$tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT); +$loadTagInfo = fn() => $changelog->getTagById($tagId); -if($tagId > 0) +if(empty($tagId)) + $isNew = true; +else try { - $tagInfo = ChangelogTag::byId($tagId); - } catch(ChangelogTagNotFoundException $ex) { - url_redirect('manage-changelog-tags'); + $isNew = false; + $tagInfo = $loadTagInfo(); + } catch(RuntimeException $ex) { + echo render_error(404); return; } -if(!empty($_POST['tag']) && is_array($_POST['tag']) && CSRF::validateRequest()) { - if(!isset($tagInfo)) { - $tagInfo = new ChangelogTag; - $isNew = true; +if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { + if(CSRF::validateRequest()) { + $changelog->deleteTag($tagInfo); + AuditLog::create(AuditLog::CHANGELOG_TAG_DELETE, [$tagInfo->getId()]); + url_redirect('manage-changelog-tags'); + } else render_error(403); + return; +} + +while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { + $name = trim((string)filter_input(INPUT_POST, 'ct_name')); + $description = trim((string)filter_input(INPUT_POST, 'ct_desc')); + $archive = !empty($_POST['ct_archive']); + + if($isNew) { + $tagInfo = $changelog->createTag($name, $description, $archive); + } else { + if($name === $tagInfo->getName()) + $name = null; + if($description === $tagInfo->getDescription()) + $description = null; + if($archive === $tagInfo->isArchived()) + $archive = null; + + if($name !== null || $description !== null || $archive !== null) + $changelog->updateTag($tagInfo, $name, $description, $archive); } - $tagInfo->setName($_POST['tag']['name']) - ->setDescription($_POST['tag']['description']) - ->setArchived(!empty($_POST['tag']['archived'])) - ->save(); - AuditLog::create( - empty($isNew) - ? AuditLog::CHANGELOG_TAG_EDIT - : AuditLog::CHANGELOG_TAG_CREATE, + $isNew ? AuditLog::CHANGELOG_TAG_CREATE : AuditLog::CHANGELOG_TAG_EDIT, [$tagInfo->getId()] ); - if(!empty($isNew)) { + if($isNew) { url_redirect('manage-changelog-tag', ['tag' => $tagInfo->getId()]); return; - } + } else $tagInfo = $loadTagInfo(); + break; } Template::render('manage.changelog.tag', [ - 'edit_tag' => $tagInfo ?? null, + 'tag_new' => $isNew, + 'tag_info' => $tagInfo ?? null, ]); diff --git a/public/manage/changelog/tags.php b/public/manage/changelog/tags.php index 297cea2..669ff52 100644 --- a/public/manage/changelog/tags.php +++ b/public/manage/changelog/tags.php @@ -1,7 +1,6 @@ ChangelogTag::all(), + 'changelog_tags' => $msz->getChangelog()->getAllTags(), ]); diff --git a/public/manage/general/emoticon.php b/public/manage/general/emoticon.php index 97473a8..05f3760 100644 --- a/public/manage/general/emoticon.php +++ b/public/manage/general/emoticon.php @@ -49,7 +49,6 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { if($order == 0) $order = null; - $reload = false; if($isNew) { $emoteInfo = $emotes->createEmote($url, $minRank, $order); } else { @@ -60,10 +59,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { if($url === $emoteInfo->getUrl()) $url = null; - if($order !== null || $minRank !== null || $url !== null) { - $reload = true; + if($order !== null || $minRank !== null || $url !== null) $emotes->updateEmote($emoteInfo, $order, $minRank, $url); - } } $sCurrent = $emoteInfo->getStringsRaw(); @@ -78,9 +75,6 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { $sCurrent = array_diff($sCurrent, $sRemove); - if(!$reload) - $reload = !empty($sRemove) || !empty(array_diff($sApply, $sCurrent)); - foreach($sApply as $string) if(!in_array($string, $sCurrent)) { $checkString = $emotes->checkEmoteString($string); @@ -101,12 +95,15 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { $sCurrent[] = $string; } - if($reload) { - if($isNew) - url_redirect('manage-general-emoticon', ['emote' => $emoteInfo->getId()]); - else - $emoteInfo = $loadEmoteInfo(); - } + AuditLog::create( + $isNew ? AuditLog::EMOTICON_CREATE : AuditLog::EMOTICON_EDIT, + [$emoteInfo->getId()] + ); + + if($isNew) { + url_redirect('manage-general-emoticon', ['emote' => $emoteInfo->getId()]); + return; + } else $emoteInfo = $loadEmoteInfo(); break; } diff --git a/public/manage/general/emoticons.php b/public/manage/general/emoticons.php index c2f2aaa..b2d56ef 100644 --- a/public/manage/general/emoticons.php +++ b/public/manage/general/emoticons.php @@ -25,17 +25,21 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) { if(!empty($_GET['delete'])) { $emotes->deleteEmote($emoteInfo); + AuditLog::create(AuditLog::EMOTICON_DELETE, [$emoteInfo->getId()]); } else { if(isset($_GET['order'])) { $order = filter_input(INPUT_GET, 'order'); $offset = $order === 'i' ? 1 : ($order === 'd' ? -1 : 0); $emotes->updateEmoteOrderOffset($emoteInfo, $offset); + AuditLog::create(AuditLog::EMOTICON_ORDER, [$emoteInfo->getId()]); } if(isset($_GET['alias'])) { $alias = (string)filter_input(INPUT_GET, 'alias'); - if($emotes->checkEmoteString($alias) === '') + if($emotes->checkEmoteString($alias) === '') { $emotes->addEmoteString($emoteInfo, $alias); + AuditLog::create(AuditLog::EMOTICON_ALIAS, [$emoteInfo->getId(), $alias]); + } } } diff --git a/src/AuditLog.php b/src/AuditLog.php index 3388580..912b05e 100644 --- a/src/AuditLog.php +++ b/src/AuditLog.php @@ -21,6 +21,7 @@ class AuditLog { public const CHANGELOG_TAG_REMOVE = 'CHANGELOG_TAG_REMOVE'; public const CHANGELOG_TAG_CREATE = 'CHANGELOG_TAG_CREATE'; public const CHANGELOG_TAG_EDIT = 'CHANGELOG_TAG_EDIT'; + public const CHANGELOG_TAG_DELETE = 'CHANGELOG_TAG_DELETE'; public const CHANGELOG_ACTION_CREATE = 'CHANGELOG_ACTION_CREATE'; public const CHANGELOG_ACTION_EDIT = 'CHANGELOG_ACTION_EDIT'; @@ -51,6 +52,12 @@ class AuditLog { public const CONFIG_UPDATE = 'CONFIG_UPDATE'; public const CONFIG_DELETE = 'CONFIG_DELETE'; + public const EMOTICON_CREATE = 'EMOTICON_CREATE'; + public const EMOTICON_EDIT = 'EMOTICON_EDIT'; + public const EMOTICON_DELETE = 'EMOTICON_DELETE'; + public const EMOTICON_ORDER = 'EMOTICON_ORDER'; + public const EMOTICON_ALIAS = 'EMOTICON_ALIAS'; + public const FORMATS = [ self::PERSONAL_EMAIL_CHANGE => 'Changed e-mail address to %s.', self::PERSONAL_PASSWORD_CHANGE => 'Changed account password.', @@ -66,6 +73,7 @@ class AuditLog { self::CHANGELOG_TAG_REMOVE => 'Removed tag #%2$d from changelog entry #%1$d.', self::CHANGELOG_TAG_CREATE => 'Created new changelog tag #%d.', self::CHANGELOG_TAG_EDIT => 'Edited changelog tag #%d.', + self::CHANGELOG_TAG_DELETE => 'Deleted changelog tag #%d.', self::CHANGELOG_ACTION_CREATE => 'Created new changelog action #%d.', self::CHANGELOG_ACTION_EDIT => 'Edited changelog action #%d.', @@ -95,6 +103,12 @@ class AuditLog { self::CONFIG_CREATE => 'Created config value with name "%s".', self::CONFIG_UPDATE => 'Updated config value with name "%s".', self::CONFIG_DELETE => 'Deleted config value with name "%s".', + + self::EMOTICON_CREATE => 'Created emoticon #%s.', + self::EMOTICON_EDIT => 'Edited emoticon #%s.', + self::EMOTICON_DELETE => 'Deleted emoticon #%s.', + self::EMOTICON_ORDER => 'Changed order of emoticon #%s.', + self::EMOTICON_ALIAS => 'Added alias "%2$s" to emoticon #%1$s.', ]; // Database fields diff --git a/src/Changelog/ChangeInfo.php b/src/Changelog/ChangeInfo.php new file mode 100644 index 0000000..0fb8cc3 --- /dev/null +++ b/src/Changelog/ChangeInfo.php @@ -0,0 +1,88 @@ +id = (string)$result->getInteger(0); + $this->userId = $result->isNull(1) ? null : (string)$result->getInteger(1); + $this->action = $result->getInteger(2); + $this->created = $result->getInteger(3); + $this->summary = $result->getString(4); + $this->body = $result->getString(5); + $this->tags = $tags; + } + + public function getId(): string { + return $this->id; + } + + public function getUserId(): ?string { + return $this->userId; + } + + public function getActionId(): int { + return $this->action; + } + + public function getAction(): string { + return Changelog::convertFromActionId($this->action); + } + + public function getActionText(): string { + return Changelog::actionText($this->action); + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function getDate(): string { + return gmdate('Y-m-d', $this->created); + } + + public function getSummary(): string { + return $this->summary; + } + + public function hasBody(): bool { + return !empty($this->body); + } + + public function getBody(): string { + return $this->body; + } + + public function getCommentsCategoryName(): string { + return sprintf('changelog-date-%s', $this->getDate()); + } + + public function getTags(): array { + return $this->tags; + } + + public function getTagIds(): array { + $ids = []; + foreach($this->tags as $tagInfo) + $ids[] = $tagInfo->getId(); + return $ids; + } + + public function hasTag(ChangeTagInfo|string $infoOrId): bool { + return in_array($infoOrId, $this->tags); + } +} diff --git a/src/Changelog/ChangeTagInfo.php b/src/Changelog/ChangeTagInfo.php new file mode 100644 index 0000000..4d8ddf7 --- /dev/null +++ b/src/Changelog/ChangeTagInfo.php @@ -0,0 +1,64 @@ +id = (string)$result->getInteger(0); + $this->name = $result->getString(1); + $this->description = $result->getString(2); + $this->created = $result->getInteger(3); + $this->archived = $result->getInteger(4); + $this->changes = $result->getInteger(5); + } + + public function getId(): string { + return $this->id; + } + + public function getName(): string { + return $this->name; + } + + public function getDescription(): string { + return $this->description; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function getArchivedTime(): int { + return $this->archived; + } + + public function getArchivedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function isArchived(): bool { + return $this->archived > 0; + } + + public function getChangesCount(): int { + return $this->changes; + } + + public function __toString(): string { + return $this->id; + } +} diff --git a/src/Changelog/Changelog.php b/src/Changelog/Changelog.php new file mode 100644 index 0000000..c5eee52 --- /dev/null +++ b/src/Changelog/Changelog.php @@ -0,0 +1,426 @@ +dbConn = $dbConn; + $this->cache = new DbStatementCache($dbConn); + } + + public static function convertFromActionId(int $actionId): string { + return match($actionId) { + 1 => 'add', + 2 => 'remove', + 3 => 'update', + 4 => 'fix', + 5 => 'import', + 6 => 'revert', + default => 'unknown', + }; + } + + public static function convertToActionId(string $action): int { + return match($action) { + 'add' => 1, + 'remove' => 2, + 'update' => 3, + 'fix' => 4, + 'import' => 5, + 'revert' => 6, + default => 0, + }; + } + + public static function actionText(string|int $action): string { + if(is_int($action)) + $action = self::convertFromActionId($action); + + return match($action) { + 'add' => 'Added', + 'remove' => 'Removed', + 'update' => 'Updated', + 'fix' => 'Fixed', + 'import' => 'Imported', + 'revert' => 'Reverted', + default => 'Changed', + }; + } + + private function readChanges(IDbResult $result, bool $withTags): array { + $changes = []; + + if($withTags) { + while($result->next()) + $changes[] = new ChangeInfo( + $result, + $this->getTagsByChange((string)$result->getInteger(0)) + ); + } else { + while($result->next()) + $changes[] = new ChangeInfo($result); + } + + return $changes; + } + + private function readTags(IDbResult $result): array { + $tags = []; + + while($result->next()) { + $tagId = (string)$result->getInteger(0); + if(array_key_exists($tagId, $this->tags)) + $tags[] = $this->tags[$tagId]; + else + $tags[] = $this->tags[$tagId] = new ChangeTagInfo($result); + } + + return $tags; + } + + public function countAllChanges( + User|string|null $userInfo = null, + DateTime|int|null $dateTime = null, + ?array $tags = null + ): int { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($dateTime instanceof DateTime) + $dateTime = $dateTime->getUnixTimeSeconds(); + + $args = 0; + $hasUserInfo = $userInfo !== null; + $hasDateTime = $dateTime !== null; + $hasTags = !empty($tags); + + $query = 'SELECT COUNT(*) FROM msz_changelog_changes'; + if($hasUserInfo) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' user_id = ?'; + } + if($hasDateTime) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))'; + } + if($hasTags) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= sprintf( + ' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))', + implode(', ', array_fill(0, count($tags), '?')) + ); + } + $stmt = $this->cache->get($query); + + $args = 0; + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasDateTime) + $stmt->addParameter(++$args, $dateTime); + if($hasTags) + foreach($tags as $tag) + $stmt->addParameter(++$args, (string)$tag); + + $stmt->execute(); + $result = $stmt->getResult(); + $count = 0; + + if($result->next()) + $count = $result->getInteger(0); + + return $count; + } + + public function getAllChanges( + bool $withTags = false, + User|string|null $userInfo = null, + DateTime|int|null $dateTime = null, + ?array $tags = null, + ?Pagination $pagination = null + ): array { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($dateTime instanceof DateTime) + $dateTime = $dateTime->getUnixTimeSeconds(); + + $args = 0; + $hasUserInfo = $userInfo !== null; + $hasDateTime = $dateTime !== null; + $hasTags = !empty($tags); + $hasPagination = $pagination !== null; + + $query = 'SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes'; + if($hasUserInfo) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' user_id = ?'; + } + if($hasDateTime) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))'; + } + if($hasTags) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= sprintf( + ' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))', + implode(', ', array_fill(0, count($tags), '?')) + ); + } + $query .= ' GROUP BY change_created, change_id ORDER BY change_created DESC, change_id DESC'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + $stmt = $this->cache->get($query); + + $args = 0; + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasDateTime) + $stmt->addParameter(++$args, $dateTime); + if($hasTags) + foreach($tags as $tag) + $stmt->addParameter(++$args, (string)$tag); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + + $stmt->execute(); + + return self::readChanges($stmt->getResult(), $withTags); + } + + public function getChangeById(string $changeId, bool $withTags = false): ChangeInfo { + $stmt = $this->cache->get('SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes WHERE change_id = ?'); + $stmt->addParameter(1, $changeId); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('No tag with that ID exists.'); + + $tags = []; + if($withTags) + $tags = $this->getTagsByChange((string)$result->getInteger(0)); + + return new ChangeInfo($result, $tags); + } + + public function createChange( + string|int $action, + string $summary, + string $body = '', + User|string|null $userInfo = null, + DateTime|int|null $createdAt = null + ): ChangeInfo { + if(is_string($action)) + $action = self::convertToActionId($action); + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($createdAt instanceof DateTime) + $createdAt = $createdAt->getUnixTimeSeconds(); + + $summary = trim($summary); + if(empty($summary)) + throw new InvalidArgumentException('$summary may not be empty'); + + $body = trim($body); + if(empty($body)) + $body = null; + + $stmt = $this->cache->get('INSERT INTO msz_changelog_changes (user_id, change_action, change_created, change_log, change_text) VALUES (?, ?, FROM_UNIXTIME(?), ?, ?)'); + $stmt->addParameter(1, $userInfo); + $stmt->addParameter(2, $action); + $stmt->addParameter(3, $createdAt); + $stmt->addParameter(4, $summary); + $stmt->addParameter(5, $body); + $stmt->execute(); + + return $this->getChangeById((string)$this->dbConn->getLastInsertId()); + } + + public function deleteChange(ChangeInfo|string $infoOrId): void { + if($infoOrId instanceof ChangeInfo) + $infoOrId = $infoOrId->getId(); + + $stmt = $this->cache->get('DELETE FROM msz_changelog_changes WHERE change_id = ?'); + $stmt->addParameter(1, $infoOrId); + $stmt->execute(); + } + + public function updateChange( + ChangeInfo|string $infoOrId, + string|int|null $action = null, + ?string $summary = null, + ?string $body = null, + bool $updateUserInfo = false, + User|string|null $userInfo = null, + DateTime|int|null $createdAt = null + ): void { + if($infoOrId instanceof ChangeInfo) + $infoOrId = $infoOrId->getId(); + + if(is_string($action)) + $action = self::convertToActionId($action); + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($createdAt instanceof DateTime) + $createdAt = $createdAt->getUnixTimeSeconds(); + + if($summary !== null) { + $summary = trim($summary); + if(empty($summary)) + throw new InvalidArgumentException('$summary may not be empty'); + } + + $hasBody = $body !== null; + if($hasBody) { + $body = trim($body); + if(empty($body)) + $body = null; + } + + $stmt = $this->cache->get('UPDATE msz_changelog_changes SET change_action = COALESCE(?, change_action), change_log = COALESCE(?, change_log), change_text = IF(?, ?, change_text), user_id = IF(?, ?, user_id), change_created = COALESCE(FROM_UNIXTIME(?), change_created) WHERE change_id = ?'); + $stmt->addParameter(1, $action); + $stmt->addParameter(2, $summary); + $stmt->addParameter(3, $hasBody ? 1 : 0); + $stmt->addParameter(4, $body); + $stmt->addParameter(5, $updateUserInfo ? 1 : 0); + $stmt->addParameter(6, $userInfo); + $stmt->addParameter(7, $createdAt); + $stmt->addParameter(8, $infoOrId); + $stmt->execute(); + } + + public function getAllTags(): array { + // only putting the changes count in here for now, it is only used in manage + return $this->readTags( + $this->dbConn->query('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), (SELECT COUNT(*) FROM msz_changelog_change_tags AS ct WHERE ct.tag_id = t.tag_id) AS `tag_changes` FROM msz_changelog_tags AS t') + ); + } + + public function getTagsByChange(ChangeInfo|string $infoOrId): array { + if($infoOrId instanceof ChangeInfo) + $infoOrId = $infoOrId->getId(); + + $stmt = $this->cache->get('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), 0 AS `tag_changes` FROM msz_changelog_tags WHERE tag_id IN (SELECT tag_id FROM msz_changelog_change_tags WHERE change_id = ?)'); + $stmt->addParameter(1, $infoOrId); + $stmt->execute(); + + return $this->readTags($stmt->getResult()); + } + + public function getTagById(string $tagId): ChangeTagInfo { + $stmt = $this->cache->get('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), 0 AS `tag_changes` FROM msz_changelog_tags WHERE tag_id = ?'); + $stmt->addParameter(1, $tagId); + $stmt->execute(); + + $result = $stmt->getResult(); + if(!$result->next()) + throw new RuntimeException('No tag with that ID exists.'); + + return new ChangeTagInfo($result); + } + + public function createTag( + string $name, + string $description, + bool $archived + ): ChangeTagInfo { + $name = trim($name); + if(empty($name)) + throw new InvalidArgumentException('$name may not be empty'); + + $description = trim($description); + if(empty($description)) + $description = null; + + $stmt = $this->cache->get('INSERT INTO msz_changelog_tags (tag_name, tag_description, tag_archived) VALUES (?, ?, IF(?, NOW(), NULL))'); + $stmt->addParameter(1, $name); + $stmt->addParameter(2, $description); + $stmt->addParameter(3, $archived ? 1 : 0); + $stmt->execute(); + + return $this->getTagById((string)$this->dbConn->getLastInsertId()); + } + + public function deleteTag(ChangeTagInfo|string $infoOrId): void { + if($infoOrId instanceof ChangeTagInfo) + $infoOrId = $infoOrId->getId(); + + $stmt = $this->cache->get('DELETE FROM msz_changelog_tags WHERE tag_id = ?'); + $stmt->addParameter(1, $infoOrId); + $stmt->execute(); + } + + public function updateTag( + ChangeTagInfo|string $infoOrId, + ?string $name = null, + ?string $description = null, + ?bool $archived = null + ): void { + if($infoOrId instanceof ChangeTagInfo) + $infoOrId = $infoOrId->getId(); + + if($name !== null) { + $name = trim($name); + if(empty($name)) + throw new InvalidArgumentException('$name may not be empty'); + } + + $hasDescription = $description !== null; + if($hasDescription) { + $description = trim($description); + if(empty($description)) + $description = null; + } + + $hasArchived = $archived !== null; + + $stmt = $this->cache->get('UPDATE msz_changelog_tags SET tag_name = COALESCE(?, tag_name), tag_description = IF(?, ?, tag_description), tag_archived = IF(?, IF(?, NOW(), NULL), tag_archived) WHERE tag_id = ?'); + $stmt->addParameter(1, $name); + $stmt->addParameter(2, $hasDescription ? 1 : 0); + $stmt->addParameter(3, $description); + $stmt->addParameter(4, $hasArchived ? 1 : 0); + $stmt->addParameter(5, $archived ? 1 : 0); + $stmt->addParameter(6, $infoOrId); + $stmt->execute(); + } + + public function addTagToChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void { + if($change instanceof ChangeInfo) + $change = $change->getId(); + if($tag instanceof ChangeTagInfo) + $tag = $tag->getId(); + + $stmt = $this->cache->get('INSERT INTO msz_changelog_change_tags (change_id, tag_id) VALUES (?, ?)'); + $stmt->addParameter(1, $change); + $stmt->addParameter(2, $tag); + $stmt->execute(); + } + + public function removeTagFromChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void { + if($change instanceof ChangeInfo) + $change = $change->getId(); + if($tag instanceof ChangeTagInfo) + $tag = $tag->getId(); + + $stmt = $this->cache->get('DELETE FROM msz_changelog_change_tags WHERE change_id = ? AND tag_id = ?'); + $stmt->addParameter(1, $change); + $stmt->addParameter(2, $tag); + $stmt->execute(); + } +} diff --git a/src/Changelog/ChangelogChange.php b/src/Changelog/ChangelogChange.php index a06e60e..74ba9a4 100644 --- a/src/Changelog/ChangelogChange.php +++ b/src/Changelog/ChangelogChange.php @@ -1,19 +1,6 @@ ['unknown', 'Changed'], - self::ACTION_ADD => ['add', 'Added'], - self::ACTION_REMOVE => ['remove', 'Removed'], - self::ACTION_UPDATE => ['update', 'Updated'], - self::ACTION_FIX => ['fix', 'Fixed'], - self::ACTION_IMPORT => ['import', 'Imported'], - self::ACTION_REVERT => ['revert', 'Reverted'], - ]; - - public const DEFAULT_DATE = '0000-00-00'; - - // Database fields - private $change_id = -1; - private $user_id = null; - private $change_action = null; // defaults null apparently, probably a previous oversight - private $change_created = null; - private $change_log = ''; - private $change_text = ''; - - private $user = null; - private $userLookedUp = false; - private $comments = null; - private $tags = null; - private $tagRelations = null; - - public const TABLE = 'changelog_changes'; - private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE; - private const SELECT = '%1$s.`change_id`, %1$s.`user_id`, %1$s.`change_action`, %1$s.`change_log`, %1$s.`change_text`' - . ', UNIX_TIMESTAMP(%1$s.`change_created`) AS `change_created`'; - - public function getId(): int { - return $this->change_id < 1 ? -1 : $this->change_id; - } - - public function getUserId(): int { - return $this->user_id < 1 ? -1 : $this->user_id; - } - public function setUserId(?int $userId): self { - $this->user_id = $userId; - $this->userLookedUp = false; - $this->user = null; - return $this; - } - public function getUser(): ?User { - if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) { - $this->userLookedUp = true; - try { - $this->user = User::byId($userId); - } catch(UserNotFoundException $ex) {} - } - return $this->user; - } - public function setUser(?User $user): self { - $this->user_id = $user === null ? null : $user->getId(); - $this->userLookedUp = true; - $this->user = $user; - return $this; - } - - public function getAction(): int { - return $this->change_action ?? self::ACTION_UNKNOWN; - } - public function setAction(int $actionId): self { - $this->change_action = $actionId; - return $this; - } - private function getActionInfo(): array { - return self::ACTION_STRINGS[$this->getAction()] ?? self::ACTION_STRINGS[self::ACTION_UNKNOWN]; - } - public function getActionClass(): string { - return $this->getActionInfo()[0]; - } - public function getActionString(): string { - return $this->getActionInfo()[1]; - } - - public function getCreatedTime(): int { - return $this->change_created ?? -1; - } - public function getDate(): string { - return ($time = $this->getCreatedTime()) < 0 ? self::DEFAULT_DATE : gmdate('Y-m-d', $time); - } - - public function getHeader(): string { - return $this->change_log; - } - public function setHeader(string $header): self { - $this->change_log = $header; - return $this; - } - - public function getBody(): string { - return $this->change_text ?? ''; - } - public function setBody(string $body): self { - $this->change_text = $body; - return $this; - } - public function hasBody(): bool { - return !empty($this->change_text); - } - public function getParsedBody(): string { - return Parser::instance(Parser::MARKDOWN)->parseText($this->getBody()); - } - - public function getCommentsCategoryName(): ?string { - return ($date = $this->getDate()) === self::DEFAULT_DATE ? null : sprintf('changelog-date-%s', $this->getDate()); - } - public function hasCommentsCategory(): bool { - return $this->getCreatedTime() >= 0; - } - public function getCommentsCategory(): CommentsCategory { - if($this->comments === null) { - $categoryName = $this->getCommentsCategoryName(); - - if(empty($categoryName)) - throw new UnexpectedValueException('Change comments category name is empty.'); - - try { - $this->comments = CommentsCategory::byName($categoryName); - } catch(CommentsCategoryNotFoundException $ex) { - $this->comments = new CommentsCategory($categoryName); - $this->comments->save(); - } - } - return $this->comments; - } - - public function getTags(): array { - if($this->tags === null) - $this->tags = ChangelogTag::byChange($this); - return $this->tags; - } - public function getTagRelations(): array { - if($this->tagRelations === null) - $this->tagRelations = ChangelogChangeTag::byChange($this); - return $this->tagRelations; - } - public function setTags(array $tags): self { - ChangelogChangeTag::purgeChange($this); - foreach($tags as $tag) - if($tag instanceof ChangelogTag) - ChangelogChangeTag::create($this, $tag); - $this->tags = $tags; - $this->tagRelations = null; - return $this; - } - public function hasTag(ChangelogTag $other): bool { - foreach($this->getTags() as $tag) - if($tag->compare($other)) - return true; - return false; - } - - public function save(): void { - $isInsert = $this->getId() < 1; - if($isInsert) { - $query = 'INSERT INTO `%1$s%2$s` (`user_id`, `change_action`, `change_log`, `change_text`)' - . ' VALUES (:user, :action, :header, :body)'; - } else { - $query = 'UPDATE `%1$s%2$s` SET `user_id` = :user, `change_action` = :action, `change_log` = :header, `change_text` = :body' - . ' WHERE `change_id` = :change'; - } - - $saveChange = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE)) - ->bind('user', $this->user_id) - ->bind('action', $this->change_action) - ->bind('header', $this->change_log) - ->bind('body', $this->change_text); - - if($isInsert) { - $this->change_id = $saveChange->executeGetId(); - $this->change_created = time(); - } else { - $saveChange->bind('change', $this->getId()) - ->execute(); - } - } - - private static function countQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`change_id`)', self::TABLE)); - } - public static function countAll(?int $date = null, ?User $user = null): int { - $countChanges = DB::prepare( - self::countQueryBase() - . ' WHERE 1' // this is still disgusting - . ($date === null ? '' : ' AND DATE(`change_created`) = :date') - . ($user === null ? '' : ' AND `user_id` = :user') - ); - if($date !== null) - $countChanges->bind('date', gmdate('Y-m-d', $date)); - if($user !== null) - $countChanges->bind('user', $user->getId()); - return (int)$countChanges->fetchColumn(); - } - - private static function memoizer(): Memoizer { - static $memoizer = null; - if($memoizer === null) - $memoizer = new Memoizer; - return $memoizer; - } - - private static function byQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); - } - public static function byId(int $changeId): self { - return self::memoizer()->find($changeId, function() use ($changeId) { - $change = DB::prepare(self::byQueryBase() . ' WHERE `change_id` = :change') - ->bind('change', $changeId) - ->fetchObject(self::class); - if(!$change) - throw new ChangelogChangeNotFoundException; - return $change; - }); - } - public static function all(?Pagination $pagination = null, ?int $date = null, ?User $user = null): array { - $changeQuery = self::byQueryBase() - . ' WHERE 1' // this is still disgusting - . ($date === null ? '' : ' AND DATE(`change_created`) = :date') - . ($user === null ? '' : ' AND `user_id` = :user') - . ' GROUP BY `change_created`, `change_id`' - . ' ORDER BY `change_created` DESC, `change_id` DESC'; - - if($pagination !== null) - $changeQuery .= ' LIMIT :range OFFSET :offset'; - - $getChanges = DB::prepare($changeQuery); - - if($date !== null) - $getChanges->bind('date', gmdate('Y-m-d', $date)); - - if($user !== null) - $getChanges->bind('user', $user->getId()); - - if($pagination !== null) - $getChanges->bind('range', $pagination->getRange()) - ->bind('offset', $pagination->getOffset()); - - return $getChanges->fetchObjects(self::class); - } } diff --git a/src/Changelog/ChangelogChangeTag.php b/src/Changelog/ChangelogChangeTag.php deleted file mode 100644 index f092f4c..0000000 --- a/src/Changelog/ChangelogChangeTag.php +++ /dev/null @@ -1,88 +0,0 @@ -change_id < 1 ? -1 : $this->change_id; - } - public function getChange(): ChangelogChange { - if($this->change === null) - $this->change = ChangelogChange::byId($this->getChangeId()); - return $this->change; - } - - public function getTagId(): int { - return $this->tag_id < 1 ? -1 : $this->tag_id; - } - public function getTag(): ChangelogTag { - if($this->tag === null) - $this->tag = ChangelogTag::byId($this->getTagId()); - return $this->tag; - } - - public static function create(ChangelogChange $change, ChangelogTag $tag, bool $return = false): ?self { - $createRelation = DB::prepare( - 'REPLACE INTO `' . DB::PREFIX . self::TABLE . '` (`change_id`, `tag_id`)' - . ' VALUES (:change, :tag)' - )->bind('change', $change->getId())->bind('tag', $tag->getId()); - - if(!$createRelation->execute()) - throw new ChangelogChangeCreationFailedException; - if(!$return) - return null; - - return self::byExact($change, $tag); - } - - public static function purgeChange(ChangelogChange $change): void { - DB::prepare( - 'DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `change_id` = :change' - )->bind('change', $change->getId())->execute(); - } - - private static function countQueryBase(string $column): string { - return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`%s`)', self::TABLE, $column)); - } - public static function countByTag(ChangelogTag $tag): int { - return (int)DB::prepare( - self::countQueryBase('change_id') - . ' WHERE `tag_id` = :tag' - )->bind('tag', $tag->getId())->fetchColumn(); - } - - private static function byQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); - } - public static function byExact(ChangelogChange $change, ChangelogTag $tag): self { - $tag = DB::prepare(self::byQueryBase() . ' WHERE `tag_id` = :tag') - ->bind('change', $change->getId()) - ->bind('tag', $tag->getId()) - ->fetchObject(self::class); - if(!$tag) - throw new ChangelogChangeTagNotFoundException; - return $tag; - } - public static function byChange(ChangelogChange $change): array { - return DB::prepare( - self::byQueryBase() - . ' WHERE `change_id` = :change' - )->bind('change', $change->getId())->fetchObjects(self::class); - } -} diff --git a/src/Changelog/ChangelogException.php b/src/Changelog/ChangelogException.php deleted file mode 100644 index d7fe4ed..0000000 --- a/src/Changelog/ChangelogException.php +++ /dev/null @@ -1,6 +0,0 @@ -tag_id < 1 ? -1 : $this->tag_id; - } - - public function getName(): string { - return $this->tag_name; - } - public function setName(string $name): self { - $this->tag_name = $name; - return $this; - } - - public function getDescription(): string { - return $this->tag_description; - } - public function hasDescription(): bool { - return !empty($this->tag_description); - } - public function setDescription(string $description): self { - $this->tag_description = $description; - return $this; - } - - public function getCreatedTime(): int { - return $this->tag_created ?? -1; - } - - public function getArchivedTime(): int { - return $this->tag_archived ?? -1; - } - public function isArchived(): bool { - return $this->getArchivedTime() >= 0; - } - public function setArchived(bool $archived): self { - if($this->isArchived() !== $archived) - $this->tag_archived = $archived ? time() : null; - return $this; - } - - public function getChangeCount(): int { - if($this->changeCount < 0) - $this->changeCount = ChangelogChangeTag::countByTag($this); - return $this->changeCount; - } - - public function compare(ChangelogTag $other): bool { - return $other === $this || $other->getId() === $this->getId(); - } - - public function save(): void { - $isInsert = $this->getId() < 1; - if($isInsert) { - $query = 'INSERT INTO `%1$s%2$s` (`tag_name`, `tag_description`, `tag_archived`)' - . ' VALUES (:name, :description, FROM_UNIXTIME(:archived))'; - } else { - $query = 'UPDATE `%1$s%2$s` SET `tag_name` = :name, `tag_description` = :description, `tag_archived` = FROM_UNIXTIME(:archived)' - . ' WHERE `tag_id` = :tag'; - } - - $saveTag = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE)) - ->bind('name', $this->tag_name) - ->bind('description', $this->tag_description) - ->bind('archived', $this->tag_archived); - - if($isInsert) { - $this->tag_id = $saveTag->executeGetId(); - $this->tag_created = time(); - } else { - $saveTag->bind('tag', $this->getId()) - ->execute(); - } - } - - private static function memoizer(): Memoizer { - static $memoizer = null; - if($memoizer === null) - $memoizer = new Memoizer; - return $memoizer; - } - - private static function byQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); - } - public static function byId(int $tagId): self { - return self::memoizer()->find($tagId, function() use ($tagId) { - $tag = DB::prepare(self::byQueryBase() . ' WHERE `tag_id` = :tag') - ->bind('tag', $tagId) - ->fetchObject(self::class); - if(!$tag) - throw new ChangelogTagNotFoundException; - return $tag; - }); - } - public static function byChange(ChangelogChange $change): array { - return DB::prepare( - self::byQueryBase() - . ' WHERE `tag_id` IN (SELECT `tag_id` FROM `' . DB::PREFIX . ChangelogChangeTag::TABLE . '` WHERE `change_id` = :change)' - )->bind('change', $change->getId())->fetchObjects(self::class); - } - public static function all(): array { - return DB::prepare(self::byQueryBase()) - ->fetchObjects(self::class); - } -} diff --git a/src/Emoticons/Emotes.php b/src/Emoticons/Emotes.php index 6d50cdf..63b18c2 100644 --- a/src/Emoticons/Emotes.php +++ b/src/Emoticons/Emotes.php @@ -28,7 +28,7 @@ class Emotes { $result = $stmt->getResult(); if(!$result->next()) - throw new RuntimeException('No emoticon with ID exists.'); + throw new RuntimeException('No emoticon with that ID exists.'); $strings = $withStrings ? $this->getEmoteStrings($emoteId) : []; diff --git a/src/Http/Handlers/AssetsHandler.php b/src/Http/Handlers/AssetsHandler.php index 1ecc07c..9b592f8 100644 --- a/src/Http/Handlers/AssetsHandler.php +++ b/src/Http/Handlers/AssetsHandler.php @@ -2,6 +2,7 @@ namespace Misuzu\Http\Handlers; use Misuzu\GitInfo; +use Misuzu\MisuzuContext; use Misuzu\Users\User; use Misuzu\Users\UserNotFoundException; use Misuzu\Users\Assets\StaticUserImageAsset; @@ -20,9 +21,9 @@ final class AssetsHandler extends Handler { ], ]; - public function __construct() { + public function __construct(MisuzuContext $context) { $GLOBALS['misuzuBypassLockdown'] = true; - parent::__construct(); + parent::__construct($context); } private static function recurse(string $dir): string { diff --git a/src/Http/Handlers/ChangelogHandler.php b/src/Http/Handlers/ChangelogHandler.php index 103e996..68de81e 100644 --- a/src/Http/Handlers/ChangelogHandler.php +++ b/src/Http/Handlers/ChangelogHandler.php @@ -2,14 +2,13 @@ namespace Misuzu\Http\Handlers; use ErrorException; +use RuntimeException; use Misuzu\Config; use Misuzu\Config\IConfig; use Misuzu\Pagination; use Misuzu\Template; -use Misuzu\Changelog\ChangelogChange; -use Misuzu\Changelog\ChangelogChangeNotFoundException; -use Misuzu\Changelog\ChangelogTag; -use Misuzu\Changelog\ChangelogTagNotFoundException; +use Misuzu\Comments\CommentsCategory; +use Misuzu\Comments\CommentsCategoryNotFoundException; use Misuzu\Feeds\Feed; use Misuzu\Feeds\FeedItem; use Misuzu\Feeds\AtomFeedSerializer; @@ -19,11 +18,13 @@ use Misuzu\Users\UserNotFoundException; class ChangelogHandler extends Handler { public function index($response, $request) { - $filterDate = $request->getParam('date'); - $filterUser = $request->getParam('user', FILTER_SANITIZE_NUMBER_INT); - //$filterTags = $request->getParam('tags'); + $filterDate = (string)$request->getParam('date'); + $filterUser = (int)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT); + $filterTags = (string)$request->getParam('tags'); - if($filterDate !== null) + if(empty($filterDate)) + $filterDate = null; + else try { $dateParts = explode('-', $filterDate, 3); $filterDate = gmmktime(12, 0, 0, (int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]); @@ -31,57 +32,102 @@ class ChangelogHandler extends Handler { return 404; } - if($filterUser !== null) + if($filterUser > 0) try { $filterUser = User::byId($filterUser); } catch(UserNotFoundException $ex) { return 404; } + else + $filterUser = null; - /*if($filterTags !== null) { - $splitTags = explode(',', $filterTags); - $filterTags = []; - for($i = 0; $i < min(10, count($splitTags)); ++$i) - try { - $filterTags[] = ChangelogTag::byId($splitTags[$i]); - } catch(ChangelogTagNotFoundException $ex) { - return 404; - } - }*/ + if(empty($filterTags)) + $filterTags = null; + else { + $filterTags = explode(',', $filterTags); + foreach($filterTags as &$tag) + $tag = trim($tag); + } - $count = $filterDate !== null ? -1 : ChangelogChange::countAll($filterDate, $filterUser); + $changelog = $this->context->getChangelog(); + $count = $changelog->countAllChanges($filterUser, $filterDate, $filterTags); $pagination = new Pagination($count, 30); if(!$pagination->hasValidOffset()) return 404; - $changes = ChangelogChange::all($pagination, $filterDate, $filterUser); - if(empty($changes)) + $changeInfos = $changelog->getAllChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination); + if(empty($changeInfos)) return 404; + $changes = []; + $userInfos = []; + + foreach($changeInfos as $changeInfo) { + $userId = $changeInfo->getUserId(); + + if(array_key_exists($userId, $userInfos)) { + $userInfo = $userInfos[$userId]; + } else { + try { + $userInfo = User::byId($userId); + } catch(UserNotFoundException $ex) { + $userInfo = null; + } + + $userInfos[$userId] = $userInfo; + } + + $changes[] = [ + 'change' => $changeInfo, + 'user' => $userInfo, + ]; + } + $response->setContent(Template::renderRaw('changelog.index', [ 'changelog_infos' => $changes, 'changelog_date' => $filterDate, 'changelog_user' => $filterUser, + 'changelog_tags' => $filterTags, 'changelog_pagination' => $pagination, 'comments_user' => User::getCurrent(), + 'comments_category' => empty($filterDate) ? null : self::getCommentsCategory($changeInfos[0]->getCommentsCategoryName()), ])); } - public function change($response, $request, int $changeId) { + private static function getCommentsCategory(string $categoryName): CommentsCategory { try { - $changeInfo = ChangelogChange::byId($changeId); - } catch(ChangelogChangeNotFoundException $ex) { + $category = CommentsCategory::byName($categoryName); + } catch(CommentsCategoryNotFoundException $ex) { + $category = new CommentsCategory($categoryName); + $category->save(); + } + + return $category; + } + + public function change($response, $request, string $changeId) { + try { + $changeInfo = $this->context->getChangelog()->getChangeById($changeId, withTags: true); + } catch(RuntimeException $ex) { return 404; } + try { + $userInfo = User::byId($changeInfo->getUserId()); + } catch(UserNotFoundException $ex) { + $userInfo = null; + } + $response->setContent(Template::renderRaw('changelog.change', [ 'change_info' => $changeInfo, + 'change_user_info' => $userInfo, 'comments_user' => User::getCurrent(), + 'comments_category' => self::getCommentsCategory($changeInfo->getCommentsCategoryName()), ])); } private function createFeed(string $feedMode): Feed { - $changes = ChangelogChange::all(new Pagination(10)); + $changes = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10)); $feed = (new Feed) ->setTitle(Config::get('site.name', IConfig::T_STR, 'Misuzu') . ' » Changelog') @@ -94,7 +140,7 @@ class ChangelogHandler extends Handler { $commentsUrl = url_prefix(false) . url('changelog-change-comments', ['change' => $change->getId()]); $feedItem = (new FeedItem) - ->setTitle($change->getActionString() . ': ' . $change->getHeader()) + ->setTitle($change->getActionText() . ': ' . $change->getSummary()) ->setCreationDate($change->getCreatedTime()) ->setUniqueId($changeUrl) ->setContentUrl($changeUrl) diff --git a/src/Http/Handlers/Handler.php b/src/Http/Handlers/Handler.php index faff757..cd1a9cf 100644 --- a/src/Http/Handlers/Handler.php +++ b/src/Http/Handlers/Handler.php @@ -1,8 +1,13 @@ context = $context; } } diff --git a/src/Http/Handlers/HomeHandler.php b/src/Http/Handlers/HomeHandler.php index 5bd7389..fb1d4d4 100644 --- a/src/Http/Handlers/HomeHandler.php +++ b/src/Http/Handlers/HomeHandler.php @@ -6,7 +6,6 @@ use Misuzu\Config\IConfig; use Misuzu\DB; use Misuzu\Pagination; use Misuzu\Template; -use Misuzu\Changelog\ChangelogChange; use Misuzu\News\NewsPost; use Misuzu\Users\User; use Misuzu\Users\UserSession; @@ -115,7 +114,7 @@ final class HomeHandler extends Handler { . ' (SELECT COUNT(`post_id`) FROM `msz_forum_posts` WHERE `post_deleted` IS NULL) AS `count_forum_posts`' )->fetch(); - $changelog = ChangelogChange::all(new Pagination(10)); + $changelog = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10)); $birthdays = User::byBirthdate(); $latestUser = !empty($birthdays) ? null : User::byLatest(); diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index 984e221..3ca607f 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -2,6 +2,7 @@ namespace Misuzu; use Misuzu\Template; +use Misuzu\Changelog\Changelog; use Misuzu\Config\IConfig; use Misuzu\Emoticons\Emotes; use Misuzu\SharpChat\SharpChatRoutes; @@ -23,12 +24,14 @@ class MisuzuContext { private Users $users; private HttpFx $router; private Emotes $emotes; + private Changelog $changelog; public function __construct(IDbConnection $dbConn, IConfig $config) { $this->dbConn = $dbConn; $this->config = $config; $this->users = new Users($this->dbConn); $this->emotes = new Emotes($this->dbConn); + $this->changelog = new Changelog($this->dbConn); } public function getDbConn(): IDbConnection { @@ -64,6 +67,10 @@ class MisuzuContext { return $this->emotes; } + public function getChangelog(): Changelog { + return $this->changelog; + } + public function setUpHttp(bool $legacy = false): void { $this->router = new HttpFx; $this->router->use('/', function($response) { @@ -101,36 +108,31 @@ class MisuzuContext { } private function registerHttpRoutes(): void { - function msz_compat_handler(string $className, string $method) { - return function(...$args) use ($className, $method) { - $className = "\\Misuzu\\Http\\Handlers\\{$className}Handler"; - return (new $className)->{$method}(...$args); - }; - } + $mszCompatHandler = fn($className, $method) => fn(...$args) => (new ("\\Misuzu\\Http\\Handlers\\{$className}Handler")($this))->{$method}(...$args); - $this->router->get('/', msz_compat_handler('Home', 'index')); + $this->router->get('/', $mszCompatHandler('Home', 'index')); - $this->router->get('/assets/:filename', msz_compat_handler('Assets', 'serveComponent')); - $this->router->get('/assets/avatar/:filename', msz_compat_handler('Assets', 'serveAvatar')); - $this->router->get('/assets/profile-background/:filename', msz_compat_handler('Assets', 'serveProfileBackground')); + $this->router->get('/assets/:filename', $mszCompatHandler('Assets', 'serveComponent')); + $this->router->get('/assets/avatar/:filename', $mszCompatHandler('Assets', 'serveAvatar')); + $this->router->get('/assets/profile-background/:filename', $mszCompatHandler('Assets', 'serveProfileBackground')); - $this->router->get('/info', msz_compat_handler('Info', 'index')); - $this->router->get('/info/:name', msz_compat_handler('Info', 'page')); - $this->router->get('/info/:project/:name', msz_compat_handler('Info', 'page')); + $this->router->get('/info', $mszCompatHandler('Info', 'index')); + $this->router->get('/info/:name', $mszCompatHandler('Info', 'page')); + $this->router->get('/info/:project/:name', $mszCompatHandler('Info', 'page')); - $this->router->get('/changelog', msz_compat_handler('Changelog', 'index')); - $this->router->get('/changelog.rss', msz_compat_handler('Changelog', 'feedRss')); - $this->router->get('/changelog.atom', msz_compat_handler('Changelog', 'feedAtom')); - $this->router->get('/changelog/change/:id', msz_compat_handler('Changelog', 'change')); + $this->router->get('/changelog', $mszCompatHandler('Changelog', 'index')); + $this->router->get('/changelog.rss', $mszCompatHandler('Changelog', 'feedRss')); + $this->router->get('/changelog.atom', $mszCompatHandler('Changelog', 'feedAtom')); + $this->router->get('/changelog/change/:id', $mszCompatHandler('Changelog', 'change')); - $this->router->get('/news', msz_compat_handler('News', 'index')); - $this->router->get('/news.rss', msz_compat_handler('News', 'feedIndexRss')); - $this->router->get('/news.atom', msz_compat_handler('News', 'feedIndexAtom')); - $this->router->get('/news/:category', msz_compat_handler('News', 'viewCategory')); - $this->router->get('/news/post/:id', msz_compat_handler('News', 'viewPost')); + $this->router->get('/news', $mszCompatHandler('News', 'index')); + $this->router->get('/news.rss', $mszCompatHandler('News', 'feedIndexRss')); + $this->router->get('/news.atom', $mszCompatHandler('News', 'feedIndexAtom')); + $this->router->get('/news/:category', $mszCompatHandler('News', 'viewCategory')); + $this->router->get('/news/post/:id', $mszCompatHandler('News', 'viewPost')); - $this->router->get('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadGET')); - $this->router->post('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadPOST')); + $this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET')); + $this->router->post('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadPOST')); new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'), $this->emotes); } diff --git a/src/url.php b/src/url.php index e09e0fc..c56a8fd 100644 --- a/src/url.php +++ b/src/url.php @@ -110,8 +110,10 @@ define('MSZ_URLS', [ 'manage-changelog-changes' => ['/manage/changelog'], 'manage-changelog-change' => ['/manage/changelog/change.php', ['c' => '']], + 'manage-changelog-change-delete' => ['/manage/changelog/change.php', ['c' => '', 'delete' => '1', 'csrf' => '{token}']], 'manage-changelog-tags' => ['/manage/changelog/tags.php'], 'manage-changelog-tag' => ['/manage/changelog/tag.php', ['t' => '']], + 'manage-changelog-tag-delete' => ['/manage/changelog/tag.php', ['t' => '', 'delete' => '1', 'csrf' => '{token}']], 'manage-news-categories' => ['/manage/news/categories.php'], 'manage-news-category' => ['/manage/news/category.php', ['c' => '']], @@ -173,8 +175,12 @@ function url_redirect(string $name, array $variables = []): void { } function url_variable(string $value, array $variables): string { - if(str_starts_with($value, '<') && str_ends_with($value, '>')) - return $variables[trim($value, '<>')] ?? ''; + if(str_starts_with($value, '<') && str_ends_with($value, '>')) { + $value = $variables[trim($value, '<>')] ?? ''; + if(is_array($value)) + $value = implode(',', $value); + return (string)$value; + } if(str_starts_with($value, '[') && str_ends_with($value, ']')) return ''; diff --git a/templates/changelog/change.twig b/templates/changelog/change.twig index 0aa37be..2ff5042 100644 --- a/templates/changelog/change.twig +++ b/templates/changelog/change.twig @@ -5,32 +5,32 @@ {% set title = 'Changelog » Change #' ~ change_info.id %} {% set canonical_url = url('changelog-change', {'change': change_info.id}) %} {% set manage_link = url('manage-changelog-change', {'change': change_info.id}) %} -{% set description = change_info.header %} +{% set description = change_info.summary %} {% block content %} -
+
- {{ change_info.actionString }} + {{ change_info.actionText }}
- {{ change_info.header }} + {{ change_info.summary }}
-
+
- {% if change_info.hasCommentsCategory %} -
- {{ container_title(' Comments for ' ~ change_info.date) }} - {{ comments_section(change_info.commentsCategory, comments_user) }} -
- {% endif %} +
+ {{ container_title(' Comments for ' ~ change_info.date) }} + {{ comments_section(comments_category, comments_user) }} +
{% endblock %} diff --git a/templates/changelog/index.twig b/templates/changelog/index.twig index fb766f8..9d59ba7 100644 --- a/templates/changelog/index.twig +++ b/templates/changelog/index.twig @@ -11,11 +11,22 @@ {% set canonical_url = url('changelog-index', { 'date': changelog_date_fmt, 'user': changelog_user.id|default(0), + 'tags': changelog_tags, 'page': changelog_pagination.page < 2 ? 0 : changelog_pagination.page, }) %} {% if is_date or is_user %} - {% set title = title ~ ' »' ~ (is_date ? ' ' ~ changelog_infos[0].date : '') ~ (is_user ? ' by ' ~ changelog_infos[0].user.username : '') %} + {% set title = title ~ ' »' %} + {% set first_change_info = changelog_infos[0] %} + + {% if is_date %} + {% set first_change_date = first_change_info.date is defined ? first_change_info.date : first_change_info.change.date %} + {% set title = title ~ ' ' ~ first_change_date %} + {% endif %} + + {% if is_user %} + {% set title = title ~ ' by ' ~ first_change_info.user.username %} + {% endif %} {% else %} {% set feeds = [ { @@ -47,7 +58,7 @@ {% if is_date %}
{{ container_title(' Comments') }} - {{ comments_section(changelog_infos[0].commentsCategory, comments_user) }} + {{ comments_section(comments_category, comments_user) }}
{% endif %} {% endblock %} diff --git a/templates/changelog/macros.twig b/templates/changelog/macros.twig index 58debbe..54c9a30 100644 --- a/templates/changelog/macros.twig +++ b/templates/changelog/macros.twig @@ -4,8 +4,10 @@
{% if changes|length > 0 %} {% for change in changes %} - {% if not hide_dates and (last_date is not defined or last_date != change.date) %} - {% set last_date = change.date %} + {% set change_date = change.change is defined ? change.change.date : change.date %} + + {% if not hide_dates and (last_date is not defined or last_date != change_date) %} + {% set last_date = change_date %} {{ last_date }} @@ -23,6 +25,11 @@ {% endmacro %} {% macro changelog_entry(change, is_small, is_manage) %} + {% set user = change.user %} + {% if change.change is defined %} + {% set change = change.change %} + {% endif %} + {% set change_url = url(is_manage ? 'manage-changelog-change' : 'changelog-change', {'change': change.id}) %}
@@ -37,22 +44,22 @@ {% endif %} - + {% if is_small %}title="{{ change.actionText }}"{% endif %}> {% if not is_small %}
- {{ change.actionString }} + {{ change.actionText }}
{% endif %}
{% if not is_small %} + href="{{ url(is_manage ? 'manage-user' : 'user-profile', {'user': user.id|default(0)}) }}" + style="--user-colour: {{ user.colour|default('inherit') }}">
- {{ change.user.username|default('Anonymous') }} + {{ user.username|default('Anonymous') }}
{% endif %} @@ -61,13 +68,13 @@
- {{ change.header }} + {{ change.summary }} {% if is_manage %}