From 1a11a8f8ba506c259a30b8fb6308cfa474325e5d Mon Sep 17 00:00:00 2001 From: flashwave Date: Mon, 17 Jul 2023 17:43:17 +0000 Subject: [PATCH] Rewrote audit log on new database backend. --- public/auth/password.php | 3 +- public/comments.php | 11 +- public/forum/post.php | 7 +- public/forum/topic.php | 13 +- public/manage/changelog/change.php | 7 +- public/manage/changelog/tag.php | 7 +- public/manage/forum/redirs.php | 4 +- public/manage/general/emoticon.php | 4 +- public/manage/general/emoticons.php | 6 +- public/manage/general/logs.php | 14 +- public/manage/general/setting-delete.php | 3 +- public/manage/general/setting.php | 6 +- public/manage/news/category.php | 7 +- public/manage/news/post.php | 7 +- public/settings/account.php | 5 +- public/settings/data.php | 3 +- public/settings/logs.php | 9 +- public/settings/sessions.php | 5 +- src/AuditLog.php | 230 ----------------------- src/AuditLog/AuditLog.php | 151 +++++++++++++++ src/AuditLog/AuditLogInfo.php | 126 +++++++++++++ src/Comments/Comments.php | 1 - src/MisuzuContext.php | 21 +++ templates/manage/general/logs.twig | 2 +- templates/user/macros.twig | 15 +- 25 files changed, 365 insertions(+), 302 deletions(-) delete mode 100644 src/AuditLog.php create mode 100644 src/AuditLog/AuditLog.php create mode 100644 src/AuditLog/AuditLogInfo.php diff --git a/public/auth/password.php b/public/auth/password.php index a293cf1..e7dac88 100644 --- a/public/auth/password.php +++ b/public/auth/password.php @@ -1,7 +1,6 @@ removeTOTPKey() ->save(); - AuditLog::create(AuditLog::PASSWORD_RESET, [], $userInfo); + $msz->createAuditLog('PASSWORD_RESET', [], $userInfo); $tokenInfo->invalidate(); diff --git a/public/comments.php b/public/comments.php index e7064b8..3bc6935 100644 --- a/public/comments.php +++ b/public/comments.php @@ -2,12 +2,7 @@ namespace Misuzu; use RuntimeException; -use Misuzu\AuditLog; -use Misuzu\Comments\CommentsCategory; -use Misuzu\Comments\CommentsPost; -use Misuzu\Comments\CommentsVote; use Misuzu\Users\User; -use Misuzu\Users\UserNotFoundException; require_once '../misuzu.php'; @@ -149,13 +144,13 @@ switch($commentMode) { $comments->deletePost($commentInfo); if($isModAction) { - AuditLog::create(AuditLog::COMMENT_ENTRY_DELETE_MOD, [ + $msz->createAuditLog('COMMENT_ENTRY_DELETE_MOD', [ $commentInfo->getId(), $commentUserId = $commentInfo->getUserId(), '', ]); } else { - AuditLog::create(AuditLog::COMMENT_ENTRY_DELETE, [$commentInfo->getId()]); + $msz->createAuditLog('COMMENT_ENTRY_DELETE', [$commentInfo->getId()]); } redirect($redirect); @@ -174,7 +169,7 @@ switch($commentMode) { $comments->restorePost($commentInfo); - AuditLog::create(AuditLog::COMMENT_ENTRY_RESTORE, [ + $msz->createAuditLog('COMMENT_ENTRY_RESTORE', [ $commentInfo->getId(), $commentUserId = $commentInfo->getUserId(), '', diff --git a/public/forum/post.php b/public/forum/post.php index 0fedca1..d67a6db 100644 --- a/public/forum/post.php +++ b/public/forum/post.php @@ -1,7 +1,6 @@ createAuditLog('FORUM_POST_DELETE', [$postInfo['post_id']]); } if(!$deletePost) { @@ -147,7 +146,7 @@ switch($postMode) { break; } - AuditLog::create(AuditLog::FORUM_POST_NUKE, [$postInfo['post_id']]); + $msz->createAuditLog('FORUM_POST_NUKE', [$postInfo['post_id']]); url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); break; @@ -184,7 +183,7 @@ switch($postMode) { break; } - AuditLog::create(AuditLog::FORUM_POST_RESTORE, [$postInfo['post_id']]); + $msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo['post_id']]); url_redirect('forum-topic', ['topic' => $postInfo['topic_id']]); break; diff --git a/public/forum/topic.php b/public/forum/topic.php index 962d636..49e2656 100644 --- a/public/forum/topic.php +++ b/public/forum/topic.php @@ -1,7 +1,6 @@ createAuditLog('FORUM_TOPIC_DELETE', [$topic['topic_id']]); if(!$deleteTopic) { echo render_error(500); @@ -207,7 +206,7 @@ if(in_array($moderationMode, $validModerationModes, true)) { break; } - AuditLog::create(AuditLog::FORUM_TOPIC_RESTORE, [$topic['topic_id']]); + $msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topic['topic_id']]); url_redirect('forum-category', [ 'forum' => $topic['forum_id'], @@ -245,7 +244,7 @@ if(in_array($moderationMode, $validModerationModes, true)) { break; } - AuditLog::create(AuditLog::FORUM_TOPIC_NUKE, [$topic['topic_id']]); + $msz->createAuditLog('FORUM_TOPIC_NUKE', [$topic['topic_id']]); url_redirect('forum-category', [ 'forum' => $topic['forum_id'], @@ -254,7 +253,7 @@ if(in_array($moderationMode, $validModerationModes, true)) { case 'bump': if($canBumpTopic && forum_topic_bump($topic['topic_id'])) { - AuditLog::create(AuditLog::FORUM_TOPIC_BUMP, [$topic['topic_id']]); + $msz->createAuditLog('FORUM_TOPIC_BUMP', [$topic['topic_id']]); } url_redirect('forum-topic', [ @@ -264,7 +263,7 @@ if(in_array($moderationMode, $validModerationModes, true)) { case 'lock': if($canLockTopic && !$topicIsLocked && forum_topic_lock($topic['topic_id'])) { - AuditLog::create(AuditLog::FORUM_TOPIC_LOCK, [$topic['topic_id']]); + $msz->createAuditLog('FORUM_TOPIC_LOCK', [$topic['topic_id']]); } url_redirect('forum-topic', [ @@ -274,7 +273,7 @@ if(in_array($moderationMode, $validModerationModes, true)) { case 'unlock': if($canLockTopic && $topicIsLocked && forum_topic_unlock($topic['topic_id'])) { - AuditLog::create(AuditLog::FORUM_TOPIC_UNLOCK, [$topic['topic_id']]); + $msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topic['topic_id']]); } url_redirect('forum-topic', [ diff --git a/public/manage/changelog/change.php b/public/manage/changelog/change.php index 8a2821a..24f80a4 100644 --- a/public/manage/changelog/change.php +++ b/public/manage/changelog/change.php @@ -4,7 +4,6 @@ namespace Misuzu; use DateTimeInterface; use RuntimeException; use Index\DateTime; -use Misuzu\AuditLog; use Misuzu\Changelog\Changelog; use Misuzu\Users\User; use Misuzu\Users\UserNotFoundException; @@ -39,7 +38,7 @@ else if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if(CSRF::validateRequest()) { $changelog->deleteChange($changeInfo); - AuditLog::create(AuditLog::CHANGELOG_ENTRY_DELETE, [$changeInfo->getId()]); + $msz->createAuditLog('CHANGELOG_ENTRY_DELETE', [$changeInfo->getId()]); url_redirect('manage-changelog-changes'); } else render_error(403); return; @@ -102,8 +101,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { } } - AuditLog::create( - $isNew ? AuditLog::CHANGELOG_ENTRY_CREATE : AuditLog::CHANGELOG_ENTRY_EDIT, + $msz->createAuditLog( + $isNew ? 'CHANGELOG_ENTRY_CREATE' : 'CHANGELOG_ENTRY_EDIT', [$changeInfo->getId()] ); diff --git a/public/manage/changelog/tag.php b/public/manage/changelog/tag.php index 8375820..65f54d2 100644 --- a/public/manage/changelog/tag.php +++ b/public/manage/changelog/tag.php @@ -2,7 +2,6 @@ namespace Misuzu; use RuntimeException; -use Misuzu\AuditLog; use Misuzu\Users\User; require_once '../../../misuzu.php'; @@ -30,7 +29,7 @@ else if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if(CSRF::validateRequest()) { $changelog->deleteTag($tagInfo); - AuditLog::create(AuditLog::CHANGELOG_TAG_DELETE, [$tagInfo->getId()]); + $msz->createAuditLog('CHANGELOG_TAG_DELETE', [$tagInfo->getId()]); url_redirect('manage-changelog-tags'); } else render_error(403); return; @@ -55,8 +54,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { $changelog->updateTag($tagInfo, $name, $description, $archive); } - AuditLog::create( - $isNew ? AuditLog::CHANGELOG_TAG_CREATE : AuditLog::CHANGELOG_TAG_EDIT, + $msz->createAuditLog( + $isNew ? 'CHANGELOG_TAG_CREATE' : 'CHANGELOG_TAG_EDIT', [$tagInfo->getId()] ); diff --git a/public/manage/forum/redirs.php b/public/manage/forum/redirs.php index e9422d9..89890cb 100644 --- a/public/manage/forum/redirs.php +++ b/public/manage/forum/redirs.php @@ -20,7 +20,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') { if($rTopicId < 1) throw new \Exception("Invalid topic id."); - AuditLog::create(AuditLog::FORUM_TOPIC_REDIR_CREATE, [$rTopicId]); + $msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]); forum_topic_redir_create($rTopicId, User::getCurrent()->getId(), $rTopicURL); url_redirect('manage-forum-topic-redirs'); return; @@ -31,7 +31,7 @@ if(filter_input(INPUT_GET, 'm') === 'explode') { throw new \Exception("Request verification failed."); $rTopicId = (int)filter_input(INPUT_GET, 't'); - AuditLog::create(AuditLog::FORUM_TOPIC_REDIR_REMOVE, [$rTopicId]); + $msz->createAuditLog('FORUM_TOPIC_REDIR_REMOVE', [$rTopicId]); forum_topic_redir_remove($rTopicId); url_redirect('manage-forum-topic-redirs'); return; diff --git a/public/manage/general/emoticon.php b/public/manage/general/emoticon.php index 05f3760..7cb1e40 100644 --- a/public/manage/general/emoticon.php +++ b/public/manage/general/emoticon.php @@ -95,8 +95,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { $sCurrent[] = $string; } - AuditLog::create( - $isNew ? AuditLog::EMOTICON_CREATE : AuditLog::EMOTICON_EDIT, + $msz->createAuditLog( + $isNew ? 'EMOTICON_CREATE' : 'EMOTICON_EDIT', [$emoteInfo->getId()] ); diff --git a/public/manage/general/emoticons.php b/public/manage/general/emoticons.php index b2d56ef..d4e0f55 100644 --- a/public/manage/general/emoticons.php +++ b/public/manage/general/emoticons.php @@ -25,20 +25,20 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) { if(!empty($_GET['delete'])) { $emotes->deleteEmote($emoteInfo); - AuditLog::create(AuditLog::EMOTICON_DELETE, [$emoteInfo->getId()]); + $msz->createAuditLog('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()]); + $msz->createAuditLog('EMOTICON_ORDER', [$emoteInfo->getId()]); } if(isset($_GET['alias'])) { $alias = (string)filter_input(INPUT_GET, 'alias'); if($emotes->checkEmoteString($alias) === '') { $emotes->addEmoteString($emoteInfo, $alias); - AuditLog::create(AuditLog::EMOTICON_ALIAS, [$emoteInfo->getId(), $alias]); + $msz->createAuditLog('EMOTICON_ALIAS', [$emoteInfo->getId(), $alias]); } } } diff --git a/public/manage/general/logs.php b/public/manage/general/logs.php index a8bd18e..0632883 100644 --- a/public/manage/general/logs.php +++ b/public/manage/general/logs.php @@ -1,7 +1,6 @@ getAuditLog(); +$pagination = new Pagination($auditLog->countLogs(), 50); if(!$pagination->hasValidOffset()) { echo render_error(404); return; } -$logs = AuditLog::all($pagination); +$logs = $auditLog->getLogs(pagination: $pagination); +$userInfos = []; +foreach($logs as $log) + if($log->hasUserId()) { + $userId = $log->getUserId(); + if(!array_key_exists($userId, $userInfos)) + $userInfos[$userId] = User::byId($userId); + } Template::render('manage.general.logs', [ 'global_logs' => $logs, 'global_logs_pagination' => $pagination, + 'global_logs_users' => $userInfos, ]); diff --git a/public/manage/general/setting-delete.php b/public/manage/general/setting-delete.php index 0db5a70..ae40483 100644 --- a/public/manage/general/setting-delete.php +++ b/public/manage/general/setting-delete.php @@ -1,7 +1,6 @@ createAuditLog('CONFIG_DELETE', [$sName]); $cfg->removeValue($sName); url_redirect('manage-general-settings'); } else { diff --git a/public/manage/general/setting.php b/public/manage/general/setting.php index 4d07be3..65c4fc0 100644 --- a/public/manage/general/setting.php +++ b/public/manage/general/setting.php @@ -52,12 +52,12 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') { $sVar['name'] = $sName; } - $sLogAction = AuditLog::CONFIG_CREATE; + $sLogAction = 'CONFIG_CREATE'; if($cfg->hasValue($sName)) { $sType = CfgTools::type($cfg->getValue($sName)); $sVar['new'] = false; - $sLogAction = AuditLog::CONFIG_UPDATE; + $sLogAction = 'CONFIG_UPDATE'; } elseif(empty($sType)) { $sType = (string)filter_input(INPUT_POST, 'conf_type'); if(empty($sType) || !CfgTools::isValidType($sType)) @@ -95,7 +95,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') { $sVar['value'] = $sValue; - AuditLog::create($sLogAction, [$sName]); + $msz->createAuditLog($sLogAction, [$sName]); $cfg->setValue($sName, $sValue); url_redirect('manage-general-settings'); return; diff --git a/public/manage/news/category.php b/public/manage/news/category.php index 668831e..d307f38 100644 --- a/public/manage/news/category.php +++ b/public/manage/news/category.php @@ -2,7 +2,6 @@ namespace Misuzu; use RuntimeException; -use Misuzu\AuditLog; use Misuzu\Users\User; require_once '../../../misuzu.php'; @@ -30,7 +29,7 @@ else if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if(CSRF::validateRequest()) { $news->deleteCategory($categoryInfo); - AuditLog::create(AuditLog::NEWS_CATEGORY_DELETE, [$categoryInfo->getId()]); + $msz->createAuditLog('NEWS_CATEGORY_DELETE', [$categoryInfo->getId()]); url_redirect('manage-news-categories'); } else render_error(403); return; @@ -55,8 +54,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { $news->updateCategory($categoryInfo, $name, $description, $hidden); } - AuditLog::create( - $isNew ? AuditLog::NEWS_CATEGORY_CREATE : AuditLog::NEWS_CATEGORY_EDIT, + $msz->createAuditLog( + $isNew ? 'NEWS_CATEGORY_CREATE' : 'NEWS_CATEGORY_EDIT', [$categoryInfo->getId()] ); diff --git a/public/manage/news/post.php b/public/manage/news/post.php index 8969a47..32752cb 100644 --- a/public/manage/news/post.php +++ b/public/manage/news/post.php @@ -2,7 +2,6 @@ namespace Misuzu; use RuntimeException; -use Misuzu\AuditLog; use Misuzu\Users\User; require_once '../../../misuzu.php'; @@ -30,7 +29,7 @@ else if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if(CSRF::validateRequest()) { $news->deletePost($postInfo); - AuditLog::create(AuditLog::NEWS_POST_DELETE, [$postInfo->getId()]); + $msz->createAuditLog('NEWS_POST_DELETE', [$postInfo->getId()]); url_redirect('manage-news-posts'); } else render_error(403); return; @@ -58,8 +57,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { $news->updatePost($postInfo, $category, $title, $body, $featured); } - AuditLog::create( - $isNew ? AuditLog::NEWS_POST_CREATE : AuditLog::NEWS_POST_EDIT, + $msz->createAuditLog( + $isNew ? 'NEWS_POST_CREATE' : 'NEWS_POST_EDIT', [$postInfo->getId()] ); diff --git a/public/settings/account.php b/public/settings/account.php index b8bd75f..56656e8 100644 --- a/public/settings/account.php +++ b/public/settings/account.php @@ -1,7 +1,6 @@ setEMailAddress($_POST['email']['new']); - AuditLog::create(AuditLog::PERSONAL_EMAIL_CHANGE, [ + $msz->createAuditLog('PERSONAL_EMAIL_CHANGE', [ $_POST['email']['new'], ]); } @@ -121,7 +120,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) { $errors[] = 'The given passwords was too weak.'; } else { $currentUser->setPassword($_POST['password']['new']); - AuditLog::create(AuditLog::PERSONAL_PASSWORD_CHANGE); + $msz->createAuditLog('PERSONAL_PASSWORD_CHANGE'); } } } diff --git a/public/settings/data.php b/public/settings/data.php index a6e9ca7..0e6b406 100644 --- a/public/settings/data.php +++ b/public/settings/data.php @@ -2,7 +2,6 @@ namespace Misuzu; use ZipArchive; -use Misuzu\AuditLog; use Misuzu\Users\User; use Misuzu\Users\UserSession; @@ -36,7 +35,7 @@ if(isset($_POST['action']) && is_string($_POST['action'])) { && $currentUser->checkPassword($_POST['password'] ?? '')) { switch($_POST['action']) { case 'data': - AuditLog::create(AuditLog::PERSONAL_DATA_DOWNLOAD); + $msz->createAuditLog('PERSONAL_DATA_DOWNLOAD'); $timeStamp = floor(time() / 3600) * 3600; $fileName = sprintf('msz-user-data-%d-%d.zip', $currentUserId, $timeStamp); diff --git a/public/settings/logs.php b/public/settings/logs.php index 80cd211..2ec4a29 100644 --- a/public/settings/logs.php +++ b/public/settings/logs.php @@ -1,7 +1,6 @@ getAuditLog(); + $loginHistoryPagination = new Pagination(UserLoginAttempt::countAll($currentUser), 15, 'hp'); -$accountLogPagination = new Pagination(AuditLog::countAll($currentUser), 15, 'ap'); +$accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 15, 'ap'); + +$auditLogs = $auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination); Template::render('settings.logs', [ 'login_history_list' => UserLoginAttempt::all($loginHistoryPagination, $currentUser), 'login_history_pagination' => $loginHistoryPagination, - 'account_log_list' => AuditLog::all($accountLogPagination, $currentUser), + 'account_log_list' => $auditLogs, 'account_log_pagination' => $accountLogPagination, ]); diff --git a/public/settings/sessions.php b/public/settings/sessions.php index cdbd32d..401fa17 100644 --- a/public/settings/sessions.php +++ b/public/settings/sessions.php @@ -1,7 +1,6 @@ delete(); - AuditLog::create(AuditLog::PERSONAL_SESSION_DESTROY, [$sessionInfo->getId()]); + $msz->createAuditLog('PERSONAL_SESSION_DESTROY', [$sessionInfo->getId()]); } } elseif($_POST['session'] === 'all') { $currentSessionKilled = true; UserSession::purgeUser($currentUser); - AuditLog::create(AuditLog::PERSONAL_SESSION_DESTROY_ALL); + $msz->createAuditLog('PERSONAL_SESSION_DESTROY_ALL'); } if($currentSessionKilled) { diff --git a/src/AuditLog.php b/src/AuditLog.php deleted file mode 100644 index 34bd244..0000000 --- a/src/AuditLog.php +++ /dev/null @@ -1,230 +0,0 @@ - 'Changed e-mail address to %s.', - self::PERSONAL_PASSWORD_CHANGE => 'Changed account password.', - self::PERSONAL_SESSION_DESTROY => 'Ended session #%d.', - self::PERSONAL_SESSION_DESTROY_ALL => 'Ended all personal sessions.', - self::PERSONAL_DATA_DOWNLOAD => 'Downloaded archive of account data.', - - self::PASSWORD_RESET => 'Successfully used the password reset form to change password.', - - self::CHANGELOG_ENTRY_CREATE => 'Created a new changelog entry #%d.', - self::CHANGELOG_ENTRY_EDIT => 'Edited changelog entry #%d.', - self::CHANGELOG_TAG_ADD => 'Added tag #%2$d to changelog entry #%1$d.', - 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.', - - self::COMMENT_ENTRY_DELETE => 'Deleted comment #%d.', - self::COMMENT_ENTRY_DELETE_MOD => 'Deleted comment #%d by user #%d %s.', - self::COMMENT_ENTRY_RESTORE => 'Restored comment #%d by user #%d %s.', - - self::NEWS_POST_CREATE => 'Created news post #%d.', - self::NEWS_POST_EDIT => 'Edited news post #%d.', - self::NEWS_POST_DELETE => 'Deleted news post #%d.', - self::NEWS_CATEGORY_CREATE => 'Created news category #%d.', - self::NEWS_CATEGORY_EDIT => 'Edited news category #%d.', - self::NEWS_CATEGORY_DELETE => 'Deleted news category #%d.', - - self::FORUM_POST_EDIT => 'Edited forum post #%d.', - self::FORUM_POST_DELETE => 'Deleted forum post #%d.', - self::FORUM_POST_RESTORE => 'Restored forum post #%d.', - self::FORUM_POST_NUKE => 'Nuked forum post #%d.', - - self::FORUM_TOPIC_DELETE => 'Deleted forum topic #%d.', - self::FORUM_TOPIC_RESTORE => 'Restored forum topic #%d.', - self::FORUM_TOPIC_NUKE => 'Nuked forum topic #%d.', - self::FORUM_TOPIC_BUMP => 'Manually bumped forum topic #%d.', - self::FORUM_TOPIC_LOCK => 'Locked forum topic #%d.', - self::FORUM_TOPIC_UNLOCK => 'Unlocked forum topic #%d.', - self::FORUM_TOPIC_REDIR_CREATE => 'Created redirect for topic #%d.', - self::FORUM_TOPIC_REDIR_REMOVE => 'Removed redirect for topic #%d.', - - 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 - private $user_id = null; - private $log_action = ''; - private $log_params = []; - private $log_created = null; - private $log_ip = '::1'; - private $log_country = 'XX'; - - private $user = null; - private $userLookedUp = false; - - public const TABLE = 'audit_log'; - private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE; - private const SELECT = '%1$s.`user_id`, %1$s.`log_action`, %1$s.`log_params`, %1$s.`log_country`' - . ', INET6_NTOA(%1$s.`log_ip`) AS `log_ip`' - . ', UNIX_TIMESTAMP(%1$s.`log_created`) AS `log_created`'; - - public function getUserId(): int { - return $this->user_id < 1 ? -1 : $this->user_id; - } - 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 getAction(): string { - return $this->log_action; - } - - public function getParams(): array { - if(is_string($this->log_params)) - $this->log_params = json_decode($this->log_params) ?? []; - return $this->log_params; - } - - public function getCreatedTime(): int { - return $this->log_created === null ? -1 : $this->log_created; - } - - public function getRemoteAddress(): string { - return $this->log_ip; - } - - public function getCountry(): string { - return $this->log_country; - } - public function getCountryName(): string { - return get_country_name($this->getCountry()); - } - - public function getString(): string { - if(!array_key_exists($this->getAction(), self::FORMATS)) - return sprintf('%s(%s)', $this->getAction(), json_encode($this->getParams())); - return vsprintf(self::FORMATS[$this->getAction()], $this->getParams()); - } - - public static function create(string $action, array $params = [], ?User $user = null): void { - $user = $user ?? User::getCurrent(); - $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '::1'; - $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX'; - - $createLog = DB::prepare( - 'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`log_action`, `user_id`, `log_params`, `log_ip`, `log_country`)' - . ' VALUES (:action, :user, :params, INET6_ATON(:ip), :country)' - ) ->bind('action', $action) - ->bind('user', $user === null ? null : $user->getId()) - ->bind('params', json_encode($params)) - ->bind('ip', $remoteAddr) - ->bind('country', $countryCode) - ->execute(); - } - - private static function countQueryBase(): string { - return sprintf(self::QUERY_SELECT, 'COUNT(*)'); - } - public static function countAll(?User $user = null): int { - $getCount = DB::prepare( - self::countQueryBase() - . ($user === null ? '' : ' WHERE `user_id` = :user') - ); - if($user !== null) - $getCount->bind('user', $user->getId()); - return (int)$getCount->fetchColumn(); - } - - private static function byQueryBase(): string { - return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); - } - public static function all(?Pagination $pagination = null, ?User $user = null): array { - $logsQuery = self::byQueryBase() - . ($user === null ? '' : ' WHERE `user_id` = :user') - . ' ORDER BY `log_created` DESC'; - - if($pagination !== null) - $logsQuery .= ' LIMIT :range OFFSET :offset'; - - $getLogs = DB::prepare($logsQuery); - - if($user !== null) - $getLogs->bind('user', $user->getId()); - - if($pagination !== null) - $getLogs->bind('range', $pagination->getRange()) - ->bind('offset', $pagination->getOffset()); - - return $getLogs->fetchObjects(self::class); - } -} diff --git a/src/AuditLog/AuditLog.php b/src/AuditLog/AuditLog.php new file mode 100644 index 0000000..f155165 --- /dev/null +++ b/src/AuditLog/AuditLog.php @@ -0,0 +1,151 @@ +dbConn = $dbConn; + $this->cache = new DbStatementCache($dbConn); + } + + public function countLogs( + User|string|null $userInfo = null, + IPAddress|string|null $remoteAddr = null + ): int { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($remoteAddr instanceof IPAddress) + $remoteAddr = (string)$remoteAddr; + + $hasUserInfo = $userInfo !== null; + $hasRemoteAddr = $remoteAddr !== null; + + $args = 0; + $query = 'SELECT COUNT(*) FROM msz_audit_log'; + if($hasUserInfo) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' user_id = ?'; + } + if($hasRemoteAddr) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' log_ip = INET6_ATON(?)'; + } + + $stmt = $this->cache->get($query); + + $args = 0; + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasRemoteAddr) + $stmt->addParameter(++$args, $remoteAddr); + + $stmt->execute(); + $result = $stmt->getResult(); + $count = 0; + + if($result->next()) + $count = $result->getInteger(0); + + return $count; + } + + public function getLogs( + User|string|null $userInfo = null, + IPAddress|string|null $remoteAddr = null, + ?Pagination $pagination = null + ): array { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($remoteAddr instanceof IPAddress) + $remoteAddr = (string)$remoteAddr; + + $hasUserInfo = $userInfo !== null; + $hasRemoteAddr = $remoteAddr !== null; + $hasPagination = $pagination !== null; + + $args = 0; + $query = 'SELECT user_id, log_action, log_params, UNIX_TIMESTAMP(log_created), INET6_NTOA(log_ip), log_country FROM msz_audit_log'; + if($hasUserInfo) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' user_id = ?'; + } + if($hasRemoteAddr) { + $query .= (++$args > 1 ? ' AND' : ' WHERE'); + $query .= ' log_ip = INET6_ATON(?)'; + } + $query .= ' ORDER BY log_created DESC'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + + $stmt = $this->cache->get($query); + + $args = 0; + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasRemoteAddr) + $stmt->addParameter(++$args, $remoteAddr); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + + $stmt->execute(); + $result = $stmt->getResult(); + $logs = []; + + while($result->next()) + $logs[] = new AuditLogInfo($result); + + return $logs; + } + + public function createLog( + User|string|null $userInfo, + string $action, + array $params = [], + IPAddress|string $remoteAddr = '::1', + string $countryCode = 'XX' + ): void { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($remoteAddr instanceof IPAddress) + $remoteAddr = (string)$remoteAddr; + + // action names should have stricter validation, + // i do want to switch to a lowercase colon separated format later but i'll save that for the unified log in Hanyuu + $actionTrim = trim($action); + if($actionTrim !== $action || empty($actionTrim)) + throw new InvalidArgumentException('$action may not be empty.'); + + if(strlen($countryCode) !== 2 || !ctype_alpha($countryCode)) + throw new InvalidArgumentException('$countryCode must be two alpha characters.'); + + foreach($params as &$param) { + if(is_array($param)) + $param = implode(', ', $param); + elseif(is_object($param)) + $param = (string)$param; + } + + $params = json_encode($params); + + $stmt = $this->cache->get('INSERT INTO msz_audit_log (user_id, log_action, log_params, log_ip, log_country) VALUES (?, ?, ?, INET6_ATON(?), UPPER(?))'); + $stmt->addParameter(1, $userInfo); + $stmt->addParameter(2, $action); + $stmt->addParameter(3, $params); + $stmt->addParameter(4, $remoteAddr); + $stmt->addParameter(5, $countryCode); + $stmt->execute(); + } +} diff --git a/src/AuditLog/AuditLogInfo.php b/src/AuditLog/AuditLogInfo.php new file mode 100644 index 0000000..169f2b6 --- /dev/null +++ b/src/AuditLog/AuditLogInfo.php @@ -0,0 +1,126 @@ +userId = $result->isNull(0) ? null : (string)$result->getInteger(0); + $this->action = $result->getString(1); + $this->params = json_decode($result->getString(2)); + $this->created = $result->getInteger(3); + $this->address = $result->isNull(4) ? '::1' : $result->getString(4); // apparently this being NULL is possible? + $this->country = $result->getString(5); + } + + public function hasUserId(): bool { + return $this->userId !== null; + } + + public function getUserId(): ?string { + return $this->userId; + } + + public function getAction(): string { + return $this->action; + } + + public function getParams(): array { + return $this->params; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function getRemoteAddressRaw(): string { + return $this->address; + } + + public function getRemoteAddress(): IPAddress { + return IPAddress::parse($this->address); + } + + public function getCountryCode(): string { + return $this->country; + } + + public function getFormatted(): string { + if(array_key_exists($this->action, self::FORMATS)) + try { + return vsprintf(self::FORMATS[$this->action], $this->params); + } catch(ValueError $ex) {} + + return sprintf('%s(%s)', $this->action, implode(', ', $this->params)); + } + + public const FORMATS = [ + 'PERSONAL_EMAIL_CHANGE' => 'Changed e-mail address to %s.', + 'PERSONAL_PASSWORD_CHANGE' => 'Changed account password.', + 'PERSONAL_SESSION_DESTROY' => 'Ended session #%d.', + 'PERSONAL_SESSION_DESTROY_ALL' => 'Ended all personal sessions.', + 'PERSONAL_DATA_DOWNLOAD' => 'Downloaded archive of account data.', + + 'PASSWORD_RESET' => 'Successfully used the password reset form to change password.', + + 'CHANGELOG_ENTRY_CREATE' => 'Created a new changelog entry #%d.', + 'CHANGELOG_ENTRY_EDIT' => 'Edited changelog entry #%d.', + 'CHANGELOG_TAG_ADD' => 'Added tag #%2$d to changelog entry #%1$d.', + 'CHANGELOG_TAG_REMOVE' => 'Removed tag #%2$d from changelog entry #%1$d.', + 'CHANGELOG_TAG_CREATE' => 'Created new changelog tag #%d.', + 'CHANGELOG_TAG_EDIT' => 'Edited changelog tag #%d.', + 'CHANGELOG_TAG_DELETE' => 'Deleted changelog tag #%d.', + 'CHANGELOG_ACTION_CREATE' => 'Created new changelog action #%d.', + 'CHANGELOG_ACTION_EDIT' => 'Edited changelog action #%d.', + + 'COMMENT_ENTRY_DELETE' => 'Deleted comment #%d.', + 'COMMENT_ENTRY_DELETE_MOD' => 'Deleted comment #%d by user #%d %s.', + 'COMMENT_ENTRY_RESTORE' => 'Restored comment #%d by user #%d %s.', + + 'NEWS_POST_CREATE' => 'Created news post #%d.', + 'NEWS_POST_EDIT' => 'Edited news post #%d.', + 'NEWS_POST_DELETE' => 'Deleted news post #%d.', + 'NEWS_CATEGORY_CREATE' => 'Created news category #%d.', + 'NEWS_CATEGORY_EDIT' => 'Edited news category #%d.', + 'NEWS_CATEGORY_DELETE' => 'Deleted news category #%d.', + + 'FORUM_POST_EDIT' => 'Edited forum post #%d.', + 'FORUM_POST_DELETE' => 'Deleted forum post #%d.', + 'FORUM_POST_RESTORE' => 'Restored forum post #%d.', + 'FORUM_POST_NUKE' => 'Nuked forum post #%d.', + + 'FORUM_TOPIC_DELETE' => 'Deleted forum topic #%d.', + 'FORUM_TOPIC_RESTORE' => 'Restored forum topic #%d.', + 'FORUM_TOPIC_NUKE' => 'Nuked forum topic #%d.', + 'FORUM_TOPIC_BUMP' => 'Manually bumped forum topic #%d.', + 'FORUM_TOPIC_LOCK' => 'Locked forum topic #%d.', + 'FORUM_TOPIC_UNLOCK' => 'Unlocked forum topic #%d.', + 'FORUM_TOPIC_REDIR_CREATE' => 'Created redirect for topic #%d.', + 'FORUM_TOPIC_REDIR_REMOVE' => 'Removed redirect for topic #%d.', + + 'CONFIG_CREATE' => 'Created config value with name "%s".', + 'CONFIG_UPDATE' => 'Updated config value with name "%s".', + 'CONFIG_DELETE' => 'Deleted config value with name "%s".', + + 'EMOTICON_CREATE' => 'Created emoticon #%s.', + 'EMOTICON_EDIT' => 'Edited emoticon #%s.', + 'EMOTICON_DELETE' => 'Deleted emoticon #%s.', + 'EMOTICON_ORDER' => 'Changed order of emoticon #%s.', + 'EMOTICON_ALIAS' => 'Added alias "%2$s" to emoticon #%1$s.', + ]; +} diff --git a/src/Comments/Comments.php b/src/Comments/Comments.php index 18e2c91..66013c8 100644 --- a/src/Comments/Comments.php +++ b/src/Comments/Comments.php @@ -7,7 +7,6 @@ use Index\Data\IDbConnection; use Index\Data\IDbResult; use Misuzu\DbStatementCache; use Misuzu\Pagination; -use Misuzu\Comments\CommentsCategory; use Misuzu\Users\User; class Comments { diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index 0439140..53ff120 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -2,12 +2,14 @@ namespace Misuzu; use Misuzu\Template; +use Misuzu\AuditLog\AuditLog; use Misuzu\Changelog\Changelog; use Misuzu\Comments\Comments; use Misuzu\Config\IConfig; use Misuzu\Emoticons\Emotes; use Misuzu\News\News; use Misuzu\SharpChat\SharpChatRoutes; +use Misuzu\Users\User; use Misuzu\Users\Users; use Index\Data\IDbConnection; use Index\Data\Migration\IDbMigrationRepo; @@ -25,6 +27,7 @@ class MisuzuContext { private IConfig $config; private Users $users; private HttpFx $router; + private AuditLog $auditLog; private Emotes $emotes; private Changelog $changelog; private News $news; @@ -34,6 +37,7 @@ class MisuzuContext { $this->dbConn = $dbConn; $this->config = $config; $this->users = new Users($this->dbConn); + $this->auditLog = new AuditLog($this->dbConn); $this->emotes = new Emotes($this->dbConn); $this->changelog = new Changelog($this->dbConn); $this->news = new News($this->dbConn); @@ -85,6 +89,23 @@ class MisuzuContext { return $this->comments; } + public function getAuditLog(): AuditLog { + return $this->auditLog; + } + + public function createAuditLog(string $action, array $params = [], User|string|null $userInfo = null): void { + if($userInfo === null && User::hasCurrent()) + $userInfo = User::getCurrent(); + + $this->auditLog->createLog( + $userInfo, + $action, + $params, + $_SERVER['REMOTE_ADDR'] ?? '::1', + $_SERVER['COUNTRY_CODE'] ?? 'XX' + ); + } + public function setUpHttp(bool $legacy = false): void { $this->router = new HttpFx; $this->router->use('/', function($response) { diff --git a/templates/manage/general/logs.twig b/templates/manage/general/logs.twig index d2e3bcd..43a52df 100644 --- a/templates/manage/general/logs.twig +++ b/templates/manage/general/logs.twig @@ -13,7 +13,7 @@ {% for log in global_logs %} - {{ user_account_log(log, true) }} + {{ user_account_log(log, global_logs_users) }} {% endfor %} {% endmacro %} -{% macro user_account_log(data, is_manage) %} +{% macro user_account_log(data, users) %} {% from 'macros.twig' import avatar %}