Rewrote warnings system to be OOP, also added permanent warnings.

This commit is contained in:
flash 2020-06-01 00:33:16 +00:00
parent 8602e6a87e
commit 73d3552dcb
23 changed files with 453 additions and 397 deletions

View file

@ -221,8 +221,6 @@ if($authToken->isValid()) {
user_bump_last_active($userInfo->getId());
$userDisplayInfo['perms'] = perms_get_user($userInfo->getId());
$userDisplayInfo['ban_expiration'] = user_warning_check_expiration($userInfo->getId(), MSZ_WARN_BAN);
$userDisplayInfo['silence_expiration'] = $userDisplayInfo['ban_expiration'] > 0 ? 0 : user_warning_check_expiration($userInfo->getId(), MSZ_WARN_SILENCE);
}
}
@ -254,12 +252,15 @@ if(Config::get('private.enabled', Config::TYPE_BOOL)) {
}
}
if(!empty($userDisplayInfo)) // delete this
// delete these
if(!empty($userDisplayInfo))
Template::set('current_user', $userDisplayInfo);
if(!empty($userInfo))
Template::set('current_user2', $userInfo);
$inManageMode = starts_with($_SERVER['REQUEST_URI'], '/manage');
$hasManageAccess = User::hasCurrent()
&& !user_warning_check_restriction(User::getCurrent()->getId())
&& !User::getCurrent()->hasActiveWarning()
&& perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_CAN_MANAGE);
Template::set('has_manage_access', $hasManageAccess);

View file

@ -6,6 +6,7 @@ use Misuzu\Net\IPAddressBlacklist;
use Misuzu\Users\User;
use Misuzu\Users\UserLoginAttempt;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserWarning;
require_once '../../misuzu.php';
@ -19,7 +20,7 @@ $notices = [];
$ipAddress = IPAddress::remote();
$remainingAttempts = UserLoginAttempt::remaining();
$restricted = IPAddressBlacklist::check($ipAddress) ? 'blacklist'
: (user_warning_check_ip($ipAddress) ? 'ban' : '');
: (UserWarning::countByRemoteAddress() > 0 ? 'ban' : '');
while(!$restricted && !empty($register)) {
if(!CSRF::validateRequest()) {

View file

@ -36,11 +36,11 @@ if($currentUserInfo === null) {
return;
}
if(user_warning_check_expiration($currentUserInfo->getId(), MSZ_WARN_BAN) > 0) {
if($currentUserInfo->isBanned()) {
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
return;
}
if(user_warning_check_expiration($currentUserInfo->getId(), MSZ_WARN_SILENCE) > 0) {
if($currentUserInfo->isSilenced()) {
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
return;
}

View file

@ -29,9 +29,8 @@ if(!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) {
return;
}
if(user_warning_check_restriction($forumUserId)) {
if($forumUser->hasActiveWarning())
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
}
Template::set('forum_perms', $perms);

View file

@ -29,11 +29,11 @@ if($currentUser === null) {
$currentUserId = $currentUser->getId();
if(user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
if($currentUser->isBanned()) {
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
return;
}
if(user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) {
if($currentUser->isSilenced()) {
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
return;
}

View file

@ -33,11 +33,11 @@ if(!empty($postMode) && !UserSession::hasCurrent()) {
$currentUser = User::getCurrent():
$currentUserId = $currentUser === null ? 0 : $currentUser->getId();
if(user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
if($currentUser->isBanned()) {
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
return;
}
if(user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) {
if($currentUser->isSilenced()) {
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
return;
}

View file

@ -16,7 +16,7 @@ if($currentUser === null) {
$currentUserId = $currentUser->getId();
if(user_warning_check_restriction($currentUserId)) {
if($currentUser->hasActiveWarning()) {
echo render_error(403);
return;
}

View file

@ -2,6 +2,7 @@
namespace Misuzu;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
require_once '../../misuzu.php';
@ -22,9 +23,8 @@ $perms = $topic
? forum_perms_get_user($topic['forum_id'], $topicUserId)[MSZ_FORUM_PERMS_GENERAL]
: 0;
if(user_warning_check_restriction($topicUserId)) {
if($topicUser->hasActiveWarning())
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
}
$topicIsDeleted = !empty($topic['topic_deleted']);
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);

View file

@ -28,9 +28,8 @@ $perms = $topic
? forum_perms_get_user($topic['forum_id'], $topicUserId)[MSZ_FORUM_PERMS_GENERAL]
: 0;
if(user_warning_check_restriction($topicUserId)) {
if($topicUser->hasActiveWarning())
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
}
$topicIsDeleted = !empty($topic['topic_deleted']);
$canDeleteAny = perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST);
@ -99,11 +98,11 @@ if(in_array($moderationMode, $validModerationModes, true)) {
return;
}
if(user_warning_check_expiration($topicUserId, MSZ_WARN_BAN) > 0) {
if($topicUser->isBanned()) {
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
return;
}
if(user_warning_check_expiration($topicUserId, MSZ_WARN_SILENCE) > 0) {
if($topicUser->isSilenced()) {
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
return;
}

View file

@ -1,8 +1,13 @@
<?php
namespace Misuzu;
use InvalidArgumentException;
use Misuzu\Net\IPAddress;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\UserWarning;
use Misuzu\Users\UserWarningNotFoundException;
use Misuzu\Users\UserWarningCreationFailedException;
require_once '../../../misuzu.php';
@ -22,111 +27,89 @@ if(!empty($_POST['lookup']) && is_string($_POST['lookup'])) {
// 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'])) {
user_warning_remove((int)($_GET['w'] ?? 0));
try {
UserWarning::byId((int)filter_input(INPUT_GET, 'w', FILTER_SANITIZE_NUMBER_INT))->delete();
} catch(UserWarningNotFoundException $ex) {}
redirect($_SERVER['HTTP_REFERER'] ?? url('manage-users-warnings'));
return;
}
if(!empty($_POST['warning']) && is_array($_POST['warning'])) {
$warningType = (int)($_POST['warning']['type'] ?? 0);
$warningDuration = 0;
$warningDuration = (int)($_POST['warning']['duration'] ?? 0);
if(user_warning_type_is_valid($warningType)) {
$warningDuration = 0;
if($warningDuration < -1) {
$customDuration = $_POST['warning']['duration_custom'] ?? '';
if(user_warning_has_duration($warningType)) {
$duration = (int)($_POST['warning']['duration'] ?? 0);
if(!empty($customDuration)) {
switch($warningDuration) {
case -100: // YYYY-MM-DD
$splitDate = array_apply(explode('-', $customDuration, 3), function ($a) {
return (int)$a;
});
if($duration > 0) {
$warningDuration = time() + $duration;
} elseif($duration < 0) {
$customDuration = $_POST['warning']['duration_custom'] ?? '';
if(!empty($customDuration)) {
switch($duration) {
case -1: // YYYY-MM-DD
$splitDate = array_apply(explode('-', $customDuration, 3), function ($a) {
return (int)$a;
});
if(checkdate($splitDate[1], $splitDate[2], $splitDate[0])) {
$warningDuration = mktime(0, 0, 0, $splitDate[1], $splitDate[2], $splitDate[0]);
}
break;
case -2: // Raw seconds
$warningDuration = time() + (int)$customDuration;
break;
case -3: // strtotime
$warningDuration = strtotime($customDuration);
break;
}
}
}
if($warningDuration <= time()) {
$notices[] = 'The duration supplied was invalid.';
}
}
$warningsUser = (int)($_POST['warning']['user'] ?? 0);
if(!user_check_super($currentUserId) && !user_check_authority($currentUserId, $warningsUser)) {
$notices[] = 'You do not have authority over this user.';
}
if(empty($notices) && $warningsUser > 0) {
$warningId = user_warning_add(
$warningsUser,
user_get_last_ip($warningsUser),
$currentUserId,
IPAddress::remote(),
$warningType,
$_POST['warning']['note'],
$_POST['warning']['private'],
$warningDuration
);
}
if(!empty($warningId) && $warningId < 0) {
switch($warningId) {
case MSZ_E_WARNING_ADD_DB:
$notices[] = 'Failed to record the warning in the database.';
if(checkdate($splitDate[1], $splitDate[2], $splitDate[0]))
$warningDuration = mktime(0, 0, 0, $splitDate[1], $splitDate[2], $splitDate[0]) - time();
break;
case MSZ_E_WARNING_ADD_TYPE:
$notices[] = 'The warning type provided was invalid.';
case -200: // Raw seconds
$warningDuration = (int)$customDuration;
break;
case MSZ_E_WARNING_ADD_USER:
$notices[] = 'The User ID provided was invalid.';
break;
case MSZ_E_WARNING_ADD_DURATION:
$notices[] = 'The duration specified was invalid.';
case -300: // strtotime
$warningDuration = strtotime($customDuration) - time();
break;
}
}
}
try {
$warningsUserInfo = User::byId((int)($_POST['warning']['user'] ?? 0));
$warningsUser = $warningsUserInfo->getId();
} catch(UserNotFoundException $ex) {
$warningsUserInfo = null;
}
if(!user_check_super($currentUserId) && !user_check_authority($currentUserId, $warningsUser)) {
$notices[] = 'You do not have authority over this user.';
}
if(empty($notices) && $warningsUser > 0) {
try {
$warningInfo = UserWarning::create(
$warningsUserInfo,
User::getCurrent(),
$warningType,
$warningDuration,
$_POST['warning']['note'],
$_POST['warning']['private']
);
} catch(InvalidArgumentException $ex) {
$notices[] = $ex->getMessage();
} catch(UserWarningCreationFailedException $ex) {
$notices[] = 'Warning creation failed.';
}
}
}
if(empty($warningsUser)) {
if(empty($warningsUser))
$warningsUser = max(0, (int)($_GET['u'] ?? 0));
}
$warningsPagination = new Pagination(user_warning_global_count($warningsUser), 50);
if(empty($warningsUserInfo))
try {
$warningsUserInfo = User::byId($warningsUser);
} catch(UserNotFoundException $ex) {
$warningsUserInfo = null;
}
$warningsPagination = new Pagination(UserWarning::countAll($warningsUserInfo), 10);
if(!$warningsPagination->hasValidOffset()) {
echo render_error(404);
return;
}
$warningsList = user_warning_global_fetch(
$warningsPagination->getOffset(),
$warningsPagination->getRange(),
$warningsUser
);
// calling array_flip since the input_select macro wants value => display, but this looks cuter
$warningDurations = array_flip([
'Pick a duration...' => 0,
@ -148,19 +131,24 @@ $warningDurations = array_flip([
'6 Months' => 60 * 60 * 24 * 365 / 12 * 6,
'9 Months' => 60 * 60 * 24 * 365 / 12 * 9,
'1 Year' => 60 * 60 * 24 * 365,
'Until (YYYY-MM-DD) ->' => -1,
'Until (Seconds) ->' => -2,
'Until (strtotime) ->' => -3,
'Permanent' => -1,
'Until (YYYY-MM-DD) ->' => -100,
'Until (Seconds) ->' => -200,
'Until (strtotime) ->' => -300,
]);
Template::render('manage.users.warnings', [
'warnings' => [
'notices' => $notices,
'pagination' => $warningsPagination,
'list' => $warningsList,
'user_id' => $warningsUser,
'username' => user_username_from_id($warningsUser),
'types' => user_warning_get_types(),
'list' => UserWarning::all($warningsUserInfo, $warningsPagination),
'user' => $warningsUserInfo,
'durations' => $warningDurations,
'types' => [
UserWarning::TYPE_NOTE => 'Note',
UserWarning::TYPE_WARN => 'Warning',
UserWarning::TYPE_MUTE => 'Silence',
UserWarning::TYPE_BAHN => 'Ban',
],
],
]);

View file

@ -26,8 +26,8 @@ $currentUser = User::getCurrent();
$viewingAsGuest = $currentUser === null;
$currentUserId = $viewingAsGuest ? 0 : $currentUser->getId();
$viewingOwnProfile = $currentUserId === $profileUser->getId();
$isBanned = user_warning_check_restriction($profileUser->getId());
define('DUMB_SHIT', true);
$isBanned = $profileUser->hasActiveWarning();
$userPerms = perms_get_user($currentUserId)[MSZ_PERMS_USER];
$canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS);
$canEdit = !$isBanned
@ -406,23 +406,11 @@ switch($profileMode) {
case '':
$template = 'profile.index';
$warnings = $viewingAsGuest
? []
: user_warning_fetch(
$profileUser->getId(),
90,
$canManageWarnings
? MSZ_WARN_TYPES_VISIBLE_TO_STAFF
: (
$viewingOwnProfile
? MSZ_WARN_TYPES_VISIBLE_TO_USER
: MSZ_WARN_TYPES_VISIBLE_TO_PUBLIC
)
);
$warnings = $profileUser->getProfileWarnings($currentUser);
Template::set([
'profile_warnings' => $warnings,
'profile_warnings_view_private' => $viewingOwnProfile,
'profile_warnings_view_private' => $canManageWarnings,
'profile_warnings_can_manage' => $canManageWarnings,
]);
break;

View file

@ -34,7 +34,7 @@ if($currentUser === null) {
return;
}
if(user_warning_check_expiration($currentUser->getId(), MSZ_WARN_BAN) > 0) {
if($currentUser->isBanned()) {
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
return;
}

View file

@ -18,7 +18,7 @@ if(!UserSession::hasCurrent()) {
$errors = [];
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$isRestricted = user_warning_check_restriction($currentUserId);
$isRestricted = $currentUser->hasActiveWarning();
$twoFactorInfo = user_totp_info($currentUserId);
$isVerifiedRequest = CSRF::validateRequest();

View file

@ -19,7 +19,7 @@ try {
$userId = $userExists ? $userInfo->getId() : 0;
$canViewImages = !$userExists
|| !user_warning_check_expiration($userId, MSZ_WARN_BAN)
|| !$userInfo->isBanned()
|| (
parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_PATH) === url('user-profile')
&& perms_check_user(MSZ_PERMS_USER, User::hasCurrent() ? User::getCurrent()->getId() : 0, MSZ_PERM_USER_MANAGE_USERS)

View file

@ -143,7 +143,7 @@ final class SockChatHandler extends Handler {
try {
$token = UserChatToken::create($currentUser);
} catch(UserChatTokenNotFoundException $ex) {
} catch(UserChatTokenCreationFailedException $ex) {
return 500;
}
@ -249,7 +249,7 @@ final class SockChatHandler extends Handler {
} else {
try {
$token = UserChatToken::byExact($userInfo, $authInfo->token);
} catch(UserChatTokenCreationFailedException $ex) {
} catch(UserChatTokenNotFoundException $ex) {
return ['success' => false, 'reason' => 'token'];
}
@ -276,7 +276,7 @@ final class SockChatHandler extends Handler {
'username' => $userInfo->getUsername(),
'colour_raw' => $userInfo->getColourRaw(),
'hierarchy' => $userInfo->getHierarchy(),
'is_silenced' => date('c', user_warning_check_expiration($userInfo->getId(), MSZ_WARN_SILENCE)),
'is_silenced' => date('c', $userInfo->isSilenced() || $userInfo->isBanned() ? ($userInfo->isActiveWarningPermanent() ? strtotime('10 years') : $userInfo->getActiveWarningExpiration()) : 0),
'perms' => $perms,
];
}

View file

@ -24,7 +24,6 @@ final class TwigMisuzu extends Twig_Extension {
public function getFunctions() {
return [
new Twig_Function('url_construct', 'url_construct'),
new Twig_Function('warning_has_duration', 'user_warning_has_duration'),
new Twig_Function('url', 'url'),
new Twig_Function('url_list', 'url_list'),
new Twig_Function('html_avatar', 'html_avatar'),

View file

@ -301,6 +301,32 @@ class User {
return $this->forumPostCount;
}
private $activeWarning = -1;
public function getActiveWarning(): ?UserWarning {
if($this->activeWarning === -1)
$this->activeWarning = UserWarning::byUserActive($this);
return $this->activeWarning;
}
public function hasActiveWarning(): bool {
return $this->getActiveWarning() !== null && !$this->getActiveWarning()->hasExpired();
}
public function isSilenced(): bool {
return $this->hasActiveWarning() && $this->getActiveWarning()->isSilence();
}
public function isBanned(): bool {
return $this->hasActiveWarning() && $this->getActiveWarning()->isBan();
}
public function getActiveWarningExpiration(): int {
return !$this->hasActiveWarning() ? 0 : $this->getActiveWarning()->getExpirationTime();
}
public function isActiveWarningPermanent(): bool {
return $this->hasActiveWarning() && $this->getActiveWarning()->isPermanent();
}
public function getProfileWarnings(?self $viewer): array {
return UserWarning::byProfile($this, $viewer);
}
public function setCurrent(): void {
self::$localUser = $this;
}

281
src/Users/UserWarning.php Normal file
View file

@ -0,0 +1,281 @@
<?php
namespace Misuzu\Users;
use InvalidArgumentException;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Net\IPAddress;
class UserWarningException extends UsersException {}
class UserWarningNotFoundException extends UserWarningException {}
class UserWarningCreationFailedException extends UserWarningException {}
class UserWarning {
// Informational notes on profile, only show up for moderators
public const TYPE_NOTE = 0;
// Warning, only shows up to moderators and the user themselves
public const TYPE_WARN = 1;
// Silences, prevent a user from speaking and is visible to any logged in user
public const TYPE_MUTE = 2;
// Banning, prevents a user from interacting in general
// User will still be able to log in and change certain details but can no longer partake in community things
public const TYPE_BAHN = 3;
private const TYPES = [self::TYPE_NOTE, self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_BAHN];
private const VISIBLE_TO_STAFF = self::TYPES;
private const VISIBLE_TO_USER = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_BAHN];
private const VISIBLE_TO_PUBLIC = [self::TYPE_MUTE, self::TYPE_BAHN];
private const HAS_DURATION = [self::TYPE_MUTE, self::TYPE_BAHN];
private const PROFILE_BACKLOG = 90;
// Database fields
private $warning_id = -1;
private $user_id = -1;
private $user_ip = '::1';
private $issuer_id = -1;
private $issuer_ip = '::1';
private $warning_created = null;
private $warning_duration = null;
private $warning_type = 0;
private $warning_note = '';
private $warning_note_private = '';
private $user = null;
private $issuer = null;
public const TABLE = 'user_warnings';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`warning_id`, %1$s.`user_id`, %1$s.`issuer_id`, %1$s.`warning_type`, %1$s.`warning_note`, %1$s.`warning_note_private`'
. ', UNIX_TIMESTAMP(%1$s.`warning_created`) AS `warning_created`'
. ', UNIX_TIMESTAMP(%1$s.`warning_duration`) AS `warning_duration`'
. ', INET6_NTOA(%1$s.`user_ip`) AS `user_ip`'
. ', INET6_NTOA(%1$s.`issuer_ip`) AS `issuer_ip`';
public function getId(): int {
return $this->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 getExpirationTime(): int {
return $this->warning_duration === null ? -1 : $this->warning_duration;
}
public function hasExpired(): bool {
return $this->hasDuration() && ($this->getExpirationTime() > 0 && $this->getExpirationTime() < time());
}
public function hasDuration(): bool {
return in_array($this->getType(), self::HAS_DURATION);
}
public function getDuration(): int {
return max(-1, $this->getExpirationTime() - $this->getCreatedTime());
}
private const DURATION_DIVS = [
31536000 => 'year',
2592000 => 'month',
604800 => 'week',
86400 => 'day',
3600 => 'hour',
60 => 'minute',
1 => 'second',
];
public function getDurationString(): string {
$duration = $this->getDuration();
if($duration < 1)
return 'permanent';
foreach(self::DURATION_DIVS as $span => $name) {
$display = floor($duration / $span);
if($display > 0)
return number_format($display) . ' ' . $name . ($display == 1 ? '' : 's');
}
return 'an amount of time';
}
public function isPermanent(): bool {
return $this->hasDuration() && $this->getDuration() < 0;
}
public function getType(): int { return $this->warning_type; }
public function isNote(): bool { return $this->getType() === self::TYPE_NOTE; }
public function isWarning(): bool { return $this->getType() === self::TYPE_WARN; }
public function isSilence(): bool { return $this->getType() === self::TYPE_MUTE; }
public function isBan(): bool { return $this->getType() === self::TYPE_BAHN; }
public function isVisibleToUser(): bool {
return in_array($this->getType(), self::VISIBLE_TO_USER);
}
public function isVisibleToPublic(): bool {
return in_array($this->getType(), self::VISIBLE_TO_PUBLIC);
}
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, int $type, int $duration, string $publicNote, ?string $privateNote = null): self {
if(!in_array($type, self::TYPES))
throw new InvalidArgumentException('Type was invalid.');
if(!in_array($type, self::HAS_DURATION))
$duration = 0;
else {
if($duration === 0)
throw new InvalidArgumentException('Duration must be non-zero.');
if($duration < 0)
$duration = -1;
}
$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(), IF(:set_duration, NOW() + INTERVAL :duration SECOND, NULL), :type, :public_note, :private_note)'
) ->bind('user', $user->getId())
->bind('user_addr', $user->getLastRemoteAddress())
->bind('issuer', $issuer->getId())
->bind('issuer_addr', $issuer->getLastRemoteAddress())
->bind('set_duration', $duration > 0 ? 1 : 0)
->bind('duration', $duration)
->bind('type', $type)
->bind('public_note', $publicNote)
->bind('private_note', $privateNote)
->executeGetId();
if($warningId < 1)
throw new UserWarningCreationFailedException;
return self::byId($warningId);
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
}
public static function countByRemoteAddress(?string $address = null, bool $withDuration = true): int {
$address = $address ?? IPAddress::remote();
return (int)DB::prepare(
self::countQueryBase()
. ' WHERE `user_ip` = INET6_ATON(:address)'
. ' AND `warning_duration` >= NOW()'
. ($withDuration ? ' AND `warning_type` IN (' . implode(',', self::HAS_DURATION) . ')' : '')
)->bind('address', $address)->fetchColumn();
}
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 UserWarningNotFoundException;
return $object;
}
public static function byUserActive(User $user): ?self {
return DB::prepare(
self::byQueryBase()
. ' WHERE `user_id` = :user'
. ' AND `warning_type` IN (' . implode(',', self::HAS_DURATION) . ')'
. ' AND (`warning_duration` IS NULL OR `warning_duration` >= NOW())'
. ' ORDER BY `warning_type` DESC, `warning_duration` DESC'
) ->bind('user', $user->getId())
->fetchObject(self::class);
}
public static function byProfile(User $user, ?User $viewer = null): array {
if($viewer === null)
return [];
$types = self::VISIBLE_TO_PUBLIC;
if(perms_check_user(MSZ_PERMS_USER, $viewer->getId(), MSZ_PERM_USER_MANAGE_WARNINGS))
$types = self::VISIBLE_TO_STAFF;
elseif($user->getId() === $viewer->getId())
$types = self::VISIBLE_TO_USER;
$getObjects = DB::prepare(
self::byQueryBase()
. ' WHERE `user_id` = :user'
. ' AND `warning_type` IN (' . implode(',', $types) . ')'
. ' AND (`warning_type` = 0 OR `warning_created` >= NOW() - INTERVAL ' . self::PROFILE_BACKLOG . ' DAY OR (`warning_duration` IS NOT NULL AND `warning_duration` >= NOW()))'
. ' 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);
}
}

View file

@ -1,211 +1,11 @@
<?php
define('MSZ_WARN_NOTE', 0);
define('MSZ_WARN_WARNING', 1);
define('MSZ_WARN_SILENCE', 2);
define('MSZ_WARN_BAN', 3);
define('MSZ_WARN_TYPES', [
MSZ_WARN_NOTE, MSZ_WARN_WARNING, MSZ_WARN_SILENCE, MSZ_WARN_BAN,
]);
define('MSZ_WARN_TYPES_HAS_DURATION', [
MSZ_WARN_SILENCE, MSZ_WARN_BAN,
]);
define('MSZ_WARN_TYPES_VISIBLE_TO_STAFF', MSZ_WARN_TYPES);
define('MSZ_WARN_TYPES_VISIBLE_TO_USER', [
MSZ_WARN_WARNING, MSZ_WARN_SILENCE, MSZ_WARN_BAN,
]);
define('MSZ_WARN_TYPES_VISIBLE_TO_PUBLIC', [
MSZ_WARN_SILENCE, MSZ_WARN_BAN,
]);
define('MSZ_WARN_TYPE_NAMES', [
MSZ_WARN_NOTE => 'Note',
MSZ_WARN_WARNING => 'Warning',
MSZ_WARN_SILENCE => 'Silence',
MSZ_WARN_BAN => 'Ban',
]);
function user_warning_type_is_valid(int $type): bool {
return in_array($type, MSZ_WARN_TYPES, true);
}
function user_warning_type_get_name(int $type): string {
return user_warning_type_is_valid($type) ? MSZ_WARN_TYPE_NAMES[$type] : '';
}
function user_warning_get_types(): array {
return MSZ_WARN_TYPE_NAMES;
}
function user_warning_has_duration(int $type): bool {
return in_array($type, MSZ_WARN_TYPES_HAS_DURATION, true);
}
define('MSZ_E_WARNING_ADD_DB', -1);
define('MSZ_E_WARNING_ADD_TYPE', -2);
define('MSZ_E_WARNING_ADD_USER', -3);
define('MSZ_E_WARNING_ADD_DURATION', -4);
function user_warning_add(
int $userId,
string $userIp,
int $issuerId,
string $issuerIp,
int $type,
string $publicNote,
string $privateNote,
?int $duration = null
): int {
if(!user_warning_type_is_valid($type))
return MSZ_E_WARNING_ADD_TYPE;
if($userId < 1)
return MSZ_E_WARNING_ADD_USER;
if(user_warning_has_duration($type)) {
if($duration <= time())
return MSZ_E_WARNING_ADD_DURATION;
} else
$duration = 0;
$addWarning = \Misuzu\DB::prepare('
INSERT INTO `msz_user_warnings`
(`user_id`, `user_ip`, `issuer_id`, `issuer_ip`, `warning_type`, `warning_note`, `warning_note_private`, `warning_duration`)
VALUES
(:user_id, INET6_ATON(:user_ip), :issuer_id, INET6_ATON(:issuer_ip), :type, :note, :note_private, :duration)
');
$addWarning->bind('user_id', $userId);
$addWarning->bind('user_ip', $userIp);
$addWarning->bind('issuer_id', $issuerId);
$addWarning->bind('issuer_ip', $issuerIp);
$addWarning->bind('type', $type);
$addWarning->bind('note', $publicNote);
$addWarning->bind('note_private', $privateNote);
$addWarning->bind('duration', $duration < 1 ? null : date('Y-m-d H:i:s', $duration));
if(!$addWarning->execute())
return MSZ_E_WARNING_ADD_DB;
return \Misuzu\DB::lastId();
}
function user_warning_count(int $userId): int {
if($userId < 1)
return 0;
$countWarnings = \Misuzu\DB::prepare('
SELECT COUNT(`warning_id`)
FROM `msz_user_warnings`
WHERE `user_id` = :user_id
');
$countWarnings->bind('user_id', $userId);
return (int)$countWarnings->fetchColumn(0, 0);
}
function user_warning_remove(int $warningId): bool {
if($warningId < 1)
return false;
$removeWarning = \Misuzu\DB::prepare('
DELETE FROM `msz_user_warnings`
WHERE `warning_id` = :warning_id
');
$removeWarning->bind('warning_id', $warningId);
return $removeWarning->execute();
}
function user_warning_fetch(
int $userId,
?int $days = null,
array $displayTypes = MSZ_WARN_TYPES
): array {
$fetchWarnings = \Misuzu\DB::prepare(sprintf(
'
SELECT
uw.`warning_id`, uw.`warning_created`, uw.`warning_type`, uw.`warning_note`,
uw.`warning_note_private`, uw.`user_id`, uw.`issuer_id`, uw.`warning_duration`,
INET6_NTOA(uw.`user_ip`) AS `user_ip`, INET6_NTOA(uw.`issuer_ip`) AS `issuer_ip`,
iu.`username` AS `issuer_username`
FROM `msz_user_warnings` AS uw
LEFT JOIN `msz_users` AS iu
ON iu.`user_id` = uw.`issuer_id`
WHERE uw.`user_id` = :user_id
AND uw.`warning_type` IN (%1$s)
%2$s
ORDER BY uw.`warning_id` DESC
',
implode(',', array_apply($displayTypes, 'intval')),
$days !== null ? 'AND (uw.`warning_created` >= NOW() - INTERVAL :days DAY OR (uw.`warning_duration` IS NOT NULL AND uw.`warning_duration` > NOW()))' : ''
));
$fetchWarnings->bind('user_id', $userId);
if($days !== null)
$fetchWarnings->bind('days', $days);
return $fetchWarnings->fetchAll();
}
function user_warning_global_count(?int $userId = null): int {
$countWarnings = \Misuzu\DB::prepare(sprintf('
SELECT COUNT(`warning_id`)
FROM `msz_user_warnings`
%s
', $userId > 0 ? 'WHERE `user_id` = :user_id' : ''));
if($userId > 0)
$countWarnings->bind('user_id', $userId);
return (int)$countWarnings->fetchColumn(0, 0);
}
function user_warning_global_fetch(int $offset = 0, int $take = 50, ?int $userId = null): array {
$fetchWarnings = \Misuzu\DB::prepare(sprintf(
'
SELECT
uw.`warning_id`, uw.`warning_created`, uw.`warning_type`, uw.`warning_note`,
uw.`warning_note_private`, uw.`user_id`, uw.`issuer_id`, uw.`warning_duration`,
INET6_NTOA(uw.`user_ip`) AS `user_ip`, INET6_NTOA(uw.`issuer_ip`) AS `issuer_ip`,
iu.`username` AS `issuer_username`, wu.`username` AS `username`
FROM `msz_user_warnings` AS uw
LEFT JOIN `msz_users` AS iu
ON iu.`user_id` = uw.`issuer_id`
LEFT JOIN `msz_users` AS wu
ON wu.`user_id` = uw.`user_id`
%1$s
ORDER BY uw.`warning_id` DESC
LIMIT :offset, :take
',
$userId > 0 ? 'WHERE uw.`user_id` = :user_id' : ''
));
$fetchWarnings->bind('offset', $offset);
$fetchWarnings->bind('take', $take);
if($userId > 0)
$fetchWarnings->bind('user_id', $userId);
return $fetchWarnings->fetchAll();
}
function user_warning_check_ip(string $address): bool {
$checkAddress = \Misuzu\DB::prepare(sprintf(
'
SELECT COUNT(`warning_id`) > 0
FROM `msz_user_warnings`
WHERE `warning_type` IN (%s)
AND `user_ip` = INET6_ATON(:address)
AND `warning_duration` IS NOT NULL
AND `warning_duration` >= NOW()
',
implode(',', MSZ_WARN_TYPES_HAS_DURATION)
));
$checkAddress->bind('address', $address);
return (bool)$checkAddress->fetchColumn(0, false);
}
define('MSZ_WARN_TYPES_HAS_DURATION', [MSZ_WARN_SILENCE, MSZ_WARN_BAN]);
function user_warning_check_expiration(int $userId, int $type): int {
if($userId < 1 || !user_warning_has_duration($type))
if($userId < 1 || !in_array($type, MSZ_WARN_TYPES_HAS_DURATION, true))
return 0;
static $memo = [];
@ -230,27 +30,3 @@ function user_warning_check_expiration(int $userId, int $type): int {
return $memo[$memoId] = (empty($expiration) ? 0 : strtotime($expiration));
}
function user_warning_check_restriction(int $userId): bool {
if($userId < 1)
return false;
static $memo = [];
if(array_key_exists($userId, $memo))
return $memo[$userId];
$checkAddress = \Misuzu\DB::prepare(sprintf(
'
SELECT COUNT(`warning_id`) > 0
FROM `msz_user_warnings`
WHERE `warning_type` IN (%s)
AND `user_id` = :user
AND `warning_duration` IS NOT NULL
AND `warning_duration` >= NOW()
',
implode(',', MSZ_WARN_TYPES_HAS_DURATION)
));
$checkAddress->bind('user', $userId);
return $memo[$userId] = (bool)$checkAddress->fetchColumn(0, false);
}

View file

@ -6,7 +6,7 @@
{% block manage_content %}
<form class="container container--lazy" action="{{ url('manage-users-warnings') }}" method="post">
{{ container_title('<i class="fas fa-users fa-fw"></i> Filters') }}
{{ input_text('lookup', null, warnings.username, 'text', 'Enter a username') }}
{{ input_text('lookup', null, warnings.user.username|default(''), 'text', 'Enter a username') }}
<button class="input__button">Filter</button>
</form>
@ -20,11 +20,11 @@
</div>
{% endif %}
{% if warnings.user_id > 0 and warnings.username|length > 0 %}{# shittiest validation in the world, but it should work #}
{% if warnings.user is not null %}
<form class="container container--lazy" method="post" action="">
{{ container_title('<i class="fas fa-user-shield fa-fw"></i> Warn ' ~ warnings.username) }}
{{ container_title('<i class="fas fa-user-shield fa-fw"></i> Warn ' ~ warnings.user.username) }}
{{ input_csrf() }}
{{ input_hidden('warning[user]', warnings.user_id) }}
{{ input_hidden('warning[user]', warnings.user.id) }}
{{ input_select('warning[type]', warnings.types) }}
{{ input_text('warning[note]', '', '', 'text', 'Public note') }}
@ -38,7 +38,7 @@
<div class="container container--lazy">
{{ container_title('<i class="fas fa-exclamation-circle fa-fw"></i> Warnings') }}
{% set warnpag = pagination(warnings.pagination, url('manage-users-warnings')) %}
{% set warnpag = pagination(warnings.pagination, url('manage-users-warnings', {'user': warnings.user.id|default(0)})) %}
{{ warnpag }}
@ -76,7 +76,7 @@
</div>
<div class="profile__warning__duration">
Expires
Duration
</div>
<div class="profile__warning__note">

View file

@ -31,10 +31,10 @@
{% include '_layout/header.twig' %}
<div class="main__wrapper">
{% if current_user.ban_expiration|default(0) > 0 or current_user.silence_expiration|default(0) > 0 %}
{% if current_user2.hasActiveWarning|default(false) %}
<div class="warning">
<div class="warning__content">
You have been {{ current_user.silence_expiration ? 'silenced' : 'banned' }} until <strong>{{ (current_user.silence_expiration ? current_user.silence_expiration : current_user.ban_expiration)|date('r') }}</strong>, view the account standing table on <a href="{{ url('user-account-standing', {'user': current_user.user_id}) }}" class="warning__link">your profile</a> to view why.
You have been {{ current_user2.isSilenced ? 'silenced' : 'banned' }} {% if current_user2.isActiveWarningPermanent %}<strong>permanently</strong>{% else %}until <strong>{{ current_user2.activeWarningExpiration|date('r') }}</strong>{% endif %}, view the account standing table on <a href="{{ url('user-account-standing', {'user': current_user2.id}) }}" class="warning__link">your profile</a> to view why.
</div>
</div>
{% endif %}

View file

@ -245,7 +245,7 @@
</div>
<div class="profile__warning__duration">
Expires
Duration
</div>
<div class="profile__warning__note">

View file

@ -433,13 +433,13 @@
{% macro user_profile_warning(warning, show_private_note, show_user_info, delete_csrf) %}
{% from 'macros.twig' import avatar %}
{% if warning.warning_type == constant('MSZ_WARN_SILENCE') %}
{% if warning.isSilence %}
{% set warning_text = 'Silence' %}
{% set warning_class = 'silence' %}
{% elseif warning.warning_type == constant('MSZ_WARN_BAN') %}
{% elseif warning.isBan %}
{% set warning_text = 'Ban' %}
{% set warning_class = 'ban' %}
{% elseif warning.warning_type == constant('MSZ_WARN_WARNING') %}
{% elseif warning.isWarning %}
{% set warning_text = 'Warning' %}
{% set warning_class = 'warning' %}
{% else %}
@ -453,41 +453,35 @@
{% if show_user_info or delete_csrf %}
<div class="profile__warning__tools">
{% if show_user_info %}
{% if warning.username is defined or warning.user_ip is defined %}
<div class="profile__warning__user">
{% if warning.username is defined %}
<div class="profile__warning__user__avatar">
{{ avatar(warning.user_id, 20, warning.username) }}
</div>
<a class="profile__warning__user__username" href="{{ url('user-profile', {'user': warning.user_id}) }}">
{{ warning.username }}
</a>
{% endif %}
{% if warning.user_ip is defined %}
<div class="profile__warning__user__ip">
{{ warning.user_ip }}
</div>
{% endif %}
<div class="profile__warning__user">
<div class="profile__warning__user__avatar">
{{ avatar(warning.user.id, 20, warning.user.username) }}
</div>
{% endif %}
<a class="profile__warning__user__username" href="{{ url('user-profile', {'user': warning.user.id}) }}">
{{ warning.user.username }}
</a>
<div class="profile__warning__user__ip">
{{ warning.userRemoteAddress }}
</div>
</div>
<div class="profile__warning__user">
<div class="profile__warning__user__avatar">
{{ avatar(warning.issuer_id, 20, warning.issuer_username) }}
{{ avatar(warning.issuer.id, 20, warning.issuer.username) }}
</div>
<a class="profile__warning__user__username" href="{{ url('user-profile', {'user': warning.user_id}) }}">
{{ warning.issuer_username }}
<a class="profile__warning__user__username" href="{{ url('user-profile', {'user': warning.issuer.id}) }}">
{{ warning.issuer.username }}
</a>
<div class="profile__warning__user__ip">
{{ warning.issuer_ip }}
{{ warning.issuerRemoteAddress }}
</div>
</div>
{% endif %}
{% if delete_csrf %}
<div class="profile__warning__options">
<a href="{{ url('manage-users-warning-delete', {'warning': warning.warning_id}) }}" class="profile__warning__option"><i class="far fa-trash-alt"></i> Delete</a>
<a href="{{ url('manage-users-warning-delete', {'warning': warning.id}) }}" class="profile__warning__option"><i class="far fa-trash-alt"></i> Delete</a>
</div>
{% endif %}
</div>
@ -498,24 +492,28 @@
{{ warning_text }}
</div>
<time datetime="{{ warning.warning_created|date('c') }}" title="{{ warning.warning_created|date('r') }}" class="profile__warning__created">
{{ warning.warning_created|time_diff }}
<time datetime="{{ warning.createdTime|date('c') }}" title="{{ warning.createdTime|date('r') }}" class="profile__warning__created">
{{ warning.createdTime|time_diff }}
</time>
{% if warning_has_duration(warning.warning_type) %}
<time datetime="{{ warning.warning_duration|date('c') }}" title="{{ warning.warning_duration|date('r') }}" class="profile__warning__duration">
{{ warning.warning_duration|time_diff }}
</time>
{% if warning.isPermanent %}
<div class="profile__warning__duration">
<b>PERMANENT</b>
</div>
{% elseif warning.hasDuration %}
<div class="profile__warning__duration">
{{ warning.durationString }}
</div>
{% else %}
<div class="profile__warning__duration"></div>
{% endif %}
<div class="profile__warning__note">
{{ warning.warning_note }}
{{ warning.publicNote }}
{% if show_private_note and warning.warning_note_private|length > 0 %}
{% if show_private_note and warning.hasPrivateNote %}
<div class="profile__warning__private">
{{ warning.warning_note_private|nl2br }}
{{ warning.privateNote|nl2br }}
</div>
{% endif %}
</div>