Finished implementation of warning and banning system, closes .

This commit is contained in:
flash 2018-12-28 06:03:42 +01:00
parent e1c2c97669
commit 698ab50435
26 changed files with 527 additions and 184 deletions

View file

@ -3,6 +3,7 @@
flex-direction: column;
justify-content: flex-end;
padding: 10px 15px;
margin: 2px 0;
&__title {
font-size: 2em;

View file

@ -125,6 +125,7 @@
&__ip {
display: inline-flex;
padding: 0 5px;
&:before {
content: "(";

View file

@ -98,6 +98,10 @@ function setCSRF(realm: string, token: string): void {
function updateCSRF(token: string, realm: string = null, name: string = 'csrf'): void
{
if (token === null) {
return;
}
const tokenSplit: string[] = token.split(';');
if (tokenSplit.length > 1) {

View file

@ -20,7 +20,7 @@ function migrate_up(PDO $conn): void
PRIMARY KEY (`warning_id`),
INDEX `user_warnings_user_foreign` (`user_id`),
INDEX `user_warnings_issuer_foreign` (`issuer_id`),
INDEX `user_warnings_indices` (`warning_created`, `warning_type`, `warning_duration`),
INDEX `user_warnings_indices` (`warning_created`, `warning_type`, `warning_duration`, `user_ip`),
CONSTRAINT `user_warnings_issuer_foreign`
FOREIGN KEY (`issuer_id`)
REFERENCES `msz_users` (`user_id`)

View file

@ -309,6 +309,8 @@ MIG;
if ($userDisplayInfo) {
$userDisplayInfo['comments_perms'] = perms_get_user(MSZ_PERMS_COMMENTS, $mszUserId);
$userDisplayInfo['ban_expiration'] = user_warning_check_expiration($userDisplayInfo['user_id'], MSZ_WARN_BAN);
$userDisplayInfo['silence_expiration'] = $userDisplayInfo['ban_expiration'] > 0 ? 0 : user_warning_check_expiration($userDisplayInfo['user_id'], MSZ_WARN_SILENCE);
}
}
@ -343,7 +345,9 @@ MIG;
}
$inManageMode = starts_with($_SERVER['REQUEST_URI'], '/manage');
$hasManageAccess = perms_check(perms_get_user(MSZ_PERMS_GENERAL, $userDisplayInfo['user_id'] ?? 0), MSZ_PERM_GENERAL_CAN_MANAGE);
$hasManageAccess = !empty($userDisplayInfo['user_id'])
&& !user_warning_check_restriction($userDisplayInfo['user_id'])
&& perms_check(perms_get_user(MSZ_PERMS_GENERAL, $userDisplayInfo['user_id']), MSZ_PERM_GENERAL_CAN_MANAGE);
tpl_var('has_manage_access', $hasManageAccess);
if ($inManageMode) {

View file

@ -23,7 +23,13 @@ $authEmail = $isSubmission ? ($_POST['auth']['email'] ?? '') : ($_GET['email'] ?
$authPassword = $_POST['auth']['password'] ?? '';
$authVerification = $_POST['auth']['verification'] ?? '';
$authRedirect = $_POST['auth']['redirect'] ?? $_GET['redirect'] ?? $_SERVER['HTTP_REFERER'] ?? '/';
$authRestricted = ip_blacklist_check(ip_remote_address());
$authRestricted = ip_blacklist_check(ip_remote_address())
? 1
: (
user_warning_check_ip(ip_remote_address())
? 2
: 0
);
tpl_vars([
'can_create_account' => $canCreateAccount,

View file

@ -23,11 +23,27 @@ if (!user_session_active()) {
return;
}
$currentUserId = user_session_current('user_id', 0);
if (user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
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) {
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
return;
}
header(csrf_http_header('comments'));
$commentPerms = comments_get_perms(user_session_current('user_id', 0));
$commentPerms = comments_get_perms($currentUserId);
switch ($_GET['m'] ?? null) {
case 'vote':
if (!$commentPerms['can_vote']) {
echo render_info_or_json($isXHR, "You're not allowed to vote on comments.", 403);
break;
}
$vote = (int)($_GET['v'] ?? 0);
if (!array_key_exists($vote, MSZ_COMMENTS_VOTE_TYPES)) {
@ -59,6 +75,11 @@ switch ($_GET['m'] ?? null) {
break;
case 'delete':
if (!$commentPerms['can_delete']) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments.", 403);
break;
}
$comment = (int)($_GET['c'] ?? 0);
$commentInfo = comments_post_get($comment, false);
@ -67,7 +88,6 @@ switch ($_GET['m'] ?? null) {
break;
}
$currentUserId = user_session_current('user_id', 0);
$isOwnComment = (int)$commentInfo['user_id'] === $currentUserId;
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment;
@ -80,11 +100,6 @@ switch ($_GET['m'] ?? null) {
break;
}
if (!$commentPerms['can_delete']) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments.", 403);
break;
}
if (!$isModAction && !$isOwnComment) {
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
break;
@ -129,8 +144,6 @@ switch ($_GET['m'] ?? null) {
break;
}
$currentUserId = user_session_current('user_id', 0);
if ($commentInfo['comment_deleted'] === null) {
echo render_info_or_json($isXHR, "This comment isn't in a deleted state.", 400);
break;

View file

@ -24,6 +24,10 @@ if (!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) {
return;
}
if (user_warning_check_restriction(user_session_current('user_id', 0))) {
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
}
tpl_var('forum_perms', $perms);
if ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK) {

View file

@ -2,6 +2,11 @@
require_once '../../misuzu.php';
if (!user_session_active()) {
echo render_error(401);
return;
}
if (user_warning_check_restriction(user_session_current('user_id', 0))) {
echo render_error(403);
return;
}

View file

@ -20,6 +20,10 @@ $perms = $topic
? forum_perms_get_user(MSZ_FORUM_PERMS_GENERAL, $topic['forum_id'], user_session_current('user_id', 0))
: 0;
if (user_warning_check_restriction(user_session_current('user_id', 0))) {
$perms &= ~MSZ_FORUM_PERM_SET_WRITE;
}
if (!$topic || ($topic['topic_deleted'] !== null && !perms_check($perms, MSZ_FORUM_PERM_DELETE_TOPIC))) {
echo render_error(404);
return;
@ -44,6 +48,9 @@ if (!$posts) {
return;
}
$canReply = empty($topic['topic_archived']) && empty($topic['topic_locked']) && empty($topic['topic_deleted'])
&& perms_check($perms, MSZ_FORUM_PERM_CREATE_POST);
forum_topic_mark_read(user_session_current('user_id', 0), $topic['topic_id'], $topic['forum_id']);
echo tpl_render('forum.topic', [
@ -53,4 +60,5 @@ echo tpl_render('forum.topic', [
'topic_posts' => $posts,
'topic_offset' => $postsOffset,
'topic_range' => $postsRange,
'can_reply' => $canReply,
]);

Binary file not shown.

After

(image error) Size: 53 KiB

View file

@ -468,20 +468,84 @@ switch ($_GET['v'] ?? null) {
break;
}
$notices = [];
if (!empty($_POST['warning']) && is_array($_POST['warning'])) {
$warningType = (int)($_POST['warning']['type'] ?? 0);
if (user_warning_type_is_valid($warningType)) {
$warningsUser = max(0, (int)($_POST['warning']['user'] ?? 0));
$warningId = user_warning_add(
$warningsUser,
user_get_last_ip($warningsUser),
user_session_current('user_id'),
ip_remote_address(),
$warningType,
$_POST['warning']['note'],
$_POST['warning']['private']
);
$warningDuration = 0;
if (user_warning_has_duration($warningType)) {
$duration = (int)($_POST['warning']['duration'] ?? 0);
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 (empty($notices) && $warningsUser > 0) {
$warningId = user_warning_add(
$warningsUser,
user_get_last_ip($warningsUser),
user_session_current('user_id'),
ip_remote_address(),
$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.';
break;
case MSZ_E_WARNING_ADD_TYPE:
$notices[] = 'The warning type provided was invalid.';
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.';
break;
}
}
}
} elseif (!empty($_POST['lookup']) && is_string($_POST['lookup'])) {
$userId = user_id_from_username($_POST['lookup']);
@ -512,8 +576,35 @@ switch ($_GET['v'] ?? null) {
$warningsOffset = max(0, (int)($_GET['o'] ?? 0));
$warningsList = user_warning_global_fetch($warningsOffset, $warningsTake, $warningsUser);
// 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,
'Until (YYYY-MM-DD) ->' => -1,
'Until (Seconds) ->' => -2,
'Until (strtotime) ->' => -3,
]);
echo tpl_render('manage.users.warnings', [
'warnings' => [
'notices' => $notices,
'count' => $warningsCount,
'take' => $warningsTake,
'offset' => $warningsOffset,
@ -521,6 +612,7 @@ switch ($_GET['v'] ?? null) {
'user_id' => $warningsUser,
'username' => user_username_from_id($warningsUser),
'types' => user_warning_get_types(),
'durations' => $warningDurations,
],
]);
break;

View file

@ -7,41 +7,55 @@ require_once '../misuzu.php';
switch ($mode) {
case 'avatar':
$userId = (int)($_GET['u'] ?? 0);
$avatar_filename = build_path(
MSZ_ROOT,
config_get_default('public/images/no-avatar.png', 'Avatar', 'default_path')
);
$user_avatar = "{$userId}.msz";
$cropped_avatar = build_path(
create_directory(build_path(MSZ_STORAGE, 'avatars/200x200')),
$user_avatar
);
if (is_file($cropped_avatar)) {
$avatar_filename = $cropped_avatar;
if (user_warning_check_expiration($userId, MSZ_WARN_BAN) > 0) {
$avatarFilename = build_path(
MSZ_ROOT,
config_get_default('public/images/banned-avatar.png', 'Avatar', 'banned_path')
);
} else {
$original_avatar = build_path(MSZ_STORAGE, 'avatars/original', $user_avatar);
$avatarFilename = build_path(
MSZ_ROOT,
config_get_default('public/images/no-avatar.png', 'Avatar', 'default_path')
);
$userAvatar = "{$userId}.msz";
$croppedAvatar = build_path(
create_directory(build_path(MSZ_STORAGE, 'avatars/200x200')),
$userAvatar
);
if (is_file($original_avatar)) {
try {
file_put_contents(
$cropped_avatar,
crop_image_centred_path($original_avatar, 200, 200)->getImagesBlob(),
LOCK_EX
);
if (is_file($croppedAvatar)) {
$avatarFilename = $croppedAvatar;
} else {
$original_avatar = build_path(MSZ_STORAGE, 'avatars/original', $userAvatar);
$avatar_filename = $cropped_avatar;
} catch (Exception $ex) {
if (is_file($original_avatar)) {
try {
file_put_contents(
$croppedAvatar,
crop_image_centred_path($original_avatar, 200, 200)->getImagesBlob(),
LOCK_EX
);
$avatarFilename = $croppedAvatar;
} catch (Exception $ex) {
}
}
}
}
header('Content-Type: ' . mime_content_type($avatar_filename));
echo file_get_contents($avatar_filename);
header('Content-Type: ' . mime_content_type($avatarFilename));
echo file_get_contents($avatarFilename);
break;
case 'background':
$userId = (int)($_GET['u'] ?? 0);
if (user_warning_check_expiration($userId, MSZ_WARN_BAN) > 0) {
echo render_error(404);
break;
}
$user_background = build_path(
create_directory(build_path(MSZ_STORAGE, 'backgrounds/original')),
"{$userId}.msz"
@ -77,9 +91,12 @@ switch ($mode) {
break;
}
$viewingAsGuest = user_session_current('user_id', 0) === 0;
$viewingOwnProfile = user_session_current('user_id', 0) === $userId;
$isRestricted = user_warning_check_restriction($userId);
$userPerms = perms_get_user(MSZ_PERMS_USER, user_session_current('user_id', 0));
$canEdit = user_session_active() && (
$canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS);
$canEdit = !$isRestricted && user_session_active() && (
$viewingOwnProfile || perms_check($userPerms, MSZ_PERM_USER_MANAGE_USERS)
);
$isEditing = $mode === 'edit';
@ -89,7 +106,19 @@ switch ($mode) {
break;
}
$warnings = user_warning_fetch($userId, 90);
$warnings = $viewingAsGuest
? []
: user_warning_fetch(
$userId,
90,
$canManageWarnings
? MSZ_WARN_TYPES_VISIBLE_TO_STAFF
: (
$viewingOwnProfile
? MSZ_WARN_TYPES_VISIBLE_TO_USER
: MSZ_WARN_TYPES_VISIBLE_TO_PUBLIC
)
);
$notices = [];
if ($isEditing) {
@ -324,6 +353,8 @@ switch ($mode) {
'is_editing' => $isEditing,
'warnings' => $warnings,
'can_view_private_note' => $viewingOwnProfile,
'can_manage_warnings' => $canManageWarnings,
'is_restricted' => $isRestricted,
'profile_fields' => user_session_active() ? user_profile_fields_display($profile, !$isEditing) : [],
'friend_info' => user_session_active() ? user_relation_info(user_session_current('user_id', 0), $profile['user_id']) : [],
]);

View file

@ -7,6 +7,11 @@ if (empty($_SERVER['HTTP_REFERER']) || !is_local_url($_SERVER['HTTP_REFERER']))
}
if (!user_session_active()) {
echo render_error(401);
return;
}
if (user_warning_check_expiration(user_session_current('user_id', 0), MSZ_WARN_BAN) > 0) {
echo render_error(403);
return;
}

View file

@ -2,7 +2,7 @@
require_once '../misuzu.php';
if (!user_session_active()) {
echo render_error(403);
echo render_error(401);
return;
}
@ -14,6 +14,7 @@ $disableAccountOptions = !MSZ_DEBUG && (
);
$currentEmail = user_email_get(user_session_current('user_id'));
$isRestricted = user_warning_check_restriction(user_session_current('user_id'));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!csrf_verify('settings', $_POST['csrf'] ?? '')) {
@ -51,7 +52,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
if (!empty($_POST['role'])) {
if (!empty($_POST['role']) && !$isRestricted) {
$roleId = (int)($_POST['role']['id'] ?? 0);
if ($roleId > 0 && user_role_has(user_session_current('user_id'), $roleId)) {
@ -179,4 +180,5 @@ echo tpl_render('user.settings', [
'logs' => $logs,
'user_roles' => $userRoles,
'user_display_role' => user_role_get_display(user_session_current('user_id')),
'is_restricted' => $isRestricted,
]);

View file

@ -4,9 +4,6 @@ define('MSZ_PERM_FORUM_MANAGE_FORUMS', 1);
define('MSZ_FORUM_PERM_LIST_FORUM', 1); // can see stats, but will get error when trying to view
define('MSZ_FORUM_PERM_VIEW_FORUM', 1 << 1);
// shorthand, never use this to SET!!!!!!!
define('MSZ_FORUM_PERM_CAN_LIST_FORUM', MSZ_FORUM_PERM_LIST_FORUM | MSZ_FORUM_PERM_VIEW_FORUM);
define('MSZ_FORUM_PERM_CREATE_TOPIC', 1 << 10);
define('MSZ_FORUM_PERM_DELETE_TOPIC', 1 << 11); // how is this different from MSZ_FORUM_PERM_DELETE_ANY_POST?
define('MSZ_FORUM_PERM_MOVE_TOPIC', 1 << 12);
@ -21,6 +18,24 @@ define('MSZ_FORUM_PERM_EDIT_ANY_POST', 1 << 22);
define('MSZ_FORUM_PERM_DELETE_POST', 1 << 23);
define('MSZ_FORUM_PERM_DELETE_ANY_POST', 1 << 24);
// shorthands, never use these to SET!!!!!!!
define('MSZ_FORUM_PERM_CAN_LIST_FORUM', MSZ_FORUM_PERM_LIST_FORUM | MSZ_FORUM_PERM_VIEW_FORUM);
define(
'MSZ_FORUM_PERM_SET_WRITE',
MSZ_FORUM_PERM_CREATE_TOPIC
| MSZ_FORUM_PERM_DELETE_TOPIC
| MSZ_FORUM_PERM_MOVE_TOPIC
| MSZ_FORUM_PERM_LOCK_TOPIC
| MSZ_FORUM_PERM_STICKY_TOPIC
| MSZ_FORUM_PERM_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_CREATE_POST
| MSZ_FORUM_PERM_EDIT_POST
| MSZ_FORUM_PERM_EDIT_ANY_POST
| MSZ_FORUM_PERM_DELETE_POST
| MSZ_FORUM_PERM_DELETE_ANY_POST
);
define('MSZ_FORUM_TYPE_DISCUSSION', 0);
define('MSZ_FORUM_TYPE_CATEGORY', 1);
define('MSZ_FORUM_TYPE_LINK', 2);

View file

@ -38,6 +38,7 @@ final class TwigMisuzu extends Twig_Extension
new Twig_Function('csrf_input', 'csrf_html'),
new Twig_Function('sql_query_count', 'db_query_count'),
new Twig_Function('url_construct', 'url_construct'),
new Twig_Function('warning_has_duration', 'user_warning_has_duration'),
new Twig_Function('startup_time', function (float $time = MSZ_STARTUP) {
return microtime(true) - $time;
}),

View file

@ -8,10 +8,16 @@ define('MSZ_WARN_TYPES', [
MSZ_WARN_NOTE, MSZ_WARN_WARNING, MSZ_WARN_SILENCE, MSZ_WARN_BAN,
]);
define('MSZ_WARN_TYPES_PUBLIC', [
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', [
@ -36,11 +42,16 @@ function user_warning_get_types(): array
return MSZ_WARN_TYPE_NAMES;
}
function user_warning_is_public(int $type): bool
function user_warning_has_duration(int $type): bool
{
return in_array($type, MSZ_WARN_TYPES_PUBLIC, true);
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,
@ -48,21 +59,30 @@ function user_warning_add(
string $issuerIp,
int $type,
string $publicNote,
string $privateNote
string $privateNote,
?int $duration = null
): int {
if (!in_array($type, MSZ_WARN_TYPES, true)) {
return -1;
if (!user_warning_type_is_valid($type)) {
return MSZ_E_WARNING_ADD_TYPE;
}
if ($userId < 1) {
return -2;
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 = db_prepare('
INSERT INTO `msz_user_warnings`
(`user_id`, `user_ip`, `issuer_id`, `issuer_ip`, `warning_type`, `warning_note`, `warning_note_private`)
(`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)
(:user_id, INET6_ATON(:user_ip), :issuer_id, INET6_ATON(:issuer_ip), :type, :note, :note_private, :duration)
');
$addWarning->bindValue('user_id', $userId);
$addWarning->bindValue('user_ip', $userIp);
@ -71,9 +91,10 @@ function user_warning_add(
$addWarning->bindValue('type', $type);
$addWarning->bindValue('note', $publicNote);
$addWarning->bindValue('note_private', $privateNote);
$addWarning->bindValue('duration', $duration < 1 ? null : date('Y-m-d H:i:s', $duration));
if (!$addWarning->execute()) {
return 0;
return MSZ_E_WARNING_ADD_DB;
}
return (int)db_last_insert_id();
@ -110,24 +131,26 @@ function user_warning_remove(int $warningId): bool
function user_warning_fetch(
int $userId,
?int $days = null
?int $days = null,
array $displayTypes = MSZ_WARN_TYPES
): array {
$fetchWarnings = 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`,
TIMESTAMPDIFF(SECOND, uw.`warning_created`, uw.`warning_duration`) AS `warning_duration_secs`,
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
%s
AND uw.`warning_type` IN (%1$s)
%2$s
ORDER BY uw.`warning_id` DESC
',
$days !== null ? 'AND uw.`warning_created` >= NOW() - INTERVAL :days DAY' : ''
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->bindValue('user_id', $userId);
@ -155,7 +178,6 @@ function user_warning_global_fetch(int $offset = 0, int $take = 50, ?int $userId
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`,
TIMESTAMPDIFF(SECOND, uw.`warning_created`, uw.`warning_duration`) AS `warning_duration_secs`,
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
@ -179,3 +201,80 @@ function user_warning_global_fetch(int $offset = 0, int $take = 50, ?int $userId
$warnings = $fetchWarnings->execute() ? $fetchWarnings->fetchAll(PDO::FETCH_ASSOC) : false;
return $warnings ? $warnings : [];
}
function user_warning_check_ip(string $address): bool
{
$checkAddress = 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->bindValue('address', $address);
return (bool)($checkAddress->execute() ? $checkAddress->fetchColumn() : false);
}
define('MSZ_WARN_EXPIRATION_CACHE', '_msz_user_warning_expiration');
function user_warning_check_expiration(int $userId, int $type): int
{
if ($userId < 1 || !user_warning_has_duration($type)) {
return 0;
}
if (!isset($GLOBALS[MSZ_WARN_EXPIRATION_CACHE]) || !is_array($GLOBALS[MSZ_WARN_EXPIRATION_CACHE])) {
$GLOBALS[MSZ_WARN_EXPIRATION_CACHE] = [];
} elseif (array_key_exists($type, $GLOBALS[MSZ_WARN_EXPIRATION_CACHE]) && array_key_exists($userId, $GLOBALS[MSZ_WARN_EXPIRATION_CACHE][$type])) {
return $GLOBALS[MSZ_WARN_EXPIRATION_CACHE][$type][$userId];
}
$getExpiration = db_prepare('
SELECT `warning_duration`
FROM `msz_user_warnings`
WHERE `warning_type` = :type
AND `user_id` = :user
AND `warning_duration` IS NOT NULL
AND `warning_duration` >= NOW()
ORDER BY `warning_duration` DESC
LIMIT 1
');
$getExpiration->bindValue('type', $type);
$getExpiration->bindValue('user', $userId);
$expiration = $getExpiration->execute() ? $getExpiration->fetchColumn() : '';
return $GLOBALS[MSZ_WARN_EXPIRATION_CACHE][$type][$userId] = (empty($expiration) ? 0 : strtotime($expiration));
}
define('MSZ_WARN_RESTRICTION_CACHE', '_msz_user_warning_restriction');
function user_warning_check_restriction(int $userId): bool
{
if ($userId < 1) {
return false;
}
if (!isset($GLOBALS[MSZ_WARN_RESTRICTION_CACHE]) || !is_array($GLOBALS[MSZ_WARN_RESTRICTION_CACHE])) {
$GLOBALS[MSZ_WARN_RESTRICTION_CACHE] = [];
} elseif (array_key_exists($userId, $GLOBALS[MSZ_WARN_RESTRICTION_CACHE])) {
return $GLOBALS[MSZ_WARN_RESTRICTION_CACHE][$userId];
}
$checkAddress = 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->bindValue('user', $userId);
return $GLOBALS[MSZ_WARN_RESTRICTION_CACHE][$userId] = (bool)($checkAddress->execute() ? $checkAddress->fetchColumn() : false);
}

View file

@ -22,7 +22,11 @@
{% if auth_restricted %}
<div class="warning auth__warning">
<div class="warning__content">
The IP address from which you are visiting the website appears on our blacklist, you are not allowed to register from this address but if you already have an account you can log in just fine using the form above. If you think this blacklisting is a mistake, please <a href="/info.php/contact" class="warning__link">contact us</a>!
{% if auth_restricted == 2 %}
A user is currently in a banned and/or silenced state from the same IP address you're currently visiting the site from. If said user isn't you and you wish to create an account, please <a href="/info.php/contact" class="warning__link">contact us</a>!
{% else %}
The IP address from which you are visiting the website appears on our blacklist, you are not allowed to register from this address but if you already have an account you can log in just fine using the form above. If you think this blacklisting is a mistake, please <a href="/info.php/contact" class="warning__link">contact us</a>!
{% endif %}
</div>
</div>
{% else %}

View file

@ -48,49 +48,58 @@
{% macro forum_category_tools(info, perms, take, offset) %}
{% from 'macros.twig' import pagination %}
<div class="container forum__actions">
<div class="forum__actions__buttons">
{% if perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_TOPIC')) %}
<a href="{{ url_construct('/forum/posting.php', {'f':info.forum_id}) }}" class="input__button">New Topic</a>
{% endif %}
</div>
{% set can_topic = perms|perms_check(constant('MSZ_FORUM_PERM_CREATE_TOPIC')) %}
{% set pag = pagination(
info.forum_topic_count,
take,
offset,
url_construct('/forum/forum.php', {'f':info.forum_id}),
false,
null,
5
) %}
<div class="forum__actions__pagination">
{{ pagination(
info.forum_topic_count,
take,
offset,
url_construct('/forum/forum.php', {'f':info.forum_id}),
false,
null,
5
) }}
{% if can_topic or pag|trim|length > 0 %}
<div class="container forum__actions">
<div class="forum__actions__buttons">
{% if can_topic %}
<a href="{{ url_construct('/forum/posting.php', {'f':info.forum_id}) }}" class="input__button">New Topic</a>
{% endif %}
</div>
<div class="forum__actions__pagination">
{{ pag }}
</div>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro forum_topic_tools(info, take, offset, can_reply) %}
{% from 'macros.twig' import pagination %}
<div class="container forum__actions">
<div class="forum__actions__buttons">
{% if can_reply %}
<a href="/forum/posting.php?t={{ info.topic_id }}" class="input__button">Reply</a>
{% endif %}
</div>
{% set pag = pagination(
info.topic_post_count,
take,
offset,
url_construct('/forum/topic.php', {'t':info.topic_id}),
false,
null,
5
) %}
<div class="forum__actions__pagination">
{{ pagination(
info.topic_post_count,
take,
offset,
url_construct('/forum/topic.php', {'t':info.topic_id}),
false,
null,
5
) }}
{% if can_reply or pag|trim|length > 0 %}
<div class="container forum__actions">
<div class="forum__actions__buttons">
{% if can_reply %}
<a href="/forum/posting.php?t={{ info.topic_id }}" class="input__button">Reply</a>
{% endif %}
</div>
<div class="forum__actions__pagination">
{{ pag }}
</div>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro forum_category_entry(forum, forum_unread, forum_type) %}

View file

@ -16,7 +16,6 @@
'o': topic_offset,
}) %}
{% set can_reply = current_user is defined and topic_info.topic_locked is null and not topic_info.topic_archived %}
{% set topic_tools = forum_topic_tools(topic_info, topic_range, topic_offset, can_reply) %}
{% block content %}

View file

@ -10,6 +10,16 @@
<button class="input__button">Filter</button>
</form>
{% if warnings.notices|length > 0 %}
<div class="warning">
<div class="warning__content">
{% for notice in warnings.notices %}
{{ notice }}
{% endfor %}
</div>
</div>
{% endif %}
{% if warnings.user_id > 0 and warnings.username|length > 0 %}{# shittiest validation in the world, but it should work #}
<form class="container container--lazy" method="post" action="">
{{ container_title('<i class="fas fa-user-shield fa-fw"></i> Warn ' ~ warnings.username) }}
@ -18,7 +28,8 @@
{{ input_select('warning[type]', warnings.types) }}
{{ input_text('warning[note]', '', '', 'text', 'Public note') }}
{{ input_text('warning[until]', '', ''|date('c'), 'datetime-local') }} (empty to set null)
{{ input_select('warning[duration]', warnings.durations) }}
{{ input_text('warning[duration_custom]', '', '', 'text', 'Custom Duration') }}
<button class="input__button">Add</button><br>
<textarea class="input__textarea" name="warning[private]" placeholder="Private note"></textarea>
@ -75,7 +86,7 @@
</div>
{% for warning in warnings.list %}
{{ user_profile_warning(warning, true, true, true, true, csrf_token('warning-delete[%d]'|format(warning.warning_id))) }}
{{ user_profile_warning(warning, true, true, csrf_token('warning-delete[%d]'|format(warning.warning_id))) }}
{% endfor %}
</div>

View file

@ -17,12 +17,19 @@
{% endif %}
</head>
<body
class="main{% if site_background is defined %} {{ site_background.settings|bg_settings('main--bg-%s')|join(' ') }}{% endif %}"
<body class="main{% if site_background is defined %} {{ site_background.settings|bg_settings('main--bg-%s')|join(' ') }}{% endif %}"
style="{% if global_accent_colour is defined %}{{ global_accent_colour|html_colour('--accent-colour') }}{% endif %}">
{% include '_layout/header.twig' %}
<div class="main__wrapper">
{% if current_user.ban_expiration|default(0) > 0 or current_user.silence_expiration|default(0) > 0 %}
<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="/profile.php?u={{ current_user.user_id }}#account-standing" class="warning__link">your profile</a> to view why.
</div>
</div>
{% endif %}
{% block content %}
This page has no content!
{% endblock %}

View file

@ -222,7 +222,7 @@
</div>
{% endmacro %}
{% macro user_profile_warning(warning, show_private_note, show_user, show_issuer, show_ips, delete_csrf) %}
{% macro user_profile_warning(warning, show_private_note, show_user_info, delete_csrf) %}
{% if warning.warning_type == constant('MSZ_WARN_SILENCE') %}
{% set warning_text = 'Silence' %}
{% set warning_class = 'silence' %}
@ -237,38 +237,37 @@
{% set warning_class = 'note' %}
{% endif %}
<div class="profile__warning profile__warning--{{ warning_class }}{% if show_user or show_issuer or delete_csrf %} profile__warning--extendo{% endif %}">
<div class="profile__warning profile__warning--{{ warning_class }}{% if show_user_info or delete_csrf %} profile__warning--extendo{% endif %}">
<div class="profile__warning__background"></div>
{% if show_user or show_issuer or delete_csrf %}
{% if show_user_info or delete_csrf %}
<div class="profile__warning__tools">
{% if show_user %}
<div class="profile__warning__user">
<div class="avatar profile__warning__user__avatar" style="background-image:url('/profile.php?m=avatar&amp;u={{ warning.user_id }}');"></div>
<div class="profile__warning__user__username">
{{ warning.username }}
{% 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="avatar profile__warning__user__avatar" style="background-image:url('/profile.php?m=avatar&amp;u={{ warning.user_id }}');"></div>
<div class="profile__warning__user__username">
{{ warning.username }}
</div>
{% endif %}
{% if warning.user_ip is defined %}
<div class="profile__warning__user__ip">
{{ warning.user_ip }}
</div>
{% endif %}
</div>
{% endif %}
{% if show_ips %}
<div class="profile__warning__user__ip">
{{ warning.user_ip }}
</div>
{% endif %}
</div>
{% endif %}
{% if show_issuer %}
<div class="profile__warning__user">
<div class="avatar profile__warning__user__avatar" style="background-image:url('/profile.php?m=avatar&amp;u={{ warning.issuer_id }}');"></div>
<div class="profile__warning__user__username">
{{ warning.issuer_username }}
</div>
{% if show_ips %}
<div class="profile__warning__user__ip">
{{ warning.issuer_ip }}
</div>
{% endif %}
<div class="profile__warning__user__ip">
{{ warning.issuer_ip }}
</div>
</div>
{% endif %}
@ -289,9 +288,9 @@
{{ warning.warning_created|time_diff }}
</time>
{% if warning.warning_duration_secs > 0 %}
{% 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_created|time_diff }}
{{ warning.warning_duration|time_diff }}
</time>
{% else %}
<div class="profile__warning__duration"></div>

View file

@ -177,12 +177,31 @@
{% endif %}
{% if warnings|length > 0 %}
<div class="container profile__warning__container">
{{ container_title('Account Standing') }}
<div class="container profile__warning__container" id="account-standing">
{{ container_title('Account Standing', false, can_manage_warnings ? '/manage/users.php?v=warnings&u=' ~ profile.user_id : '') }}
<div class="profile__warning">
<div class="profile__warning__background"></div>
{% if can_manage_warnings %}
<div class="profile__warning__tools">
<div class="profile__warning__user">
<div class="profile__warning__user__ip">
User IP
</div>
</div>
<div class="profile__warning__user">
<div class="profile__warning__user__username">
Issuer
</div>
<div class="profile__warning__user__ip">
Issuer IP
</div>
</div>
</div>
{% endif %}
<div class="profile__warning__content">
<div class="profile__warning__type">
Type
@ -203,7 +222,7 @@
</div>
{% for warning in warnings %}
{{ user_profile_warning(warning, can_view_private_note) }}
{{ user_profile_warning(warning, can_view_private_note, can_manage_warnings, can_manage_warnings ? csrf_token('warning-delete[%d]'|format(warning.warning_id)) : '') }}
{% endfor %}
</div>
{% endif %}

View file

@ -5,11 +5,11 @@
{% set title = 'Settings' %}
{% set menu = {
'account': '<i class="fas fa-user fa-fw"></i> Account',
'roles': '<i class="fas fa-user-check"></i> Roles',
'sessions': '<i class="fas fa-key fa-fw"></i> Sessions',
'login-attempts': '<i class="fas fa-user-lock fa-fw"></i> Login Attempts',
'account-log': '<i class="fas fa-file-alt fa-fw"></i> Account Log',
'account': ['<i class="fas fa-user fa-fw"></i> Account', true],
'roles': ['<i class="fas fa-user-check"></i> Roles', not is_restricted],
'sessions': ['<i class="fas fa-key fa-fw"></i> Sessions', true],
'login-attempts': ['<i class="fas fa-user-lock fa-fw"></i> Login Attempts', true],
'account-log': ['<i class="fas fa-file-alt fa-fw"></i> Account Log', true],
} %}
{% block content %}
@ -28,10 +28,12 @@
<div class="container settings__container settings__wrapper__menu">
{{ container_title('Settings') }}
{% for id, text in menu %}
<a href="#{{ id }}" class="settings__wrapper__link">
{{ text|raw }}
</a>
{% for id, item in menu %}
{% if item[1] %}
<a href="#{{ id }}" class="settings__wrapper__link">
{{ item[0]|raw }}
</a>
{% endif %}
{% endfor %}
</div>
@ -108,49 +110,51 @@
{% endif %}
</form>
<div class="container settings__container" id="roles">
{{ container_title('<i class="fas fa-user-check fa-fw"></i> Roles') }}
{% if not is_restricted %}
<div class="container settings__container" id="roles">
{{ container_title('<i class="fas fa-user-check fa-fw"></i> Roles') }}
<div class="settings__description">
<p>This is a listing of the user roles you're a part of, you can select which you want to leave or which one you want to boast as your main role which will change your username colour accordingly.</p>
</div>
<div class="settings__description">
<p>This is a listing of the user roles you're a part of, you can select which you want to leave or which one you want to boast as your main role which will change your username colour accordingly.</p>
</div>
<div class="settings__role__collection">
{% for role in user_roles %}
{% set is_display_role = user_display_role == role.role_id %}
<div class="settings__role__collection">
{% for role in user_roles %}
{% set is_display_role = user_display_role == role.role_id %}
<div class="settings__role" style="{{ role.role_colour|html_colour('--accent-colour') }}">
<div class="settings__role__content">
<div class="settings__role__name">
{{ role.role_name }}
<div class="settings__role" style="{{ role.role_colour|html_colour('--accent-colour') }}">
<div class="settings__role__content">
<div class="settings__role__name">
{{ role.role_name }}
</div>
<div class="settings__role__description">
{{ role.role_description }}
</div>
<form class="settings__role__options" method="post" action="/settings.php">
{{ input_csrf('settings') }}
{{ input_hidden('role[id]', role.role_id) }}
<button class="settings__role__option{% if is_display_role %} settings__role__option--disabled{% endif %}"
name="role[mode]" value="display" title="Set this as your display role"
{% if is_display_role %}disabled{% endif %}>
<i class="far {{ is_display_role ? 'fa-check-square' : 'fa-square' }}"></i>
</button>
<button class="settings__role__option{% if not role.role_can_leave %} settings__role__option--disabled{% endif %}"
name="role[mode]" value="leave" title="Leave this role"
onclick="return confirm('Are you sure you want to remove {{ role.role_name|replace({"'": "\'"}) }} from your account?')"
{% if not role.role_can_leave %}disabled{% endif %}>
<i class="fas fa-times-circle"></i>
</button>
</form>
</div>
<div class="settings__role__description">
{{ role.role_description }}
</div>
<form class="settings__role__options" method="post" action="/settings.php">
{{ input_csrf('settings') }}
{{ input_hidden('role[id]', role.role_id) }}
<button class="settings__role__option{% if is_display_role %} settings__role__option--disabled{% endif %}"
name="role[mode]" value="display" title="Set this as your display role"
{% if is_display_role %}disabled{% endif %}>
<i class="far {{ is_display_role ? 'fa-check-square' : 'fa-square' }}"></i>
</button>
<button class="settings__role__option{% if not role.role_can_leave %} settings__role__option--disabled{% endif %}"
name="role[mode]" value="leave" title="Leave this role"
onclick="return confirm('Are you sure you want to remove {{ role.role_name|replace({"'": "\'"}) }} from your account?')"
{% if not role.role_can_leave %}disabled{% endif %}>
<i class="fas fa-times-circle"></i>
</button>
</form>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
<div class="container settings__container" id="sessions">
{{ container_title('<i class="fas fa-key fa-fw"></i> Sessions') }}