diff --git a/assets/misuzu.css/main.css b/assets/misuzu.css/main.css index 00dabb6..260b598 100644 --- a/assets/misuzu.css/main.css +++ b/assets/misuzu.css/main.css @@ -176,7 +176,7 @@ html { @include profile/header.css; @include profile/profile.css; @include profile/signature.css; -@include profile/warning.css; +@include profile/warnings.css; @include search/anchor.css; @include search/categories.css; diff --git a/assets/misuzu.css/manage/_manage.css b/assets/misuzu.css/manage/_manage.css index a003cf7..cf36949 100644 --- a/assets/misuzu.css/manage/_manage.css +++ b/assets/misuzu.css/manage/_manage.css @@ -43,3 +43,5 @@ @include manage/user-item.css; @include manage/user.css; @include manage/users.css; +@include manage/warning.css; +@include manage/warnings.css; diff --git a/assets/misuzu.css/manage/ban.css b/assets/misuzu.css/manage/ban.css index a0ff4b9..e9381bc 100644 --- a/assets/misuzu.css/manage/ban.css +++ b/assets/misuzu.css/manage/ban.css @@ -1,7 +1,3 @@ -.manage__ban { - /**/ -} - .manage__ban__field { margin: 2px; margin-bottom: 8px; diff --git a/assets/misuzu.css/manage/warning.css b/assets/misuzu.css/manage/warning.css new file mode 100644 index 0000000..1d2edad --- /dev/null +++ b/assets/misuzu.css/manage/warning.css @@ -0,0 +1,37 @@ +.manage__warning__field { + margin: 2px; + margin-bottom: 8px; +} + +.manage__warning__title { + font-size: 1.4em; + line-height: 1.5em; + padding: 0 4px; +} + +.manage__warning__desc { + font-size: .9em; + line-height: 1.5em; + font-style: italic; + border-bottom: 1px solid var(--accent-colour); + padding: 2px 4px; + margin-bottom: 1px; +} + +.manage__warning__body { + padding: 2px; + width: 100%; +} +.manage__warning__body textarea { + min-width: 100%; + max-width: 100%; + width: 100%; + min-height: 100px; +} + +.manage__warning__actions { + display: flex; + justify-content: center; + padding: 10px; + padding-top: 0; +} diff --git a/assets/misuzu.css/manage/warnings.css b/assets/misuzu.css/manage/warnings.css new file mode 100644 index 0000000..859122c --- /dev/null +++ b/assets/misuzu.css/manage/warnings.css @@ -0,0 +1,91 @@ +.manage__warnings__pagination { + margin: 2px; +} + +.manage__warnings__actions { + display: flex; + gap: 2px; + margin: 2px; +} + +.manage__warnings__item { + padding: 0 2px; + margin: 2px; + border-top: 1px solid var(--accent-colour); +} +.manage__warnings__item:not(:last-child) { + border-bottom: 1px solid var(--accent-colour); +} + +.manage__warnings__item__header { + display: flex; + overflow: hidden; + align-items: center; +} + +.manage__warnings__item__attributes { + flex-grow: 1; + flex-shrink: 1; + display: flex; + gap: 12px; + margin: 0 4px; + flex-wrap: wrap; +} + +.manage__warnings__item__attribute { + display: flex; + gap: 4px; + align-items: center; +} +.manage__warnings__item__created__icon { + font-size: 16px; +} + +.manage__warnings__item__actions { + display: flex; + flex-grow: 0; + flex-shrink: 0; + gap: 1px; + padding: 1px; + margin: 1px; +} +.manage__warnings__item__action { + width: 36px; + height: 36px; +} + +.manage__warnings__item__author a, +.manage__warnings__item__user a { + color: inherit; + text-decoration: none; +} +.manage__warnings__item__author__name a, +.manage__warnings__item__user__name a { + font-weight: bold; + display: inline-block; + padding-top: 2px; + border-bottom: 2px solid var(--user-colour, #fff); +} +.manage__warnings__item__user__filter a { + padding: 2px 4px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.2); + transition: background .2s; +} +.manage__warnings__item__user__filter a:hover, +.manage__warnings__item__user__filter a:focus { + background: rgba(255, 255, 255, 0.4); +} +.manage__warnings__item__user__filter a:active { + background: rgba(255, 255, 255, 0.1); +} + +.manage__warnings__item__reason { + margin: 1px 4px; + padding: 2px 4px; + border-top: 1px solid var(--accent-colour); +} +.manage__warnings__item__reason p { + padding-left: 4px; + border-left: 2px solid var(--accent-colour); +} diff --git a/assets/misuzu.css/profile/warning.css b/assets/misuzu.css/profile/warning.css deleted file mode 100644 index ad35b65..0000000 --- a/assets/misuzu.css/profile/warning.css +++ /dev/null @@ -1,135 +0,0 @@ -.profile__warning { - margin: 2px; - border-radius: 2px; - border: 1px solid var(--accent-colour); -} -.profile__warning__container { - margin: 2px 0; -} - -.profile__warning--warning { - --accent-colour: #666; -} - -.profile__warning--ban { - --accent-colour: #c33; -} - -.profile__warning--extendo { - margin: 4px; -} - -.profile__warning__background { - background-color: var(--accent-colour); - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.profile__warning__content { - background-color: var(--background-colour-translucent-9); - display: flex; - padding: 1px; -} - -.profile__warning__type, -.profile__warning__created, -.profile__warning__duration { - display: inline-flex; - align-items: center; - justify-content: center; -} - -.profile__warning__type { - min-width: 80px; - background-color: var(--accent-colour); - border-radius: 1px; - padding: 0 4px; -} - -.profile__warning__created, -.profile__warning__duration { - min-width: 100px; - padding: 0 4px; -} - -.profile__warning__note { - padding: 1px 4px; - flex: 1 1 auto; -} - -.profile__warning__private { - border-top: 1px solid var(--accent-colour); - margin-top: 1px; - width: 100%; - opacity: .5; - transition: opacity .2s; -} -.profile__warning__private:hover, -.profile__warning__private:active, -.profile__warning__private:focus { - opacity: 1; -} - -.profile__warning__tools { - display: flex; - padding-bottom: 1px; -} - -.profile__warning__options { - flex: 1 1 auto; - display: flex; - justify-content: flex-end; - align-items: center; -} - -.profile__warning__option { - padding: 2px 5px; - color: inherit; - text-decoration: none; -} - -.profile__warning__user { - display: flex; - padding: 2px; - min-width: 300px; -} - -.profile__warning__user__avatar { - width: 20px; - height: 20px; -} - -.profile__warning__user__username { - padding: 0 5px; - min-width: 60px; - color: inherit; - text-decoration: none; -} -.profile__warning__user__username:hover, -.profile__warning__user__username:focus, -.profile__warning__user__username:active { - text-decoration: underline; -} - -.profile__warning__user__ip { - display: inline-flex; - padding: 0 5px; -} -.profile__warning__user__ip:before { content: "("; } -.profile__warning__user__ip:after { content: ")"; } - - -@media (max-width: 800px) { - .profile__warning__content { - flex-wrap: wrap; - } - .profile__warning__tools { - flex-direction: column; - } - .profile__warning__options { - justify-content: flex-start; - } -} diff --git a/assets/misuzu.css/profile/warnings.css b/assets/misuzu.css/profile/warnings.css new file mode 100644 index 0000000..5fc00d8 --- /dev/null +++ b/assets/misuzu.css/profile/warnings.css @@ -0,0 +1,26 @@ +.profile__warnings { + display: flex; + flex-direction: column; + padding: 2px 5px; +} + +.profile__warnings__item { + padding-bottom: 5px; +} +.profile__warnings__item:not(:last-child) { + border-bottom: 1px solid #222; +} + +.profile__warnings__datetime { + font-size: .9em; + line-height: 1.5em; + font-style: italic; + padding-top: 2px; +} + +.profile__warnings__body { + padding: 0 5px; +} +.profile__warnings__body p { + line-height: 1.4em; +} diff --git a/database/2023_07_26_210150_redo_warnings_table.php b/database/2023_07_26_210150_redo_warnings_table.php new file mode 100644 index 0000000..7ad629f --- /dev/null +++ b/database/2023_07_26_210150_redo_warnings_table.php @@ -0,0 +1,43 @@ +execute(' + CREATE TABLE msz_users_warnings ( + warn_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + user_id INT(10) UNSIGNED NOT NULL, + mod_id INT(10) UNSIGNED NULL DEFAULT NULL, + warn_body TEXT NOT NULL, + warn_created TIMESTAMP NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (warn_id), + KEY users_warnings_user_foreign (user_id), + KEY users_warnings_mod_foreign (mod_id), + KEY users_warnings_created_index (warn_created), + CONSTRAINT users_warnings_user_foreign + FOREIGN KEY (user_id) + REFERENCES msz_users (user_id) + ON UPDATE CASCADE + ON DELETE CASCADE, + CONSTRAINT users_warnings_mod_foreign + FOREIGN KEY (mod_id) + REFERENCES msz_users (user_id) + ON UPDATE CASCADE + ON DELETE SET NULL + ) ENGINE=InnoDB COLLATE=utf8mb4_bin + '); + + // migrate existing warnings, public and private note have been merged but that's fine in prod + // still specifying type = 1 as well even though that should be the only type remaining + $conn->execute(' + INSERT INTO msz_users_warnings (user_id, mod_id, warn_body, warn_created) + SELECT user_id, issuer_id, TRIM(CONCAT(COALESCE(warning_note, ""), "\n", COALESCE(warning_note_private, ""))), warning_created + FROM msz_user_warnings + WHERE warning_type = 1 + '); + + // drop the old table with non-plural "user" + $conn->execute('DROP TABLE msz_user_warnings'); + } +} diff --git a/public-legacy/manage/users/ban.php b/public-legacy/manage/users/ban.php index 1dd4854..fc26dc3 100644 --- a/public-legacy/manage/users/ban.php +++ b/public-legacy/manage/users/ban.php @@ -100,6 +100,5 @@ $durations = array_flip([ Template::render('manage.users.ban', [ 'ban_user' => $userInfo, - 'ban_mod' => $modInfo, 'ban_durations' => $durations, ]); diff --git a/public-legacy/manage/users/warning.php b/public-legacy/manage/users/warning.php new file mode 100644 index 0000000..1696984 --- /dev/null +++ b/public-legacy/manage/users/warning.php @@ -0,0 +1,54 @@ +getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) { + echo render_error(403); + return; +} + +$warns = $msz->getWarnings(); + +if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) { + if(CSRF::validateRequest()) { + try { + $warnInfo = $warns->getWarning((string)filter_input(INPUT_GET, 'w')); + } catch(RuntimeException $ex) { + echo render_error(404); + return; + } + + $warns->deleteWarnings($warnInfo); + $msz->createAuditLog('WARN_DELETE', [$warnInfo->getId(), $warnInfo->getUserId()]); + url_redirect('manage-users-warnings', ['user' => $warnInfo->getUserId()]); + } else render_error(403); + return; +} + +try { + $userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT)); +} catch(RuntimeException $ex) { + echo render_error(404); + return; +} + +$modInfo = User::getCurrent(); + +while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { + $body = trim((string)filter_input(INPUT_POST, 'uw_body')); + Template::set('warn_value_body', $body); + + $warnInfo = $warns->createWarning( + $userInfo, $body, modInfo: $modInfo + ); + + $msz->createAuditLog('WARN_CREATE', [$warnInfo->getId(), $userInfo->getId()]); + url_redirect('manage-users-warnings', ['user' => $userInfo->getId()]); + return; +} + +Template::render('manage.users.warning', [ + 'warn_user' => $userInfo, +]); diff --git a/public-legacy/manage/users/warnings.php b/public-legacy/manage/users/warnings.php index a9e030c..b2ab99a 100644 --- a/public-legacy/manage/users/warnings.php +++ b/public-legacy/manage/users/warnings.php @@ -2,115 +2,62 @@ namespace Misuzu; use RuntimeException; -use InvalidArgumentException; use Misuzu\Users\User; -use Misuzu\Users\UserWarning; if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) { echo render_error(403); return; } -$notices = []; -$currentUser = User::getCurrent(); -$currentUserId = $currentUser->getId(); +$userInfos = [ + (string)User::getCurrent()->getId() => User::getCurrent(), +]; -if(!empty($_POST['lookup']) && is_string($_POST['lookup'])) { +$filterUser = null; +if(filter_has_var(INPUT_GET, 'u')) { + $filterUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); try { - $userId = User::byUsername((string)filter_input(INPUT_POST, 'lookup'))->getId(); + $filterUser = User::byId($filterUserId); + $userInfos[(string)$filterUser->getId()] = $filterUser; } catch(RuntimeException $ex) { - $userId = 0; - } - url_redirect('manage-users-warnings', ['user' => $userId]); - return; -} - -// instead of just kinda taking $_GET['w'] this should really fetch the info from the database -// and make sure that the user has authority -if(!empty($_GET['delete'])) { - try { - UserWarning::byId((int)filter_input(INPUT_GET, 'w', FILTER_SANITIZE_NUMBER_INT))->delete(); - } catch(RuntimeException $ex) {} - redirect($_SERVER['HTTP_REFERER'] ?? url('manage-users-warnings')); - return; -} - -if(!empty($_POST['warning']) && is_array($_POST['warning'])) { - try { - $warningsUserInfo = User::byId((int)($_POST['warning']['user'] ?? 0)); - $warningsUser = $warningsUserInfo->getId(); - - if(!$currentUser->hasAuthorityOver($warningsUserInfo)) - $notices[] = 'You do not have authority over this user.'; - } catch(RuntimeException $ex) { - $notices[] = 'This user doesn\'t exist.'; - } - - if(empty($notices) && !empty($warningsUserInfo)) { - try { - $warningInfo = UserWarning::create( - $warningsUserInfo, - $currentUser, - $_POST['warning']['note'], - $_POST['warning']['private'] - ); - } catch(InvalidArgumentException $ex) { - $notices[] = $ex->getMessage(); - } catch(RuntimeException $ex) { - $notices[] = 'Warning creation failed.'; - } + echo render_error(404); + return; } } -if(empty($warningsUser)) - $warningsUser = max(0, (int)($_GET['u'] ?? 0)); +$warns = $msz->getWarnings(); +$pagination = new Pagination($warns->countWarnings(userInfo: $filterUser), 10); -if(empty($warningsUserInfo)) - try { - $warningsUserInfo = User::byId($warningsUser); - } catch(RuntimeException $ex) { - $warningsUserInfo = null; - } - -$warningsPagination = new Pagination(UserWarning::countAll($warningsUserInfo), 10); - -if(!$warningsPagination->hasValidOffset()) { +if(!$pagination->hasValidOffset()) { echo render_error(404); return; } -// calling array_flip since the input_select macro wants value => display, but this looks cuter -$warningDurations = array_flip([ - 'Pick a duration...' => 0, - '5 Minutes' => 60 * 5, - '15 Minutes' => 60 * 15, - '30 Minutes' => 60 * 30, - '45 Minutes' => 60 * 45, - '1 Hour' => 60 * 60, - '2 Hours' => 60 * 60 * 2, - '3 Hours' => 60 * 60 * 3, - '6 Hours' => 60 * 60 * 6, - '12 Hours' => 60 * 60 * 12, - '1 Day' => 60 * 60 * 24, - '2 Days' => 60 * 60 * 24 * 2, - '1 Week' => 60 * 60 * 24 * 7, - '2 Weeks' => 60 * 60 * 24 * 7 * 2, - '1 Month' => 60 * 60 * 24 * 365 / 12, - '3 Months' => 60 * 60 * 24 * 365 / 12 * 3, - '6 Months' => 60 * 60 * 24 * 365 / 12 * 6, - '9 Months' => 60 * 60 * 24 * 365 / 12 * 9, - '1 Year' => 60 * 60 * 24 * 365, - 'Permanent' => -1, - 'Until (YYYY-MM-DD) ->' => -100, - 'Until (Seconds) ->' => -200, - 'Until (strtotime) ->' => -300, -]); +$warnList = []; +$warnInfos = $warns->getWarnings(userInfo: $filterUser, pagination: $pagination); + +foreach($warnInfos as $warnInfo) { + if(array_key_exists($warnInfo->getUserId(), $userInfos)) + $userInfo = $userInfos[$warnInfo->getUserId()]; + else + $userInfos[$warnInfo->getUserId()] = $userInfo = User::byId((int)$warnInfo->getUserId()); + + if(!$warnInfo->hasModId()) + $modInfo = null; + elseif(array_key_exists($warnInfo->getModId(), $userInfos)) + $modInfo = $userInfos[$warnInfo->getModId()]; + else + $userInfos[$warnInfo->getModId()] = $modInfo = User::byId((int)$warnInfo->getModId()); + + $warnList[] = [ + 'info' => $warnInfo, + 'user' => $userInfo, + 'mod' => $modInfo, + ]; +} Template::render('manage.users.warnings', [ - 'warnings' => [ - 'notices' => $notices, - 'pagination' => $warningsPagination, - 'list' => UserWarning::all($warningsUserInfo, $warningsPagination), - 'user' => $warningsUserInfo, - ], + 'manage_warns' => $warnList, + 'manage_warns_pagination' => $pagination, + 'manage_warns_filter_user' => $filterUser, ]); diff --git a/public-legacy/profile.php b/public-legacy/profile.php index 477044c..873c3ba 100644 --- a/public-legacy/profile.php +++ b/public-legacy/profile.php @@ -8,7 +8,6 @@ use Misuzu\Parsers\Parser; use Misuzu\Profile\ProfileFields; use Misuzu\Users\User; use Misuzu\Users\UserSession; -use Misuzu\Users\UserWarning; use Misuzu\Users\Assets\UserBackgroundAsset; $userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0; @@ -376,65 +375,62 @@ switch($profileMode) { case '': $template = 'profile.index'; - $warnings = UserWarning::byProfile($profileUser, $currentUser); - Template::set([ - 'profile_warnings' => $warnings, - 'profile_warnings_view_private' => $canManageWarnings, - 'profile_warnings_can_manage' => $canManageWarnings, - ]); + if(!$viewingAsGuest) { + Template::set('profile_warnings', $msz->getWarnings()->getWarningsWithDefaultBacklog($profileUser)); - if(!$viewingAsGuest && (!$isBanned || $canEdit)) { - $activeCategoryStats = forum_get_user_most_active_category_info($profileUser->getId()); - $activeCategoryInfo = empty($activeCategoryStats->forum_id) ? null : forum_get($activeCategoryStats->forum_id); + if((!$isBanned || $canEdit)) { + $activeCategoryStats = forum_get_user_most_active_category_info($profileUser->getId()); + $activeCategoryInfo = empty($activeCategoryStats->forum_id) ? null : forum_get($activeCategoryStats->forum_id); - $activeTopicStats = forum_get_user_most_active_topic_info($profileUser->getId()); - $activeTopicInfo = empty($activeTopicStats->topic_id) ? null : forum_topic_get($activeTopicStats->topic_id); + $activeTopicStats = forum_get_user_most_active_topic_info($profileUser->getId()); + $activeTopicInfo = empty($activeTopicStats->topic_id) ? null : forum_topic_get($activeTopicStats->topic_id); - $profileFieldValues = $profileFields->getFieldValues($profileUser); - $profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues); - $profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues); + $profileFieldValues = $profileFields->getFieldValues($profileUser); + $profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues); + $profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues); - $profileFieldRawValues = []; - $profileFieldLinkValues = []; - $profileFieldDisplayValues = []; + $profileFieldRawValues = []; + $profileFieldLinkValues = []; + $profileFieldDisplayValues = []; - // using field infos as the basis for now, uses the correct ordering - foreach($profileFieldInfos as $fieldInfo) { - unset($fieldValue); + // using field infos as the basis for now, uses the correct ordering + foreach($profileFieldInfos as $fieldInfo) { + unset($fieldValue); - foreach($profileFieldValues as $fieldValueTest) - if($fieldValueTest->getFieldId() === $fieldInfo->getId()) { - $fieldValue = $fieldValueTest; - break; - } - - $fieldName = $fieldInfo->getName(); - - if(isset($fieldValue)) { - foreach($profileFieldFormats as $fieldFormatTest) - if($fieldFormatTest->getId() === $fieldValue->getFormatId()) { - $fieldFormat = $fieldFormatTest; + foreach($profileFieldValues as $fieldValueTest) + if($fieldValueTest->getFieldId() === $fieldInfo->getId()) { + $fieldValue = $fieldValueTest; break; } - $profileFieldRawValues[$fieldName] = $fieldValue->getValue(); - $profileFieldDisplayValues[$fieldName] = $fieldFormat->formatDisplay($fieldValue->getValue()); - if($fieldFormat->hasLinkFormat()) - $profileFieldLinkValues[$fieldName] = $fieldFormat->formatLink($fieldValue->getValue()); - } - } + $fieldName = $fieldInfo->getName(); - Template::set([ - 'profile_active_category_stats' => $activeCategoryStats, - 'profile_active_category_info' => $activeCategoryInfo, - 'profile_active_topic_stats' => $activeTopicStats, - 'profile_active_topic_info' => $activeTopicInfo, - 'profile_fields_infos' => $profileFieldInfos, - 'profile_fields_raw_values' => $profileFieldRawValues, - 'profile_fields_display_values' => $profileFieldDisplayValues, - 'profile_fields_link_values' => $profileFieldLinkValues, - ]); + if(isset($fieldValue)) { + foreach($profileFieldFormats as $fieldFormatTest) + if($fieldFormatTest->getId() === $fieldValue->getFormatId()) { + $fieldFormat = $fieldFormatTest; + break; + } + + $profileFieldRawValues[$fieldName] = $fieldValue->getValue(); + $profileFieldDisplayValues[$fieldName] = $fieldFormat->formatDisplay($fieldValue->getValue()); + if($fieldFormat->hasLinkFormat()) + $profileFieldLinkValues[$fieldName] = $fieldFormat->formatLink($fieldValue->getValue()); + } + } + + Template::set([ + 'profile_active_category_stats' => $activeCategoryStats, + 'profile_active_category_info' => $activeCategoryInfo, + 'profile_active_topic_stats' => $activeTopicStats, + 'profile_active_topic_info' => $activeTopicInfo, + 'profile_fields_infos' => $profileFieldInfos, + 'profile_fields_raw_values' => $profileFieldRawValues, + 'profile_fields_display_values' => $profileFieldDisplayValues, + 'profile_fields_link_values' => $profileFieldLinkValues, + ]); + } } break; } diff --git a/src/AuditLog/AuditLogInfo.php b/src/AuditLog/AuditLogInfo.php index 105f916..e2413ee 100644 --- a/src/AuditLog/AuditLogInfo.php +++ b/src/AuditLog/AuditLogInfo.php @@ -129,5 +129,8 @@ class AuditLogInfo { 'BAN_CREATE' => 'Added ban #%d to user #%d.', 'BAN_DELETE' => 'Removed ban #%d from user #%d.', + + 'WARN_CREATE' => 'Added warning #%d to user #%d.', + 'WARN_DELETE' => 'Removed warning #%d from user #%d.', ]; } diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index 4c575ae..17393f9 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -15,6 +15,7 @@ use Misuzu\Users\Bans; use Misuzu\Users\BanInfo; use Misuzu\Users\ModNotes; use Misuzu\Users\User; +use Misuzu\Users\Warnings; use Index\Data\IDbConnection; use Index\Data\Migration\IDbMigrationRepo; use Index\Data\Migration\DbMigrationManager; @@ -42,6 +43,7 @@ class MisuzuContext { private RecoveryTokens $recoveryTokens; private ModNotes $modNotes; private Bans $bans; + private Warnings $warnings; public function __construct(IDbConnection $dbConn, IConfig $config) { $this->dbConn = $dbConn; @@ -55,6 +57,7 @@ class MisuzuContext { $this->recoveryTokens = new RecoveryTokens($this->dbConn); $this->modNotes = new ModNotes($this->dbConn); $this->bans = new Bans($this->dbConn); + $this->warnings = new Warnings($this->dbConn); } public function getDbConn(): IDbConnection { @@ -118,6 +121,10 @@ class MisuzuContext { return $this->bans; } + public function getWarnings(): Warnings { + return $this->warnings; + } + private array $activeBansCache = []; public function tryGetActiveBan(User|string|null $userInfo = null): ?BanInfo { diff --git a/src/Users/UserWarning.php b/src/Users/UserWarning.php deleted file mode 100644 index 7aae196..0000000 --- a/src/Users/UserWarning.php +++ /dev/null @@ -1,176 +0,0 @@ -warning_id; - } - - public function getUserId(): int { - return $this->user_id; - } - public function getUser(): User { - if($this->user === null) - $this->user = User::byId($this->getUserId()); - return $this->user; - } - - public function getUserRemoteAddress(): string { - return $this->user_ip; - } - - public function getIssuerId(): int { - return $this->issuer_id; - } - public function getIssuer(): User { - if($this->issuer === null) - $this->issuer = User::byId($this->getIssuerId()); - return $this->issuer; - } - - public function getIssuerRemoteAddress(): string { - return $this->issuer_ip; - } - - public function getCreatedTime(): int { - return $this->warning_created === null ? -1 : $this->warning_created; - } - - public function getType(): int { - return $this->warning_type; - } - - public function getPublicNote(): string { - return $this->warning_note; - } - - public function getPrivateNote(): string { - return $this->warning_note_private ?? ''; - } - public function hasPrivateNote(): bool { - return !empty($this->warning_note_private); - } - - public function delete(): void { - DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `warning_id` = :warning') - ->bind('warning', $this->warning_id) - ->execute(); - } - - public static function create( - User $user, - User $issuer, - string $publicNote, - ?string $privateNote = null, - ?string $targetAddr = null, - ?string $issuerAddr = null - ): self { - $targetAddr ??= $user->getLastRemoteAddress(); - $issuerAddr ??= $issuer->getLastRemoteAddress(); - - $warningId = DB::prepare( - 'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `user_ip`, `issuer_id`, `issuer_ip`, `warning_created`, `warning_duration`, `warning_type`, `warning_note`, `warning_note_private`)' - . ' VALUES (:user, INET6_ATON(:user_addr), :issuer, INET6_ATON(:issuer_addr), NOW(), NULL, 1, :public_note, :private_note)' - ) ->bind('user', $user->getId()) - ->bind('user_addr', $targetAddr) - ->bind('issuer', $issuer->getId()) - ->bind('issuer_addr', $issuerAddr) - ->bind('public_note', $publicNote) - ->bind('private_note', $privateNote) - ->executeGetId(); - - if($warningId < 1) - throw new RuntimeException('Failed to create new warning.'); - - return self::byId($warningId); - } - - 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 byId(int $warningId): self { - $object = DB::prepare( - self::byQueryBase() . ' WHERE `warning_id` = :warning' - ) ->bind('warning', $warningId) - ->fetchObject(self::class); - if(!$object) - throw new RuntimeException('Not warning with that ID could be found.'); - return $object; - } - public static function byProfile(User $user, ?User $viewer = null): array { - if($viewer === null - || !($user->getId() === $viewer->getId() - || perms_check_user(MSZ_PERMS_USER, $viewer->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) - ) return []; - - $getObjects = DB::prepare( - self::byQueryBase() - . ' WHERE `user_id` = :user' - . ' AND `warning_created` >= NOW() - INTERVAL ' . self::PROFILE_BACKLOG . ' DAY' - . ' ORDER BY `warning_created` DESC' - ); - - $getObjects->bind('user', $user->getId()); - - return $getObjects->fetchObjects(self::class); - } - public static function all(?User $user = null, ?Pagination $pagination = null): array { - $query = self::byQueryBase() - . ($user === null ? '' : ' WHERE `user_id` = :user') - . ' ORDER BY `warning_created` DESC'; - - if($pagination !== null) - $query .= ' LIMIT :range OFFSET :offset'; - - $getObjects = DB::prepare($query); - - if($user !== null) - $getObjects->bind('user', $user->getId()); - - if($pagination !== null) - $getObjects->bind('range', $pagination->getRange()) - ->bind('offset', $pagination->getOffset()); - - return $getObjects->fetchObjects(self::class); - } -} diff --git a/src/Users/WarningInfo.php b/src/Users/WarningInfo.php new file mode 100644 index 0000000..f07c92d --- /dev/null +++ b/src/Users/WarningInfo.php @@ -0,0 +1,57 @@ +id = (string)$result->getInteger(0); + $this->userId = (string)$result->getInteger(1); + $this->modId = $result->isNull(2) ? null : (string)$result->getInteger(2); + $this->body = $result->getString(3); + $this->created = $result->getInteger(4); + } + + public function getId(): string { + return $this->id; + } + + public function getUserId(): string { + return $this->userId; + } + + public function hasModId(): bool { + return $this->modId !== null; + } + + public function getModId(): ?string { + return $this->modId; + } + + public function hasBody(): bool { + return $this->body !== ''; + } + + public function getBody(): string { + return $this->body; + } + + public function getBodyLines(): array { + return explode("\n", $this->body); + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } +} diff --git a/src/Users/Warnings.php b/src/Users/Warnings.php new file mode 100644 index 0000000..f9093ff --- /dev/null +++ b/src/Users/Warnings.php @@ -0,0 +1,177 @@ +dbConn = $dbConn; + $this->cache = new DbStatementCache($dbConn); + } + + public function countWarnings( + User|string|null $userInfo = null, + ?int $backlog = null + ): int { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + + $hasUserInfo = $userInfo !== null; + $hasBacklog = $backlog !== null; + + $args = 0; + $query = 'SELECT COUNT(*) FROM msz_users_warnings'; + if($hasUserInfo) { + ++$args; + $query .= ' WHERE user_id = ?'; + } + if($hasBacklog) { + if($backlog < 1) + throw new InvalidArgumentException('$backlog must be either null to disable it or be greater than 0.'); + $query .= sprintf(' %s warn_created > NOW() - INTERVAL ? SECOND', ++$args > 1 ? 'AND' : 'WHERE'); + } + + $args = 0; + $stmt = $this->cache->get($query); + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasBacklog) + $stmt->addParameter(++$args, $backlog); + $stmt->execute(); + + $result = $stmt->getResult(); + $count = 0; + + if($result->next()) + $count = $result->getInteger(0); + + return $count; + } + + public function getWarningsWithDefaultBacklog( + User|string|null $userInfo = null, + ?Pagination $pagination = null + ): array { + return $this->getWarnings( + $userInfo, + self::VISIBLE_BACKLOG, + $pagination + ); + } + + public function getWarnings( + User|string|null $userInfo = null, + ?int $backlog = null, + ?Pagination $pagination = null + ): array { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + + $hasUserInfo = $userInfo !== null; + $hasBacklog = $backlog !== null; + $hasPagination = $pagination !== null; + + $args = 0; + $query = 'SELECT warn_id, user_id, mod_id, warn_body, UNIX_TIMESTAMP(warn_created) FROM msz_users_warnings'; + if($hasUserInfo) { + ++$args; + $query .= ' WHERE user_id = ?'; + } + if($hasBacklog) { + if($backlog < 1) + throw new InvalidArgumentException('$backlog must be either null to disable it or be greater than 0.'); + $query .= sprintf(' %s warn_created > NOW() - INTERVAL ? SECOND', ++$args > 1 ? 'AND' : 'WHERE'); + } + $query .= ' ORDER BY warn_created DESC'; + if($hasPagination) + $query .= ' LIMIT ? OFFSET ?'; + + $args = 0; + $stmt = $this->cache->get($query); + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo); + if($hasBacklog) + $stmt->addParameter(++$args, $backlog); + if($hasPagination) { + $stmt->addParameter(++$args, $pagination->getRange()); + $stmt->addParameter(++$args, $pagination->getOffset()); + } + $stmt->execute(); + + $result = $stmt->getResult(); + $warns = []; + + while($result->next()) + $warns[] = new WarningInfo($result); + + return $warns; + } + + public function getWarning(string $warnId): WarningInfo { + $stmt = $this->cache->get('SELECT warn_id, user_id, mod_id, warn_body, UNIX_TIMESTAMP(warn_created) FROM msz_users_warnings WHERE warn_id = ?'); + $stmt->addParameter(1, $warnId); + $stmt->execute(); + + $result = $stmt->getResult(); + + if(!$result->next()) + throw new RuntimeException('Could not find warning info for ID $warnId.'); + + return new WarningInfo($result); + } + + public function createWarning( + User|string $userInfo, + string $body, + User|string|null $modInfo + ): WarningInfo { + if($userInfo instanceof User) + $userInfo = (string)$userInfo->getId(); + if($modInfo instanceof User) + $modInfo = (string)$modInfo->getId(); + + $stmt = $this->cache->get('INSERT INTO msz_users_warnings (user_id, mod_id, warn_body) VALUES (?, ?, ?)'); + $stmt->addParameter(1, $userInfo); + $stmt->addParameter(2, $modInfo); + $stmt->addParameter(3, $body); + $stmt->execute(); + + return $this->getWarning((string)$this->dbConn->getLastInsertId()); + } + + public function deleteWarnings(WarningInfo|string|array $warnInfos): void { + if(!is_array($warnInfos)) + $warnInfos = [$warnInfos]; + + $stmt = $this->cache->get(sprintf( + 'DELETE FROM msz_users_warnings WHERE warn_id IN (%s)', + DbTools::prepareListString($warnInfos) + )); + + $args = 0; + foreach($warnInfos as $warnInfo) { + if($warnInfo instanceof WarningInfo) + $warnInfo = $warnInfo->getId(); + elseif(!is_string($warnInfo)) + throw new InvalidArgumentException('$warnInfos must be strings of instances of WarningInfo.'); + + $stmt->addParameter(++$args, $warnInfo); + } + + $stmt->execute(); + } +} diff --git a/src/url.php b/src/url.php index 6e33370..1435802 100644 --- a/src/url.php +++ b/src/url.php @@ -125,7 +125,8 @@ define('MSZ_URLS', [ 'manage-users' => ['/manage/users'], 'manage-user' => ['/manage/users/user.php', ['u' => '']], 'manage-users-warnings' => ['/manage/users/warnings.php', ['u' => '']], - 'manage-users-warning-delete' => ['/manage/users/warnings.php', ['w' => '', 'delete' => '1', 'csrf' => '{csrf}']], + 'manage-users-warning' => ['/manage/users/warning.php', ['u' => '']], + 'manage-users-warning-delete' => ['/manage/users/warning.php', ['w' => '', 'delete' => '1', 'csrf' => '{csrf}']], 'manage-users-notes' => ['/manage/users/notes.php', ['u' => '']], 'manage-users-note' => ['/manage/users/note.php', ['n' => '', 'u' => '']], 'manage-users-note-delete' => ['/manage/users/note.php', ['n' => '', 'delete' => '1', 'csrf' => '{csrf}']], diff --git a/templates/manage/users/note.twig b/templates/manage/users/note.twig index d7e1ddd..e10f290 100644 --- a/templates/manage/users/note.twig +++ b/templates/manage/users/note.twig @@ -4,7 +4,7 @@ {% block manage_content %}
- {{ container_title(' ' ~ (note_new ? ('Adding note to ' ~ note_user.username) : ('Editing note #' ~ note_info.id))) }} + {{ container_title(' ' ~ (note_new ? ('Adding mod note to ' ~ note_user.username) : ('Editing mod note #' ~ note_info.id))) }}
{{ input_csrf() }} diff --git a/templates/manage/users/notes.twig b/templates/manage/users/notes.twig index 028ef20..9c65100 100644 --- a/templates/manage/users/notes.twig +++ b/templates/manage/users/notes.twig @@ -6,7 +6,7 @@ {% block manage_content %}
- {{ container_title(' User Notes') }} + {{ container_title(' Moderator Notes') }}
Private moderator notes, can be used for anything you'd like to share internally. diff --git a/templates/manage/users/warning.twig b/templates/manage/users/warning.twig index acc97c8..c9f4d08 100644 --- a/templates/manage/users/warning.twig +++ b/templates/manage/users/warning.twig @@ -1,8 +1,25 @@ {% extends 'manage/users/master.twig' %} -{% from 'macros.twig' import pagination, container_title %} -{% from 'user/macros.twig' import user_profile_warning %} -{% from '_layout/input.twig' import input_text, input_csrf, input_select, input_hidden %} +{% from 'macros.twig' import pagination, container_title, avatar %} +{% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_select %} {% block manage_content %} +
+ {{ container_title(' Issuing a warning to user #' ~ warn_user.id ~ ' ' ~ warn_user.username) }} + + {{ input_csrf() }} + +
+
Warning Body
+
A concise explanation of what the user did to deserve this warning and what they should do to prevent further incidents. Please keep in mind that warnings remain publicly visible for 90 days both to the person receiving the warning and other logging in people viewing their profile.
+
+ +
+
+ +
+ +
+ +
{% endblock %} diff --git a/templates/manage/users/warnings.twig b/templates/manage/users/warnings.twig index c772dc9..6a06df9 100644 --- a/templates/manage/users/warnings.twig +++ b/templates/manage/users/warnings.twig @@ -1,92 +1,84 @@ {% extends 'manage/users/master.twig' %} -{% from 'macros.twig' import pagination, container_title %} -{% from 'user/macros.twig' import user_profile_warning %} -{% from '_layout/input.twig' import input_text, input_csrf, input_select, input_hidden %} +{% from 'macros.twig' import pagination, container_title, avatar %} + +{% set warns_pagination = pagination(manage_warns_pagination, url('manage-users-warnings', {'user': manage_warns_filter_user.id|default(0)})) %} +{% set warns_filtering = manage_warns_filter_user is not null %} {% block manage_content %} -
- {{ container_title(' Filters') }} - {{ input_text('lookup', null, warnings.user.username|default(''), 'text', 'Enter a username') }} - -
- - {% if warnings.notices|length > 0 %} -
-
- {% for notice in warnings.notices %} - {{ notice }} - {% endfor %} -
-
- {% endif %} - - {% if warnings.user is not null %} -
- {{ container_title(' Warn ' ~ warnings.user.username) }} - {{ input_csrf() }} - {{ input_hidden('warning[user]', warnings.user.id) }} - - {{ input_text('warning[note]', '', '', 'text', 'Public note') }} -
- - -
- {% endif %} - -
+
{{ container_title(' Warnings') }} - {% set warnpag = pagination(warnings.pagination, url('manage-users-warnings', {'user': warnings.user.id|default(0)})) %} - {{ warnpag }} - -
-
-
- -
-
-
- User -
-
- User IP -
-
- -
-
- Issuer -
-
- Issuer IP -
-
-
- -
-
- Type -
- -
- Created -
- -
- Duration -
- -
- Note -
-
-
- - {% for warning in warnings.list %} - {{ user_profile_warning(warning, true, true, csrf_token()) }} - {% endfor %} +
+ List of user warnings. + {% if not warns_filtering %}Filter by a user to issue a new warning.{% endif %}
- {{ warnpag }} + {% if warns_pagination|trim|length > 0 %} +
+ {{ warns_pagination }} +
+ {% endif %} + + {% if warns_filtering %} + + {% endif %} + +
+ {% for warn in manage_warns %} +
+
+
+ {% if warn.mod is not null %} + + {% endif %} +
+
+
+ +
+
+
+
Subject
+ + + {% if not warns_filtering %} +
+ Filter +
+ {% endif %} +
+
+
+ +
+
+
+ {% for line in warn.info.bodyLines %} +

{{ line }}

+ {% endfor %} +
+
+ {% endfor %} +
+ + {% if warns_pagination|trim|length > 0 %} +
+ {{ warns_pagination }} +
+ {% endif %}
{% endblock %} diff --git a/templates/profile/index.twig b/templates/profile/index.twig index 4f64b10..25d4249 100644 --- a/templates/profile/index.twig +++ b/templates/profile/index.twig @@ -1,6 +1,5 @@ {% extends 'profile/master.twig' %} {% from 'macros.twig' import container_title %} -{% from 'user/macros.twig' import user_profile_warning %} {% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_file, input_file_raw, input_select %} {% if profile_user is defined %} @@ -81,7 +80,8 @@ {% set show_background_settings = profile_is_editing and perms.edit_background %} {% set show_birthdate = profile_is_editing and perms.edit_birthdate %} {% set show_active_forum_info = not profile_is_editing and (profile_active_category_info.forum_id|default(0) > 0 or profile_active_topic_info.topic_id|default(0) > 0) %} - {% set show_sidebar = (not profile_is_banned or profile_can_edit) and profile_is_guest or show_profile_fields or show_background_settings or show_birthdate or show_active_forum_info %} + {% set show_warnings = profile_warnings is defined and profile_warnings|length > 0 %} + {% set show_sidebar = (not profile_is_banned or profile_can_edit) and (profile_is_guest or show_profile_fields or show_background_settings or show_birthdate or show_active_forum_info or show_warnings) %} {% if show_sidebar %}
@@ -265,6 +265,26 @@
{% endif %} + {% if show_warnings %} +
+ {{ container_title('Warnings') }} + +
+ {% for warning in profile_warnings %} +
+
+ +
+
+ {% for line in warning.bodyLines %} +

{{ line }}

+ {% endfor %} +
+
+ {% endfor %} +
+
+ {% endif %}
{% endif %} @@ -303,57 +323,6 @@ {% endif %}
{% endif %} - - {% if profile_warnings|length > 0 or profile_warnings_can_manage %} -
- {{ container_title('Account Standing', false, profile_warnings_can_manage ? url('manage-users-warnings', {'user': profile_user.id}) : '') }} - -
-
- - {% if profile_warnings_can_manage %} -
-
-
- User IP -
-
- -
-
- Issuer -
-
- Issuer IP -
-
-
- {% endif %} - -
-
- Type -
- -
- Created -
- -
- Duration -
- -
- Note -
-
-
- - {% for warning in profile_warnings %} - {{ user_profile_warning(warning, profile_warnings_view_private, profile_warnings_can_manage, profile_warnings_can_manage ? csrf_token() : '') }} - {% endfor %} -
- {% endif %} {% endif %}
diff --git a/templates/user/macros.twig b/templates/user/macros.twig index 7cc3967..b7e0284 100644 --- a/templates/user/macros.twig +++ b/templates/user/macros.twig @@ -248,70 +248,3 @@ {% endmacro %} - -{% macro user_profile_warning(warning, show_private_note, show_user_info, delete_csrf) %} - {% from 'macros.twig' import avatar %} - - -{% endmacro %}