Rewrote audit log on new database backend.

This commit is contained in:
flash 2023-07-17 17:43:17 +00:00
parent 96be282a93
commit 1a11a8f8ba
25 changed files with 365 additions and 302 deletions

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Config\IConfig;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
@ -79,7 +78,7 @@ while($canResetPassword) {
->removeTOTPKey()
->save();
AuditLog::create(AuditLog::PASSWORD_RESET, [], $userInfo);
$msz->createAuditLog('PASSWORD_RESET', [], $userInfo);
$tokenInfo->invalidate();

View file

@ -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(),
'<username>',
]);
} 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(),
'<username>',

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
@ -104,7 +103,7 @@ switch($postMode) {
$deletePost = forum_post_delete($postInfo['post_id']);
if($deletePost) {
AuditLog::create(AuditLog::FORUM_POST_DELETE, [$postInfo['post_id']]);
$msz->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;

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
@ -164,7 +163,7 @@ if(in_array($moderationMode, $validModerationModes, true)) {
$deleteTopic = forum_topic_delete($topic['topic_id']);
if($deleteTopic)
AuditLog::create(AuditLog::FORUM_TOPIC_DELETE, [$topic['topic_id']]);
$msz->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', [

View file

@ -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()]
);

View file

@ -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()]
);

View file

@ -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;

View file

@ -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()]
);

View file

@ -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]);
}
}
}

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Pagination;
use Misuzu\Users\User;
@ -12,16 +11,25 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent(
return;
}
$pagination = new Pagination(AuditLog::countAll(), 50);
$auditLog = $msz->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,
]);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Config;
use Misuzu\Config\CfgTools;
use Misuzu\Users\User;
@ -22,7 +21,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
if(!CSRF::validateRequest())
throw new \Exception("Request verification failed.");
AuditLog::create(AuditLog::CONFIG_DELETE, [$sName]);
$msz->createAuditLog('CONFIG_DELETE', [$sName]);
$cfg->removeValue($sName);
url_redirect('manage-general-settings');
} else {

View file

@ -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;

View file

@ -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()]
);

View file

@ -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()]
);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Users\User;
@ -103,7 +102,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
}
} else {
$currentUser->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');
}
}
}

View file

@ -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);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Pagination;
use Misuzu\Users\User;
use Misuzu\Users\UserLoginAttempt;
@ -15,12 +14,16 @@ if($currentUser === null) {
return;
}
$auditLog = $msz->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,
]);

View file

@ -1,7 +1,6 @@
<?php
namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserSessionNotFoundException;
@ -38,12 +37,12 @@ if(!empty($_POST['session']) && CSRF::validateRequest()) {
}
$sessionInfo->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) {

View file

@ -1,230 +0,0 @@
<?php
namespace Misuzu;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class AuditLog {
public const PERSONAL_EMAIL_CHANGE = 'PERSONAL_EMAIL_CHANGE';
public const PERSONAL_PASSWORD_CHANGE = 'PERSONAL_PASSWORD_CHANGE';
public const PERSONAL_SESSION_DESTROY = 'PERSONAL_SESSION_DESTROY';
public const PERSONAL_SESSION_DESTROY_ALL = 'PERSONAL_SESSION_DESTROY_ALL';
public const PERSONAL_DATA_DOWNLOAD = 'PERSONAL_DATA_DOWNLOAD';
public const PASSWORD_RESET = 'PASSWORD_RESET';
public const CHANGELOG_ENTRY_CREATE = 'CHANGELOG_ENTRY_CREATE';
public const CHANGELOG_ENTRY_EDIT = 'CHANGELOG_ENTRY_EDIT';
public const CHANGELOG_TAG_ADD = 'CHANGELOG_TAG_ADD';
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';
public const COMMENT_ENTRY_DELETE = 'COMMENT_ENTRY_DELETE';
public const COMMENT_ENTRY_DELETE_MOD = 'COMMENT_ENTRY_DELETE_MOD';
public const COMMENT_ENTRY_RESTORE = 'COMMENT_ENTRY_RESTORE';
public const NEWS_POST_CREATE = 'NEWS_POST_CREATE';
public const NEWS_POST_EDIT = 'NEWS_POST_EDIT';
public const NEWS_POST_DELETE = 'NEWS_POST_DELETE';
public const NEWS_CATEGORY_CREATE = 'NEWS_CATEGORY_CREATE';
public const NEWS_CATEGORY_EDIT = 'NEWS_CATEGORY_EDIT';
public const NEWS_CATEGORY_DELETE = 'NEWS_CATEGORY_DELETE';
public const FORUM_TOPIC_DELETE = 'FORUM_TOPIC_DELETE';
public const FORUM_TOPIC_RESTORE = 'FORUM_TOPIC_RESTORE';
public const FORUM_TOPIC_NUKE = 'FORUM_TOPIC_NUKE';
public const FORUM_TOPIC_BUMP = 'FORUM_TOPIC_BUMP';
public const FORUM_TOPIC_LOCK = 'FORUM_TOPIC_LOCK';
public const FORUM_TOPIC_UNLOCK = 'FORUM_TOPIC_UNLOCK';
public const FORUM_TOPIC_REDIR_CREATE = 'FORUM_TOPIC_REDIR_CREATE';
public const FORUM_TOPIC_REDIR_REMOVE = 'FORUM_TOPIC_REDIR_REMOVE';
public const FORUM_POST_EDIT = 'FORUM_POST_EDIT';
public const FORUM_POST_DELETE = 'FORUM_POST_DELETE';
public const FORUM_POST_RESTORE = 'FORUM_POST_RESTORE';
public const FORUM_POST_NUKE = 'FORUM_POST_NUKE';
public const CONFIG_CREATE = 'CONFIG_CREATE';
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.',
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);
}
}

151
src/AuditLog/AuditLog.php Normal file
View file

@ -0,0 +1,151 @@
<?php
namespace Misuzu\AuditLog;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Misuzu\DbStatementCache;
use Misuzu\Pagination;
use Misuzu\Users\User;
class AuditLog {
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->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();
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace Misuzu\AuditLog;
use ValueError;
use Index\DateTime;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
use Misuzu\Users\User;
class AuditLogInfo {
private ?string $userId;
private string $action;
private array $params;
private int $created;
private string $address;
private string $country;
public function __construct(IDbResult $result) {
$this->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.',
];
}

View file

@ -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 {

View file

@ -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) {

View file

@ -13,7 +13,7 @@
</div>
{% for log in global_logs %}
{{ user_account_log(log, true) }}
{{ user_account_log(log, global_logs_users) }}
{% endfor %}
<div class="settings__account-logs__pagination">

View file

@ -327,23 +327,24 @@
</div>
{% endmacro %}
{% macro user_account_log(data, is_manage) %}
{% macro user_account_log(data, users) %}
{% from 'macros.twig' import avatar %}
<div class="settings__account-log">
{% if is_manage %}
<a href="{{ url('user-profile', {'user': data.user.id}) }}" class="settings__account-log__user" style="--user-colour: {{ data.user.colour }}">
<div class="settings__account-log__user__avatar">{{ avatar(data.user.id, 20, data.user.username) }}</div>
<div class="settings__account-log__user__name">{{ data.user.username }}</div>
{% if data.hasUserId and users[data.userId] is defined %}
{% set user = users[data.userId] %}
<a href="{{ url('user-profile', {'user': user.id}) }}" class="settings__account-log__user" style="--user-colour: {{ user.colour }}">
<div class="settings__account-log__user__avatar">{{ avatar(user.id, 20, user.username) }}</div>
<div class="settings__account-log__user__name">{{ user.username }}</div>
</a>
{% endif %}
<div class="settings__account-log__container">
<div class="settings__account-log__important">
<div class="flag flag--{{ data.country|lower }} settings__login-attempt__flag" title="{{ data.countryName }}">{{ data.country }}</div>
<div class="flag flag--{{ data.countryCode|lower }} settings__login-attempt__flag" title="{{ data.countryCode|country_name }}">{{ data.countryCode }}</div>
<div class="settings__login-attempt__description">
{{ data.string }}
{{ data.formatted }}
</div>
</div>