Rewrote the user information class.

This one took multiple days and it pretty invasive into the core of Misuzu so issue might (will) arise, there's also some features that have gone temporarily missing in the mean time and some inefficiencies introduced that will be fixed again at a later time.
The old class isn't gone entirely because I still have to figure out what I'm gonna do about validation, but for the most part this knocks out one of the "layers of backwards compatibility", as I've been referring to it, and is moving us closer to a future where Flashii actually gets real updates.
If you run into anything that's broken and you're inhibited from reporting it through the forum, do it through chat or mail me at flashii-issues@flash.moe.
This commit is contained in:
flash 2023-08-02 22:12:47 +00:00
parent 57081d858d
commit 383e2ed0e0
119 changed files with 1992 additions and 1816 deletions

View file

@ -1,21 +1,24 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use RuntimeException; use Exception;
use Misuzu\Users\User;
if(User::hasCurrent()) { if($msz->isLoggedIn()) {
url_redirect('index'); url_redirect('index');
return; return;
} }
$users = $msz->getUsers();
$sessions = $msz->getSessions();
$loginAttempts = $msz->getLoginAttempts();
if(!empty($_GET['resolve'])) { if(!empty($_GET['resolve'])) {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
try { try {
// Only works for usernames, this is by design // Only works for usernames, this is by design
$userInfo = User::byUsername((string)filter_input(INPUT_GET, 'name')); $userInfo = $users->getUser((string)filter_input(INPUT_GET, 'name'), 'name');
} catch(RuntimeException $ex) { } catch(Exception $ex) {
echo json_encode([ echo json_encode([
'id' => 0, 'id' => 0,
'name' => '', 'name' => '',
@ -25,8 +28,8 @@ if(!empty($_GET['resolve'])) {
} }
echo json_encode([ echo json_encode([
'id' => $userInfo->getId(), 'id' => (int)$userInfo->getId(),
'name' => $userInfo->getUsername(), 'name' => $userInfo->getName(),
'avatar' => url('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]), 'avatar' => url('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]),
]); ]);
return; return;
@ -37,9 +40,6 @@ $ipAddress = $_SERVER['REMOTE_ADDR'];
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX'; $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$sessions = $msz->getSessions();
$loginAttempts = $msz->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress); $remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
$siteIsPrivate = $cfg->getBoolean('private.enable'); $siteIsPrivate = $cfg->getBoolean('private.enable');
@ -91,26 +91,26 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
$loginFailedError = "Invalid username or password, {$attemptsRemainingError}."; $loginFailedError = "Invalid username or password, {$attemptsRemainingError}.";
try { try {
$userInfo = User::byUsernameOrEMailAddress($_POST['login']['username']); $userInfo = $users->getUser($_POST['login']['username'], 'login');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo); $loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo);
$notices[] = $loginFailedError; $notices[] = $loginFailedError;
break; break;
} }
if(!$userInfo->hasPassword()) { if(!$userInfo->hasPasswordHash()) {
$notices[] = 'Your password has been invalidated, please reset it.'; $notices[] = 'Your password has been invalidated, please reset it.';
break; break;
} }
if($userInfo->isDeleted() || !$userInfo->checkPassword($_POST['login']['password'])) { if($userInfo->isDeleted() || !$userInfo->verifyPassword($_POST['login']['password'])) {
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo); $loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
$notices[] = $loginFailedError; $notices[] = $loginFailedError;
break; break;
} }
if($userInfo->passwordNeedsRehash()) if($userInfo->passwordNeedsRehash())
$userInfo->setPassword($_POST['login']['password'])->save(); $users->updateUser($userInfo, password: $_POST['login']['password']);
if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userInfo->getId(), $loginPermVal)) { if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userInfo->getId(), $loginPermVal)) {
$notices[] = "Login succeeded, but you're not allowed to browse the site right now."; $notices[] = "Login succeeded, but you're not allowed to browse the site right now.";

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn()) {
if(!User::hasCurrent()) {
url_redirect('index'); url_redirect('index');
return; return;
} }

View file

@ -4,11 +4,15 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\User;
if(User::hasCurrent()) { if($msz->isLoggedIn()) {
url_redirect('settings-account'); url_redirect('settings-account');
return; return;
} }
$users = $msz->getUsers();
$recoveryTokens = $msz->getRecoveryTokens();
$loginAttempts = $msz->getLoginAttempts();
$reset = !empty($_POST['reset']) && is_array($_POST['reset']) ? $_POST['reset'] : []; $reset = !empty($_POST['reset']) && is_array($_POST['reset']) ? $_POST['reset'] : [];
$forgot = !empty($_POST['forgot']) && is_array($_POST['forgot']) ? $_POST['forgot'] : []; $forgot = !empty($_POST['forgot']) && is_array($_POST['forgot']) ? $_POST['forgot'] : [];
$userId = !empty($reset['user']) ? (int)$reset['user'] : ( $userId = !empty($reset['user']) ? (int)$reset['user'] : (
@ -17,7 +21,7 @@ $userId = !empty($reset['user']) ? (int)$reset['user'] : (
if($userId > 0) if($userId > 0)
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser((string)$userId, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
url_redirect('auth-forgot'); url_redirect('auth-forgot');
return; return;
@ -28,8 +32,6 @@ $ipAddress = $_SERVER['REMOTE_ADDR'];
$siteIsPrivate = $cfg->getBoolean('private.enable'); $siteIsPrivate = $cfg->getBoolean('private.enable');
$canResetPassword = $siteIsPrivate ? $cfg->getBoolean('private.allow_password_reset', true) : true; $canResetPassword = $siteIsPrivate ? $cfg->getBoolean('private.allow_password_reset', true) : true;
$recoveryTokens = $msz->getRecoveryTokens();
$loginAttempts = $msz->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress); $remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
while($canResetPassword) { while($canResetPassword) {
@ -69,9 +71,7 @@ while($canResetPassword) {
// also disables two factor auth to prevent getting locked out of account entirely // also disables two factor auth to prevent getting locked out of account entirely
// this behaviour should really be replaced with recovery keys... // this behaviour should really be replaced with recovery keys...
$userInfo->setPassword($passwordNew) $users->updateUser($userInfo, password: $passwordNew, totpKey: '');
->removeTOTPKey()
->save();
$msz->createAuditLog('PASSWORD_RESET', [], $userInfo); $msz->createAuditLog('PASSWORD_RESET', [], $userInfo);
@ -98,7 +98,7 @@ while($canResetPassword) {
} }
try { try {
$forgotUser = User::byEMailAddress($forgot['email']); $forgotUser = $users->getUser($forgot['email'], 'email');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
unset($forgotUser); unset($forgotUser);
} }
@ -114,12 +114,12 @@ while($canResetPassword) {
$tokenInfo = $recoveryTokens->createToken($forgotUser, $ipAddress); $tokenInfo = $recoveryTokens->createToken($forgotUser, $ipAddress);
$recoveryMessage = Mailer::template('password-recovery', [ $recoveryMessage = Mailer::template('password-recovery', [
'username' => $forgotUser->getUsername(), 'username' => $forgotUser->getName(),
'token' => $tokenInfo->getCode(), 'token' => $tokenInfo->getCode(),
]); ]);
$recoveryMail = Mailer::sendMessage( $recoveryMail = Mailer::sendMessage(
[$forgotUser->getEMailAddress() => $forgotUser->getUsername()], [$forgotUser->getEMailAddress() => $forgotUser->getName()],
$recoveryMessage['subject'], $recoveryMessage['message'] $recoveryMessage['subject'], $recoveryMessage['message']
); );

View file

@ -4,13 +4,14 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\User;
if(User::hasCurrent()) { if($msz->isLoggedIn()) {
url_redirect('index'); url_redirect('index');
return; return;
} }
$users = $msz->getUsers(); $users = $msz->getUsers();
$roles = $msz->getRoles(); $roles = $msz->getRoles();
$config = $msz->getConfig();
$register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : []; $register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : [];
$notices = []; $notices = [];
@ -83,22 +84,26 @@ while(!$restricted && !empty($register)) {
if(!empty($notices)) if(!empty($notices))
break; break;
$defaultRoleInfo = $roles->getDefaultRole();
try { try {
$createUser = User::create( $userInfo = $users->createUser(
$register['username'], $register['username'],
$register['password'], $register['password'],
$register['email'], $register['email'],
$ipAddress, $ipAddress,
$countryCode $countryCode,
$defaultRoleInfo
); );
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$notices[] = 'Something went wrong while creating your account, please alert an administrator or a developer about this!'; $notices[] = 'Something went wrong while creating your account, please alert an administrator or a developer about this!';
break; break;
} }
$users->addRoles($createUser, $roles->getDefaultRole()); $users->addRoles($userInfo, $defaultRoleInfo);
$config->setString('users.newest', $userInfo->getId());
url_redirect('auth-login-welcome', ['username' => $createUser->getUsername()]); url_redirect('auth-login-welcome', ['username' => $userInfo->getName()]);
return; return;
} }

View file

@ -1,18 +1,15 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User;
if(!isset($userInfoReal) || !$authToken->hasImpersonatedUserId() || !CSRF::validateRequest()) { if(!isset($userInfoReal) || !$authToken->hasImpersonatedUserId() || !CSRF::validateRequest()) {
url_redirect('index'); url_redirect('index');
return; return;
} }
$impUserId = $authToken->getImpersonatedUserId();
$authToken->removeImpersonatedUserId(); $authToken->removeImpersonatedUserId();
$authToken->applyCookie(); $authToken->applyCookie();
$impUserId = User::hasCurrent() ? User::getCurrent()->getId() : 0;
url_redirect( url_redirect(
$impUserId > 0 ? 'manage-user' : 'index', $impUserId > 0 ? 'manage-user' : 'index',
['user' => $impUserId] ['user' => $impUserId]

View file

@ -2,23 +2,24 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\TOTPGenerator;
if(User::hasCurrent()) { if($msz->isLoggedIn()) {
url_redirect('index'); url_redirect('index');
return; return;
} }
$users = $msz->getUsers();
$sessions = $msz->getSessions();
$tfaSessions = $msz->getTFASessions();
$loginAttempts = $msz->getLoginAttempts();
$ipAddress = $_SERVER['REMOTE_ADDR']; $ipAddress = $_SERVER['REMOTE_ADDR'];
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX'; $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : []; $twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : [];
$notices = []; $notices = [];
$sessions = $msz->getSessions();
$tfaSessions = $msz->getTFASessions();
$loginAttempts = $msz->getLoginAttempts();
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress); $remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
$tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : ( $tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
@ -31,7 +32,7 @@ if(empty($tokenUserId)) {
return; return;
} }
$userInfo = User::byId((int)$tokenUserId); $userInfo = $users->getUser($tokenUserId, 'id');
// checking user_totp_key specifically because there's a fringe chance that // checking user_totp_key specifically because there's a fringe chance that
// there's a token present, but totp is actually disabled // there's a token present, but totp is actually disabled
@ -60,7 +61,7 @@ while(!empty($twofactor)) {
} }
$clientInfo = ClientInfo::fromRequest(); $clientInfo = ClientInfo::fromRequest();
$totp = $userInfo->createTOTPGenerator(); $totp = new TOTPGenerator($userInfo->getTOTPKey());
if(!in_array($twofactor['code'], $totp->generateRange())) { if(!in_array($twofactor['code'], $totp->generateRange())) {
$notices[] = sprintf( $notices[] = sprintf(

View file

@ -2,7 +2,6 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
// basing whether or not this is an xhr request on whether a referrer header is present // basing whether or not this is an xhr request on whether a referrer header is present
// this page is never directy accessed, under normal circumstances // this page is never directy accessed, under normal circumstances
@ -18,9 +17,8 @@ if(!CSRF::validateRequest()) {
return; return;
} }
$currentUserInfo = User::getCurrent(); if(!$msz->isLoggedIn()) {
if($currentUserInfo === null) { echo render_info('You must be logged in to manage comments.', 403);
echo render_info('You must be logged in to manage comments.', 401);
return; return;
} }
@ -29,8 +27,9 @@ if($msz->hasActiveBan()) {
return; return;
} }
$comments = $msz->getComments(); $currentUserInfo = $msz->getActiveUser();
$comments = $msz->getComments();
$commentPerms = perms_for_comments($currentUserInfo->getId()); $commentPerms = perms_for_comments($currentUserInfo->getId());
$commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); $commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
@ -127,7 +126,7 @@ switch($commentMode) {
break; break;
} }
$isOwnComment = $commentInfo->getUserId() === (string)$currentUserInfo->getId(); $isOwnComment = $commentInfo->getUserId() === $currentUserInfo->getId();
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment; $isModAction = $commentPerms['can_delete_any'] && !$isOwnComment;
if(!$isModAction && !$isOwnComment) { if(!$isModAction && !$isOwnComment) {

View file

@ -1,8 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User;
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0; $forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
$forumId = max($forumId, 0); $forumId = max($forumId, 0);
@ -12,8 +10,8 @@ if($forumId === 0) {
} }
$forum = forum_get($forumId); $forum = forum_get($forumId);
$forumUser = User::getCurrent(); $forumUser = $msz->getActiveUser();
$forumUserId = $forumUser === null ? 0 : $forumUser->getId(); $forumUserId = $forumUser === null ? '0' : $forumUser->getId();
if(empty($forum) || ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK && empty($forum['forum_link']))) { if(empty($forum) || ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK && empty($forum['forum_link']))) {
echo render_error(404); echo render_error(404);
@ -75,4 +73,5 @@ Template::render('forum.forum', [
'forum_info' => $forum, 'forum_info' => $forum,
'forum_topics' => $topics, 'forum_topics' => $topics,
'forum_pagination' => $forumPagination, 'forum_pagination' => $forumPagination,
'forum_show_mark_as_read' => $forumUser !== null,
]); ]);

View file

@ -1,13 +1,11 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User;
$indexMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $indexMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0; $forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
$currentUser = User::getCurrent(); $currentUser = $msz->getActiveUser();
$currentUserId = $currentUser === null ? 0 : $currentUser->getId(); $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
switch($indexMode) { switch($indexMode) {
case 'mark': case 'mark':
@ -34,6 +32,7 @@ switch($indexMode) {
Template::render('forum.index', [ Template::render('forum.index', [
'forum_categories' => $categories, 'forum_categories' => $categories,
'forum_empty' => $blankForum, 'forum_empty' => $blankForum,
'forum_show_mark_as_read' => $currentUser !== null,
]); ]);
break; break;
} }

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_FORUM, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_FORUM, User::getCurrent()->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -1,21 +1,19 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User;
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0; $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
$postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1'; $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$postRequestVerified = CSRF::validateRequest(); $postRequestVerified = CSRF::validateRequest();
if(!empty($postMode) && !User::hasCurrent()) { if(!empty($postMode) && !$msz->isLoggedIn()) {
echo render_info('You must be logged in to manage posts.', 401); echo render_info('You must be logged in to manage posts.', 401);
return; return;
} }
$currentUser = User::getCurrent(); $currentUser = $msz->getActiveUser();
$currentUserId = $currentUser === null ? 0 : $currentUser->getId(); $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
if($postMode !== '' && $msz->hasActiveBan()) { if($postMode !== '' && $msz->hasActiveBan()) {
echo render_info('You have been banned, check your profile for more information.', 403); echo render_info('You have been banned, check your profile for more information.', 403);

View file

@ -2,15 +2,13 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
$currentUser = User::getCurrent(); if(!$msz->isLoggedIn()) {
if($currentUser === null) {
echo render_error(401); echo render_error(401);
return; return;
} }
$currentUser = $msz->getActiveUser();
$currentUserId = $currentUser->getId(); $currentUserId = $currentUser->getId();
if($msz->hasActiveBan()) { if($msz->hasActiveBan()) {
echo render_error(403); echo render_error(403);

View file

@ -1,15 +1,13 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User;
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0; $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
$topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0; $topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
$moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1'; $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$topicUser = User::getCurrent(); $topicUser = $msz->getActiveUser();
$topicUserId = $topicUser === null ? 0 : $topicUser->getId(); $topicUserId = $topicUser === null ? '0' : $topicUser->getId();
if($topicId < 1 && $postId > 0) { if($topicId < 1 && $postId > 0) {
$postInfo = forum_post_find($postId, $topicUserId); $postInfo = forum_post_find($postId, $topicUserId);
@ -77,7 +75,7 @@ if(in_array($moderationMode, $validModerationModes, true)) {
return; return;
} }
if(!User::hasCurrent()) { if(!$msz->isLoggedIn()) {
echo render_info('You must be logged in to manage posts.', 401); echo render_info('You must be logged in to manage posts.', 401);
return; return;
} }
@ -329,4 +327,5 @@ Template::render('forum.topic', [
'topic_can_nuke_or_restore' => $canNukeOrRestore, 'topic_can_nuke_or_restore' => $canNukeOrRestore,
'topic_can_bump' => $canBumpTopic, 'topic_can_bump' => $canBumpTopic,
'topic_can_lock' => $canLockTopic, 'topic_can_lock' => $canLockTopic,
'topic_user_id' => $topicUserId,
]); ]);

View file

@ -5,9 +5,8 @@ use DateTimeInterface;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\DateTime;
use Misuzu\Changelog\Changelog; use Misuzu\Changelog\Changelog;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -115,4 +114,5 @@ Template::render('manage.changelog.change', [
'change_info' => $changeInfo ?? null, 'change_info' => $changeInfo ?? null,
'change_tags' => $changeTags, 'change_tags' => $changeTags,
'change_actions' => $changeActions, 'change_actions' => $changeActions,
'change_author_id' => $msz->getActiveUser()->getId(),
]); ]);

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -20,6 +19,7 @@ if(!$changelogPagination->hasValidOffset()) {
$changeInfos = $changelog->getAllChanges(withTags: true, pagination: $changelogPagination); $changeInfos = $changelog->getAllChanges(withTags: true, pagination: $changelogPagination);
$changes = []; $changes = [];
$userInfos = []; $userInfos = [];
$userColours = [];
foreach($changeInfos as $changeInfo) { foreach($changeInfos as $changeInfo) {
$userId = $changeInfo->getUserId(); $userId = $changeInfo->getUserId();
@ -28,7 +28,8 @@ foreach($changeInfos as $changeInfo) {
$userInfo = $userInfos[$userId]; $userInfo = $userInfos[$userId];
} else { } else {
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser($userId, 'id');
$userColours[$userId] = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
} }
@ -39,6 +40,7 @@ foreach($changeInfos as $changeInfo) {
$changes[] = [ $changes[] = [
'change' => $changeInfo, 'change' => $changeInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColours[$userId] ?? \Index\Colour\Colour::none(),
]; ];
} }

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_CHANGELOG, $msz->getActiveUser()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_FORUM_TOPIC_REDIRS)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_FORUM_TOPIC_REDIRS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -19,7 +17,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
throw new \Exception("Invalid topic id."); throw new \Exception("Invalid topic id.");
$msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]); $msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]);
forum_topic_redir_create($rTopicId, User::getCurrent()->getId(), $rTopicURL); forum_topic_redir_create($rTopicId, $msz->getActiveUser()->getId(), $rTopicURL);
url_redirect('manage-forum-topic-redirs'); url_redirect('manage-forum-topic-redirs');
return; return;
} }

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -2,13 +2,13 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_VIEW_LOGS)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_VIEW_LOGS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$users = $msz->getUsers();
$auditLog = $msz->getAuditLog(); $auditLog = $msz->getAuditLog();
$pagination = new Pagination($auditLog->countLogs(), 50); $pagination = new Pagination($auditLog->countLogs(), 50);
@ -19,15 +19,20 @@ if(!$pagination->hasValidOffset()) {
$logs = $auditLog->getLogs(pagination: $pagination); $logs = $auditLog->getLogs(pagination: $pagination);
$userInfos = []; $userInfos = [];
$userColours = [];
foreach($logs as $log) foreach($logs as $log)
if($log->hasUserId()) { if($log->hasUserId()) {
$userId = $log->getUserId(); $userId = $log->getUserId();
if(!array_key_exists($userId, $userInfos)) if(!array_key_exists($userId, $userInfos)) {
$userInfos[$userId] = User::byId($userId); $userInfos[$userId] = $users->getUser($userId, 'id');
$userColours[$userId] = $users->getUserColour($userInfos[$userId]);
}
} }
Template::render('manage.general.logs', [ Template::render('manage.general.logs', [
'global_logs' => $logs, 'global_logs' => $logs,
'global_logs_pagination' => $pagination, 'global_logs_pagination' => $pagination,
'global_logs_users' => $userInfos, 'global_logs_users' => $userInfos,
'global_logs_users_colours' => $userColours,
]); ]);

View file

@ -2,10 +2,8 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Config\CfgTools; use Misuzu\Config\CfgTools;
use Misuzu\Users\User;
if(!User::hasCurrent() if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
|| !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -2,10 +2,8 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Config\DbConfig; use Misuzu\Config\DbConfig;
use Misuzu\Users\User;
if(!User::hasCurrent() if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
|| !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -1,10 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
if(!User::hasCurrent()
|| !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -40,7 +39,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$body = trim((string)filter_input(INPUT_POST, 'np_body')); $body = trim((string)filter_input(INPUT_POST, 'np_body'));
if($isNew) { if($isNew) {
$postInfo = $news->createPost($category, $title, $body, $featured, User::getCurrent()); $postInfo = $news->createPost($category, $title, $body, $featured, $msz->getActiveUser());
} else { } else {
if($category === $postInfo->getCategoryId()) if($category === $postInfo->getCategoryId())
$category = null; $category = null;

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_NEWS, $msz->getActiveUser()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -4,9 +4,8 @@ namespace Misuzu;
use DateTimeInterface; use DateTimeInterface;
use RuntimeException; use RuntimeException;
use Index\DateTime; use Index\DateTime;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_BANS)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_BANS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -29,14 +28,16 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete'))
return; return;
} }
$users = $msz->getUsers();
try { try {
$userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT)); $userInfo = $users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
} }
$modInfo = User::getCurrent(); $modInfo = $msz->getActiveUser();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT); $expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT);

View file

@ -2,23 +2,28 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_BANS)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_BANS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$users = $msz->getUsers();
$userInfos = [ $userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(), $msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
]; ];
$filterUser = null; $filterUser = null;
if(filter_has_var(INPUT_GET, 'u')) { if(filter_has_var(INPUT_GET, 'u')) {
$filterUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try { try {
$filterUser = User::byId($filterUserId); $filterUser = $users->getUser($filterUserId, 'id');
$userInfos[(string)$filterUser->getId()] = $filterUser; $userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
@ -40,19 +45,34 @@ foreach($banInfos as $banInfo) {
if(array_key_exists($banInfo->getUserId(), $userInfos)) if(array_key_exists($banInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$banInfo->getUserId()]; $userInfo = $userInfos[$banInfo->getUserId()];
else else
$userInfos[$banInfo->getUserId()] = $userInfo = User::byId((int)$banInfo->getUserId()); $userInfos[$banInfo->getUserId()] = $userInfo = $users->getUser($banInfo->getUserId(), 'id');
if(!$banInfo->hasModId()) if(array_key_exists($userInfo->getId(), $userColours))
$modInfo = null; $userColour = $userColours[$userInfo->getId()];
elseif(array_key_exists($banInfo->getModId(), $userInfos))
$modInfo = $userInfos[$banInfo->getModId()];
else else
$userInfos[$banInfo->getModId()] = $modInfo = User::byId((int)$banInfo->getModId()); $userColours[$userInfo->getId()] = $userColour = $users->getUserColour($userInfo);
if(!$banInfo->hasModId()) {
$modInfo = null;
$modColour = null;
} else {
if(array_key_exists($banInfo->getModId(), $userInfos))
$modInfo = $userInfos[$banInfo->getModId()];
else
$userInfos[$banInfo->getModId()] = $modInfo = $users->getUser($banInfo->getModId(), 'id');
if(array_key_exists($modInfo->getId(), $userColours))
$modColour = $userColours[$modInfo->getId()];
else
$userColours[$modInfo->getId()] = $modColour = $users->getUserColour($modInfo);
}
$banList[] = [ $banList[] = [
'info' => $banInfo, 'info' => $banInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColour,
'mod' => $modInfo, 'mod' => $modInfo,
'mod_colour' => $modColour,
]; ];
} }

View file

@ -1,21 +1,41 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_USERS)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$pagination = new Pagination(User::countAll(true), 30); $users = $msz->getUsers();
$roles = $msz->getRoles();
$pagination = new Pagination($users->countUsers(), 30);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset()) {
echo render_error(404); echo render_error(404);
return; return;
} }
$userList = [];
$userInfos = $users->getUsers(pagination: $pagination, orderBy: 'id');
$roleInfos = [];
foreach($userInfos as $userInfo) {
$displayRoleId = $userInfo->getDisplayRoleId() ?? '1';
if(array_key_exists($displayRoleId, $roleInfos))
$roleInfo = $roleInfos[$displayRoleId];
else
$roleInfos[$displayRoleId] = $roleInfo = $roles->getRole($displayRoleId);
$colour = $userInfo->hasColour() ? $userInfo->getColour() : $roleInfo->getColour();
$userList[] = [
'info' => $userInfo,
'role' => $roleInfo,
'colour' => $colour,
];
}
Template::render('manage.users.users', [ Template::render('manage.users.users', [
'manage_users' => User::all(true, $pagination), 'manage_users' => $userList,
'manage_users_pagination' => $pagination, 'manage_users_pagination' => $pagination,
]); ]);

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -17,19 +16,20 @@ if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId)) {
return; return;
} }
$users = $msz->getUsers();
$modNotes = $msz->getModNotes(); $modNotes = $msz->getModNotes();
if($hasUserId) { if($hasUserId) {
$isNew = true; $isNew = true;
try { try {
$userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT)); $userInfo = $users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
} }
$authorInfo = User::getCurrent(); $authorInfo = $msz->getActiveUser();
} elseif($hasNoteId) { } elseif($hasNoteId) {
$isNew = false; $isNew = false;
@ -49,8 +49,8 @@ if($hasUserId) {
return; return;
} }
$userInfo = User::byId((int)$noteInfo->getUserId()); $userInfo = $users->getUser($noteInfo->getUserId(), 'id');
$authorInfo = $noteInfo->hasAuthorId() ? User::byId((int)$noteInfo->getAuthorId()) : null; $authorInfo = $noteInfo->hasAuthorId() ? $users->getUser($noteInfo->getAuthorId(), 'id') : null;
} }
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
@ -83,5 +83,7 @@ Template::render('manage.users.note', [
'note_new' => $isNew, 'note_new' => $isNew,
'note_info' => $noteInfo ?? null, 'note_info' => $noteInfo ?? null,
'note_user' => $userInfo, 'note_user' => $userInfo,
'note_user_colour' => $users->getUserColour($userInfo),
'note_author' => $authorInfo, 'note_author' => $authorInfo,
'note_author_colour' => $users->getUserColour($authorInfo),
]); ]);

View file

@ -2,23 +2,28 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_NOTES)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$users = $msz->getUsers();
$userInfos = [ $userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(), $msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
]; ];
$filterUser = null; $filterUser = null;
if(filter_has_var(INPUT_GET, 'u')) { if(filter_has_var(INPUT_GET, 'u')) {
$filterUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try { try {
$filterUser = User::byId($filterUserId); $filterUser = $users->getUser($filterUserId, 'id');
$userInfos[(string)$filterUser->getId()] = $filterUser; $userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
@ -40,19 +45,34 @@ foreach($noteInfos as $noteInfo) {
if(array_key_exists($noteInfo->getUserId(), $userInfos)) if(array_key_exists($noteInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$noteInfo->getUserId()]; $userInfo = $userInfos[$noteInfo->getUserId()];
else else
$userInfos[$noteInfo->getUserId()] = $userInfo = User::byId((int)$noteInfo->getUserId()); $userInfos[$noteInfo->getUserId()] = $userInfo = $users->getUser($noteInfo->getUserId(), 'id');
if(!$noteInfo->hasAuthorId()) if(array_key_exists($userInfo->getId(), $userColours))
$authorInfo = null; $userColour = $userColours[$userInfo->getId()];
elseif(array_key_exists($noteInfo->getAuthorId(), $userInfos))
$authorInfo = $userInfos[$noteInfo->getAuthorId()];
else else
$userInfos[$noteInfo->getAuthorId()] = $authorInfo = User::byId((int)$noteInfo->getAuthorId()); $userColours[$userInfo->getId()] = $userColour = $users->getUserColour($userInfo);
if(!$noteInfo->hasAuthorId()) {
$authorInfo = null;
$authorColour = null;
} else {
if(array_key_exists($noteInfo->getAuthorId(), $userInfos))
$authorInfo = $userInfos[$noteInfo->getAuthorId()];
else
$userInfos[$noteInfo->getAuthorId()] = $modInfo = $users->getUser($noteInfo->getAuthorId(), 'id');
if(array_key_exists($authorInfo->getId(), $userColours))
$authorColour = $userColours[$authorInfo->getId()];
else
$userColours[$authorInfo->getId()] = $authorColour = $users->getUserColour($authorInfo);
}
$notes[] = [ $notes[] = [
'info' => $noteInfo, 'info' => $noteInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColour,
'author' => $authorInfo, 'author' => $authorInfo,
'author_colour' => $authorColour,
]; ];
} }

View file

@ -4,13 +4,13 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Colour\ColourRGB; use Index\Colour\ColourRGB;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$users = $msz->getUsers();
$roles = $msz->getRoles(); $roles = $msz->getRoles();
if(filter_has_var(INPUT_GET, 'r')) { if(filter_has_var(INPUT_GET, 'r')) {
@ -25,15 +25,16 @@ if(filter_has_var(INPUT_GET, 'r')) {
} }
} else $isNew = true; } else $isNew = true;
$currentUser = User::getCurrent(); $currentUser = $msz->getActiveUser();
$currentUserId = $currentUser->getId(); $canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_PERMS);
$canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
if($canEditPerms) if($canEditPerms)
$permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0)); $permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0));
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(!$isNew && !$currentUser->isSuper() && $roleInfo->getRank() >= $currentUser->getRank()) { $userRank = $users->getUserRank($currentUser);
if(!$isNew && !$currentUser->isSuperUser() && $roleInfo->getRank() >= $userRank) {
echo 'You aren\'t allowed to edit this role.'; echo 'You aren\'t allowed to edit this role.';
break; break;
} }
@ -62,7 +63,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
'role_ur_col_blue' => $colourBlue, 'role_ur_col_blue' => $colourBlue,
]); ]);
if(!$currentUser->isSuper() && $roleRank >= $currentUser->getRank()) { if(!$currentUser->isSuperUser() && $roleRank >= $userRank) {
echo 'You aren\'t allowed to make a role with equal rank to your own.'; echo 'You aren\'t allowed to make a role with equal rank to your own.';
break; break;
} }

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
echo render_error(403); echo render_error(403);
return; return;
} }

View file

@ -5,7 +5,7 @@ use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Misuzu\Users\User; use Misuzu\Users\User;
if(!User::hasCurrent()) { if(!$msz->isLoggedIn()) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -13,15 +13,15 @@ if(!User::hasCurrent()) {
$users = $msz->getUsers(); $users = $msz->getUsers();
$roles = $msz->getRoles(); $roles = $msz->getRoles();
$currentUser = User::getCurrent(); $currentUser = $msz->getActiveUser();
$currentUserId = $currentUser->getId();
$canManageUsers = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_USERS); $canManageUsers = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_USERS);
$canManagePerms = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS); $canManagePerms = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_PERMS);
$canManageNotes = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_NOTES); $canManageNotes = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_NOTES);
$canManageWarnings = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_WARNINGS); $canManageWarnings = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_WARNINGS);
$canManageBans = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_BANS); $canManageBans = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_BANS);
$canImpersonate = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_IMPERSONATE); $canImpersonate = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_IMPERSONATE);
$canSendTestMail = $currentUser->isSuperUser();
$hasAccess = $canManageUsers || $canManageNotes || $canManageWarnings || $canManageBans; $hasAccess = $canManageUsers || $canManageNotes || $canManageWarnings || $canManageBans;
if(!$hasAccess) { if(!$hasAccess) {
@ -33,13 +33,16 @@ $notices = [];
$userId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); $userId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser($userId, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
} }
$canEdit = $canManageUsers && ($currentUser->isSuper() || $currentUser->getId() === $userInfo->getId() || $currentUser->getRank() > $userInfo->getRank()); $currentUserRank = $users->getUserRank($currentUser);
$userRank = $users->getUserRank($userInfo);
$canEdit = $canManageUsers && ($currentUser->isSuperUser() || (string)$currentUser->getId() === $userInfo->getId() || $currentUserRank > $userRank);
$canEditPerms = $canEdit && $canManagePerms; $canEditPerms = $canEdit && $canManagePerms;
$permissions = $canEditPerms ? manage_perms_list(perms_get_user_raw($userId)) : []; $permissions = $canEditPerms ? manage_perms_list(perms_get_user_raw($userId)) : [];
@ -50,15 +53,15 @@ if(CSRF::validateRequest() && $canEdit) {
} elseif(!is_string($_POST['impersonate_user']) || $_POST['impersonate_user'] !== 'meow') { } elseif(!is_string($_POST['impersonate_user']) || $_POST['impersonate_user'] !== 'meow') {
$notices[] = 'You didn\'t say the magic word.'; $notices[] = 'You didn\'t say the magic word.';
} else { } else {
$allowToImpersonate = $currentUser->isSuper(); $allowToImpersonate = $currentUser->isSuperUser();
if(!$allowToImpersonate) { if(!$allowToImpersonate) {
$allowImpersonateUsers = $msz->getConfig()->getArray(sprintf('impersonate.allow.u%s', $currentUser->getId())); $allowImpersonateUsers = $msz->getConfig()->getArray(sprintf('impersonate.allow.u%s', $currentUser->getId()));
$allowToImpersonate = in_array((string)$userInfo->getId(), $allowImpersonateUsers, true); $allowToImpersonate = in_array($userInfo->getId(), $allowImpersonateUsers, true);
} }
if($allowToImpersonate) { if($allowToImpersonate) {
$msz->createAuditLog('USER_IMPERSONATE', [$userInfo->getId(), $userInfo->getUsername()]); $msz->createAuditLog('USER_IMPERSONATE', [$userInfo->getId(), $userInfo->getName()]);
$authToken->setImpersonatedUserId($userInfo->getId()); $authToken->setImpersonatedUserId($userInfo->getId());
$authToken->applyCookie(); $authToken->applyCookie();
url_redirect('index'); url_redirect('index');
@ -68,13 +71,13 @@ if(CSRF::validateRequest() && $canEdit) {
} }
if(!empty($_POST['send_test_email'])) { if(!empty($_POST['send_test_email'])) {
if(!$currentUser->isSuper()) { if(!$canSendTestMail) {
$notices[] = 'You must be a super user to do this.'; $notices[] = 'You must be a super user to do this.';
} elseif(!is_string($_POST['send_test_email']) || $_POST['send_test_email'] !== 'yes_send_it') { } elseif(!is_string($_POST['send_test_email']) || $_POST['send_test_email'] !== 'yes_send_it') {
$notices[] = 'Invalid request thing shut the fuck up.'; $notices[] = 'Invalid request thing shut the fuck up.';
} else { } else {
$testMail = Mailer::sendMessage( $testMail = Mailer::sendMessage(
[$userInfo->getEMailAddress() => $userInfo->getUsername()], [$userInfo->getEMailAddress() => $userInfo->getName()],
'Flashii Test E-mail', 'Flashii Test E-mail',
'You were sent this e-mail to validate if you can receive e-mails from Flashii. You may discard it.' 'You were sent this e-mail to validate if you can receive e-mails from Flashii. You may discard it.'
); );
@ -100,7 +103,7 @@ if(CSRF::validateRequest() && $canEdit) {
$removeRoles = []; $removeRoles = [];
foreach($existingRoles as $roleInfo) { foreach($existingRoles as $roleInfo) {
if($roleInfo->isDefault() || !($currentUser->isSuper() || $currentUser->getRank() > $roleInfo->getRank())) if($roleInfo->isDefault() || !($currentUser->isSuperUser() || $userRank > $roleInfo->getRank()))
continue; continue;
if(!in_array($roleInfo->getId(), $applyRoles)) if(!in_array($roleInfo->getId(), $applyRoles))
@ -119,7 +122,7 @@ if(CSRF::validateRequest() && $canEdit) {
continue; continue;
} }
if(!$currentUser->isSuper() && $currentUser->getRank() <= $roleInfo->getRank()) if(!$currentUser->isSuperUser() && $userRank <= $roleInfo->getRank())
continue; continue;
if(!in_array($roleInfo, $existingRoles)) if(!in_array($roleInfo, $existingRoles))
@ -152,10 +155,9 @@ if(CSRF::validateRequest() && $canEdit) {
$users->updateUser( $users->updateUser(
userInfo: $userInfo, userInfo: $userInfo,
displayRoleInfo: $displayRole, displayRoleInfo: $displayRole,
countryCode: (string)($_POST['user']['country'] ?? 'XX'),
title: (string)($_POST['user']['title'] ?? '')
); );
$userInfo->setCountry((string)($_POST['user']['country'] ?? ''))
->setTitle((string)($_POST['user']['title'] ?? ''));
} }
} }
@ -169,7 +171,7 @@ if(CSRF::validateRequest() && $canEdit) {
} }
if(empty($notices)) if(empty($notices))
$userInfo->setColour($setColour); $users->updateUser(userInfo: $userInfo, colour: $setColour);
} }
if(!empty($_POST['password']) && is_array($_POST['password'])) { if(!empty($_POST['password']) && is_array($_POST['password'])) {
@ -182,13 +184,10 @@ if(CSRF::validateRequest() && $canEdit) {
elseif(!empty(User::validatePassword($passwordNewValue))) elseif(!empty(User::validatePassword($passwordNewValue)))
$notices[] = 'New password is too weak.'; $notices[] = 'New password is too weak.';
else else
$userInfo->setPassword($passwordNewValue); $users->updateUser(userInfo: $userInfo, password: $passwordNewValue);
} }
} }
if(empty($notices))
$userInfo->save();
if($canEditPerms && !empty($_POST['perms']) && is_array($_POST['perms'])) { if($canEditPerms && !empty($_POST['perms']) && is_array($_POST['perms'])) {
$perms = manage_perms_apply($permissions, $_POST['perms']); $perms = manage_perms_apply($permissions, $_POST['perms']);
@ -203,6 +202,9 @@ if(CSRF::validateRequest() && $canEdit) {
// this smells, make it refresh/apply in a non-retarded way // this smells, make it refresh/apply in a non-retarded way
$permissions = manage_perms_list(perms_get_user_raw($userId)); $permissions = manage_perms_list(perms_get_user_raw($userId));
} }
url_redirect('manage-user', ['user' => $userInfo->getId()]);
return;
} }
$rolesAll = $roles->getRoles(); $rolesAll = $roles->getRoles();
@ -219,5 +221,6 @@ Template::render('manage.users.user', [
'can_manage_warnings' => $canManageWarnings, 'can_manage_warnings' => $canManageWarnings,
'can_manage_bans' => $canManageBans, 'can_manage_bans' => $canManageBans,
'can_impersonate' => $canImpersonate, 'can_impersonate' => $canImpersonate,
'can_send_test_mail' => $canSendTestMail,
'permissions' => $permissions ?? [], 'permissions' => $permissions ?? [],
]); ]);

View file

@ -2,9 +2,8 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
@ -27,14 +26,16 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete'))
return; return;
} }
$users = $msz->getUsers();
try { try {
$userInfo = User::byId((int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT)); $userInfo = $users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
} }
$modInfo = User::getCurrent(); $modInfo = $msz->getActiveUser();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$body = trim((string)filter_input(INPUT_POST, 'uw_body')); $body = trim((string)filter_input(INPUT_POST, 'uw_body'));

View file

@ -2,23 +2,28 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) { if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$users = $msz->getUsers();
$userInfos = [ $userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(), $msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
]; ];
$filterUser = null; $filterUser = null;
if(filter_has_var(INPUT_GET, 'u')) { if(filter_has_var(INPUT_GET, 'u')) {
$filterUserId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try { try {
$filterUser = User::byId($filterUserId); $filterUser = $users->getUser($filterUserId, 'id');
$userInfos[(string)$filterUser->getId()] = $filterUser; $userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
@ -40,19 +45,34 @@ foreach($warnInfos as $warnInfo) {
if(array_key_exists($warnInfo->getUserId(), $userInfos)) if(array_key_exists($warnInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$warnInfo->getUserId()]; $userInfo = $userInfos[$warnInfo->getUserId()];
else else
$userInfos[$warnInfo->getUserId()] = $userInfo = User::byId((int)$warnInfo->getUserId()); $userInfos[$warnInfo->getUserId()] = $userInfo = $users->getUser($warnInfo->getUserId(), 'id');
if(!$warnInfo->hasModId()) if(array_key_exists($userInfo->getId(), $userColours))
$modInfo = null; $userColour = $userColours[$userInfo->getId()];
elseif(array_key_exists($warnInfo->getModId(), $userInfos))
$modInfo = $userInfos[$warnInfo->getModId()];
else else
$userInfos[$warnInfo->getModId()] = $modInfo = User::byId((int)$warnInfo->getModId()); $userColours[$userInfo->getId()] = $userColour = $users->getUserColour($userInfo);
if(!$warnInfo->hasModId()) {
$modInfo = null;
$modColour = null;
} else {
if(array_key_exists($warnInfo->getModId(), $userInfos))
$modInfo = $userInfos[$warnInfo->getModId()];
else
$userInfos[$warnInfo->getModId()] = $modInfo = $users->getUser($warnInfo->getModId(), 'id');
if(array_key_exists($modInfo->getId(), $userColours))
$modColour = $userColours[$modInfo->getId()];
else
$userColours[$modInfo->getId()] = $modColour = $users->getUserColour($modInfo);
}
$warnList[] = [ $warnList[] = [
'info' => $warnInfo, 'info' => $warnInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColour,
'mod' => $modInfo, 'mod' => $modInfo,
'mod_colour' => $modColour,
]; ];
} }

View file

@ -2,8 +2,15 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!$msz->isLoggedIn()) {
echo render_error(403);
return;
}
// TODO: restore forum-topics and forum-posts orderings
$users = $msz->getUsers();
$roles = $msz->getRoles(); $roles = $msz->getRoles();
$roleId = filter_has_var(INPUT_GET, 'r') ? (string)filter_input(INPUT_GET, 'r') : null; $roleId = filter_has_var(INPUT_GET, 'r') ? (string)filter_input(INPUT_GET, 'r') : null;
@ -11,47 +18,35 @@ $orderBy = strtolower((string)filter_input(INPUT_GET, 'ss'));
$orderDir = strtolower((string)filter_input(INPUT_GET, 'sd')); $orderDir = strtolower((string)filter_input(INPUT_GET, 'sd'));
$orderDirs = [ $orderDirs = [
'asc' => 'Ascending', 'asc' => 'In Order',
'desc' => 'Descending', 'desc' => 'Reverse Order',
]; ];
$defaultOrder = 'last-online'; $defaultOrder = 'active';
$orderFields = [ $orderFields = [
'id' => [ 'id' => [
'column' => 'u.`user_id`',
'default-dir' => 'asc',
'title' => 'User ID', 'title' => 'User ID',
], ],
'name' => [ 'name' => [
'column' => 'u.`username`',
'default-dir' => 'asc',
'title' => 'Username', 'title' => 'Username',
], ],
'country' => [ 'country' => [
'column' => 'u.`user_country`',
'default-dir' => 'asc',
'title' => 'Country', 'title' => 'Country',
], ],
'created' => [
'title' => 'Registration Date',
],
'active' => [
'title' => 'Last Online',
],
'registered' => [ 'registered' => [
'column' => 'u.`user_created`', 'alt' => 'created',
'default-dir' => 'desc',
'title' => 'Registration Date', 'title' => 'Registration Date',
], ],
'last-online' => [ 'last-online' => [
'column' => 'u.`user_active`', 'alt' => 'active',
'default-dir' => 'desc',
'title' => 'Last Online', 'title' => 'Last Online',
], ],
'forum-topics' => [
'column' => '`user_count_topics`',
'default-dir' => 'desc',
'title' => 'Forum Topics',
],
'forum-posts' => [
'column' => '`user_count_posts`',
'default-dir' => 'desc',
'title' => 'Forum Posts',
],
]; ];
if(empty($orderBy)) { if(empty($orderBy)) {
@ -61,14 +56,17 @@ if(empty($orderBy)) {
return; return;
} }
if(array_key_exists('alt', $orderFields[$orderBy]))
$orderBy = $orderFields[$orderBy]['alt'];
if(empty($orderDir)) { if(empty($orderDir)) {
$orderDir = $orderFields[$orderBy]['default-dir']; $orderDir = 'asc';
} elseif(!array_key_exists($orderDir, $orderDirs)) { } elseif(!array_key_exists($orderDir, $orderDirs)) {
echo render_error(400); echo render_error(400);
return; return;
} }
$canManageUsers = perms_check_user(MSZ_PERMS_USER, User::hasCurrent() ? User::getCurrent()->getId() : 0, MSZ_PERM_USER_MANAGE_USERS); $canManageUsers = perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_USERS);
if($roleId === null) { if($roleId === null) {
$roleInfo = $roles->getDefaultRole(); $roleInfo = $roles->getDefaultRole();
@ -81,57 +79,35 @@ if($roleId === null) {
} }
} }
$deleted = $canManageUsers ? null : false;
$pagination = new Pagination($roles->countRoleUsers($roleInfo), 15);
$rolesAll = $roles->getRoles(hidden: false); $rolesAll = $roles->getRoles(hidden: false);
$pagination = new Pagination($users->countUsers(roleInfo: $roleInfo, deleted: $deleted), 15);
$getUsers = DB::prepare(sprintf( $userList = [];
' $userInfos = $users->getUsers(
SELECT roleInfo: $roleInfo,
:current_user_id AS `current_user_id`, deleted: $deleted,
u.`user_id`, u.`username`, u.`user_country`, orderBy: $orderBy,
u.`user_created`, u.`user_active`, r.`role_id`, reverseOrder: $orderDir !== 'asc',
COALESCE(u.`user_title`, r.`role_title`) AS `user_title`, pagination: $pagination,
COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`, );
(
SELECT COUNT(`topic_id`)
FROM `msz_forum_topics`
WHERE `user_id` = u.`user_id`
AND `topic_deleted` IS NULL
) AS `user_count_topics`,
(
SELECT COUNT(`post_Id`)
FROM `msz_forum_posts`
WHERE `user_id` = u.`user_id`
AND `post_deleted` IS NULL
) AS `user_count_posts`
FROM `msz_users` AS u
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
LEFT JOIN `msz_users_roles` AS ur
ON ur.`user_id` = u.`user_id`
WHERE ur.`role_id` = :role_id
%1$s
ORDER BY %2$s %3$s
LIMIT %4$d, %5$d
',
$canManageUsers ? '' : 'AND u.`user_deleted` IS NULL',
$orderFields[$orderBy]['column'],
$orderDir,
$pagination->getOffset(),
$pagination->getRange()
));
$getUsers->bind('role_id', $roleInfo->getId());
$getUsers->bind('current_user_id', User::hasCurrent() ? User::getCurrent()->getId() : 0);
$users = $getUsers->fetchAll();
if(empty($users)) foreach($userInfos as $userInfo)
$userList[] = [
'info' => $userInfo,
'colour' => $users->getUserColour($userInfo),
'ftopics' => forum_get_user_topic_count($userInfo),
'fposts' => forum_get_user_post_count($userInfo),
];
if(empty($userList))
http_response_code(404); http_response_code(404);
Template::render('user.listing', [ Template::render('user.listing', [
'roles' => $rolesAll, 'roles' => $rolesAll,
'role' => $roleInfo, 'role' => $roleInfo,
'users' => $users, 'users' => $userList,
'order_fields' => $orderFields, 'order_fields' => $orderFields,
'order_directions' => $orderDirs, 'order_directions' => $orderDirs,
'order_field' => $orderBy, 'order_field' => $orderBy,

View file

@ -4,50 +4,61 @@ namespace Misuzu;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\ByteFormat; use Index\ByteFormat;
use Index\DateTime;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\Assets\UserAvatarAsset;
use Misuzu\Users\Assets\UserBackgroundAsset; use Misuzu\Users\Assets\UserBackgroundAsset;
$userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0; $userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0;
$profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : ''; $profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST); $isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
$currentUser = User::getCurrent(); $users = $msz->getUsers();
$viewingAsGuest = $currentUser === null;
$currentUserId = $viewingAsGuest ? 0 : $currentUser->getId(); $viewerInfo = $msz->getActiveUser();
$viewingAsGuest = $viewerInfo === null;
$viewerId = $viewingAsGuest ? '0' : $viewerInfo->getId();
try { try {
$profileUser = User::findForProfile($userId); $userInfo = $users->getUser($userId, 'profile');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
http_response_code(404); http_response_code(404);
Template::render('profile.index', [ Template::render('profile.index', [
'profile_is_guest' => $viewingAsGuest, 'profile_is_guest' => $viewingAsGuest,
'profile_is_deleted' => true, 'profile_is_deleted' => true,
'profile_is_banned' => false,
]); ]);
return; return;
} }
if($profileUser->isDeleted()) { if($userInfo->isDeleted()) {
http_response_code(404); http_response_code(404);
Template::render('profile.index', [ Template::render('profile.index', [
'profile_is_guest' => $viewingAsGuest, 'profile_is_guest' => $viewingAsGuest,
'profile_is_deleted' => true, 'profile_is_deleted' => true,
'profile_is_banned' => false,
]); ]);
return; return;
} }
$notices = []; $notices = [];
$activeBanInfo = $msz->tryGetActiveBan($profileUser); $userRank = $users->getUserRank($userInfo);
$viewerRank = $users->getUserRank($viewerInfo);
$activeBanInfo = $msz->tryGetActiveBan($userInfo);
$isBanned = $activeBanInfo !== null; $isBanned = $activeBanInfo !== null;
$profileFields = $msz->getProfileFields(); $profileFields = $msz->getProfileFields();
$viewingOwnProfile = $currentUserId === $profileUser->getId(); $viewingOwnProfile = (string)$viewerId === $userInfo->getId();
$userPerms = perms_get_user($currentUserId)[MSZ_PERMS_USER]; $userPerms = perms_get_user($viewerId)[MSZ_PERMS_USER];
$canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS); $canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS);
$canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $currentUser->isSuper() || ( $canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $viewerInfo->isSuperUser() || (
perms_check($userPerms, MSZ_PERM_USER_MANAGE_USERS) perms_check($userPerms, MSZ_PERM_USER_MANAGE_USERS)
&& ($currentUser->getId() === $profileUser->getId() || $currentUser->getRank() > $profileUser->getRank()) && ($viewingOwnProfile || $viewerRank > $userRank)
)); ));
$avatarInfo = new UserAvatarAsset($userInfo);
$backgroundInfo = new UserBackgroundAsset($userInfo);
if($isEditing) { if($isEditing) {
if(!$canEdit) { if(!$canEdit) {
@ -103,9 +114,9 @@ if($isEditing) {
} }
if(!empty($profileFieldsRemove)) if(!empty($profileFieldsRemove))
$profileFields->removeFieldValues($profileUser, $profileFieldsRemove); $profileFields->removeFieldValues($userInfo, $profileFieldsRemove);
if(!empty($profileFieldsSetInfos)) if(!empty($profileFieldsSetInfos))
$profileFields->setFieldValues($profileUser, $profileFieldsSetInfos, $profileFieldsSetValues); $profileFields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues);
} }
} }
@ -118,7 +129,7 @@ if($isEditing) {
$aboutValid = User::validateProfileAbout($aboutParse, $aboutText); $aboutValid = User::validateProfileAbout($aboutParse, $aboutText);
if($aboutValid === '') if($aboutValid === '')
$profileUser->setProfileAboutText($aboutText)->setProfileAboutParser($aboutParse); $users->updateUser($userInfo, aboutContent: $aboutText, aboutParser: $aboutParse);
else switch($aboutValid) { else switch($aboutValid) {
case 'parser': case 'parser':
$notices[] = 'The selected about section parser is invalid.'; $notices[] = 'The selected about section parser is invalid.';
@ -142,7 +153,7 @@ if($isEditing) {
$sigValid = User::validateForumSignature($sigParse, $sigText); $sigValid = User::validateForumSignature($sigParse, $sigText);
if($sigValid === '') if($sigValid === '')
$profileUser->setForumSignatureText($sigText)->setForumSignatureParser($sigParse); $users->updateUser($userInfo, signatureContent: $sigText, signatureParser: $sigParse);
else switch($sigValid) { else switch($sigValid) {
case 'parser': case 'parser':
$notices[] = 'The selected forum signature parser is invalid.'; $notices[] = 'The selected forum signature parser is invalid.';
@ -167,7 +178,7 @@ if($isEditing) {
$birthValid = User::validateBirthdate($birthYear, $birthMonth, $birthDay); $birthValid = User::validateBirthdate($birthYear, $birthMonth, $birthDay);
if($birthValid === '') if($birthValid === '')
$profileUser->setBirthdate($birthYear, $birthMonth, $birthDay); $users->updateUser($userInfo, birthYear: $birthYear, birthMonth: $birthMonth, birthDay: $birthDay);
else switch($birthValid) { else switch($birthValid) {
case 'year': case 'year':
$notices[] = 'The given birth year is invalid.'; $notices[] = 'The given birth year is invalid.';
@ -183,8 +194,6 @@ if($isEditing) {
} }
if(!empty($_FILES['avatar'])) { if(!empty($_FILES['avatar'])) {
$avatarInfo = $profileUser->getAvatarInfo();
if(!empty($_POST['avatar']['delete'])) { if(!empty($_POST['avatar']['delete'])) {
$avatarInfo->delete(); $avatarInfo->delete();
} else { } else {
@ -230,8 +239,6 @@ if($isEditing) {
} }
if(!empty($_FILES['background'])) { if(!empty($_FILES['background'])) {
$backgroundInfo = $profileUser->getBackgroundInfo();
if((int)($_POST['background']['attach'] ?? -1) === 0) { if((int)($_POST['background']['attach'] ?? -1) === 0) {
$backgroundInfo->delete(); $backgroundInfo->delete();
} else { } else {
@ -278,9 +285,9 @@ if($isEditing) {
->setSlide(!empty($_POST['background']['attr']['slide'])); ->setSlide(!empty($_POST['background']['attr']['slide']));
} }
} }
}
$profileUser->saveProfile(); $users->updateUser($userInfo, backgroundSettings: $backgroundInfo->getSettings());
}
} }
// Unset $isEditing and hope the user doesn't refresh their profile! // Unset $isEditing and hope the user doesn't refresh their profile!
@ -315,7 +322,7 @@ $profileStats = DB::prepare('
) AS `comments_count` ) AS `comments_count`
FROM `msz_users` AS u FROM `msz_users` AS u
WHERE `user_id` = :user_id WHERE `user_id` = :user_id
')->bind('user_id', $profileUser->getId())->fetch(); ')->bind('user_id', $userInfo->getId())->fetch();
switch($profileMode) { switch($profileMode) {
default: default:
@ -324,7 +331,7 @@ switch($profileMode) {
case 'forum-topics': case 'forum-topics':
$template = 'profile.topics'; $template = 'profile.topics';
$topicsCount = forum_topic_count_user($profileUser->getId(), $currentUserId); $topicsCount = forum_topic_count_user($userInfo->getId(), $viewerId);
$topicsPagination = new Pagination($topicsCount, 20); $topicsPagination = new Pagination($topicsCount, 20);
if(!$topicsPagination->hasValidOffset()) { if(!$topicsPagination->hasValidOffset()) {
@ -333,13 +340,13 @@ switch($profileMode) {
} }
$topics = forum_topic_listing_user( $topics = forum_topic_listing_user(
$profileUser->getId(), $currentUserId, $userInfo->getId(), $viewerId,
$topicsPagination->getOffset(), $topicsPagination->getRange() $topicsPagination->getOffset(), $topicsPagination->getRange()
); );
Template::set([ Template::set([
'title' => $profileUser->getUsername() . ' / topics', 'title' => $userInfo->getName() . ' / topics',
'canonical_url' => url('user-profile-forum-topics', ['user' => $profileUser->getId(), 'page' => Pagination::param()]), 'canonical_url' => url('user-profile-forum-topics', ['user' => $userInfo->getId(), 'page' => Pagination::param()]),
'profile_topics' => $topics, 'profile_topics' => $topics,
'profile_topics_pagination' => $topicsPagination, 'profile_topics_pagination' => $topicsPagination,
]); ]);
@ -347,7 +354,7 @@ switch($profileMode) {
case 'forum-posts': case 'forum-posts':
$template = 'profile.posts'; $template = 'profile.posts';
$postsCount = forum_post_count_user($profileUser->getId()); $postsCount = forum_post_count_user($userInfo->getId());
$postsPagination = new Pagination($postsCount, 20); $postsPagination = new Pagination($postsCount, 20);
if(!$postsPagination->hasValidOffset()) { if(!$postsPagination->hasValidOffset()) {
@ -356,7 +363,7 @@ switch($profileMode) {
} }
$posts = forum_post_listing( $posts = forum_post_listing(
$profileUser->getId(), $userInfo->getId(),
$postsPagination->getOffset(), $postsPagination->getOffset(),
$postsPagination->getRange(), $postsPagination->getRange(),
false, false,
@ -364,8 +371,8 @@ switch($profileMode) {
); );
Template::set([ Template::set([
'title' => $profileUser->getUsername() . ' / posts', 'title' => $userInfo->getName() . ' / posts',
'canonical_url' => url('user-profile-forum-posts', ['user' => $profileUser->getId(), 'page' => Pagination::param()]), 'canonical_url' => url('user-profile-forum-posts', ['user' => $userInfo->getId(), 'page' => Pagination::param()]),
'profile_posts' => $posts, 'profile_posts' => $posts,
'profile_posts_pagination' => $postsPagination, 'profile_posts_pagination' => $postsPagination,
]); ]);
@ -375,16 +382,16 @@ switch($profileMode) {
$template = 'profile.index'; $template = 'profile.index';
if(!$viewingAsGuest) { if(!$viewingAsGuest) {
Template::set('profile_warnings', $msz->getWarnings()->getWarningsWithDefaultBacklog($profileUser)); Template::set('profile_warnings', $msz->getWarnings()->getWarningsWithDefaultBacklog($userInfo));
if((!$isBanned || $canEdit)) { if((!$isBanned || $canEdit)) {
$activeCategoryStats = forum_get_user_most_active_category_info($profileUser->getId()); $activeCategoryStats = forum_get_user_most_active_category_info($userInfo->getId());
$activeCategoryInfo = empty($activeCategoryStats->forum_id) ? null : forum_get($activeCategoryStats->forum_id); $activeCategoryInfo = empty($activeCategoryStats->forum_id) ? null : forum_get($activeCategoryStats->forum_id);
$activeTopicStats = forum_get_user_most_active_topic_info($profileUser->getId()); $activeTopicStats = forum_get_user_most_active_topic_info($userInfo->getId());
$activeTopicInfo = empty($activeTopicStats->topic_id) ? null : forum_topic_get($activeTopicStats->topic_id); $activeTopicInfo = empty($activeTopicStats->topic_id) ? null : forum_topic_get($activeTopicStats->topic_id);
$profileFieldValues = $profileFields->getFieldValues($profileUser); $profileFieldValues = $profileFields->getFieldValues($userInfo);
$profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues); $profileFieldInfos = $profileFieldInfos ?? $profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues);
$profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues); $profileFieldFormats = $profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues);
@ -435,8 +442,9 @@ switch($profileMode) {
if(!empty($template)) { if(!empty($template)) {
Template::render($template, [ Template::render($template, [
'profile_viewer' => $currentUser, 'profile_viewer' => $viewerInfo,
'profile_user' => $profileUser, 'profile_user' => $userInfo,
'profile_colour' => $users->getUserColour($userInfo),
'profile_stats' => $profileStats, 'profile_stats' => $profileStats,
'profile_mode' => $profileMode, 'profile_mode' => $profileMode,
'profile_notices' => $notices, 'profile_notices' => $notices,
@ -446,5 +454,7 @@ if(!empty($template)) {
'profile_is_guest' => $viewingAsGuest, 'profile_is_guest' => $viewingAsGuest,
'profile_is_deleted' => false, 'profile_is_deleted' => false,
'profile_ban_info' => $activeBanInfo, 'profile_ban_info' => $activeBanInfo,
'profile_avatar_info' => $avatarInfo,
'profile_background_info' => $backgroundInfo,
]); ]);
} }

View file

@ -3,20 +3,26 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Comments\CommentsCategory; use Misuzu\Comments\CommentsCategory;
use Misuzu\Users\User;
if(!$msz->isLoggedIn()) {
echo render_error(403);
return;
}
$searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : ''; $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
if(!empty($searchQuery)) { if(!empty($searchQuery)) {
$forumTopics = forum_topic_listing_search($searchQuery, User::hasCurrent() ? User::getCurrent()->getId() : 0); $forumTopics = forum_topic_listing_search($searchQuery, $msz->getActiveUser()->getId());
$forumPosts = forum_post_search($searchQuery); $forumPosts = forum_post_search($searchQuery);
// this sure is an expansion // this sure is an expansion
$news = $msz->getNews(); $news = $msz->getNews();
$users = $msz->getUsers();
$comments = $msz->getComments(); $comments = $msz->getComments();
$newsPosts = []; $newsPosts = [];
$newsPostInfos = $news->getPostsBySearchQuery($searchQuery); $newsPostInfos = $news->getPostsBySearchQuery($searchQuery);
$newsUserInfos = []; $newsUserInfos = [];
$newsUserColours = [];
$newsCategoryInfos = []; $newsCategoryInfos = [];
foreach($newsPostInfos as $postInfo) { foreach($newsPostInfos as $postInfo) {
@ -27,7 +33,8 @@ if(!empty($searchQuery)) {
$userInfo = $newsUserInfos[$userId]; $userInfo = $newsUserInfos[$userId];
} else { } else {
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser($userId, 'id');
$newsUserColours[$userId] = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
} }
@ -47,6 +54,7 @@ if(!empty($searchQuery)) {
'post' => $postInfo, 'post' => $postInfo,
'category' => $categoryInfo, 'category' => $categoryInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $newsUserColours[$userId] ?? \Index\Colour\Colour::none(),
'comments_count' => $commentsCount, 'comments_count' => $commentsCount,
]; ];
} }
@ -77,13 +85,13 @@ if(!empty($searchQuery)) {
GROUP BY u.`user_id` GROUP BY u.`user_id`
'); ');
$findUsers->bind('query', $searchQuery); $findUsers->bind('query', $searchQuery);
$users = $findUsers->fetchAll(); $userList = $findUsers->fetchAll();
} }
Template::render('home.search', [ Template::render('home.search', [
'search_query' => $searchQuery, 'search_query' => $searchQuery,
'forum_topics' => $forumTopics ?? [], 'forum_topics' => $forumTopics ?? [],
'forum_posts' => $forumPosts ?? [], 'forum_posts' => $forumPosts ?? [],
'users' => $users ?? [], 'users' => $userList ?? [],
'news_posts' => $newsPosts ?? [], 'news_posts' => $newsPosts ?? [],
]); ]);

View file

@ -6,7 +6,7 @@ use Misuzu\Users\User;
use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions; use chillerlan\QRCode\QROptions;
if(!User::hasCurrent()) { if(!$msz->isLoggedIn()) {
echo render_error(401); echo render_error(401);
return; return;
} }
@ -14,8 +14,7 @@ if(!User::hasCurrent()) {
$errors = []; $errors = [];
$users = $msz->getUsers(); $users = $msz->getUsers();
$roles = $msz->getRoles(); $roles = $msz->getRoles();
$currentUser = User::getCurrent(); $userInfo = $msz->getActiveUser();
$currentUserId = $currentUser->getId();
$isRestricted = $msz->hasActiveBan(); $isRestricted = $msz->hasActiveBan();
$isVerifiedRequest = CSRF::validateRequest(); $isVerifiedRequest = CSRF::validateRequest();
@ -30,14 +29,14 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
switch($_POST['role']['mode'] ?? '') { switch($_POST['role']['mode'] ?? '') {
case 'display': case 'display':
$users->updateUser( $users->updateUser(
$currentUser, $userInfo,
displayRoleInfo: $roleInfo displayRoleInfo: $roleInfo
); );
break; break;
case 'leave': case 'leave':
if($roleInfo->isLeavable()) if($roleInfo->isLeavable())
$users->removeRoles($currentUser, $roleInfo); $users->removeRoles($userInfo, $roleInfo);
else else
$errors[] = "You're not allow to leave this role, an administrator has to remove it for you."; $errors[] = "You're not allow to leave this role, an administrator has to remove it for you.";
break; break;
@ -45,39 +44,39 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
} }
} }
if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $currentUser->hasTOTPKey() !== (bool)$_POST['tfa']['enable']) { if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $userInfo->hasTOTPKey() !== (bool)$_POST['tfa']['enable']) {
$totpKey = '';
if((bool)$_POST['tfa']['enable']) { if((bool)$_POST['tfa']['enable']) {
$tfaKey = TOTPGenerator::generateKey(); $totpKey = TOTPGenerator::generateKey();
$tfaIssuer = $cfg->getString('site.name', 'Misuzu'); $totpIssuer = $cfg->getString('site.name', 'Misuzu');
$tfaQrcode = (new QRCode(new QROptions([ $totpQrcode = (new QRCode(new QROptions([
'version' => 5, 'version' => 5,
'outputType' => QRCode::OUTPUT_IMAGE_JPG, 'outputType' => QRCode::OUTPUT_IMAGE_JPG,
'eccLevel' => QRCode::ECC_L, 'eccLevel' => QRCode::ECC_L,
])))->render(sprintf('otpauth://totp/%s:%s?%s', $tfaIssuer, $currentUser->getUsername(), http_build_query([ ])))->render(sprintf('otpauth://totp/%s:%s?%s', $totpIssuer, $userInfo->getName(), http_build_query([
'secret' => $tfaKey, 'secret' => $totpKey,
'issuer' => $tfaIssuer, 'issuer' => $totpIssuer,
]))); ])));
Template::set([ Template::set([
'settings_2fa_code' => $tfaKey, 'settings_2fa_code' => $totpKey,
'settings_2fa_image' => $tfaQrcode, 'settings_2fa_image' => $totpQrcode,
]); ]);
$currentUser->setTOTPKey($tfaKey);
} else {
$currentUser->removeTOTPKey();
} }
$users->updateUser(userInfo: $userInfo, totpKey: $totpKey);
} }
if($isVerifiedRequest && !empty($_POST['current_password'])) { if($isVerifiedRequest && !empty($_POST['current_password'])) {
if(!$currentUser->checkPassword($_POST['current_password'] ?? '')) { if(!$userInfo->verifyPassword($_POST['current_password'] ?? '')) {
$errors[] = 'Your password was incorrect.'; $errors[] = 'Your password was incorrect.';
} else { } else {
// Changing e-mail // Changing e-mail
if(!empty($_POST['email']['new'])) { if(!empty($_POST['email']['new'])) {
if(empty($_POST['email']['confirm']) || $_POST['email']['new'] !== $_POST['email']['confirm']) { if(empty($_POST['email']['confirm']) || $_POST['email']['new'] !== $_POST['email']['confirm']) {
$errors[] = 'The addresses you entered did not match each other.'; $errors[] = 'The addresses you entered did not match each other.';
} elseif($currentUser->getEMailAddress() === mb_strtolower($_POST['email']['confirm'])) { } elseif($userInfo->getEMailAddress() === mb_strtolower($_POST['email']['confirm'])) {
$errors[] = 'This is already your e-mail address!'; $errors[] = 'This is already your e-mail address!';
} else { } else {
$checkMail = User::validateEMailAddress($_POST['email']['new'], true); $checkMail = User::validateEMailAddress($_POST['email']['new'], true);
@ -100,10 +99,8 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
$errors[] = 'Unknown e-mail validation error.'; $errors[] = 'Unknown e-mail validation error.';
} }
} else { } else {
$currentUser->setEMailAddress($_POST['email']['new']); $users->updateUser(userInfo: $userInfo, emailAddr: $_POST['email']['new']);
$msz->createAuditLog('PERSONAL_EMAIL_CHANGE', [ $msz->createAuditLog('PERSONAL_EMAIL_CHANGE', [$_POST['email']['new']]);
$_POST['email']['new'],
]);
} }
} }
} }
@ -118,7 +115,7 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
if($checkPassword !== '') { if($checkPassword !== '') {
$errors[] = 'The given passwords was too weak.'; $errors[] = 'The given passwords was too weak.';
} else { } else {
$currentUser->setPassword($_POST['password']['new']); $users->updateUser(userInfo: $userInfo, password: $_POST['password']['new']);
$msz->createAuditLog('PERSONAL_PASSWORD_CHANGE'); $msz->createAuditLog('PERSONAL_PASSWORD_CHANGE');
} }
} }
@ -126,20 +123,15 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
} }
} }
// THIS FUCKING SUCKS AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA // reload $userInfo object
if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest) { if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
$currentUser->save(); $userInfo = $users->getUser($userInfo->getId(), 'id');
// force a page refresh for now to deal with the User object and new shit desyncing $userRoles = $roles->getRoles(userInfo: $userInfo);
url_redirect('settings-account');
return;
}
$userRoles = $roles->getRoles(userInfo: $currentUser);
Template::render('settings.account', [ Template::render('settings.account', [
'errors' => $errors, 'errors' => $errors,
'settings_user' => $currentUser, 'settings_user' => $userInfo,
'settings_roles' => $userRoles, 'settings_roles' => $userRoles,
'is_restricted' => $isRestricted, 'is_restricted' => $isRestricted,
]); ]);

View file

@ -4,18 +4,19 @@ namespace Misuzu;
use ZipArchive; use ZipArchive;
use Index\XString; use Index\XString;
use Index\IO\FileStream; use Index\IO\FileStream;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
if(!User::hasCurrent()) { if(!$msz->isLoggedIn()) {
echo render_error(401); echo render_error(401);
return; return;
} }
$dbConn = $msz->getDbConn(); $dbConn = $msz->getDbConn();
function db_to_zip(ZipArchive $archive, int $userId, string $baseName, array $fieldInfos, string $userIdField = 'user_id'): string { function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, array $fieldInfos, string $userIdField = 'user_id'): string {
global $dbConn; global $dbConn;
$userId = $userInfo->getId();
$fields = []; $fields = [];
foreach($fieldInfos as $key => $fieldInfo) { foreach($fieldInfos as $key => $fieldInfo) {
@ -41,7 +42,7 @@ function db_to_zip(ZipArchive $archive, int $userId, string $baseName, array $fi
$fieldInfos[$key] = $fieldInfo; $fieldInfos[$key] = $fieldInfo;
} }
$tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . sprintf('msz-user-data-%d-%s-%s.tmp', $userId, $baseName, XString::random(8)); $tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . sprintf('msz-user-data-%s-%s-%s.tmp', $userId, $baseName, XString::random(8));
$tmpStream = FileStream::newWrite($tmpName); $tmpStream = FileStream::newWrite($tmpName);
try { try {
@ -99,18 +100,17 @@ function db_to_zip(ZipArchive $archive, int $userId, string $baseName, array $fi
} }
$errors = []; $errors = [];
$currentUser = User::getCurrent(); $userInfo = $msz->getActiveUser();
$currentUserId = $currentUser->getId();
if(isset($_POST['action']) && is_string($_POST['action'])) { if(isset($_POST['action']) && is_string($_POST['action'])) {
if(isset($_POST['password']) && is_string($_POST['password']) if(isset($_POST['password']) && is_string($_POST['password'])
&& ($currentUser->checkPassword($_POST['password'] ?? ''))) { && ($userInfo->verifyPassword($_POST['password'] ?? ''))) {
switch($_POST['action']) { switch($_POST['action']) {
case 'data': case 'data':
$msz->createAuditLog('PERSONAL_DATA_DOWNLOAD'); $msz->createAuditLog('PERSONAL_DATA_DOWNLOAD');
$timeStamp = floor(time() / 3600) * 3600; $timeStamp = floor(time() / 3600) * 3600;
$fileName = sprintf('msz-user-data-%d-%d.zip', $currentUserId, $timeStamp); $fileName = sprintf('msz-user-data-%d-%d.zip', $userInfo->getId(), $timeStamp);
$filePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName; $filePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName;
$archive = new ZipArchive; $archive = new ZipArchive;
@ -119,27 +119,27 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
$tmpFiles = []; $tmpFiles = [];
try { try {
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'audit_log', ['user_id:s:n', 'log_action:s', 'log_params:j', 'log_created:t', 'log_ip:a:n', 'log_country:s']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'audit_log', ['user_id:s:n', 'log_action:s', 'log_params:j', 'log_created:t', 'log_ip:a:n', 'log_country:s']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'auth_tfa', ['user_id:s', 'tfa_token:n', 'tfa_created:t']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'auth_tfa', ['user_id:s', 'tfa_token:n', 'tfa_created:t']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'changelog_changes', ['change_id:s', 'user_id:s:n', 'change_action:s:n', 'change_created:t', 'change_log:s', 'change_text:s:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'changelog_changes', ['change_id:s', 'user_id:s:n', 'change_action:s:n', 'change_created:t', 'change_log:s', 'change_text:s:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'comments_categories', ['category_id:s', 'category_name:s', 'owner_id:s:n', 'category_created:t', 'category_locked:t:n'], 'owner_id'); $tmpFiles[] = db_to_zip($archive, $userInfo, 'comments_categories', ['category_id:s', 'category_name:s', 'owner_id:s:n', 'category_created:t', 'category_locked:t:n'], 'owner_id');
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'comments_posts', ['comment_id:s', 'category_id:s', 'user_id:s:n', 'comment_reply_to:s:n', 'comment_text:s', 'comment_created:t', 'comment_pinned:t:n', 'comment_edited:t:n', 'comment_deleted:t:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'comments_posts', ['comment_id:s', 'category_id:s', 'user_id:s:n', 'comment_reply_to:s:n', 'comment_text:s', 'comment_created:t', 'comment_pinned:t:n', 'comment_edited:t:n', 'comment_deleted:t:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'comments_votes', ['comment_id:s', 'user_id:s', 'comment_vote:i']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'comments_votes', ['comment_id:s', 'user_id:s', 'comment_vote:i']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'forum_permissions', ['user_id:s:n', 'role_id:s:n', 'forum_id:s', 'forum_perms_allow:i', 'forum_perms_deny:i']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_permissions', ['user_id:s:n', 'role_id:s:n', 'forum_id:s', 'forum_perms_allow:i', 'forum_perms_deny:i']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'forum_posts', ['post_id:s', 'topic_id:s', 'forum_id:s', 'user_id:s:n', 'post_ip:a', 'post_text:s', 'post_parse:i', 'post_display_signature:b', 'post_created:t', 'post_edited:t:n', 'post_deleted:t:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_posts', ['post_id:s', 'topic_id:s', 'forum_id:s', 'user_id:s:n', 'post_ip:a', 'post_text:s', 'post_parse:i', 'post_display_signature:b', 'post_created:t', 'post_edited:t:n', 'post_deleted:t:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'forum_topics', ['topic_id:s', 'forum_id:s', 'user_id:s:n', 'topic_type:i', 'topic_title:s', 'topic_count_views:i', 'topic_created:t', 'topic_bumped:t', 'topic_deleted:t:n', 'topic_locked:t:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics', ['topic_id:s', 'forum_id:s', 'user_id:s:n', 'topic_type:i', 'topic_title:s', 'topic_count_views:i', 'topic_created:t', 'topic_bumped:t', 'topic_deleted:t:n', 'topic_locked:t:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'forum_topics_redirects', ['topic_id:s', 'user_id:s:n', 'topic_redir_url:s', 'topic_redir_created:t']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_redirects', ['topic_id:s', 'user_id:s:n', 'topic_redir_url:s', 'topic_redir_created:t']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'forum_topics_track', ['user_id:s', 'topic_id:s', 'forum_id:s', 'track_last_read:t']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'forum_topics_track', ['user_id:s', 'topic_id:s', 'forum_id:s', 'track_last_read:t']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'login_attempts', ['user_id:s:n', 'attempt_success:b', 'attempt_ip:a', 'attempt_country:s', 'attempt_created:t', 'attempt_user_agent:s']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'login_attempts', ['user_id:s:n', 'attempt_success:b', 'attempt_ip:a', 'attempt_country:s', 'attempt_created:t', 'attempt_user_agent:s']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'news_posts', ['post_id:s', 'category_id:s', 'user_id:s:n', 'comment_section_id:s:n', 'post_is_featured:b', 'post_title:s', 'post_text:s', 'post_scheduled:t', 'post_created:t', 'post_updated:t', 'post_deleted:t:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'news_posts', ['post_id:s', 'category_id:s', 'user_id:s:n', 'comment_section_id:s:n', 'post_is_featured:b', 'post_title:s', 'post_text:s', 'post_scheduled:t', 'post_created:t', 'post_updated:t', 'post_deleted:t:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'permissions', ['user_id:s:n', 'role_id:s:n', 'general_perms_allow:i', 'general_perms_deny:i', 'user_perms_allow:i', 'user_perms_deny:i', 'changelog_perms_allow:i', 'changelog_perms_deny:i', 'news_perms_allow:i', 'news_perms_deny:i', 'forum_perms_allow:i', 'forum_perms_deny:i', 'comments_perms_allow:i', 'comments_perms_deny:i']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'permissions', ['user_id:s:n', 'role_id:s:n', 'general_perms_allow:i', 'general_perms_deny:i', 'user_perms_allow:i', 'user_perms_deny:i', 'changelog_perms_allow:i', 'changelog_perms_deny:i', 'news_perms_allow:i', 'news_perms_deny:i', 'forum_perms_allow:i', 'forum_perms_deny:i', 'comments_perms_allow:i', 'comments_perms_deny:i']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'profile_fields_values', ['field_id:s', 'user_id:s', 'format_id:s', 'field_value:s']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_fields_values', ['field_id:s', 'user_id:s', 'format_id:s', 'field_value:s']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'sessions', ['session_id:s', 'user_id:s', 'session_key:n', 'session_ip:a', 'session_ip_last:a:n', 'session_user_agent:s', 'session_country:s', 'session_expires:t', 'session_expires_bump:b', 'session_created:t', 'session_active:t:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'sessions', ['session_id:s', 'user_id:s', 'session_key:n', 'session_ip:a', 'session_ip_last:a:n', 'session_user_agent:s', 'session_country:s', 'session_expires:t', 'session_expires_bump:b', 'session_created:t', 'session_active:t:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'users', ['user_id:s', 'username:s', 'password:n', 'email:s', 'register_ip:a', 'last_ip:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n', 'display_role:s:n', 'user_totp_key:n', 'user_about_content:s:n', 'user_about_parser:i', 'user_signature_content:s:n', 'user_signature_parser:i', 'user_birthdate:s:n', 'user_background_settings:i:n', 'user_title:s:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'users', ['user_id:s', 'username:s', 'password:n', 'email:s', 'register_ip:a', 'last_ip:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n', 'display_role:s:n', 'user_totp_key:n', 'user_about_content:s:n', 'user_about_parser:i', 'user_signature_content:s:n', 'user_signature_parser:i', 'user_birthdate:s:n', 'user_background_settings:i:n', 'user_title:s:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'users_bans', ['ban_id:s', 'user_id:s', 'mod_id:n', 'ban_severity:i', 'ban_reason_public:s', 'ban_reason_private:s', 'ban_created:t', 'ban_expires:t:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_bans', ['ban_id:s', 'user_id:s', 'mod_id:n', 'ban_severity:i', 'ban_reason_public:s', 'ban_reason_private:s', 'ban_created:t', 'ban_expires:t:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'users_password_resets', ['user_id:s', 'reset_ip:a', 'reset_requested:t', 'verification_code:n']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_password_resets', ['user_id:s', 'reset_ip:a', 'reset_requested:t', 'verification_code:n']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'users_warnings', ['warn_id:s', 'user_id:s', 'mod_id:n', 'warn_body:s', 'warn_created:t']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_warnings', ['warn_id:s', 'user_id:s', 'mod_id:n', 'warn_body:s', 'warn_created:t']);
$tmpFiles[] = db_to_zip($archive, $currentUserId, 'users_roles', ['user_id:s', 'role_id:s']); $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_roles', ['user_id:s', 'role_id:s']);
$archive->close(); $archive->close();
} finally { } finally {

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; if(!$msz->isLoggedIn()) {
if(!User::hasCurrent()) {
echo render_error(401); echo render_error(401);
return; return;
} }

View file

@ -2,10 +2,8 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User;
$currentUser = User::getCurrent();
$currentUser = $msz->getActiveUser();
if($currentUser === null) { if($currentUser === null) {
echo render_error(401); echo render_error(401);
return; return;

View file

@ -2,16 +2,15 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
if(!User::hasCurrent()) { if(!$msz->isLoggedIn()) {
echo render_error(401); echo render_error(401);
return; return;
} }
$errors = []; $errors = [];
$sessions = $msz->getSessions(); $sessions = $msz->getSessions();
$currentUser = User::getCurrent(); $currentUser = $msz->getActiveUser();
$activeSessionToken = $authToken->getSessionToken(); $activeSessionToken = $authToken->getSessionToken();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
@ -27,7 +26,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$sessionInfo = $sessions->getSession(sessionId: $sessionId); $sessionInfo = $sessions->getSession(sessionId: $sessionId);
} catch(RuntimeException $ex) {} } catch(RuntimeException $ex) {}
if(empty($sessionInfo) || $sessionInfo->getUserId() !== (string)$currentUser->getId()) { if(empty($sessionInfo) || $sessionInfo->getUserId() !== $currentUser->getId()) {
$errors[] = "That session doesn't exist."; $errors[] = "That session doesn't exist.";
break; break;
} }

View file

@ -2,7 +2,6 @@
namespace Misuzu; namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
@ -50,7 +49,6 @@ $globals = $cfg->getValues([
['site.name:s', 'Misuzu'], ['site.name:s', 'Misuzu'],
'site.desc:s', 'site.desc:s',
'site.url:s', 'site.url:s',
'sockChat.chatPath.normal:s',
'eeprom.path:s', 'eeprom.path:s',
'eeprom.app:s', 'eeprom.app:s',
['auth.secret:s', 'meow'], ['auth.secret:s', 'meow'],
@ -63,7 +61,6 @@ Template::set('globals', [
'site_name' => $globals['site.name'], 'site_name' => $globals['site.name'],
'site_description' => $globals['site.desc'], 'site_description' => $globals['site.desc'],
'site_url' => $globals['site.url'], 'site_url' => $globals['site.url'],
'site_chat' => $globals['sockChat.chatPath.normal'],
'eeprom' => [ 'eeprom' => [
'path' => $globals['eeprom.path'], 'path' => $globals['eeprom.path'],
'app' => $globals['eeprom.app'], 'app' => $globals['eeprom.app'],
@ -81,7 +78,7 @@ AuthToken::setSecretKey($globals['auth.secret']);
if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) { if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) {
$authToken = new AuthToken; $authToken = new AuthToken;
$authToken->setUserId(filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT) ?? 0); $authToken->setUserId(filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT) ?? '0');
$authToken->setSessionToken(filter_input(INPUT_COOKIE, 'msz_sid') ?? ''); $authToken->setSessionToken(filter_input(INPUT_COOKIE, 'msz_sid') ?? '');
if($authToken->isValid()) if($authToken->isValid())
@ -93,27 +90,26 @@ if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) {
if(!isset($authToken)) if(!isset($authToken))
$authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth') ?? ''); $authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth') ?? '');
if($authToken->isValid()) { $users = $msz->getUsers();
$sessions = $msz->getSessions(); $sessions = $msz->getSessions();
$authToken->setCurrent();
if($authToken->isValid()) {
try { try {
$sessionInfo = $sessions->getSession(sessionToken: $authToken->getSessionToken()); $sessionInfo = $sessions->getSession(sessionToken: $authToken->getSessionToken());
if($sessionInfo->hasExpired()) { if($sessionInfo->hasExpired()) {
$sessions->deleteSessions(sessionInfos: $sessionInfo); $sessions->deleteSessions(sessionInfos: $sessionInfo);
} elseif($sessionInfo->getUserId() === (string)$authToken->getUserId()) { } elseif($sessionInfo->getUserId() === $authToken->getUserId()) {
$userInfo = User::byId((int)$sessionInfo->getUserId()); $userInfo = $users->getUser($authToken->getUserId(), 'id');
if(!$userInfo->isDeleted()) { if(!$userInfo->isDeleted()) {
$userInfo->setCurrent(); $users->recordUserActivity($userInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
$userInfo->bumpActivity($_SERVER['REMOTE_ADDR']); $sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
$sessions->updateSession(sessionInfo: $sessionInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
if($sessionInfo->shouldBumpExpires()) if($sessionInfo->shouldBumpExpires())
$authToken->applyCookie($sessionInfo->getExpiresTime()); $authToken->applyCookie($sessionInfo->getExpiresTime());
if($authToken->hasImpersonatedUserId()) { if($authToken->hasImpersonatedUserId()) {
$allowToImpersonate = $userInfo->isSuper(); $allowToImpersonate = $userInfo->isSuperUser();
$impersonatedUserId = $authToken->getImpersonatedUserId(); $impersonatedUserId = $authToken->getImpersonatedUserId();
if(!$allowToImpersonate) { if(!$allowToImpersonate) {
@ -126,13 +122,11 @@ if($authToken->isValid()) {
$userInfoReal = $userInfo; $userInfoReal = $userInfo;
try { try {
$userInfo = User::byId($impersonatedUserId); $userInfo = $users->getUser($impersonatedUserId, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = $userInfoReal; $userInfo = $userInfoReal;
$removeImpersonationData = true; $removeImpersonationData = true;
} }
$userInfo->setCurrent();
} }
if($removeImpersonationData) { if($removeImpersonationData) {
@ -140,46 +134,51 @@ if($authToken->isValid()) {
$authToken->applyCookie(); $authToken->applyCookie();
} }
} }
$msz->setAuthInfo($authToken, $userInfo, $userInfoReal ?? null);
} }
} }
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
User::unsetCurrent();
}
if(!User::hasCurrent())
AuthToken::nukeCookie(); AuthToken::nukeCookie();
}
} }
if(!empty($userInfo))
$userInfo = $users->getUser((string)$userInfo->getId(), 'id');
if(!empty($userInfoReal))
$userInfoReal = $users->getUser((string)$userInfoReal->getId(), 'id');
CSRF::init( CSRF::init(
$globals['csrf.secret'], $globals['csrf.secret'],
(User::hasCurrent() ? $authToken->getSessionToken() : $_SERVER['REMOTE_ADDR']) ($msz->isLoggedIn() ? $authToken->getSessionToken() : $_SERVER['REMOTE_ADDR'])
); );
if(!empty($userInfo)) { if(!empty($userInfo)) {
Template::set('current_user', $userInfo); Template::set('current_user', $userInfo);
Template::set('current_user_ban_info', $msz->tryGetActiveBan()); Template::set('current_user_ban_info', $msz->tryGetActiveBan());
} }
if(!empty($userInfoReal))
if(!empty($userInfoReal)) {
Template::set('current_user_real', $userInfoReal); Template::set('current_user_real', $userInfoReal);
Template::set('current_user_real_colour', $users->getUserColour($userInfoReal));
}
$inManageMode = str_starts_with($_SERVER['REQUEST_URI'], '/manage'); $inManageMode = str_starts_with($_SERVER['REQUEST_URI'], '/manage');
$hasManageAccess = User::hasCurrent()
&& !$msz->hasActiveBan()
&& perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_CAN_MANAGE);
Template::set('has_manage_access', $hasManageAccess);
$canViewForumLeaderboard = User::hasCurrent() Template::set('header_menu', $msz->getHeaderMenu($userInfo ?? null));
&& !$msz->hasActiveBan() Template::set('user_menu', $msz->getUserMenu($userInfo ?? null, $inManageMode));
&& perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD); Template::set('display_debug_info', MSZ_DEBUG || (!empty($userInfo) && $userInfo->isSuperUser()));
Template::set('can_view_forum_leaderboard', $canViewForumLeaderboard);
if($inManageMode) { if($inManageMode) {
$hasManageAccess = $msz->isLoggedIn() && !$msz->hasActiveBan()
&& perms_check_user(MSZ_PERMS_GENERAL, $msz->getActiveUser()->getId(), MSZ_PERM_GENERAL_CAN_MANAGE);
if(!$hasManageAccess) { if(!$hasManageAccess) {
echo render_error(403); echo render_error(403);
exit; exit;
} }
Template::set('manage_menu', manage_get_menu(User::getCurrent()->getId())); Template::set('manage_menu', manage_get_menu($userInfo->getId()));
} }
$mszRequestPath = $request->getPath(); $mszRequestPath = $request->getPath();

View file

@ -7,7 +7,7 @@ use Index\Data\IDbConnection;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class AuditLog { class AuditLog {
private DbStatementCache $cache; private DbStatementCache $cache;
@ -17,11 +17,11 @@ class AuditLog {
} }
public function countLogs( public function countLogs(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null IPAddress|string|null $remoteAddr = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
@ -58,12 +58,12 @@ class AuditLog {
} }
public function getLogs( public function getLogs(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
@ -108,14 +108,14 @@ class AuditLog {
} }
public function createLog( public function createLog(
User|string|null $userInfo, UserInfo|string|null $userInfo,
string $action, string $action,
array $params = [], array $params = [],
IPAddress|string $remoteAddr = '::1', IPAddress|string $remoteAddr = '::1',
string $countryCode = 'XX' string $countryCode = 'XX'
): void { ): void {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;

View file

@ -5,7 +5,6 @@ use ValueError;
use Index\DateTime; use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\Users\User;
class AuditLogInfo { class AuditLogInfo {
private ?string $userId; private ?string $userId;

View file

@ -7,7 +7,7 @@ use Index\Data\IDbConnection;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\ClientInfo; use Misuzu\ClientInfo;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class LoginAttempts { class LoginAttempts {
public const REMAINING_MAX = 5; public const REMAINING_MAX = 5;
@ -21,12 +21,12 @@ class LoginAttempts {
public function countAttempts( public function countAttempts(
?bool $success = null, ?bool $success = null,
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null TimeSpan|int|null $timeRange = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan) if($timeRange instanceof TimeSpan)
@ -79,13 +79,13 @@ class LoginAttempts {
public function getAttempts( public function getAttempts(
?bool $success = null, ?bool $success = null,
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null, TimeSpan|int|null $timeRange = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan) if($timeRange instanceof TimeSpan)
@ -142,12 +142,12 @@ class LoginAttempts {
string $countryCode, string $countryCode,
string $userAgentString, string $userAgentString,
?ClientInfo $clientInfo = null, ?ClientInfo $clientInfo = null,
User|string|null $userInfo = null UserInfo|string|null $userInfo = null
): void { ): void {
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString)); $clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));

View file

@ -9,7 +9,7 @@ use Index\Net\IPAddress;
use Index\Serialisation\Base32; use Index\Serialisation\Base32;
use Misuzu\ClientInfo; use Misuzu\ClientInfo;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class RecoveryTokens { class RecoveryTokens {
private DbStatementCache $cache; private DbStatementCache $cache;
@ -24,13 +24,13 @@ class RecoveryTokens {
} }
public function getToken( public function getToken(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null, IPAddress|string|null $remoteAddr = null,
?string $verifyCode = null, ?string $verifyCode = null,
?bool $isUnused = null ?bool $isUnused = null
): RecoveryTokenInfo { ): RecoveryTokenInfo {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
@ -75,11 +75,11 @@ class RecoveryTokens {
} }
public function createToken( public function createToken(
User|string $userInfo, UserInfo|string $userInfo,
IPAddress|string $remoteAddr IPAddress|string $remoteAddr
): RecoveryTokenInfo { ): RecoveryTokenInfo {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
$verifyCode = self::generateCode(); $verifyCode = self::generateCode();

View file

@ -10,7 +10,7 @@ use Index\Data\IDbConnection;
use Index\Net\IPAddress; use Index\Net\IPAddress;
use Misuzu\ClientInfo; use Misuzu\ClientInfo;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class Sessions { class Sessions {
private IDbConnection $dbConn; private IDbConnection $dbConn;
@ -26,10 +26,10 @@ class Sessions {
} }
public function countSessions( public function countSessions(
User|string|null $userInfo = null UserInfo|string|null $userInfo = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
@ -56,11 +56,11 @@ class Sessions {
} }
public function getSessions( public function getSessions(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasPagination = $pagination !== null; $hasPagination = $pagination !== null;
@ -128,14 +128,14 @@ class Sessions {
} }
public function createSession( public function createSession(
User|string $userInfo, UserInfo|string $userInfo,
IPAddress|string $remoteAddr, IPAddress|string $remoteAddr,
string $countryCode, string $countryCode,
string $userAgentString, string $userAgentString,
?ClientInfo $clientInfo = null ?ClientInfo $clientInfo = null
): SessionInfo { ): SessionInfo {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress) if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr; $remoteAddr = (string)$remoteAddr;
@ -157,7 +157,7 @@ class Sessions {
public function deleteSessions( public function deleteSessions(
SessionInfo|string|array|null $sessionInfos = null, SessionInfo|string|array|null $sessionInfos = null,
string|array|null $sessionTokens = null, string|array|null $sessionTokens = null,
User|string|array|null $userInfos = null UserInfo|string|array|null $userInfos = null
): void { ): void {
$hasSessionInfos = $sessionInfos !== null; $hasSessionInfos = $sessionInfos !== null;
$hasSessionTokens = $sessionTokens !== null; $hasSessionTokens = $sessionTokens !== null;
@ -235,10 +235,10 @@ class Sessions {
if($hasUserInfos) if($hasUserInfos)
foreach($userInfos as $userInfo) { foreach($userInfos as $userInfo) {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
elseif(!is_string($userInfo)) elseif(!is_string($userInfo))
throw new InvalidArgumentException('$userInfos must be strings or instances of User.'); throw new InvalidArgumentException('$userInfos must be strings or instances of UserInfo.');
$stmt->addParameter(++$args, $userInfo); $stmt->addParameter(++$args, $userInfo);
} }
@ -246,7 +246,7 @@ class Sessions {
$stmt->execute(); $stmt->execute();
} }
public function updateSession( public function recordSessionActivity(
SessionInfo|string|null $sessionInfo = null, SessionInfo|string|null $sessionInfo = null,
?string $sessionToken = null, ?string $sessionToken = null,
IPAddress|string|null $remoteAddr = null IPAddress|string|null $remoteAddr = null

View file

@ -4,7 +4,7 @@ namespace Misuzu\Auth;
use Index\XString; use Index\XString;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class TwoFactorAuthSessions { class TwoFactorAuthSessions {
private DbStatementCache $cache; private DbStatementCache $cache;
@ -17,9 +17,9 @@ class TwoFactorAuthSessions {
return XString::random(32); return XString::random(32);
} }
public function createToken(User|string $userInfo): string { public function createToken(UserInfo|string $userInfo): string {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$token = self::generateToken(); $token = self::generateToken();

View file

@ -4,7 +4,7 @@ namespace Misuzu;
use Index\IO\MemoryStream; use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64; use Index\Serialisation\UriBase64;
use Misuzu\Auth\SessionInfo; use Misuzu\Auth\SessionInfo;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
/* Map of props /* Map of props
* u - User ID * u - User ID
@ -54,12 +54,11 @@ class AuthToken {
return true; return true;
} }
public function getUserId(): int { public function getUserId(): string {
$value = (int)$this->getProperty('u'); return $this->getProperty('u');
return $value < 1 ? -1 : $value;
} }
public function setUserId(int $userId): self { public function setUserId(string $userId): self {
$this->setProperty('u', (string)$userId); $this->setProperty('u', $userId);
return $this; return $this;
} }
@ -177,7 +176,7 @@ class AuthToken {
return time() - self::EPOCH; return time() - self::EPOCH;
} }
public static function create(User $userInfo, SessionInfo $sessionInfo): self { public static function create(UserInfo $userInfo, SessionInfo $sessionInfo): self {
$token = new AuthToken; $token = new AuthToken;
$token->setUserId($userInfo->getId()); $token->setUserId($userInfo->getId());
$token->setSessionToken($sessionInfo->getToken()); $token->setSessionToken($sessionInfo->getToken());
@ -213,23 +212,4 @@ class AuthToken {
setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true); setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true); setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
} }
// please never use the below functions beyond the scope of the sharpchat auth stuff
// a better mechanism for keeping a global instance of this available
// that isn't a $GLOBAL variable or static instance needs to be established, for User as well
private static $localToken = null;
public function setCurrent(): void {
self::$localToken = $this;
}
public static function unsetCurrent(): void {
self::$localToken = null;
}
public static function getCurrent(): ?self {
return self::$localToken;
}
public static function hasCurrent(): bool {
return self::$localToken !== null;
}
} }

View file

@ -9,7 +9,7 @@ use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class Changelog { class Changelog {
// not a strict list but useful to have // not a strict list but useful to have
@ -96,12 +96,12 @@ class Changelog {
} }
public function countAllChanges( public function countAllChanges(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null, DateTime|int|null $dateTime = null,
?array $tags = null ?array $tags = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($dateTime instanceof DateTime) if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds(); $dateTime = $dateTime->getUnixTimeSeconds();
@ -149,13 +149,13 @@ class Changelog {
public function getAllChanges( public function getAllChanges(
bool $withTags = false, bool $withTags = false,
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null, DateTime|int|null $dateTime = null,
?array $tags = null, ?array $tags = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($dateTime instanceof DateTime) if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds(); $dateTime = $dateTime->getUnixTimeSeconds();
@ -224,13 +224,13 @@ class Changelog {
string|int $action, string|int $action,
string $summary, string $summary,
string $body = '', string $body = '',
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null DateTime|int|null $createdAt = null
): ChangeInfo { ): ChangeInfo {
if(is_string($action)) if(is_string($action))
$action = self::convertToActionId($action); $action = self::convertToActionId($action);
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($createdAt instanceof DateTime) if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds(); $createdAt = $createdAt->getUnixTimeSeconds();
@ -268,7 +268,7 @@ class Changelog {
?string $summary = null, ?string $summary = null,
?string $body = null, ?string $body = null,
bool $updateUserInfo = false, bool $updateUserInfo = false,
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null DateTime|int|null $createdAt = null
): void { ): void {
if($infoOrId instanceof ChangeInfo) if($infoOrId instanceof ChangeInfo)
@ -276,8 +276,8 @@ class Changelog {
if(is_string($action)) if(is_string($action))
$action = self::convertToActionId($action); $action = self::convertToActionId($action);
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($createdAt instanceof DateTime) if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds(); $createdAt = $createdAt->getUnixTimeSeconds();

View file

@ -7,7 +7,7 @@ use Index\Data\DbStatementCache;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class Comments { class Comments {
private IDbConnection $dbConn; private IDbConnection $dbConn;
@ -18,9 +18,9 @@ class Comments {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
public function countAllCategories(User|string|null $owner = null): int { public function countAllCategories(UserInfo|string|null $owner = null): int {
if($owner instanceof User) if($owner instanceof UserInfo)
$owner = (string)$owner->getId(); $owner = $owner->getId();
$hasOwner = $owner !== null; $hasOwner = $owner !== null;
@ -42,11 +42,11 @@ class Comments {
} }
public function getCategories( public function getCategories(
User|string|null $owner = null, UserInfo|string|null $owner = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($owner instanceof User) if($owner instanceof UserInfo)
$owner = (string)$owner->getId(); $owner = $owner->getId();
$hasOwner = $owner !== null; $hasOwner = $owner !== null;
$hasPagination = $pagination !== null; $hasPagination = $pagination !== null;
@ -139,15 +139,15 @@ class Comments {
return $count > 0; return $count > 0;
} }
public function ensureCategory(string $name, User|string|null $owner = null): CommentsCategoryInfo { public function ensureCategory(string $name, UserInfo|string|null $owner = null): CommentsCategoryInfo {
if($this->checkCategoryNameExists($name)) if($this->checkCategoryNameExists($name))
return $this->getCategoryByName($name); return $this->getCategoryByName($name);
return $this->createCategory($name, $owner); return $this->createCategory($name, $owner);
} }
public function createCategory(string $name, User|string|null $owner = null): CommentsCategoryInfo { public function createCategory(string $name, UserInfo|string|null $owner = null): CommentsCategoryInfo {
if($owner instanceof User) if($owner instanceof UserInfo)
$owner = (string)$owner->getId(); $owner = $owner->getId();
$name = trim($name); $name = trim($name);
if(empty($name)) if(empty($name))
@ -174,12 +174,12 @@ class Comments {
CommentsCategoryInfo|string $category, CommentsCategoryInfo|string $category,
?string $name = null, ?string $name = null,
bool $updateOwner = false, bool $updateOwner = false,
User|string|null $owner = null UserInfo|string|null $owner = null
): void { ): void {
if($category instanceof CommentsCategoryInfo) if($category instanceof CommentsCategoryInfo)
$category = $category->getId(); $category = $category->getId();
if($owner instanceof User) if($owner instanceof UserInfo)
$owner = (string)$owner->getId(); $owner = $owner->getId();
if($name !== null) { if($name !== null) {
$name = trim($name); $name = trim($name);
@ -368,7 +368,7 @@ class Comments {
public function createPost( public function createPost(
CommentsCategoryInfo|string|null $category, CommentsCategoryInfo|string|null $category,
CommentsPostInfo|string|null $parent, CommentsPostInfo|string|null $parent,
User|string|null $user, UserInfo|string|null $user,
string $body, string $body,
bool $pin = false bool $pin = false
): CommentsPostInfo { ): CommentsPostInfo {
@ -383,8 +383,8 @@ class Comments {
} }
if($category === null) if($category === null)
throw new InvalidArgumentException('$category is null; at least a $category or $parent must be specified.'); throw new InvalidArgumentException('$category is null; at least a $category or $parent must be specified.');
if($user instanceof User) if($user instanceof UserInfo)
$user = (string)$user->getId(); $user = $user->getId();
if(empty(trim($body))) if(empty(trim($body)))
throw new InvalidArgumentException('$body may not be empty.'); throw new InvalidArgumentException('$body may not be empty.');
@ -459,12 +459,12 @@ class Comments {
public function getPostVote( public function getPostVote(
CommentsPostInfo|string $post, CommentsPostInfo|string $post,
User|string|null $user UserInfo|string|null $user
): CommentsPostVoteInfo { ): CommentsPostVoteInfo {
if($post instanceof CommentsPostInfo) if($post instanceof CommentsPostInfo)
$post = $post->getId(); $post = $post->getId();
if($user instanceof User) if($user instanceof UserInfo)
$user = (string)$user->getId(); $user = $user->getId();
// SUM() here makes it so a result row is always returned, albeit with just NULLs // SUM() here makes it so a result row is always returned, albeit with just NULLs
$stmt = $this->cache->get('SELECT comment_id, user_id, SUM(comment_vote) FROM msz_comments_votes WHERE comment_id = ? AND user_id = ?'); $stmt = $this->cache->get('SELECT comment_id, user_id, SUM(comment_vote) FROM msz_comments_votes WHERE comment_id = ? AND user_id = ?');
@ -481,15 +481,15 @@ class Comments {
public function addPostVote( public function addPostVote(
CommentsPostInfo|string $post, CommentsPostInfo|string $post,
User|string $user, UserInfo|string $user,
int $weight int $weight
): void { ): void {
if($weight === 0) if($weight === 0)
return; return;
if($post instanceof CommentsPostInfo) if($post instanceof CommentsPostInfo)
$post = $post->getId(); $post = $post->getId();
if($user instanceof User) if($user instanceof UserInfo)
$user = (string)$user->getId(); $user = $user->getId();
$stmt = $this->cache->get('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) VALUES (?, ?, ?)'); $stmt = $this->cache->get('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) VALUES (?, ?, ?)');
$stmt->addParameter(1, $post); $stmt->addParameter(1, $post);
@ -498,22 +498,22 @@ class Comments {
$stmt->execute(); $stmt->execute();
} }
public function addPostPositiveVote(CommentsPostInfo|string $post, User|string $user): void { public function addPostPositiveVote(CommentsPostInfo|string $post, UserInfo|string $user): void {
$this->addPostVote($post, $user, 1); $this->addPostVote($post, $user, 1);
} }
public function addPostNegativeVote(CommentsPostInfo|string $post, User|string $user): void { public function addPostNegativeVote(CommentsPostInfo|string $post, UserInfo|string $user): void {
$this->addPostVote($post, $user, -1); $this->addPostVote($post, $user, -1);
} }
public function removePostVote( public function removePostVote(
CommentsPostInfo|string $post, CommentsPostInfo|string $post,
User|string $user UserInfo|string $user
): void { ): void {
if($post instanceof CommentsPostInfo) if($post instanceof CommentsPostInfo)
$post = $post->getId(); $post = $post->getId();
if($user instanceof User) if($user instanceof UserInfo)
$user = (string)$user->getId(); $user = $user->getId();
$stmt = $this->cache->get('DELETE FROM msz_comments_votes WHERE comment_id = ? AND user_id = ?'); $stmt = $this->cache->get('DELETE FROM msz_comments_votes WHERE comment_id = ? AND user_id = ?');
$stmt->addParameter(1, $post); $stmt->addParameter(1, $post);

View file

@ -3,7 +3,7 @@ namespace Misuzu\Comments;
use Index\DateTime; use Index\DateTime;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class CommentsCategoryInfo { class CommentsCategoryInfo {
private string $id; private string $id;
@ -38,11 +38,11 @@ class CommentsCategoryInfo {
return $this->ownerId; return $this->ownerId;
} }
public function isOwner(User|string $user): bool { public function isOwner(UserInfo|string $user): bool {
if($this->ownerId === null) if($this->ownerId === null)
return false; return false;
if($user instanceof User) if($user instanceof UserInfo)
$user = (string)$user->getId(); $user = $user->getId();
return $user === $this->ownerId; return $user === $this->ownerId;
} }

View file

@ -3,24 +3,26 @@ namespace Misuzu\Comments;
use stdClass; use stdClass;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\MisuzuContext;
use Misuzu\Users\Users;
class CommentsEx { class CommentsEx {
private Comments $comments; public function __construct(
private array $userInfos; private MisuzuContext $context,
private Comments $comments,
public function __construct(Comments $comments, array $userInfos = []) { private Users $users,
$this->comments = $comments; private array $userInfos = [],
$this->userInfos = $userInfos; private array $userColours = []
} ) {}
public function getCommentsForLayout(CommentsCategoryInfo|string $category): object { public function getCommentsForLayout(CommentsCategoryInfo|string $category): object {
$info = new stdClass; $info = new stdClass;
if(is_string($category)) if(is_string($category))
$category = $this->comments->ensureCategory($category); $category = $this->comments->ensureCategory($category);
$hasUser = User::hasCurrent(); $hasUser = $this->context->isLoggedIn();
$info->user = $hasUser ? User::getCurrent() : null; $info->user = $hasUser ? $this->context->getActiveUser() : null;
$info->colour = $hasUser ? $this->users->getUserColour($info->user) : null;
$info->perms = $hasUser ? perms_for_comments($info->user->getId()) : []; $info->perms = $hasUser ? perms_for_comments($info->user->getId()) : [];
$info->category = $category; $info->category = $category;
$info->posts = []; $info->posts = [];
@ -37,20 +39,28 @@ class CommentsEx {
$userId = $postInfo->getUserId(); $userId = $postInfo->getUserId();
if(array_key_exists($userId, $this->userInfos)) { if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId]; $userInfo = $this->userInfos[$userId];
$userColour = $this->userColours[$userId];
} else { } else {
try { try {
$userInfo = User::byId($userId); $userInfo = $this->users->getUser($userId, 'id');
$userColour = $this->users->getUserColour($userInfo);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
$userColour = null;
} }
$this->userInfos[$userId] = $userInfo; $this->userInfos[$userId] = $userInfo;
$this->userColours[$userId] = $userColour;
} }
} else $userInfo = null; } else {
$userInfo = null;
$userColour = null;
}
$info = new stdClass; $info = new stdClass;
$info->post = $postInfo; $info->post = $postInfo;
$info->user = $userInfo; $info->user = $userInfo;
$info->colour = $userColour;
$info->vote = $this->comments->getPostVote($postInfo, $userInfo); $info->vote = $this->comments->getPostVote($postInfo, $userInfo);
$info->replies = []; $info->replies = [];

View file

@ -1,63 +0,0 @@
<?php
namespace Misuzu\Comments;
use RuntimeException;
use Misuzu\DB;
use Misuzu\Users\User;
class CommentsParser {
private const MARKUP_USERNAME = '#\B(?:@{1}(' . User::NAME_REGEX . '))#u';
private const MARKUP_USERID = '#\B(?:@{2}([0-9]+))#u';
public static function parseForStorage(string $text): string {
return preg_replace_callback(self::MARKUP_USERNAME, function ($matches) {
try {
return sprintf('@@%d', User::byUsername($matches[1])->getId());
} catch(RuntimeException $ex) {
return $matches[0];
}
}, $text);
}
public static function parseForDisplay(string $text): string {
$text = htmlentities($text);
$text = preg_replace_callback(
'/(^|[\n ])([\w]*?)([\w]*?:\/\/[\w]+[^ \,\"\n\r\t<]*)/is',
function ($matches) {
$matches[0] = trim($matches[0]);
$url = parse_url($matches[0]);
if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true))
return $matches[0];
return sprintf(' <a href="%1$s" class="link" target="_blank" rel="noreferrer noopener">%1$s</a>', $matches[0]);
},
$text
);
$text = preg_replace_callback(self::MARKUP_USERID, function ($matches) {
$getInfo = DB::prepare('
SELECT
u.`user_id`, u.`username`,
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE `user_id` = :user_id
');
$getInfo->bind('user_id', $matches[1]);
$info = $getInfo->fetch();
if(empty($info))
return $matches[0];
return sprintf(
'<a href="%s" class="comment__mention", style="%s">@%s</a>',
url('user-profile', ['user' => $info['user_id']]),
html_colour($info['user_colour']),
$info['username']
);
}, $text);
return nl2br($text);
}
}

View file

@ -424,7 +424,8 @@ function forum_mark_read(?int $forumId, int $userId): void {
function forum_posting_info(int $userId): array { function forum_posting_info(int $userId): array {
$getPostingInfo = \Misuzu\DB::prepare(' $getPostingInfo = \Misuzu\DB::prepare('
SELECT SELECT
u.`user_country`, u.`user_created`, u.`user_id`, u.`username`, u.`user_country`, u.`user_created`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `colour`,
( (
SELECT COUNT(`post_id`) SELECT COUNT(`post_id`)
FROM `msz_forum_posts` FROM `msz_forum_posts`
@ -440,6 +441,8 @@ function forum_posting_info(int $userId): array {
LIMIT 1 LIMIT 1
) AS `user_post_parse` ) AS `user_post_parse`
FROM `msz_users` as u FROM `msz_users` as u
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
WHERE `user_id` = :user_id WHERE `user_id` = :user_id
'); ');
$getPostingInfo->bind('user_id', $userId); $getPostingInfo->bind('user_id', $userId);
@ -527,7 +530,9 @@ function forum_count_synchronise(int $forumId = MSZ_FORUM_ROOT, bool $save = tru
return compact('topics', 'posts'); return compact('topics', 'posts');
} }
function forum_get_user_most_active_category_info(int $userId): ?object { function forum_get_user_most_active_category_info(string|int $userId): ?object {
if(is_string($userId))
$userId = (int)$userId;
if($userId < 1) if($userId < 1)
return null; return null;
@ -541,3 +546,41 @@ function forum_get_user_most_active_category_info(int $userId): ?object {
return $getActiveForum->fetchObject(); return $getActiveForum->fetchObject();
} }
function forum_get_user_topic_count(\Misuzu\Users\UserInfo|string|int $userId): int {
if(is_int($userId))
$userId = (string)$userId;
elseif($userId instanceof \Misuzu\Users\UserInfo)
$userId = $userId->getId();
global $db;
static $stmt = null;
if($stmt === null)
$stmt = $db->prepare('SELECT COUNT(*) FROM msz_forum_topics WHERE user_id = ? AND topic_deleted IS NULL');
else
$stmt->reset();
$stmt->addParameter(1, $userId);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
function forum_get_user_post_count(\Misuzu\Users\UserInfo|string|int $userId): int {
if(is_int($userId))
$userId = (string)$userId;
elseif($userId instanceof \Misuzu\Users\UserInfo)
$userId = $userId->getId();
global $db;
static $stmt = null;
if($stmt === null)
$stmt = $db->prepare('SELECT COUNT(*) FROM msz_forum_posts WHERE user_id = ? AND post_deleted IS NULL');
else
$stmt->reset();
$stmt->addParameter(1, $userId);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}

View file

@ -3,17 +3,19 @@ namespace Misuzu\Http\Handlers;
use RuntimeException; use RuntimeException;
use Misuzu\GitInfo; use Misuzu\GitInfo;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
use Misuzu\Users\Assets\StaticUserImageAsset; use Misuzu\Users\Assets\StaticUserImageAsset;
use Misuzu\Users\Assets\UserImageAssetInterface;
use Misuzu\Users\Assets\UserAssetScalableInterface; use Misuzu\Users\Assets\UserAssetScalableInterface;
use Misuzu\Users\Assets\UserAvatarAsset;
use Misuzu\Users\Assets\UserBackgroundAsset;
use Misuzu\Users\Assets\UserImageAssetInterface;
final class AssetsHandler extends Handler { final class AssetsHandler extends Handler {
private function canViewAsset($request, User $assetUser): bool { private function canViewAsset($request, UserInfo $assetUser): bool {
return !$this->context->hasActiveBan($assetUser) || ( return !$this->context->hasActiveBan($assetUser) || (
User::hasCurrent() $this->context->isLoggedIn()
&& parse_url($request->getHeaderFirstLine('Referer'), PHP_URL_PATH) === url('user-profile') && parse_url($request->getHeaderFirstLine('Referer'), PHP_URL_PATH) === url('user-profile')
&& perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS) && perms_check_user(MSZ_PERMS_USER, $this->context->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_USERS)
); );
} }
@ -48,12 +50,14 @@ final class AssetsHandler extends Handler {
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC); $assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
try { try {
$userInfo = User::byId($userId); $userInfo = $this->context->getUsers()->getUser($userId, 'id');
if(!$this->canViewAsset($request, $userInfo)) { if(!$this->canViewAsset($request, $userInfo)) {
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/banned-avatar.png', MSZ_PUBLIC); $assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/banned-avatar.png', MSZ_PUBLIC);
} elseif($userInfo->hasAvatar()) { } else {
$assetInfo = $userInfo->getAvatarInfo(); $userAssetInfo = new UserAvatarAsset($userInfo);
if($userAssetInfo->isPresent())
$assetInfo = $userAssetInfo;
} }
} catch(RuntimeException $ex) {} } catch(RuntimeException $ex) {}
@ -68,15 +72,21 @@ final class AssetsHandler extends Handler {
return 404; return 404;
try { try {
$userInfo = User::byId($userId); $userInfo = $this->context->getUsers()->getUser($userId, 'id');
} catch(RuntimeException $ex) {} } catch(RuntimeException $ex) {}
if(empty($userInfo) || !$userInfo->hasBackground() || !$this->canViewAsset($request, $userInfo)) { if(!empty($userInfo)) {
$userAssetInfo = new UserBackgroundAsset($userInfo);
if($userAssetInfo->isPresent() && $this->canViewAsset($request, $userInfo))
$assetInfo = $userAssetInfo;
}
if(!isset($assetInfo)) {
$response->setContent(''); $response->setContent('');
return 404; return 404;
} }
$this->serveUserAsset($response, $request, $userInfo->getBackgroundInfo()); $this->serveUserAsset($response, $request, $assetInfo);
} }
public function serveLegacy($response, $request) { public function serveLegacy($response, $request) {

View file

@ -10,16 +10,18 @@ use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem; use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer; use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer; use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\Users\User;
class ChangelogHandler extends Handler { class ChangelogHandler extends Handler {
private array $userInfos = []; private array $userInfos = [];
private array $userColours = [];
public function index($response, $request) { public function index($response, $request) {
$filterDate = (string)$request->getParam('date'); $filterDate = (string)$request->getParam('date');
$filterUser = (int)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT); $filterUser = (int)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
$filterTags = (string)$request->getParam('tags'); $filterTags = (string)$request->getParam('tags');
$users = $this->context->getUsers();
if(empty($filterDate)) if(empty($filterDate))
$filterDate = null; $filterDate = null;
else else
@ -32,7 +34,7 @@ class ChangelogHandler extends Handler {
if($filterUser > 0) if($filterUser > 0)
try { try {
$filterUser = User::byId($filterUser); $filterUser = $users->getUser((string)$filterUser, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return 404; return 404;
} }
@ -64,19 +66,24 @@ class ChangelogHandler extends Handler {
if(array_key_exists($userId, $this->userInfos)) { if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId]; $userInfo = $this->userInfos[$userId];
$userColour = $this->userColours[$userId];
} else { } else {
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser($userId, 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
$userColour = null;
} }
$this->userInfos[$userId] = $userInfo; $this->userInfos[$userId] = $userInfo;
$this->userColours[$userId] = $userColour;
} }
$changes[] = [ $changes[] = [
'change' => $changeInfo, 'change' => $changeInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColour,
]; ];
} }
@ -91,7 +98,7 @@ class ChangelogHandler extends Handler {
} }
private function getCommentsInfo(string $categoryName): object { private function getCommentsInfo(string $categoryName): object {
$comments = new CommentsEx($this->context->getComments(), $this->userInfos); $comments = new CommentsEx($this->context, $this->context->getComments(), $this->context->getUsers(), $this->userInfos, $this->userColours);
return $comments->getCommentsForLayout($categoryName); return $comments->getCommentsForLayout($categoryName);
} }
@ -102,15 +109,20 @@ class ChangelogHandler extends Handler {
return 404; return 404;
} }
$users = $this->context->getUsers();
try { try {
$userInfo = User::byId($changeInfo->getUserId()); $userInfo = $users->getUser($changeInfo->getUserId(), 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
$userColour = null;
} }
$response->setContent(Template::renderRaw('changelog.change', [ $response->setContent(Template::renderRaw('changelog.change', [
'change_info' => $changeInfo, 'change_info' => $changeInfo,
'change_user_info' => $userInfo, 'change_user_info' => $userInfo,
'change_user_colour' => $userColour,
'comments_info' => $this->getCommentsInfo($changeInfo->getCommentsCategoryName()), 'comments_info' => $this->getCommentsInfo($changeInfo->getCommentsCategoryName()),
])); ]));
} }

View file

@ -3,11 +3,10 @@ namespace Misuzu\Http\Handlers;
use Misuzu\CSRF; use Misuzu\CSRF;
use Misuzu\Template; use Misuzu\Template;
use Misuzu\Users\User;
final class ForumHandler extends Handler { final class ForumHandler extends Handler {
public function markAsReadGET($response, $request) { public function markAsReadGET($response, $request) {
if(!User::hasCurrent()) if(!$this->context->isLoggedIn())
return 403; return 403;
$forumId = (int)$request->getParam('forum', FILTER_SANITIZE_NUMBER_INT); $forumId = (int)$request->getParam('forum', FILTER_SANITIZE_NUMBER_INT);
@ -22,7 +21,7 @@ final class ForumHandler extends Handler {
} }
public function markAsReadPOST($response, $request) { public function markAsReadPOST($response, $request) {
if(!User::hasCurrent()) if(!$this->context->isLoggedIn())
return 403; return 403;
if(!$request->isFormContent()) if(!$request->isFormContent())
@ -33,7 +32,7 @@ final class ForumHandler extends Handler {
return 400; return 400;
$forumId = (int)$request->getContent()->getParam('forum', FILTER_SANITIZE_NUMBER_INT); $forumId = (int)$request->getContent()->getParam('forum', FILTER_SANITIZE_NUMBER_INT);
forum_mark_read($forumId, User::getCurrent()->getId()); forum_mark_read($forumId, $this->context->getActiveUser()->getId());
$redirect = url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]); $redirect = url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]);
$response->redirect($redirect, false); $response->redirect($redirect, false);

View file

@ -2,11 +2,11 @@
namespace Misuzu\Http\Handlers; namespace Misuzu\Http\Handlers;
use RuntimeException; use RuntimeException;
use Index\DateTime;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Template; use Misuzu\Template;
use Misuzu\Comments\CommentsCategory; use Misuzu\Comments\CommentsCategory;
use Misuzu\Users\User;
final class HomeHandler extends Handler { final class HomeHandler extends Handler {
private const STATS = [ private const STATS = [
@ -19,13 +19,14 @@ final class HomeHandler extends Handler {
]; ];
public function index($response, $request): void { public function index($response, $request): void {
if(User::hasCurrent()) if($this->context->isLoggedIn())
$this->home($response, $request); $this->home($response, $request);
else else
$this->landing($response, $request); $this->landing($response, $request);
} }
public function landing($response, $request): void { public function landing($response, $request): void {
$users = $this->context->getUsers();
$config = $this->context->getConfig(); $config = $this->context->getConfig();
$counters = $this->context->getCounters(); $counters = $this->context->getCounters();
@ -50,15 +51,14 @@ final class HomeHandler extends Handler {
); );
$stats = $counters->get(self::STATS); $stats = $counters->get(self::STATS);
$onlineUsers = DB::query( $onlineUserInfos = $users->getUsers(
'SELECT u.`user_id`, u.`username`, COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`' lastActiveInMinutes: 5,
. ' FROM `msz_users` AS u' deleted: false,
. ' LEFT JOIN `msz_roles` AS r' orderBy: 'random',
. ' ON r.`role_id` = u.`display_role`' );
. ' WHERE u.`user_active` >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)'
. ' ORDER BY u.`user_active` DESC, RAND()' // can also chat here, whoa
. ' LIMIT 50' $stats['users:online:recent'] = count($onlineUserInfos);
)->fetchAll();
// TODO: don't hardcode forum ids // TODO: don't hardcode forum ids
$featuredForums = $config->getArray('landing.forum_categories'); $featuredForums = $config->getArray('landing.forum_categories');
@ -103,7 +103,7 @@ final class HomeHandler extends Handler {
$response->setContent(Template::renderRaw('home.landing', [ $response->setContent(Template::renderRaw('home.landing', [
'statistics' => $stats, 'statistics' => $stats,
'online_users' => $onlineUsers, 'online_users' => $onlineUserInfos,
'featured_news' => $featuredNews, 'featured_news' => $featuredNews,
'linked_data' => $linkedData, 'linked_data' => $linkedData,
'forum_popular' => $popularTopics, 'forum_popular' => $popularTopics,
@ -113,10 +113,13 @@ final class HomeHandler extends Handler {
public function home($response, $request): void { public function home($response, $request): void {
$news = $this->context->getNews(); $news = $this->context->getNews();
$users = $this->context->getUsers();
$config = $this->context->getConfig();
$comments = $this->context->getComments(); $comments = $this->context->getComments();
$counters = $this->context->getCounters(); $counters = $this->context->getCounters();
$featuredNews = []; $featuredNews = [];
$userInfos = []; $userInfos = [];
$userColours = [];
$categoryInfos = []; $categoryInfos = [];
$featuredNewsInfos = $news->getAllPosts( $featuredNewsInfos = $news->getAllPosts(
onlyFeatured: true, onlyFeatured: true,
@ -129,11 +132,14 @@ final class HomeHandler extends Handler {
if(array_key_exists($userId, $userInfos)) { if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId]; $userInfo = $userInfos[$userId];
$userColour = $userColours[$userId];
} else { } else {
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser($userId, 'id');
$userColour = $userColours[$userId] = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
$userColour = $userColours[$userId] = null;
} }
$userInfos[$userId] = $userInfo; $userInfos[$userId] = $userInfo;
@ -151,6 +157,7 @@ final class HomeHandler extends Handler {
'post' => $postInfo, 'post' => $postInfo,
'category' => $categoryInfo, 'category' => $categoryInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColour,
'comments_count' => $commentsCount, 'comments_count' => $commentsCount,
]; ];
} }
@ -158,25 +165,41 @@ final class HomeHandler extends Handler {
$stats = $counters->get(self::STATS); $stats = $counters->get(self::STATS);
$changelog = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10)); $changelog = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10));
$birthdays = User::byBirthdate(); $birthdays = [];
$latestUser = !empty($birthdays) ? null : User::byLatest(); $birthdayInfos = $users->getUsers(deleted: false, birthdate: DateTime::now(), orderBy: 'random');
foreach($birthdayInfos as $birthdayInfo)
$birthdays[] = [
'info' => $birthdayInfo,
'colour' => $users->getUserColour($birthdayInfo),
];
$onlineUsers = DB::query( $newestMember = [];
'SELECT u.`user_id`, u.`username`, COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`' if(empty($birthdays)) {
. ' FROM `msz_users` AS u' $newestMemberId = $config->getString('users.newest');
. ' LEFT JOIN `msz_roles` AS r' if(!empty($newestMemberId))
. ' ON r.`role_id` = u.`display_role`' try {
. ' WHERE u.`user_active` >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)' $newestMemberInfo = $users->getUser($newestMemberId, 'id');
. ' ORDER BY u.`user_active` DESC, RAND()' $newestMember['info'] = $newestMemberInfo;
)->fetchAll(); $newestMember['colour'] = $users->getUserColour($newestMemberInfo);
} catch(RuntimeException $ex) {
$newestMember = [];
$config->removeValues('users.newest');
}
}
$onlineUserInfos = $users->getUsers(
lastActiveInMinutes: 5,
deleted: false,
orderBy: 'random',
);
// today we cheat // today we cheat
$stats['users:online:recent'] = count($onlineUsers); $stats['users:online:recent'] = count($onlineUserInfos);
$response->setContent(Template::renderRaw('home.home', [ $response->setContent(Template::renderRaw('home.home', [
'statistics' => $stats, 'statistics' => $stats,
'latest_user' => $latestUser, 'newest_member' => $newestMember,
'online_users' => $onlineUsers, 'online_users' => $onlineUserInfos,
'birthdays' => $birthdays, 'birthdays' => $birthdays,
'featured_changelog' => $changelog, 'featured_changelog' => $changelog,
'featured_news' => $featuredNews, 'featured_news' => $featuredNews,

View file

@ -13,14 +13,15 @@ use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer; use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\News\NewsCategoryInfo; use Misuzu\News\NewsCategoryInfo;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
final class NewsHandler extends Handler { final class NewsHandler extends Handler {
private function fetchPostInfo(array $postInfos, array $categoryInfos = []): array { private function fetchPostInfo(array $postInfos, array $categoryInfos = []): array {
$news = $this->context->getNews(); $news = $this->context->getNews();
$users = $this->context->getUsers();
$comments = $this->context->getComments(); $comments = $this->context->getComments();
$posts = []; $posts = [];
$userInfos = []; $userInfos = [];
$userColours = [];
foreach($postInfos as $postInfo) { foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId(); $userId = $postInfo->getUserId();
@ -28,14 +29,18 @@ final class NewsHandler extends Handler {
if(array_key_exists($userId, $userInfos)) { if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId]; $userInfo = $userInfos[$userId];
$userColour = $userColours[$userId];
} else { } else {
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser($userId, 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
$userColour = null;
} }
$userInfos[$userId] = $userInfo; $userInfos[$userId] = $userInfo;
$userColours[$userId] = $userColour;
} }
if(array_key_exists($categoryId, $categoryInfos)) if(array_key_exists($categoryId, $categoryInfos))
@ -50,6 +55,7 @@ final class NewsHandler extends Handler {
'post' => $postInfo, 'post' => $postInfo,
'category' => $categoryInfo, 'category' => $categoryInfo,
'user' => $userInfo, 'user' => $userInfo,
'user_colour' => $userColour,
'comments_count' => $commentsCount, 'comments_count' => $commentsCount,
]; ];
} }
@ -111,6 +117,7 @@ final class NewsHandler extends Handler {
public function viewPost($response, $request, string $postId) { public function viewPost($response, $request, string $postId) {
$news = $this->context->getNews(); $news = $this->context->getNews();
$users = $this->context->getUsers();
$comments = $this->context->getComments(); $comments = $this->context->getComments();
try { try {
@ -137,17 +144,20 @@ final class NewsHandler extends Handler {
} }
$userInfo = null; $userInfo = null;
$userColour = null;
if($postInfo->hasUserId()) if($postInfo->hasUserId())
try { try {
$userInfo = User::byId($postInfo->getUserId()); $userInfo = $users->getUser($postInfo->getUserId(), 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {} } catch(RuntimeException $ex) {}
$comments = new CommentsEx($comments); $comments = new CommentsEx($this->context, $comments, $users);
$response->setContent(Template::renderRaw('news.post', [ $response->setContent(Template::renderRaw('news.post', [
'post_info' => $postInfo, 'post_info' => $postInfo,
'post_category_info' => $categoryInfo, 'post_category_info' => $categoryInfo,
'post_user_info' => $userInfo, 'post_user_info' => $userInfo,
'post_user_colour' => $userColour,
'comments_info' => $comments->getCommentsForLayout($commentsCategory), 'comments_info' => $comments->getCommentsForLayout($commentsCategory),
])); ]));
} }
@ -170,7 +180,7 @@ final class NewsHandler extends Handler {
$userName = 'Author'; $userName = 'Author';
if($userInfo !== null) { if($userInfo !== null) {
$userId = $userInfo->getId(); $userId = $userInfo->getId();
$userName = $userInfo->getUsername(); $userName = $userInfo->getName();
} }
$postUrl = url_prefix(false) . url('news-post', ['post' => $postInfo->getId()]); $postUrl = url_prefix(false) . url('news-post', ['post' => $postInfo->getId()]);
@ -199,6 +209,7 @@ final class NewsHandler extends Handler {
private function fetchPostInfoForFeed(array $postInfos): array { private function fetchPostInfoForFeed(array $postInfos): array {
$news = $this->context->getNews(); $news = $this->context->getNews();
$users = $this->context->getUsers();
$posts = []; $posts = [];
$userInfos = []; $userInfos = [];
@ -209,7 +220,7 @@ final class NewsHandler extends Handler {
$userInfo = $userInfos[$userId]; $userInfo = $userInfos[$userId];
} else { } else {
try { try {
$userInfo = User::byId($userId); $userInfo = $users->getUser($userId, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = null; $userInfo = null;
} }

View file

@ -1,30 +0,0 @@
<?php
namespace Misuzu;
use InvalidArgumentException;
use Index\XArray;
// get rid of this garbage as soon as possible
class Memoizer {
private $collection = [];
public function find($find, callable $create) {
if(is_int($find) || is_string($find)) {
if(!isset($this->collection[$find]))
$this->collection[$find] = $create();
return $this->collection[$find];
}
if(is_callable($find)) {
$item = XArray::first($this->collection, $find) ?? $create();
if(method_exists($item, 'getId'))
$this->collection[$item->getId()] = $item;
else
$this->collection[] = $item;
return $item;
}
throw new InvalidArgumentException('Wasn\'t able to figure out your $find argument.');
}
}

View file

@ -20,8 +20,8 @@ use Misuzu\Users\Bans;
use Misuzu\Users\BanInfo; use Misuzu\Users\BanInfo;
use Misuzu\Users\ModNotes; use Misuzu\Users\ModNotes;
use Misuzu\Users\Roles; use Misuzu\Users\Roles;
use Misuzu\Users\User;
use Misuzu\Users\Users; use Misuzu\Users\Users;
use Misuzu\Users\UserInfo;
use Misuzu\Users\Warnings; use Misuzu\Users\Warnings;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigrationRepo; use Index\Data\Migration\IDbMigrationRepo;
@ -168,12 +168,61 @@ class MisuzuContext {
return $this->profileFields; return $this->profileFields;
} }
private ?AuthToken $authToken = null;
private ?UserInfo $activeUser = null;
private ?UserInfo $activeUserReal = null;
public function setAuthInfo(AuthToken $authToken, ?UserInfo $userInfo, ?UserInfo $realUserInfo): void {
$this->authToken = $authToken;
$this->activeUser = $userInfo;
$this->activeUserReal = $realUserInfo;
}
public function removeAuthInfo(): void {
$this->authToken = null;
$this->activeUser = null;
$this->activeUserReal = null;
}
public function hasAuthToken(): bool {
return $this->authToken !== null;
}
public function getAuthToken(): ?AuthToken {
return $this->authToken;
}
public function isLoggedIn(): bool {
return $this->authToken !== null && $this->activeUser !== null;
}
public function isImpersonating(): bool {
return $this->activeUser !== null && $this->activeUserReal !== null
&& $this->activeUser->getId() !== $this->activeUserReal->getId();
}
public function hasActiveUser(): bool {
return $this->activeUser !== null;
}
public function getActiveUser(): ?UserInfo {
return $this->activeUser;
}
public function hasRealActiveUser(): bool {
return $this->activeUserReal !== null;
}
public function getRealActiveUser(): ?UserInfo {
return $this->activeUserReal;
}
private array $activeBansCache = []; private array $activeBansCache = [];
public function tryGetActiveBan(User|string|null $userInfo = null): ?BanInfo { public function tryGetActiveBan(UserInfo|string|null $userInfo = null): ?BanInfo {
if($userInfo === null) { if($userInfo === null) {
if(User::hasCurrent()) if($this->isLoggedIn())
$userInfo = User::getCurrent(); $userInfo = $this->getActiveUser();
else return null; else return null;
} }
@ -184,13 +233,13 @@ class MisuzuContext {
return $this->activeBansCache[$userId] = $this->bans->tryGetActiveBan($userId); return $this->activeBansCache[$userId] = $this->bans->tryGetActiveBan($userId);
} }
public function hasActiveBan(User|string|null $userInfo = null): bool { public function hasActiveBan(UserInfo|string|null $userInfo = null): bool {
return $this->tryGetActiveBan($userInfo) !== null; return $this->tryGetActiveBan($userInfo) !== null;
} }
public function createAuditLog(string $action, array $params = [], User|string|null $userInfo = null): void { public function createAuditLog(string $action, array $params = [], UserInfo|string|null $userInfo = null): void {
if($userInfo === null && User::hasCurrent()) if($userInfo === null && $this->isLoggedIn())
$userInfo = User::getCurrent(); $userInfo = $this->getActiveUser();
$this->auditLog->createLog( $this->auditLog->createLog(
$userInfo, $userInfo,
@ -201,6 +250,124 @@ class MisuzuContext {
); );
} }
public function getHeaderMenu(?UserInfo $userInfo): array {
$hasUserInfo = $userInfo?->isDeleted() === false;
$menu = [];
$home = [
'title' => 'Home',
'url' => url('index'),
'menu' => [],
];
if($hasUserInfo)
$home['menu'][] = [
'title' => 'Members',
'url' => url('user-list'),
];
$home['menu'][] = [
'title' => 'Changelog',
'url' => url('changelog-index'),
];
$home['menu'][] = [
'title' => 'Contact',
'url' => url('info', ['title' => 'contact']),
];
$home['menu'][] = [
'title' => 'Rules',
'url' => url('info', ['title' => 'rules']),
];
$menu[] = $home;
$menu[] = [
'title' => 'News',
'url' => url('news-index'),
];
$forum = [
'title' => 'Forum',
'url' => url('forum-index'),
'menu' => [],
];
if($hasUserInfo && perms_check_user(MSZ_PERMS_GENERAL, $userInfo->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD))
$forum['menu'][] = [
'title' => 'Leaderboard',
'url' => url('forum-leaderboard'),
];
$menu[] = $forum;
$chatPath = $this->config->getString('sockChat.chatPath.normal');
if(!empty($chatPath))
$menu[] = [
'title' => 'Chat',
'url' => $chatPath,
];
return $menu;
}
public function getUserMenu(?UserInfo $userInfo, bool $inBroomCloset): array {
$menu = [];
if($userInfo === null) {
$menu[] = [
'title' => 'Register',
'url' => url('auth-register'),
'icon' => 'fas fa-user-plus fa-fw',
];
$menu[] = [
'title' => 'Log in',
'url' => url('auth-login'),
'icon' => 'fas fa-sign-in-alt fa-fw',
];
} else {
$menu[] = [
'title' => 'Profile',
'url' => url('user-profile', ['user' => $userInfo->getId()]),
'icon' => 'fas fa-user fa-fw',
];
$menu[] = [
'title' => 'Settings',
'url' => url('settings-index'),
'icon' => 'fas fa-cog fa-fw',
];
$menu[] = [
'title' => 'Search',
'url' => url('search-index'),
'icon' => 'fas fa-search fa-fw',
];
if(!$this->hasActiveBan($userInfo) && perms_check_user(MSZ_PERMS_GENERAL, $userInfo->getId(), MSZ_PERM_GENERAL_CAN_MANAGE)) {
// restore behaviour where clicking this button switches between
// site version and broom version
if($inBroomCloset)
$menu[] = [
'title' => 'Exit Broom Closet',
'url' => url('index'),
'icon' => 'fas fa-door-open fa-fw',
];
else
$menu[] = [
'title' => 'Enter Broom Closet',
'url' => url('manage-index'),
'icon' => 'fas fa-door-closed fa-fw',
];
}
$menu[] = [
'title' => 'Log out',
'url' => url('auth-logout'),
'icon' => 'fas fa-sign-out-alt fa-fw',
];
}
return $menu;
}
public function setUpHttp(bool $legacy = false): void { public function setUpHttp(bool $legacy = false): void {
$this->router = new HttpFx; $this->router = new HttpFx;
$this->router->use('/', function($response) { $this->router->use('/', function($response) {
@ -263,8 +430,8 @@ class MisuzuContext {
$this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET')); $this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET'));
$this->router->post('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadPOST')); $this->router->post('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadPOST'));
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'), $this->bans, $this->emotes, $this->sessions); new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'), $this, $this->bans, $this->emotes, $this->users, $this->sessions);
new SatoriRoutes($this->dbConn, $this->config->scopeTo('satori'), $this->router, $this->profileFields); new SatoriRoutes($this->dbConn, $this->config->scopeTo('satori'), $this->router, $this->users, $this->profileFields);
} }
private function registerLegacyRedirects(): void { private function registerLegacyRedirects(): void {

View file

@ -9,7 +9,7 @@ use Index\Data\IDbConnection;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategoryInfo; use Misuzu\Comments\CommentsCategoryInfo;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class News { class News {
private IDbConnection $dbConn; private IDbConnection $dbConn;
@ -359,13 +359,13 @@ class News {
string $title, string $title,
string $body, string $body,
bool $featured = false, bool $featured = false,
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $schedule = null DateTime|int|null $schedule = null
): NewsPostInfo { ): NewsPostInfo {
if($categoryInfo instanceof NewsCategoryInfo) if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId(); $categoryInfo = $categoryInfo->getId();
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($schedule instanceof DateTime) if($schedule instanceof DateTime)
$schedule = $schedule->getUnixTimeSeconds(); $schedule = $schedule->getUnixTimeSeconds();
@ -424,15 +424,15 @@ class News {
?string $body = null, ?string $body = null,
?bool $featured = null, ?bool $featured = null,
bool $updateUserInfo = false, bool $updateUserInfo = false,
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
DateTime|int|null $schedule = null DateTime|int|null $schedule = null
): void { ): void {
if($postInfo instanceof NewsPostInfo) if($postInfo instanceof NewsPostInfo)
$postInfo = $postInfo->getId(); $postInfo = $postInfo->getId();
if($categoryInfo instanceof NewsCategoryInfo) if($categoryInfo instanceof NewsCategoryInfo)
$categoryInfo = $categoryInfo->getId(); $categoryInfo = $categoryInfo->getId();
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($schedule instanceof DateTime) if($schedule instanceof DateTime)
$schedule = $schedule->getUnixTimeSeconds(); $schedule = $schedule->getUnixTimeSeconds();

View file

@ -7,7 +7,7 @@ use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Data\IDbResult; use Index\Data\IDbResult;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
class ProfileFields { class ProfileFields {
private DbStatementCache $cache; private DbStatementCache $cache;
@ -141,8 +141,8 @@ class ProfileFields {
return new ProfileFieldFormatInfo($result); return new ProfileFieldFormatInfo($result);
} }
public function getFieldValues(User|string $userInfo): array { public function getFieldValues(UserInfo|string $userInfo): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
// i don't really want to bother with the join for the ordering so i'll just do that somewhere in PHP for now // i don't really want to bother with the join for the ordering so i'll just do that somewhere in PHP for now
@ -162,11 +162,11 @@ class ProfileFields {
public function getFieldValue( public function getFieldValue(
ProfileFieldInfo|string $fieldInfo, ProfileFieldInfo|string $fieldInfo,
User|string $userInfo UserInfo|string $userInfo
): ProfileFieldValueInfo { ): ProfileFieldValueInfo {
if($fieldInfo instanceof ProfileFieldInfo) if($fieldInfo instanceof ProfileFieldInfo)
$fieldInfo = $fieldInfo->getId(); $fieldInfo = $fieldInfo->getId();
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
$stmt = $this->cache->get('SELECT field_id, user_id, format_id, field_value FROM msz_profile_fields_values WHERE field_id = ? AND user_id = ?'); $stmt = $this->cache->get('SELECT field_id, user_id, format_id, field_value FROM msz_profile_fields_values WHERE field_id = ? AND user_id = ?');
@ -183,7 +183,7 @@ class ProfileFields {
} }
public function setFieldValues( public function setFieldValues(
User|string $userInfo, UserInfo|string $userInfo,
ProfileFieldInfo|string|array $fieldInfos, ProfileFieldInfo|string|array $fieldInfos,
string|array $values string|array $values
): void { ): void {
@ -202,7 +202,7 @@ class ProfileFields {
if($fieldsCount !== count($values)) if($fieldsCount !== count($values))
throw new InvalidArgumentException('$fieldsInfos and $values have the same amount of values and be in the same order.'); throw new InvalidArgumentException('$fieldsInfos and $values have the same amount of values and be in the same order.');
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
$rows = []; $rows = [];
@ -241,12 +241,12 @@ class ProfileFields {
} }
public function removeFieldValues( public function removeFieldValues(
User|string $userInfo, UserInfo|string $userInfo,
ProfileFieldInfo|string|array $fieldInfos ProfileFieldInfo|string|array $fieldInfos
): void { ): void {
if(empty($fieldInfos)) if(empty($fieldInfos))
return; return;
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->getId();
if(!is_array($fieldInfos)) if(!is_array($fieldInfos))

View file

@ -6,22 +6,27 @@ use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Http\HttpFx; use Index\Http\HttpFx;
use Index\Routing\IRouter; use Index\Routing\IRouter;
use Misuzu\Pagination;
use Misuzu\Config\IConfig; use Misuzu\Config\IConfig;
use Misuzu\Profile\ProfileFields; use Misuzu\Profile\ProfileFields;
use Misuzu\Users\Users;
final class SatoriRoutes { final class SatoriRoutes {
private IDbConnection $dbConn; private IDbConnection $dbConn;
private IConfig $config; private IConfig $config;
private Users $users;
private ProfileFields $profileFields; private ProfileFields $profileFields;
public function __construct( public function __construct(
IDbConnection $dbConn, IDbConnection $dbConn,
IConfig $config, IConfig $config,
IRouter $router, IRouter $router,
Users $users,
ProfileFields $profileFields ProfileFields $profileFields
) { ) {
$this->dbConn = $dbConn; $this->dbConn = $dbConn;
$this->config = $config; $this->config = $config;
$this->users = $users;
$this->profileFields = $profileFields; $this->profileFields = $profileFields;
// Simplify default error pages // Simplify default error pages
@ -138,23 +143,20 @@ final class SatoriRoutes {
$backlogDays = $this->config->getInteger('users.backlog', 7); $backlogDays = $this->config->getInteger('users.backlog', 7);
$startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); $startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT);
$stmt = $this->dbConn->prepare( $userInfos = $this->users->getUsers(
'SELECT user_id, username FROM msz_users' after: $startId,
. ' WHERE user_id > ? AND user_created >= NOW() - INTERVAL ? DAY' newerThanDays: $backlogDays,
. ' ORDER BY user_id LIMIT ?' orderBy: 'id',
pagination: new Pagination($batchSize),
deleted: false
); );
$stmt->addParameter(1, $startId);
$stmt->addParameter(2, $backlogDays);
$stmt->addParameter(3, $batchSize);
$stmt->execute();
$users = []; $users = [];
$result = $stmt->getResult();
while($result->next()) foreach($userInfos as $userInfo)
$users[] = [ $users[] = [
'user_id' => $result->getInteger(0), 'user_id' => (int)$userInfo->getId(),
'username' => $result->getString(1), 'username' => $userInfo->getName(),
]; ];
return $users; return $users;

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Misuzu\SharpChat; namespace Misuzu\SharpChat;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
final class SharpChatPerms { final class SharpChatPerms {
private const P_KICK_USER = 0x00000001; private const P_KICK_USER = 0x00000001;
@ -30,22 +30,23 @@ final class SharpChatPerms {
private const PERMS_MANAGE_FORUM = self::P_CREATE_CHANNEL | self::P_SET_CHAN_PERMA | self::P_SET_CHAN_PASS private const PERMS_MANAGE_FORUM = self::P_CREATE_CHANNEL | self::P_SET_CHAN_PERMA | self::P_SET_CHAN_PASS
| self::P_SET_CHAN_HIER | self::P_DELETE_CHANNEL | self::P_JOIN_ANY_CHAN; | self::P_SET_CHAN_HIER | self::P_DELETE_CHANNEL | self::P_JOIN_ANY_CHAN;
public static function convert(User $userInfo): int { public static function convert(UserInfo $userInfo): int {
$userInfo = (int)$userInfo->getId();
$perms = self::PERMS_DEFAULT; $perms = self::PERMS_DEFAULT;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_USERS)) if(perms_check_user(MSZ_PERMS_USER, $userInfo, MSZ_PERM_USER_MANAGE_USERS))
$perms |= self::PERMS_MANAGE_USERS; $perms |= self::PERMS_MANAGE_USERS;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) if(perms_check_user(MSZ_PERMS_USER, $userInfo, MSZ_PERM_USER_MANAGE_WARNINGS))
$perms |= self::P_KICK_USER; $perms |= self::P_KICK_USER;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_MANAGE_BANS)) if(perms_check_user(MSZ_PERMS_USER, $userInfo, MSZ_PERM_USER_MANAGE_BANS))
$perms |= self::P_BAN_USER; $perms |= self::P_BAN_USER;
if(perms_check_user(MSZ_PERMS_USER, $userInfo->getId(), MSZ_PERM_USER_CHANGE_BACKGROUND)) if(perms_check_user(MSZ_PERMS_USER, $userInfo, MSZ_PERM_USER_CHANGE_BACKGROUND))
$perms |= self::PERMS_CHANGE_BACKG; $perms |= self::PERMS_CHANGE_BACKG;
if(perms_check_user(MSZ_PERMS_FORUM, $userInfo->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) if(perms_check_user(MSZ_PERMS_FORUM, $userInfo, MSZ_PERM_FORUM_MANAGE_FORUMS))
$perms |= self::PERMS_MANAGE_FORUM; $perms |= self::PERMS_MANAGE_FORUM;
return $perms; return $perms;

View file

@ -6,31 +6,36 @@ use Index\Colour\Colour;
use Index\Routing\IRouter; use Index\Routing\IRouter;
use Index\Http\HttpFx; use Index\Http\HttpFx;
use Misuzu\AuthToken; use Misuzu\AuthToken;
use Misuzu\MisuzuContext;
use Misuzu\Auth\Sessions; use Misuzu\Auth\Sessions;
use Misuzu\Config\IConfig; use Misuzu\Config\IConfig;
use Misuzu\Emoticons\Emotes; use Misuzu\Emoticons\Emotes;
use Misuzu\Users\Bans; use Misuzu\Users\Bans;
use Misuzu\Users\Users;
// Replace
use Misuzu\Users\User;
final class SharpChatRoutes { final class SharpChatRoutes {
private IConfig $config; private IConfig $config;
private MisuzuContext $context;
private Bans $bans; private Bans $bans;
private Emotes $emotes; private Emotes $emotes;
private Users $users;
private Sessions $sessions; private Sessions $sessions;
private string $hashKey; private string $hashKey;
public function __construct( public function __construct(
IRouter $router, IRouter $router,
IConfig $config, IConfig $config,
MisuzuContext $context,
Bans $bans, Bans $bans,
Emotes $emotes, Emotes $emotes,
Users $users,
Sessions $sessions Sessions $sessions
) { ) {
$this->config = $config; $this->config = $config;
$this->context = $context;
$this->bans = $bans; $this->bans = $bans;
$this->emotes = $emotes; $this->emotes = $emotes;
$this->users = $users;
$this->sessions = $sessions; $this->sessions = $sessions;
$this->hashKey = $this->config->getString('hashKey', 'woomy'); $this->hashKey = $this->config->getString('hashKey', 'woomy');
@ -93,15 +98,15 @@ final class SharpChatRoutes {
} }
public function getLogin($response, $request): void { public function getLogin($response, $request): void {
$currentUser = User::getCurrent(); if(!$this->context->isLoggedIn()) {
$configKey = $request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal'; $response->redirect(url('auth-login'));
$chatPath = $this->config->getString($configKey, '/'); return;
}
$response->redirect( $response->redirect($this->config->getString(
$currentUser === null ($request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal'),
? url('auth-login') '/'
: $chatPath ));
);
} }
public function getToken($response, $request) { public function getToken($response, $request) {
@ -127,13 +132,13 @@ final class SharpChatRoutes {
if($request->getMethod() === 'OPTIONS') if($request->getMethod() === 'OPTIONS')
return 204; return 204;
if(!AuthToken::hasCurrent()) if(!$this->context->hasAuthToken())
return ['ok' => false, 'err' => 'token']; return ['ok' => false, 'err' => 'token'];
$token = AuthToken::getCurrent(); $tokenInfo = $this->context->getAuthToken();
try { try {
$sessionInfo = $this->sessions->getSession(sessionToken: $token->getSessionToken()); $sessionInfo = $this->sessions->getSession(sessionToken: $tokenInfo->getSessionToken());
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return ['ok' => false, 'err' => 'session']; return ['ok' => false, 'err' => 'session'];
} }
@ -141,15 +146,15 @@ final class SharpChatRoutes {
if($sessionInfo->hasExpired()) if($sessionInfo->hasExpired())
return ['ok' => false, 'err' => 'expired']; return ['ok' => false, 'err' => 'expired'];
$userInfo = User::byId((int)$sessionInfo->getUserId()); $userInfo = $this->users->getUser($sessionInfo->getUserId(), 'id');
$userId = $token->hasImpersonatedUserId() && $userInfo->isSuper() $userId = $tokenInfo->hasImpersonatedUserId() && $userInfo->isSuperUser()
? $token->getImpersonatedUserId() ? $tokenInfo->getImpersonatedUserId()
: $userInfo->getId(); : $userInfo->getId();
return [ return [
'ok' => true, 'ok' => true,
'usr' => (int)$userId, 'usr' => (int)$userId,
'tkn' => $token->pack(), 'tkn' => $tokenInfo->pack(),
]; ];
} }
@ -179,7 +184,7 @@ final class SharpChatRoutes {
return 403; return 403;
foreach($bumpList as $userId => $ipAddr) foreach($bumpList as $userId => $ipAddr)
User::byId($userId)->bumpActivity($ipAddr); $this->users->recordUserActivity($userId, remoteAddr: $ipAddr);
} }
public function postVerify($response, $request) { public function postVerify($response, $request) {
@ -222,14 +227,14 @@ final class SharpChatRoutes {
return ['success' => false, 'reason' => 'expired']; return ['success' => false, 'reason' => 'expired'];
} }
$this->sessions->updateSession(sessionInfo: $sessionInfo, remoteAddr: $ipAddress); $this->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $ipAddress);
$userInfo = User::byId((int)$sessionInfo->getUserId()); $userInfo = $this->users->getUser($sessionInfo->getUserId(), 'id');
if($authTokenInfo->hasImpersonatedUserId() && $userInfo->isSuper()) { if($authTokenInfo->hasImpersonatedUserId() && $userInfo->isSuperUser()) {
$userInfoReal = $userInfo; $userInfoReal = $userInfo;
try { try {
$userInfo = User::byId($authTokenInfo->getImpersonatedUserId()); $userInfo = $this->users->getUser($authTokenInfo->getImpersonatedUserId(), 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userInfo = $userInfoReal; $userInfo = $userInfoReal;
} }
@ -241,15 +246,17 @@ final class SharpChatRoutes {
if(empty($userInfo)) if(empty($userInfo))
return ['success' => false, 'reason' => 'user']; return ['success' => false, 'reason' => 'user'];
$userInfo->bumpActivity($ipAddress); $this->users->recordUserActivity($userInfo, remoteAddr: $ipAddress);
$userColour = $this->users->getUserColour($userInfo);
$userRank = $this->users->getUserRank($userInfo);
return [ return [
'success' => true, 'success' => true,
'user_id' => $userInfo->getId(), 'user_id' => (int)$userInfo->getId(),
'username' => $userInfo->getUsername(), 'username' => $userInfo->getName(),
'colour_raw' => Colour::toMisuzu($userInfo->getColour()), 'colour_raw' => Colour::toMisuzu($userColour),
'rank' => $rank = $userInfo->getRank(), 'rank' => $userRank,
'hierarchy' => $rank, 'hierarchy' => $userRank,
'perms' => SharpChatPerms::convert($userInfo), 'perms' => SharpChatPerms::convert($userInfo),
]; ];
} }
@ -274,14 +281,15 @@ final class SharpChatRoutes {
if(array_key_exists($userId, $userInfos)) if(array_key_exists($userId, $userInfos))
$userInfo = $userInfos[$userId]; $userInfo = $userInfos[$userId];
else else
$userInfos[$userId] = $userInfo = User::byId((int)$userId); $userInfos[$userId] = $userInfo = $this->users->getUser($userId, 'id');
$userColour = $this->users->getUserColour($userInfo);
$isPerma = $banInfo->isPermanent(); $isPerma = $banInfo->isPermanent();
$list[] = [ $list[] = [
'is_ban' => true, 'is_ban' => true,
'user_id' => $userId, 'user_id' => $userId,
'user_name' => $userInfo->getUsername(), 'user_name' => $userInfo->getName(),
'user_colour' => Colour::toMisuzu($userInfo->getColour()), 'user_colour' => Colour::toMisuzu($userColour),
'ip_addr' => '::', 'ip_addr' => '::',
'is_perma' => $isPerma, 'is_perma' => $isPerma,
'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime()) 'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime())
@ -307,7 +315,7 @@ final class SharpChatRoutes {
if($userIdIsName) if($userIdIsName)
try { try {
$userInfo = User::byUsername($userId); $userInfo = $this->users->getUser($userId, 'name');
$userId = (string)$userInfo->getId(); $userId = (string)$userInfo->getId();
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$userId = ''; $userId = '';
@ -355,6 +363,7 @@ final class SharpChatRoutes {
if(empty($reason)) if(empty($reason))
$reason = 'Banned through chat.'; $reason = 'Banned through chat.';
// maybe also adds the last couple lines of the chat log to the private reason
$comment = sprintf('User IP address: %s, Moderator IP address: %s', $userAddr, $modAddr); $comment = sprintf('User IP address: %s, Moderator IP address: %s', $userAddr, $modAddr);
if($isPermanent) if($isPermanent)
@ -374,13 +383,13 @@ final class SharpChatRoutes {
$modId = 69; $modId = 69;
try { try {
$modInfo = User::byId((int)$modId); $modInfo = $this->users->getUser($modId, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return 404; return 404;
} }
try { try {
$userInfo = User::byId((int)$userId); $userInfo = $this->users->getUser($userId, 'id');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return 404; return 404;
} }

View file

@ -5,7 +5,6 @@ use Index\ByteFormat;
use Index\DateTime; use Index\DateTime;
use Index\Environment; use Index\Environment;
use Misuzu\MisuzuContext; use Misuzu\MisuzuContext;
use Misuzu\Comments\CommentsParser;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
@ -24,7 +23,6 @@ final class TwigMisuzu extends AbstractExtension {
new TwigFilter('html_colour', 'html_colour'), new TwigFilter('html_colour', 'html_colour'),
new TwigFilter('country_name', 'get_country_name'), new TwigFilter('country_name', 'get_country_name'),
new TwigFilter('parse_text', fn(string $text, int $parser): string => Parser::instance($parser)->parseText($text)), new TwigFilter('parse_text', fn(string $text, int $parser): string => Parser::instance($parser)->parseText($text)),
new TwigFilter('parse_comment', fn(string $text): string => CommentsParser::parseForDisplay($text)),
new TwigFilter('perms_check', 'perms_check'), new TwigFilter('perms_check', 'perms_check'),
new TwigFilter('time_format', [$this, 'timeFormat']), new TwigFilter('time_format', [$this, 'timeFormat']),
]; ];

View file

@ -2,7 +2,6 @@
namespace Misuzu\Users\Assets; namespace Misuzu\Users\Assets;
use Misuzu\Imaging\Image; use Misuzu\Imaging\Image;
use Misuzu\Users\User;
class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterface { class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterface {
private const FORMAT = 'avatars/%s/%d.msz'; private const FORMAT = 'avatars/%s/%d.msz';
@ -30,10 +29,10 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
} }
public function getUrl(): string { public function getUrl(): string {
return url('user-avatar', ['user' => $this->getUser()->getId()]); return url('user-avatar', ['user' => $this->getUserId()]);
} }
public function getScaledUrl(int $dims): string { public function getScaledUrl(int $dims): string {
return url('user-avatar', ['user' => $this->getUser()->getId(), 'res' => $dims]); return url('user-avatar', ['user' => $this->getUserId(), 'res' => $dims]);
} }
public static function clampDimensions(int $dimensions): int { public static function clampDimensions(int $dimensions): int {
@ -45,10 +44,10 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
} }
public function getFileName(): string { public function getFileName(): string {
return sprintf('avatar-%1$d.%2$s', $this->getUser()->getId(), $this->getFileExtension()); return sprintf('avatar-%1$d.%2$s', $this->getUserId(), $this->getFileExtension());
} }
public function getScaledFileName(int $dims): string { public function getScaledFileName(int $dims): string {
return sprintf('avatar-%1$d-%3$dx%3$d.%2$s', $this->getUser()->getId(), $this->getScaledFileExtension($dims), self::clampDimensions($dims)); return sprintf('avatar-%1$d-%3$dx%3$d.%2$s', $this->getUserId(), $this->getScaledFileExtension($dims), self::clampDimensions($dims));
} }
public function getScaledMimeType(int $dims): string { public function getScaledMimeType(int $dims): string {
@ -64,11 +63,11 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
} }
public function getRelativePath(): string { public function getRelativePath(): string {
return sprintf(self::FORMAT, self::DIR_ORIG, $this->getUser()->getId()); return sprintf(self::FORMAT, self::DIR_ORIG, $this->getUserId());
} }
public function getScaledRelativePath(int $dims): string { public function getScaledRelativePath(int $dims): string {
$dims = self::clampDimensions($dims); $dims = self::clampDimensions($dims);
return sprintf(self::FORMAT, sprintf(self::DIR_SIZE, $dims), $this->getUser()->getId()); return sprintf(self::FORMAT, sprintf(self::DIR_SIZE, $dims), $this->getUserId());
} }
public function getScaledPath(int $dims): string { public function getScaledPath(int $dims): string {

View file

@ -2,7 +2,7 @@
namespace Misuzu\Users\Assets; namespace Misuzu\Users\Assets;
use InvalidArgumentException; use InvalidArgumentException;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
// attachment and attributes are to be stored in the same byte // attachment and attributes are to be stored in the same byte
// left half is for attributes, right half is for attachments // left half is for attributes, right half is for attachments
@ -46,6 +46,17 @@ class UserBackgroundAsset extends UserImageAsset {
]; ];
} }
private int $settings;
public function __construct(UserInfo $userInfo) {
parent::__construct($userInfo);
$this->settings = (int)$userInfo->getBackgroundSettings();
}
public function getSettings(): int {
return $this->settings;
}
public function getMaxWidth(): int { public function getMaxWidth(): int {
global $cfg; global $cfg;
return $cfg->getInteger('background.max_width', self::MAX_WIDTH); return $cfg->getInteger('background.max_width', self::MAX_WIDTH);
@ -60,25 +71,25 @@ class UserBackgroundAsset extends UserImageAsset {
} }
public function getUrl(): string { public function getUrl(): string {
return url('user-background', ['user' => $this->getUser()->getId()]); return url('user-background', ['user' => $this->getUserId()]);
} }
public function getFileName(): string { public function getFileName(): string {
return sprintf('background-%1$d.%2$s', $this->getUser()->getId(), $this->getFileExtension()); return sprintf('background-%1$d.%2$s', $this->getUserId(), $this->getFileExtension());
} }
public function getRelativePath(): string { public function getRelativePath(): string {
return sprintf(self::FORMAT, $this->getUser()->getId()); return sprintf(self::FORMAT, $this->getUserId());
} }
public function getAttachment(): int { public function getAttachment(): int {
return $this->getUser()->getBackgroundSettings() & 0x0F; return $this->settings & 0x0F;
} }
public function getAttachmentString(): string { public function getAttachmentString(): string {
return self::ATTACHMENT_STRINGS[$this->getAttachment()] ?? ''; return self::ATTACHMENT_STRINGS[$this->getAttachment()] ?? '';
} }
public function setAttachment(int $attach): self { public function setAttachment(int $attach): self {
$this->getUser()->setBackgroundSettings($this->getAttributes() | ($attach & 0x0F)); $this->settings = $this->getAttributes() | ($attach & 0x0F);
return $this; return $this;
} }
public function setAttachmentString(string $attach): self { public function setAttachmentString(string $attach): self {
@ -89,32 +100,28 @@ class UserBackgroundAsset extends UserImageAsset {
} }
public function getAttributes(): int { public function getAttributes(): int {
return $this->getUser()->getBackgroundSettings() & 0xF0; return $this->settings & 0xF0;
} }
public function setAttributes(int $attrib): self { public function setAttributes(int $attrib): self {
$this->getUser()->setBackgroundSettings($this->getAttachment() | ($attrib & 0xF0)); $this->settings = $this->getAttachment() | ($attrib & 0xF0);
return $this; return $this;
} }
public function isBlend(): bool { public function isBlend(): bool {
return ($this->getAttributes() & self::ATTRIB_BLEND) > 0; return ($this->getAttributes() & self::ATTRIB_BLEND) > 0;
} }
public function setBlend(bool $blend): self { public function setBlend(bool $blend): self {
$this->getUser()->setBackgroundSettings( $this->settings = $blend
$blend ? ($this->settings | self::ATTRIB_BLEND)
? ($this->getUser()->getBackgroundSettings() | self::ATTRIB_BLEND) : ($this->settings & ~self::ATTRIB_BLEND);
: ($this->getUser()->getBackgroundSettings() & ~self::ATTRIB_BLEND)
);
return $this; return $this;
} }
public function isSlide(): bool { public function isSlide(): bool {
return ($this->getAttributes() & self::ATTRIB_SLIDE) > 0; return ($this->getAttributes() & self::ATTRIB_SLIDE) > 0;
} }
public function setSlide(bool $slide): self { public function setSlide(bool $slide): self {
$this->getUser()->setBackgroundSettings( $this->settings = $slide
$slide ? ($this->settings | self::ATTRIB_SLIDE)
? ($this->getUser()->getBackgroundSettings() | self::ATTRIB_SLIDE) : ($this->settings & ~self::ATTRIB_SLIDE);
: ($this->getUser()->getBackgroundSettings() & ~self::ATTRIB_SLIDE)
);
return $this; return $this;
} }
@ -135,6 +142,6 @@ class UserBackgroundAsset extends UserImageAsset {
public function delete(): void { public function delete(): void {
parent::delete(); parent::delete();
$this->getUser()->setBackgroundSettings(0); $this->settings = 0;
} }
} }

View file

@ -3,7 +3,7 @@ namespace Misuzu\Users\Assets;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\UserInfo;
abstract class UserImageAsset implements UserImageAssetInterface { abstract class UserImageAsset implements UserImageAssetInterface {
public const PUBLIC_STORAGE = '/msz-storage'; public const PUBLIC_STORAGE = '/msz-storage';
@ -18,14 +18,14 @@ abstract class UserImageAsset implements UserImageAssetInterface {
self::TYPE_GIF => 'gif', self::TYPE_GIF => 'gif',
]; ];
private $user; protected string $userId;
public function __construct(User $user) { public function __construct(UserInfo $userInfo) {
$this->user = $user; $this->userId = (string)$userInfo->getId();
} }
public function getUser(): User { public function getUserId(): string {
return $this->user; return $this->userId;
} }
public abstract function getMaxWidth(): int; public abstract function getMaxWidth(): int;

View file

@ -8,7 +8,6 @@ use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User;
class Bans { class Bans {
public const SEVERITY_MAX = 10; public const SEVERITY_MAX = 10;
@ -24,11 +23,11 @@ class Bans {
} }
public function countBans( public function countBans(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?bool $activeOnly = null ?bool $activeOnly = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasActiveOnly = $activeOnly !== null; $hasActiveOnly = $activeOnly !== null;
@ -61,13 +60,13 @@ class Bans {
} }
public function getBans( public function getBans(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?bool $activeOnly = null, ?bool $activeOnly = null,
?bool $activeFirst = null, ?bool $activeFirst = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasActiveOnly = $activeOnly !== null; $hasActiveOnly = $activeOnly !== null;
@ -124,11 +123,11 @@ class Bans {
} }
public function tryGetActiveBan( public function tryGetActiveBan(
User|string $userInfo, UserInfo|string $userInfo,
int $minimumSeverity = self::SEVERITY_MIN int $minimumSeverity = self::SEVERITY_MIN
): ?BanInfo { ): ?BanInfo {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
// orders by ban_expires descending with NULLs (permanent) first // orders by ban_expires descending with NULLs (permanent) first
$stmt = $this->cache->get('SELECT ban_id, user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, UNIX_TIMESTAMP(ban_created), UNIX_TIMESTAMP(ban_expires) FROM msz_users_bans WHERE user_id = ? AND ban_severity >= ? AND (ban_expires IS NULL OR ban_expires > NOW()) ORDER BY ban_expires IS NULL DESC, ban_expires DESC'); $stmt = $this->cache->get('SELECT ban_id, user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, UNIX_TIMESTAMP(ban_created), UNIX_TIMESTAMP(ban_expires) FROM msz_users_bans WHERE user_id = ? AND ban_severity >= ? AND (ban_expires IS NULL OR ban_expires > NOW()) ORDER BY ban_expires IS NULL DESC, ban_expires DESC');
@ -141,19 +140,19 @@ class Bans {
} }
public function createBan( public function createBan(
User|string $userInfo, UserInfo|string $userInfo,
DateTime|int|null $expires, DateTime|int|null $expires,
string $publicReason, string $publicReason,
string $privateReason, string $privateReason,
int $severity = self::SEVERITY_DEFAULT, int $severity = self::SEVERITY_DEFAULT,
User|string|null $modInfo = null UserInfo|string|null $modInfo = null
): BanInfo { ): BanInfo {
if($severity < self::SEVERITY_MIN || $severity > self::SEVERITY_MAX) if($severity < self::SEVERITY_MIN || $severity > self::SEVERITY_MAX)
throw new InvalidArgumentException('$severity may not be less than -10 or more than 10.'); throw new InvalidArgumentException('$severity may not be less than -10 or more than 10.');
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($modInfo instanceof User) if($modInfo instanceof UserInfo)
$modInfo = (string)$modInfo->getId(); $modInfo = $modInfo->getId();
if($expires instanceof DateTime) if($expires instanceof DateTime)
$expires = $expires->getUnixTimeSeconds(); $expires = $expires->getUnixTimeSeconds();

View file

@ -7,7 +7,6 @@ use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User;
class ModNotes { class ModNotes {
private IDbConnection $dbConn; private IDbConnection $dbConn;
@ -19,13 +18,13 @@ class ModNotes {
} }
public function countNotes( public function countNotes(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
User|string|null $authorInfo = null UserInfo|string|null $authorInfo = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($authorInfo instanceof User) if($authorInfo instanceof UserInfo)
$authorInfo = (string)$authorInfo->getId(); $authorInfo = $authorInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasAuthorInfo = $authorInfo !== null; $hasAuthorInfo = $authorInfo !== null;
@ -57,14 +56,14 @@ class ModNotes {
} }
public function getNotes( public function getNotes(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
User|string|null $authorInfo = null, UserInfo|string|null $authorInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($authorInfo instanceof User) if($authorInfo instanceof UserInfo)
$authorInfo = (string)$authorInfo->getId(); $authorInfo = $authorInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasAuthorInfo = $authorInfo !== null; $hasAuthorInfo = $authorInfo !== null;
@ -116,15 +115,15 @@ class ModNotes {
} }
public function createNote( public function createNote(
User|string $userInfo, UserInfo|string $userInfo,
string $title, string $title,
string $body, string $body,
User|string|null $authorInfo = null UserInfo|string|null $authorInfo = null
): ModNoteInfo { ): ModNoteInfo {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($authorInfo instanceof User) if($authorInfo instanceof UserInfo)
$authorInfo = (string)$authorInfo->getId(); $authorInfo = $authorInfo->getId();
$stmt = $this->cache->get('INSERT INTO msz_users_modnotes (user_id, author_id, note_title, note_body) VALUES (?, ?, ?, ?)'); $stmt = $this->cache->get('INSERT INTO msz_users_modnotes (user_id, author_id, note_title, note_body) VALUES (?, ?, ?, ?)');
$stmt->addParameter(1, $userInfo); $stmt->addParameter(1, $userInfo);

View file

@ -73,6 +73,10 @@ class RoleInfo implements Stringable {
return $this->colour !== null && ($this->colour & 0x40000000) === 0; return $this->colour !== null && ($this->colour & 0x40000000) === 0;
} }
public function getColourRaw(): ?int {
return $this->colour;
}
public function getColour(): Colour { public function getColour(): Colour {
return $this->colour === null ? Colour::none() : Colour::fromMisuzu($this->colour); return $this->colour === null ? Colour::none() : Colour::fromMisuzu($this->colour);
} }

View file

@ -21,11 +21,11 @@ class Roles {
} }
public function countRoles( public function countRoles(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?bool $hidden = null ?bool $hidden = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasHidden = $hidden !== null; $hasHidden = $hidden !== null;
@ -55,12 +55,12 @@ class Roles {
} }
public function getRoles( public function getRoles(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?bool $hidden = null, ?bool $hidden = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasHidden = $hidden !== null; $hasHidden = $hidden !== null;

View file

@ -1,29 +1,10 @@
<?php <?php
namespace Misuzu\Users; namespace Misuzu\Users;
use DateTime;
use DateTimeZone;
use InvalidArgumentException;
use RuntimeException;
use Index\XString; use Index\XString;
use Index\Colour\Colour;
use Misuzu\DateCheck; use Misuzu\DateCheck;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\TOTPGenerator;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\Assets\UserAvatarAsset;
use Misuzu\Users\Assets\UserBackgroundAsset;
// Quick note to myself and others about the `display_role` column in the users database and its corresponding methods in this class.
// Never ever EVER use it for ANYTHING other than determining display colours, there's a small chance that it might not be accurate.
// And even if it were, roles properties are aggregated and thus must all be accounted for.
// TODO
// - Search for comments starting with TODO
// - Move background settings and about shit to a separate users_profiles table (should birthdate be profile specific?)
// - Create a users_stats table containing static counts for things like followers, followings, topics, posts, etc.
class User { class User {
public const NAME_MIN_LENGTH = 3; // Minimum username length public const NAME_MIN_LENGTH = 3; // Minimum username length
@ -33,374 +14,12 @@ class User {
// Minimum amount of unique characters for passwords // Minimum amount of unique characters for passwords
public const PASSWORD_UNIQUE = 6; public const PASSWORD_UNIQUE = 6;
// Password hashing algorithm
public const PASSWORD_ALGO = PASSWORD_ARGON2ID;
// Maximum length of profile about section // Maximum length of profile about section
public const PROFILE_ABOUT_MAX_LENGTH = 50000; public const PROFILE_ABOUT_MAX_LENGTH = 50000;
// Maximum length of forum signature // Maximum length of forum signature
public const FORUM_SIGNATURE_MAX_LENGTH = 2000; public const FORUM_SIGNATURE_MAX_LENGTH = 2000;
// Database fields
private $user_id = -1;
private $username = '';
private $password = '';
private $email = '';
private $register_ip = '::1';
private $last_ip = '::1';
private $user_super = 0;
private $user_country = 'XX';
private $user_colour = null;
private $user_created = null;
private $user_active = null;
private $user_deleted = null;
private $display_role = 1;
private $user_totp_key = null;
private $user_about_content = null;
private $user_about_parser = 0;
private $user_signature_content = null;
private $user_signature_parser = 0;
private $user_birthdate = null;
private $user_background_settings = 0;
private $user_title = null;
private static $localUser = null;
private const QUERY_SELECT = 'SELECT %1$s FROM `msz_users`';
private const SELECT = '`user_id`, `username`, `password`, `email`, `user_super`, `user_title`'
. ', `user_country`, `user_colour`, `display_role`, `user_totp_key`'
. ', `user_about_content`, `user_about_parser`'
. ', `user_signature_content`, `user_signature_parser`'
. ', `user_birthdate`, `user_background_settings`'
. ', INET6_NTOA(`register_ip`) AS `register_ip`'
. ', INET6_NTOA(`last_ip`) AS `last_ip`'
. ', UNIX_TIMESTAMP(`user_created`) AS `user_created`'
. ', UNIX_TIMESTAMP(`user_active`) AS `user_active`'
. ', UNIX_TIMESTAMP(`user_deleted`) AS `user_deleted`';
public function getId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function getUsername(): string {
return $this->username;
}
public function getEmailAddress(): string {
return $this->email;
}
public function getRegisterRemoteAddress(): string {
return $this->register_ip ?? '::1';
}
public function getLastRemoteAddress(): string {
return $this->last_ip ?? '::1';
}
public function isSuper(): bool {
return boolval($this->user_super);
}
public function hasCountry(): bool {
return $this->user_country !== 'XX';
}
public function getCountry(): string {
return $this->user_country ?? 'XX';
}
public function setCountry(string $country): self {
$this->user_country = strtoupper(substr($country, 0, 2));
return $this;
}
public function getCountryName(): string {
return get_country_name($this->getCountry());
}
private $userColour = null;
private $realColour = null;
public function getColour(): Colour { // Swaps role colour in if user has no personal colour
if($this->realColour === null) {
$this->realColour = $this->getUserColour();
if($this->realColour->shouldInherit()) {
$stmt = DB::prepare('SELECT role_colour FROM msz_roles WHERE role_id = (SELECT display_role FROM msz_users WHERE user_id = :user)');
$stmt->bind('user', $this->user_id);
$rawColour = $stmt->fetchColumn();
$this->realColour = $rawColour === null ? Colour::none() : Colour::fromMisuzu($rawColour);
}
}
return $this->realColour;
}
public function setColour(?Colour $colour): self {
return $this->setColourRaw($colour === null ? null : Colour::toMisuzu($colour));
}
public function getUserColour(): Colour { // Only ever gets the user's actual colour
if($this->userColour === null)
$this->userColour = Colour::fromMisuzu($this->getColourRaw());
return $this->userColour;
}
public function getColourRaw(): int {
return $this->user_colour ?? 0x40000000;
}
public function setColourRaw(?int $colour): self {
$this->user_colour = $colour;
$this->userColour = null;
$this->realColour = null;
return $this;
}
public function getCreatedTime(): int {
return $this->user_created === null ? -1 : $this->user_created;
}
public function hasBeenActive(): bool {
return $this->user_active !== null;
}
public function getActiveTime(): int {
return $this->user_active === null ? -1 : $this->user_active;
}
private $userRank = null;
public function getRank(): int {
if($this->userRank === null)
$this->userRank = (int)DB::prepare(
'SELECT MAX(`role_hierarchy`)'
. ' FROM `msz_roles`'
. ' WHERE `role_id` IN (SELECT `role_id` FROM `msz_users_roles` WHERE `user_id` = :user)'
)->bind('user', $this->getId())->fetchColumn();
return $this->userRank;
}
public function getDisplayRoleId(): int {
return $this->display_role < 1 ? -1 : $this->display_role;
}
public function createTOTPGenerator(): TOTPGenerator {
return new TOTPGenerator($this->user_totp_key);
}
public function hasTOTPKey(): bool {
return !empty($this->user_totp_key);
}
public function getTOTPKey(): string {
return $this->user_totp_key ?? '';
}
public function setTOTPKey(string $key): self {
$this->user_totp_key = $key;
return $this;
}
public function removeTOTPKey(): self {
$this->user_totp_key = null;
return $this;
}
public function hasProfileAbout(): bool {
return !empty($this->user_about_content);
}
public function getProfileAboutText(): string {
return $this->user_about_content ?? '';
}
public function setProfileAboutText(string $text): self {
$this->user_about_content = empty($text) ? null : $text;
return $this;
}
public function getProfileAboutParser(): int {
return $this->hasProfileAbout() ? $this->user_about_parser : Parser::BBCODE;
}
public function setProfileAboutParser(int $parser): self {
$this->user_about_parser = $parser;
return $this;
}
public function getProfileAboutParsed(): string {
if(!$this->hasProfileAbout())
return '';
return Parser::instance($this->getProfileAboutParser())
->parseText(htmlspecialchars($this->getProfileAboutText()));
}
public function hasForumSignature(): bool {
return !empty($this->user_signature_content);
}
public function getForumSignatureText(): string {
return $this->user_signature_content ?? '';
}
public function setForumSignatureText(string $text): self {
$this->user_signature_content = empty($text) ? null : $text;
return $this;
}
public function getForumSignatureParser(): int {
return $this->hasForumSignature() ? $this->user_signature_parser : Parser::BBCODE;
}
public function setForumSignatureParser(int $parser): self {
$this->user_signature_parser = $parser;
return $this;
}
public function getForumSignatureParsed(): string {
if(!$this->hasForumSignature())
return '';
return Parser::instance($this->getForumSignatureParser())
->parseText(htmlspecialchars($this->getForumSignatureText()));
}
// Address these through getBackgroundInfo()
public function getBackgroundSettings(): int {
return $this->user_background_settings;
}
public function setBackgroundSettings(int $settings): self {
$this->user_background_settings = $settings;
return $this;
}
public function hasTitle(): bool {
return !empty($this->user_title);
}
public function getTitle(): string {
return $this->user_title ?? '';
}
public function setTitle(string $title): self {
$this->user_title = empty($title) ? null : $title;
return $this;
}
public function hasBirthdate(): bool {
return $this->user_birthdate !== null;
}
public function getBirthdate(): DateTime {
return new DateTime($this->user_birthdate ?? '0000-01-01', new DateTimeZone('UTC'));
}
public function setBirthdate(int $year, int $month, int $day): self {
// lowest leap year mariadb supports lol
// should probably split the date field and year field in the db but i'm afraid of internal dependencies rn
if($year < 1004)
$year = 1004;
$this->user_birthdate = $month < 1 || $day < 1 ? null : sprintf('%04d-%02d-%02d', $year, $month, $day);
return $this;
}
public function hasAge(): bool {
return $this->hasBirthdate() && (int)$this->getBirthdate()->format('Y') > 1900;
}
public function getAge(): int {
if(!$this->hasAge())
return -1;
return (int)$this->getBirthdate()->diff(new DateTime('now', new DateTimeZone('UTC')))->format('%y');
}
public function bumpActivity(string $lastRemoteAddress): void {
$this->user_active = time();
$this->last_ip = $lastRemoteAddress;
DB::prepare(
'UPDATE `msz_users`'
. ' SET `user_active` = FROM_UNIXTIME(:active), `last_ip` = INET6_ATON(:address)'
. ' WHERE `user_id` = :user'
) ->bind('user', $this->user_id)
->bind('active', $this->user_active)
->bind('address', $this->last_ip)
->execute();
}
/************
* PASSWORD *
************/
public static function hashPassword(string $password): string {
return password_hash($password, self::PASSWORD_ALGO);
}
public function hasPassword(): bool {
return !empty($this->password);
}
public function checkPassword(string $password): bool {
return $this->hasPassword() && password_verify($password, $this->password);
}
public function passwordNeedsRehash(): bool {
return password_needs_rehash($this->password, self::PASSWORD_ALGO);
}
public function removePassword(): self {
$this->password = null;
return $this;
}
public function setPassword(string $password): self {
$this->password = self::hashPassword($password);
return $this;
}
/************
* DELETING *
************/
public function getDeletedTime(): int {
return $this->user_deleted === null ? -1 : $this->user_deleted;
}
public function isDeleted(): bool {
return $this->getDeletedTime() >= 0;
}
/**********
* ASSETS *
**********/
private $avatarAsset = null;
public function getAvatarInfo(): UserAvatarAsset {
if($this->avatarAsset === null)
$this->avatarAsset = new UserAvatarAsset($this);
return $this->avatarAsset;
}
public function hasAvatar(): bool {
return $this->getAvatarInfo()->isPresent();
}
private $backgroundAsset = null;
public function getBackgroundInfo(): UserBackgroundAsset {
if($this->backgroundAsset === null)
$this->backgroundAsset = new UserBackgroundAsset($this);
return $this->backgroundAsset;
}
public function hasBackground(): bool {
return $this->getBackgroundInfo()->isPresent();
}
/***************
* FORUM STATS *
***************/
private $forumTopicCount = -1;
private $forumPostCount = -1;
public function getForumTopicCount(): int {
if($this->forumTopicCount < 0)
$this->forumTopicCount = (int)DB::prepare('SELECT COUNT(*) FROM `msz_forum_topics` WHERE `user_id` = :user AND `topic_deleted` IS NULL')
->bind('user', $this->getId())
->fetchColumn();
return $this->forumTopicCount;
}
public function getForumPostCount(): int {
if($this->forumPostCount < 0)
$this->forumPostCount = (int)DB::prepare('SELECT COUNT(*) FROM `msz_forum_posts` WHERE `user_id` = :user AND `post_deleted` IS NULL')
->bind('user', $this->getId())
->fetchColumn();
return $this->forumPostCount;
}
/**************
* LOCAL USER *
**************/
public function setCurrent(): void {
self::$localUser = $this;
}
public static function unsetCurrent(): void {
self::$localUser = null;
}
public static function getCurrent(): ?self {
return self::$localUser;
}
public static function hasCurrent(): bool {
return self::$localUser !== null;
}
/**************
* VALIDATION *
**************/
public static function validateUsername(string $name): string { public static function validateUsername(string $name): string {
if($name !== trim($name)) if($name !== trim($name))
return 'trim'; return 'trim';
@ -508,217 +127,4 @@ class User {
return ''; return '';
} }
/*********************
* CREATION + SAVING *
*********************/
public function save(): void {
$save = DB::prepare(
'UPDATE `msz_users`'
. ' SET `username` = :username, `email` = :email, `password` = :password'
. ', `user_super` = :is_super, `user_country` = :country, `user_colour` = :colour, `user_title` = :title'
. ', `user_totp_key` = :totp'
. ' WHERE `user_id` = :user'
) ->bind('user', $this->user_id)
->bind('username', $this->username)
->bind('email', $this->email)
->bind('password', $this->password)
->bind('is_super', $this->user_super)
->bind('country', $this->user_country)
->bind('colour', $this->user_colour)
->bind('totp', $this->user_totp_key)
->bind('title', $this->user_title)
->execute();
}
public function saveProfile(): void {
$save = DB::prepare(
'UPDATE `msz_users`'
. ' SET `user_about_content` = :about_content, `user_about_parser` = :about_parser'
. ', `user_signature_content` = :signature_content, `user_signature_parser` = :signature_parser'
. ', `user_background_settings` = :background_settings, `user_birthdate` = :birthdate'
. ' WHERE `user_id` = :user'
) ->bind('user', $this->user_id)
->bind('about_content', $this->user_about_content)
->bind('about_parser', $this->user_about_parser)
->bind('signature_content', $this->user_signature_content)
->bind('signature_parser', $this->user_signature_parser)
->bind('background_settings', $this->user_background_settings)
->bind('birthdate', $this->user_birthdate)
->execute();
}
public static function create(
string $username,
string $password,
string $email,
string $ipAddress,
string $countryCode = 'XX'
): self {
$createUser = DB::prepare(
'INSERT INTO `msz_users` (`username`, `password`, `email`, `register_ip`, `last_ip`, `user_country`, `display_role`)'
. ' VALUES (:username, :password, LOWER(:email), INET6_ATON(:register_ip), INET6_ATON(:last_ip), :user_country, 1)'
) ->bind('username', $username)
->bind('email', $email)
->bind('register_ip', $ipAddress)
->bind('last_ip', $ipAddress)
->bind('password', self::hashPassword($password))
->bind('user_country', $countryCode)
->executeGetId();
if($createUser < 1)
throw new RuntimeException('User creation failed.');
return self::byId($createUser);
}
/************
* FETCHING *
************/
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, 'COUNT(*)');
}
public static function countAll(bool $showDeleted = false): int {
return (int)DB::prepare(
self::countQueryBase()
. ($showDeleted ? '' : ' WHERE `user_deleted` IS NULL')
)->fetchColumn();
}
private static function memoizer() {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, self::SELECT);
}
public static function byId(string|int $userId): ?self {
// newer classes all treat ids as if they're strings
// php plays nice with it but may as well be sure since phpstan screams about it
if(is_string($userId))
$userId = (int)$userId;
return self::memoizer()->find($userId, function() use ($userId) {
$user = DB::prepare(self::byQueryBase() . ' WHERE `user_id` = :user_id')
->bind('user_id', $userId)
->fetchObject(self::class);
if(!$user)
throw new RuntimeException('Failed to fetch user by ID.');
return $user;
});
}
public static function byUsername(string $username): ?self {
if(empty($username))
throw new InvalidArgumentException('$username may not be empty.');
$username = mb_strtolower($username);
if(str_starts_with($username, 'flappyzor'))
return self::byId(14);
return self::memoizer()->find(function($user) use ($username) {
return mb_strtolower($user->getUsername()) === $username;
}, function() use ($username) {
$user = DB::prepare(self::byQueryBase() . ' WHERE LOWER(`username`) = :username')
->bind('username', $username)
->fetchObject(self::class);
if(!$user)
throw new RuntimeException('Failed to find user by ID.');
return $user;
});
}
public static function byEMailAddress(string $address): ?self {
if(empty($address))
throw new InvalidArgumentException('$address may not be empty.');
$address = mb_strtolower($address);
return self::memoizer()->find(function($user) use ($address) {
return mb_strtolower($user->getEmailAddress()) === $address;
}, function() use ($address) {
$user = DB::prepare(self::byQueryBase() . ' WHERE LOWER(`email`) = :email')
->bind('email', $address)
->fetchObject(self::class);
if(!$user)
throw new RuntimeException('Failed to find user by e-mail address.');
return $user;
});
}
public static function byUsernameOrEMailAddress(string $usernameOrAddress): self {
if(empty($usernameOrAddress))
throw new InvalidArgumentException('$usernameOrAddress may not be empty.');
$usernameOrAddressLower = mb_strtolower($usernameOrAddress);
if(!str_contains($usernameOrAddressLower, '@') && str_starts_with($usernameOrAddressLower, 'flappyzor'))
return self::byId(14);
return self::memoizer()->find(function($user) use ($usernameOrAddressLower) {
return mb_strtolower($user->getUsername()) === $usernameOrAddressLower
|| mb_strtolower($user->getEmailAddress()) === $usernameOrAddressLower;
}, function() use ($usernameOrAddressLower) {
$user = DB::prepare(self::byQueryBase() . ' WHERE LOWER(`email`) = :email OR LOWER(`username`) = :username')
->bind('email', $usernameOrAddressLower)
->bind('username', $usernameOrAddressLower)
->fetchObject(self::class);
if(!$user)
throw new RuntimeException('Failed to find user by name or e-mail address.');
return $user;
});
}
public static function byLatest(): ?self {
return DB::prepare(self::byQueryBase() . ' WHERE `user_deleted` IS NULL ORDER BY `user_id` DESC LIMIT 1')
->fetchObject(self::class);
}
public static function findForProfile($userIdOrName): ?self {
if(empty($userIdOrName))
throw new InvalidArgumentException('$userIdOrName may not be empty.');
$userIdOrNameLower = mb_strtolower($userIdOrName);
if(str_starts_with($userIdOrNameLower, 'flappyzor'))
return self::byId(14);
return self::memoizer()->find(function($user) use ($userIdOrNameLower) {
return $user->getId() == $userIdOrNameLower || mb_strtolower($user->getUsername()) === $userIdOrNameLower;
}, function() use ($userIdOrName) {
$user = DB::prepare(self::byQueryBase() . ' WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')
->bind('user_id', (int)$userIdOrName)
->bind('username', (string)$userIdOrName)
->fetchObject(self::class);
if(!$user)
throw new RuntimeException('Failed to find user by ID or name.');
return $user;
});
}
public static function byBirthdate(?DateTime $date = null): array {
$date = $date === null ? new DateTime('now', new DateTimeZone('UTC')) : (clone $date)->setTimezone(new DateTimeZone('UTC'));
return DB::prepare(self::byQueryBase() . ' WHERE `user_deleted` IS NULL AND `user_birthdate` LIKE :date')
->bind('date', $date->format('%-m-d'))
->fetchObjects(self::class);
}
public static function all(bool $showDeleted = false, ?Pagination $pagination = null): array {
$query = self::byQueryBase();
if(!$showDeleted)
$query .= ' WHERE `user_deleted` IS NULL';
$query .= ' ORDER BY `user_id` ASC';
if($pagination !== null)
$query .= ' LIMIT :range OFFSET :offset';
$getObjects = DB::prepare($query);
if($pagination !== null)
$getObjects->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getObjects->fetchObjects(self::class);
}
} }

231
src/Users/UserInfo.php Normal file
View file

@ -0,0 +1,231 @@
<?php
namespace Misuzu\Users;
use Index\DateTime;
use Index\TimeZoneInfo;
use Index\Colour\Colour;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
class UserInfo {
private string $id;
private string $name;
private ?string $passwordHash;
private string $emailAddr;
private string $registerRemoteAddr;
private string $lastRemoteAddr;
private bool $super;
private string $countryCode;
private ?int $colour;
private int $created;
private ?int $lastActive;
private ?int $deleted;
private ?string $displayRoleId;
private ?string $totpKey;
private ?string $aboutContent;
private int $aboutParser;
private ?string $signatureContent;
private int $signatureParser;
private ?string $birthdate;
private ?int $backgroundSettings;
private ?string $title;
public function __construct(IDbResult $result) {
$this->id = (string)$result->getInteger(0);
$this->name = $result->getString(1);
$this->passwordHash = $result->isNull(2) ? null : $result->getString(2);
$this->emailAddr = $result->getString(3);
$this->registerRemoteAddr = $result->getString(4);
$this->lastRemoteAddr = $result->isNull(5) ? null : $result->getString(5);
$this->super = $result->getInteger(6) !== 0;
$this->countryCode = $result->getString(7);
$this->colour = $result->isNull(8) ? null : $result->getInteger(8);
$this->created = $result->getInteger(9);
$this->lastActive = $result->isNull(10) ? null : $result->getInteger(10);
$this->deleted = $result->isNull(11) ? null : $result->getInteger(11);
$this->displayRoleId = $result->isNull(12) ? null : (string)$result->getInteger(12);
$this->totpKey = $result->isNull(13) ? null : $result->getString(13);
$this->aboutContent = $result->isNull(14) ? null : $result->getString(14);
$this->aboutParser = $result->getInteger(15);
$this->signatureContent = $result->isNull(16) ? null : $result->getString(16);
$this->signatureParser = $result->getInteger(17);
$this->birthdate = $result->isNull(18) ? null : $result->getString(18);
$this->backgroundSettings = $result->isNull(19) ? null : $result->getInteger(19);
$this->title = $result->getString(20);
}
public function getId(): string {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function hasPasswordHash(): bool {
return $this->passwordHash !== null && $this->passwordHash !== '';
}
public function getPasswordHash(): ?string {
return $this->passwordHash;
}
public function passwordNeedsRehash(): bool {
return $this->hasPasswordHash() && Users::passwordNeedsRehash($this->passwordHash);
}
public function verifyPassword(string $password): bool {
return $this->hasPasswordHash() && password_verify($password, $this->passwordHash);
}
public function getEMailAddress(): string {
return $this->emailAddr;
}
public function getRegisterRemoteAddressRaw(): string {
return $this->registerRemoteAddr;
}
public function getRegisterRemoteAddress(): IPAddress {
return IPAddress::parse($this->registerRemoteAddr);
}
public function getLastRemoteAddressRaw(): string {
return $this->lastRemoteAddr;
}
public function getLastRemoteAddress(): IPAddress {
return IPAddress::parse($this->lastRemoteAddr);
}
public function isSuperUser(): bool {
return $this->super;
}
public function hasCountryCode(): bool {
return $this->countryCode !== 'XX';
}
public function getCountryCode(): string {
return $this->countryCode;
}
public function hasColour(): bool {
return $this->colour !== null && ($this->colour & 0x40000000) === 0;
}
public function getColourRaw(): ?int {
return $this->colour;
}
public function getColour(): Colour {
return $this->colour === null ? Colour::none() : Colour::fromMisuzu($this->colour);
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
public function hasLastActive(): bool {
return $this->lastActive !== null;
}
public function getLastActiveTime(): ?int {
return $this->lastActive;
}
public function getLastActiveAt(): ?DateTime {
return $this->lastActive === null ? null : DateTime::fromUnixTimeSeconds($this->lastActive);
}
public function isDeleted(): bool {
return $this->deleted !== null;
}
public function getDeletedTime(): ?int {
return $this->deleted;
}
public function getDeletedAt(): ?DateTime {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted);
}
public function hasDisplayRoleId(): bool {
return $this->displayRoleId !== null;
}
public function getDisplayRoleId(): ?string {
return $this->displayRoleId;
}
public function hasTOTPKey(): bool {
return $this->totpKey !== null;
}
public function getTOTPKey(): ?string {
return $this->totpKey;
}
public function hasAboutContent(): bool {
return $this->aboutContent !== null && $this->aboutContent !== '';
}
public function getAboutContent(): ?string {
return $this->aboutContent;
}
public function getAboutParser(): int {
return $this->aboutParser;
}
public function hasSignatureContent(): bool {
return $this->signatureContent !== null && $this->signatureContent !== '';
}
public function getSignatureContent(): ?string {
return $this->signatureContent;
}
public function getSignatureParser(): int {
return $this->signatureParser;
}
public function hasBirthdate(): bool {
return $this->birthdate !== null;
}
public function getBirthdateRaw(): ?string {
return $this->birthdate;
}
public function getBirthdate(): ?DateTime {
return $this->birthdate === null ? null : DateTime::createFromFormat('Y-m-d', $this->birthdate, TimeZoneInfo::utc());
}
public function getAge(): int {
$birthdate = $this->getBirthdate();
if($birthdate === null || $birthdate->getYear() < 1900)
return -1;
return (int)$birthdate->diff(DateTime::now())->format('%y');
}
public function hasBackgroundSettings(): bool {
return $this->backgroundSettings !== null;
}
public function getBackgroundSettings(): ?int {
return $this->backgroundSettings;
}
public function hasTitle(): bool {
return $this->title !== null && $this->title !== '';
}
public function getTitle(): ?string {
return $this->title;
}
}

View file

@ -2,40 +2,406 @@
namespace Misuzu\Users; namespace Misuzu\Users;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Colour\Colour;
use Index\Data\DbStatementCache; use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Misuzu\Pagination;
class Users { class Users {
//private IDbConnection $dbConn; private IDbConnection $dbConn;
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) { public function __construct(IDbConnection $dbConn) {
//$this->dbConn = $dbConn; $this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
public function updateUser( private const PASSWORD_ALGO = PASSWORD_ARGON2ID;
User|string $userInfo, private const PASSWORD_OPTS = [];
public static function passwordHash(string $password): string {
return password_hash($password, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
}
public static function passwordNeedsRehash(string $passwordHash): bool {
return password_needs_rehash($passwordHash, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
}
public function countUsers(
RoleInfo|string|null $roleInfo = null,
UserInfo|string|null $after = null,
?int $lastActiveInMinutes = null,
?int $newerThanDays = null,
?DateTime $birthdate = null,
?bool $deleted = null
): int {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
if($after instanceof UserInfo)
$after = $after->getId();
$hasRoleInfo = $roleInfo !== null;
$hasAfter = $after !== null;
$hasLastActiveInMinutes = $lastActiveInMinutes !== null;
$hasNewerThanDays = $newerThanDays !== null;
$hasBirthdate = $birthdate !== null;
$hasDeleted = $deleted !== null;
$args = 0;
$query = 'SELECT COUNT(*) FROM msz_users';
if($hasRoleInfo) {
++$args;
$query .= ' WHERE user_id IN (SELECT user_id FROM msz_users_roles WHERE role_id = ?)';
}
if($hasAfter)
$query .= sprintf(' %s user_id > ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasLastActiveInMinutes)
$query .= sprintf(' %s user_active > NOW() - INTERVAL ? MINUTE', ++$args > 1 ? 'AND' : 'WHERE');
if($hasNewerThanDays)
$query .= sprintf(' %s user_created > NOW() - INTERVAL ? DAY', ++$args > 1 ? 'AND' : 'WHERE');
if($hasDeleted)
$query .= sprintf(' %s user_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');
$args = 0;
$stmt = $this->cache->get($query);
if($hasRoleInfo)
$stmt->addParameter(++$args, $roleInfo);
if($hasAfter)
$stmt->addParameter(++$args, $after);
if($hasLastActiveInMinutes)
$stmt->addParameter(++$args, $lastActiveInMinutes);
if($hasNewerThanDays)
$stmt->addParameter(++$args, $newerThanDays);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
private const GET_USERS_SORT = [
'id' => ['user_id', false],
'name' => ['username', false],
'country' => ['user_country', false],
'created' => ['user_created', true],
'active' => ['user_active', true],
'random' => ['RAND()', null],
];
public function getUsers(
RoleInfo|string|null $roleInfo = null,
UserInfo|string|null $after = null,
?int $lastActiveInMinutes = null,
?int $newerThanDays = null,
?DateTime $birthdate = null,
?bool $deleted = null,
?string $orderBy = null,
?bool $reverseOrder = null,
?Pagination $pagination = null
): array {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
if($after instanceof UserInfo)
$after = $after->getId();
$hasRoleInfo = $roleInfo !== null;
$hasAfter = $after !== null;
$hasLastActiveInMinutes = $lastActiveInMinutes !== null;
$hasNewerThanDays = $newerThanDays !== null;
$hasBirthdate = $birthdate !== null;
$hasDeleted = $deleted !== null;
$hasOrderBy = $orderBy !== null;
$hasReverseOrder = $reverseOrder !== null;
$hasPagination = $pagination !== null;
if($hasOrderBy) {
if(!array_key_exists($orderBy, self::GET_USERS_SORT))
throw new InvalidArgumentException('Invalid sort specified.');
$orderBy = self::GET_USERS_SORT[$orderBy];
if($hasReverseOrder && $reverseOrder && $orderBy[1] !== null)
$orderBy[1] = !$orderBy[1];
}
$args = 0;
$query = 'SELECT user_id, username, password, email, INET6_NTOA(register_ip), INET6_NTOA(last_ip), user_super, user_country, user_colour, UNIX_TIMESTAMP(user_created), UNIX_TIMESTAMP(user_active), UNIX_TIMESTAMP(user_deleted), display_role, user_totp_key, user_about_content, user_about_parser, user_signature_content, user_signature_parser, user_birthdate, user_background_settings, user_title FROM msz_users';
if($hasRoleInfo) {
++$args;
$query .= ' WHERE user_id IN (SELECT user_id FROM msz_users_roles WHERE role_id = ?)';
}
if($hasAfter)
$query .= sprintf(' %s user_id > ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasLastActiveInMinutes)
$query .= sprintf(' %s user_active > NOW() - INTERVAL ? MINUTE', ++$args > 1 ? 'AND' : 'WHERE');
if($hasNewerThanDays)
$query .= sprintf(' %s user_created > NOW() - INTERVAL ? DAY', ++$args > 1 ? 'AND' : 'WHERE');
if($hasDeleted)
$query .= sprintf(' %s user_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');
if($hasBirthdate)
$query .= sprintf(' %s user_birthdate LIKE ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasOrderBy) {
$query .= sprintf(' ORDER BY %s', $orderBy[0]);
if($orderBy !== null)
$query .= ' ' . ($orderBy[1] ? 'DESC' : 'ASC');
}
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$args = 0;
$stmt = $this->cache->get($query);
if($hasRoleInfo)
$stmt->addParameter(++$args, $roleInfo);
if($hasAfter)
$stmt->addParameter(++$args, $after);
if($hasLastActiveInMinutes)
$stmt->addParameter(++$args, $lastActiveInMinutes);
if($hasNewerThanDays)
$stmt->addParameter(++$args, $newerThanDays);
if($hasBirthdate)
$stmt->addParameter(++$args, $birthdate->format('%-m-d'));
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
$users = [];
$result = $stmt->getResult();
while($result->next())
$users[] = new UserInfo($result);
return $users;
}
public const GET_USER_ID = 0x01;
public const GET_USER_NAME = 0x02;
public const GET_USER_MAIL = 0x04;
private const GET_USER_SELECT_ALIASES = [
'id' => self::GET_USER_ID,
'name' => self::GET_USER_NAME,
'email' => self::GET_USER_MAIL,
'profile' => self::GET_USER_ID | self::GET_USER_NAME,
'login' => self::GET_USER_NAME | self::GET_USER_MAIL,
'recovery' => self::GET_USER_MAIL,
];
public function getUser(string $value, int|string $select = self::GET_USER_ID): UserInfo {
if($value === '')
throw new InvalidArgumentException('$value may not be empty.');
if(is_string($select)) {
if(!array_key_exists($select, self::GET_USER_SELECT_ALIASES))
throw new InvalidArgumentException('Invalid $select alias.');
$select = self::GET_USER_SELECT_ALIASES[$select];
} elseif($select === 0)
throw new InvalidArgumentException('$select may not be zero.');
$selectId = ($select & self::GET_USER_ID) > 0;
$selectName = ($select & self::GET_USER_NAME) > 0;
$selectMail = ($select & self::GET_USER_MAIL) > 0;
if(!$selectId && !$selectName && !$selectMail)
throw new InvalidArgumentException('$select flagset is invalid.');
$args = 0;
$query = 'SELECT user_id, username, password, email, INET6_NTOA(register_ip), INET6_NTOA(last_ip), user_super, user_country, user_colour, UNIX_TIMESTAMP(user_created), UNIX_TIMESTAMP(user_active), UNIX_TIMESTAMP(user_deleted), display_role, user_totp_key, user_about_content, user_about_parser, user_signature_content, user_signature_parser, user_birthdate, user_background_settings, user_title FROM msz_users';
if($selectId) {
++$args;
$query .= ' WHERE user_id = ?';
}
if($selectName) // change the collation for both name and email to a case insensitive one
$query .= sprintf(' %s LOWER(username) = LOWER(?)', ++$args > 1 ? 'OR' : 'WHERE');
if($selectMail)
$query .= sprintf(' %s LOWER(email) = LOWER(?)', ++$args > 1 ? 'OR' : 'WHERE');
$args = 0;
$stmt = $this->cache->get($query);
if($selectId)
$stmt->addParameter(++$args, $value);
if($selectName)
$stmt->addParameter(++$args, $value);
if($selectMail)
$stmt->addParameter(++$args, $value);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('User not found.');
return new UserInfo($result);
}
public function createUser(
string $name,
string $password,
string $email,
IPAddress|string $remoteAddr,
string $countryCode,
RoleInfo|string|null $displayRoleInfo = null RoleInfo|string|null $displayRoleInfo = null
): UserInfo {
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
if($displayRoleInfo instanceof RoleInfo)
$displayRoleInfo = $displayRoleInfo->getId();
elseif($displayRoleInfo === null)
$displayRoleInfo = Roles::DEFAULT_ROLE;
$password = self::passwordHash($password);
// todo: validation
$stmt = $this->cache->get('INSERT INTO msz_users (username, password, email, register_ip, last_ip, user_country, display_role) VALUES (?, ?, ?, INET6_ATON(?), INET6_ATON(?), ?, ?)');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $password);
$stmt->addParameter(3, $email);
$stmt->addParameter(4, $remoteAddr);
$stmt->addParameter(5, $remoteAddr);
$stmt->addParameter(6, $countryCode);
$stmt->addParameter(7, $displayRoleInfo);
$stmt->execute();
return $this->getUser((string)$this->dbConn->getLastInsertId(), self::GET_USER_ID);
}
public function updateUser(
UserInfo|string $userInfo,
?string $name = null,
?string $emailAddr = null,
?string $password = null,
?string $countryCode = null,
?Colour $colour = null,
RoleInfo|string|null $displayRoleInfo = null,
?string $totpKey = null,
?string $aboutContent = null,
?int $aboutParser = null,
?string $signatureContent = null,
?int $signatureParser = null,
?int $birthYear = null,
?int $birthMonth = null,
?int $birthDay = null,
?int $backgroundSettings = null,
?string $title = null
): void { ): void {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($displayRoleInfo instanceof RoleInfo) if($displayRoleInfo instanceof RoleInfo)
$displayRoleInfo = $displayRoleInfo->getId(); $displayRoleInfo = $displayRoleInfo->getId();
$stmt = $this->cache->get('UPDATE msz_users SET display_role = COALESCE(?, display_role) WHERE user_id = ?'); // do sanity checks on values at some point lol
$stmt->addParameter(1, $displayRoleInfo); $fields = [];
$values = [];
if($name !== null) {
$fields[] = 'username = ?';
$values[] = $name;
}
if($emailAddr !== null) {
$fields[] = 'email = ?';
$values[] = $emailAddr;
}
if($password !== null) {
$fields[] = 'password = ?';
$values[] = $password === '' ? null : self::passwordHash($password);
}
if($countryCode !== null) {
$fields[] = 'user_country = ?';
$values[] = $countryCode;
}
if($colour !== null) {
$fields[] = 'user_colour = ?';
$values[] = $colour->shouldInherit() ? null : Colour::toMisuzu($colour);
}
if($displayRoleInfo !== null) {
$fields[] = 'display_role = ?';
$values[] = $displayRoleInfo;
}
if($totpKey !== null) {
$fields[] = 'user_totp_key = ?';
$values[] = $totpKey === '' ? null : $totpKey;
}
if($aboutContent !== null) {
$fields[] = 'user_about_content = ?';
$values[] = $aboutContent;
}
if($aboutParser !== null) {
$fields[] = 'user_about_parser = ?';
$values[] = $aboutParser;
}
if($signatureContent !== null) {
$fields[] = 'user_signature_content = ?';
$values[] = $signatureContent;
}
if($signatureParser !== null) {
$fields[] = 'user_signature_parser = ?';
$values[] = $signatureParser;
}
if($birthMonth !== null && $birthDay !== null) {
// lowest leap year MariaDB accepts, used a 'no year' value
if($birthYear < 1004)
$birthYear = 1004;
$fields[] = 'user_birthdate = ?';
$values[] = $birthMonth < 1 || $birthDay < 1 ? null : sprintf('%04d-%02d-%02d', $birthYear, $birthMonth, $birthDay);
}
if($backgroundSettings !== null) {
$fields[] = 'user_background_settings = ?';
$values[] = $backgroundSettings;
}
if($title !== null) {
$fields[] = 'user_title = ?';
$values[] = $title;
}
if(empty($fields))
return;
$args = 0;
$stmt = $this->cache->get(sprintf('UPDATE msz_users SET %s WHERE user_id = ?', implode(', ', $fields)));
foreach($values as $value)
$stmt->addParameter(++$args, $value);
$stmt->addParameter(++$args, $userInfo);
$stmt->execute();
}
public function recordUserActivity(
UserInfo|string $userInfo,
IPAddress|string $remoteAddr
): void {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$stmt = $this->cache->get('UPDATE msz_users SET user_active = NOW(), last_ip = INET6_ATON(?) WHERE user_id = ?');
$stmt->addParameter(1, $remoteAddr);
$stmt->addParameter(2, $userInfo); $stmt->addParameter(2, $userInfo);
$stmt->execute(); $stmt->execute();
} }
public function hasRole( public function hasRole(
User|string $userInfo, UserInfo|string $userInfo,
RoleInfo|string $roleInfo RoleInfo|string $roleInfo
): bool { ): bool {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($roleInfo instanceof RoleInfo) if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId(); $roleInfo = $roleInfo->getId();
@ -43,11 +409,11 @@ class Users {
} }
public function hasRoles( public function hasRoles(
User|string $userInfo, UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos RoleInfo|string|array $roleInfos
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if(!is_array($roleInfos)) if(!is_array($roleInfos))
$roleInfos = [$roleInfos]; $roleInfos = [$roleInfos];
elseif(empty($roleInfos)) elseif(empty($roleInfos))
@ -81,11 +447,11 @@ class Users {
} }
public function addRoles( public function addRoles(
User|string $userInfo, UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos RoleInfo|string|array $roleInfos
): void { ): void {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if(!is_array($roleInfos)) if(!is_array($roleInfos))
$roleInfos = [$roleInfos]; $roleInfos = [$roleInfos];
elseif(empty($roleInfos)) elseif(empty($roleInfos))
@ -111,11 +477,11 @@ class Users {
} }
public function removeRoles( public function removeRoles(
User|string $userInfo, UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos RoleInfo|string|array $roleInfos
): void { ): void {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if(!is_array($roleInfos)) if(!is_array($roleInfos))
$roleInfos = [$roleInfos]; $roleInfos = [$roleInfos];
elseif(empty($roleInfos)) elseif(empty($roleInfos))
@ -139,4 +505,39 @@ class Users {
$stmt->execute(); $stmt->execute();
} }
// the below two funcs should probably be moved to a higher level location so caching can be introduced
// without cluttering the data source interface <-- real words i just wrote
public function getUserColour(UserInfo|string $userInfo): Colour {
if($userInfo instanceof UserInfo) {
if($userInfo->hasColour())
return $userInfo->getColour();
$query = '?';
$value = $userInfo->getDisplayRoleId();
} else {
$query = '(SELECT display_role FROM msz_users WHERE user_id = ?)';
$value = $userInfo;
}
$stmt = $this->cache->get(sprintf('SELECT role_colour FROM msz_roles WHERE role_id = %s', $query));
$stmt->addParameter(1, $value);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none();
}
public function getUserRank(UserInfo|string $userInfo): int {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$stmt = $this->cache->get('SELECT MAX(role_hierarchy) FROM msz_roles WHERE role_id IN (SELECT role_id FROM msz_users_roles WHERE user_id = ?)');
$stmt->addParameter(1, $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
} }

View file

@ -7,7 +7,6 @@ use Index\Data\DbStatementCache;
use Index\Data\DbTools; use Index\Data\DbTools;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Users\User;
// this system is currently kinda useless because it only silently shows up on profiles // this system is currently kinda useless because it only silently shows up on profiles
// planning a notification system anyway so that should probably hook into // planning a notification system anyway so that should probably hook into
@ -24,11 +23,11 @@ class Warnings {
} }
public function countWarnings( public function countWarnings(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?int $backlog = null ?int $backlog = null
): int { ): int {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasBacklog = $backlog !== null; $hasBacklog = $backlog !== null;
@ -63,7 +62,7 @@ class Warnings {
} }
public function getWarningsWithDefaultBacklog( public function getWarningsWithDefaultBacklog(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
return $this->getWarnings( return $this->getWarnings(
@ -74,12 +73,12 @@ class Warnings {
} }
public function getWarnings( public function getWarnings(
User|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
?int $backlog = null, ?int $backlog = null,
?Pagination $pagination = null ?Pagination $pagination = null
): array { ): array {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
$hasBacklog = $backlog !== null; $hasBacklog = $backlog !== null;
@ -135,14 +134,14 @@ class Warnings {
} }
public function createWarning( public function createWarning(
User|string $userInfo, UserInfo|string $userInfo,
string $body, string $body,
User|string|null $modInfo UserInfo|string|null $modInfo
): WarningInfo { ): WarningInfo {
if($userInfo instanceof User) if($userInfo instanceof UserInfo)
$userInfo = (string)$userInfo->getId(); $userInfo = $userInfo->getId();
if($modInfo instanceof User) if($modInfo instanceof UserInfo)
$modInfo = (string)$modInfo->getId(); $modInfo = $modInfo->getId();
$stmt = $this->cache->get('INSERT INTO msz_users_warnings (user_id, mod_id, warn_body) VALUES (?, ?, ?)'); $stmt = $this->cache->get('INSERT INTO msz_users_warnings (user_id, mod_id, warn_body) VALUES (?, ?, ?)');
$stmt->addParameter(1, $userInfo); $stmt->addParameter(1, $userInfo);

View file

@ -95,10 +95,11 @@ function perms_get_blank(array $modes = MSZ_PERM_MODES): array {
return array_fill_keys($modes, 0); return array_fill_keys($modes, 0);
} }
function perms_get_user(int $user): array { function perms_get_user(int|string $user): array {
if($user < 1) { if(is_string($user))
$user = (int)$user;
if($user < 1)
return perms_get_blank(); return perms_get_blank();
}
static $memo = []; static $memo = [];
@ -128,10 +129,11 @@ function perms_get_user(int $user): array {
return $memo[$user] = $getPerms->fetch(); return $memo[$user] = $getPerms->fetch();
} }
function perms_delete_user(int $user): bool { function perms_delete_user(int|string $user): bool {
if($user < 1) { if(is_string($user))
$user = (int)$user;
if($user < 1)
return false; return false;
}
$deletePermissions = \Misuzu\DB::prepare(' $deletePermissions = \Misuzu\DB::prepare('
DELETE FROM `msz_permissions` DELETE FROM `msz_permissions`
@ -167,10 +169,11 @@ function perms_get_role(int $role): array {
return $memo[$role] = $getPerms->fetch(); return $memo[$role] = $getPerms->fetch();
} }
function perms_get_user_raw(int $user): array { function perms_get_user_raw(int|string $user): array {
if($user < 1) { if(is_string($user))
$user = (int)$user;
if($user < 1)
return perms_create(); return perms_create();
}
$getPerms = \Misuzu\DB::prepare(sprintf(' $getPerms = \Misuzu\DB::prepare(sprintf('
SELECT `%s` SELECT `%s`
@ -188,10 +191,11 @@ function perms_get_user_raw(int $user): array {
return $perms; return $perms;
} }
function perms_set_user_raw(int $user, array $perms): bool { function perms_set_user_raw(int|string $user, array $perms): bool {
if($user < 1) { if(is_string($user))
$user = (int)$user;
if($user < 1)
return false; return false;
}
$realPerms = perms_create(); $realPerms = perms_create();
$permKeys = array_keys($realPerms); $permKeys = array_keys($realPerms);
@ -245,7 +249,9 @@ function perms_check(?int $perms, ?int $perm, bool $strict = false): bool {
return $strict ? $and === $perm : $and > 0; return $strict ? $and === $perm : $and > 0;
} }
function perms_check_user(string $prefix, ?int $userId, int $perm, bool $strict = false): bool { function perms_check_user(string $prefix, int|string|null $userId, int $perm, bool $strict = false): bool {
if(is_string($userId))
$userId = (int)$userId;
return $userId > 0 && perms_check(perms_get_user($userId)[$prefix] ?? 0, $perm, $strict); return $userId > 0 && perms_check(perms_get_user($userId)[$prefix] ?? 0, $perm, $strict);
} }
@ -257,13 +263,13 @@ function perms_check_bulk(int $perms, array $set, bool $strict = false): array {
return $set; return $set;
} }
function perms_check_user_bulk(string $prefix, ?int $userId, array $set, bool $strict = false): array { function perms_check_user_bulk(string $prefix, int|string|null $userId, array $set, bool $strict = false): array {
$perms = perms_get_user($userId)[$prefix] ?? 0; $perms = perms_get_user($userId)[$prefix] ?? 0;
return perms_check_bulk($perms, $set, $strict); return perms_check_bulk($perms, $set, $strict);
} }
function perms_for_comments(string|int $userId): array { function perms_for_comments(string|int $userId): array {
return perms_check_user_bulk(MSZ_PERMS_COMMENTS, (int)$userId, [ return perms_check_user_bulk(MSZ_PERMS_COMMENTS, $userId, [
'can_comment' => MSZ_PERM_COMMENTS_CREATE, 'can_comment' => MSZ_PERM_COMMENTS_CREATE,
'can_delete' => MSZ_PERM_COMMENTS_DELETE_OWN | MSZ_PERM_COMMENTS_DELETE_ANY, 'can_delete' => MSZ_PERM_COMMENTS_DELETE_OWN | MSZ_PERM_COMMENTS_DELETE_ANY,
'can_delete_any' => MSZ_PERM_COMMENTS_DELETE_ANY, 'can_delete_any' => MSZ_PERM_COMMENTS_DELETE_ANY,

View file

@ -16,7 +16,7 @@
<div class="comment__container"> <div class="comment__container">
<div class="avatar comment__avatar"> <div class="avatar comment__avatar">
{{ avatar(user.id, reply_mode ? 40 : 50, user.username) }} {{ avatar(user.id, reply_mode ? 40 : 50, user.name) }}
</div> </div>
<div class="comment__content"> <div class="comment__content">
<textarea <textarea
@ -40,7 +40,7 @@
</form> </form>
{% endmacro %} {% endmacro %}
{% macro comments_entry(comment, indent, category, user, perms) %} {% macro comments_entry(comment, indent, category, user, colour, perms) %}
{% from 'macros.twig' import avatar %} {% from 'macros.twig' import avatar %}
{% from '_layout/input.twig' import input_checkbox_raw %} {% from '_layout/input.twig' import input_checkbox_raw %}
@ -72,7 +72,7 @@
</div> </div>
{% else %} {% else %}
<a class="comment__avatar" href="{{ url('user-profile', {'user': poster.id}) }}"> <a class="comment__avatar" href="{{ url('user-profile', {'user': poster.id}) }}">
{{ avatar(poster.id, indent > 1 ? 40 : 50, poster.username) }} {{ avatar(poster.id, indent > 1 ? 40 : 50, poster.name) }}
</a> </a>
{% endif %} {% endif %}
<div class="comment__content"> <div class="comment__content">
@ -80,7 +80,7 @@
{% if not hide_details %} {% if not hide_details %}
<a class="comment__user comment__user--link" <a class="comment__user comment__user--link"
href="{{ url('user-profile', {'user': poster.id}) }}" href="{{ url('user-profile', {'user': poster.id}) }}"
style="--user-colour: {{ poster.colour}}">{{ poster.username }}</a> style="--user-colour: {{ colour }}">{{ poster.name }}</a>
{% endif %} {% endif %}
<a class="comment__link" href="#comment-{{ comment.id }}"> <a class="comment__link" href="#comment-{{ comment.id }}">
<time class="comment__date" <time class="comment__date"
@ -102,7 +102,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="comment__text"> <div class="comment__text">
{{ hide_details ? '(deleted)' : body|parse_comment|raw }} {{ hide_details ? '(deleted)' : body }}
</div> </div>
<div class="comment__actions"> <div class="comment__actions">
{% if not comment.deleted and user is not null %} {% if not comment.deleted and user is not null %}
@ -152,7 +152,7 @@
{% endif %} {% endif %}
{% if replies|length > 0 %} {% if replies|length > 0 %}
{% for reply in replies %} {% for reply in replies %}
{{ comments_entry(reply, indent + 1, category, user, perms) }} {{ comments_entry(reply, indent + 1, category, user, colour, perms) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
@ -162,6 +162,7 @@
{% macro comments_section(category) %} {% macro comments_section(category) %}
{% set user = category.user %} {% set user = category.user %}
{% set colour = category.colour %}
{% set posts = category.posts %} {% set posts = category.posts %}
{% set perms = category.perms %} {% set perms = category.perms %}
{% set category = category.category %} {% set category = category.category %}
@ -206,7 +207,7 @@
{% if posts|length > 0 %} {% if posts|length > 0 %}
{% from _self import comments_entry %} {% from _self import comments_entry %}
{% for comment in posts %} {% for comment in posts %}
{{ comments_entry(comment, 1, category, user, perms) }} {{ comments_entry(comment, 1, category, user, colour, perms) }}
{% endfor %} {% endfor %}
{% else %} {% else %}
<div class="comments__none" id="_no_comments_notice_{{ category.id }}"> <div class="comments__none" id="_no_comments_notice_{{ category.id }}">

View file

@ -13,7 +13,7 @@
<a href="https://git.flash.moe/flashii/misuzu/src/tag/{{ git_tag }}" target="_blank" rel="noreferrer noopener" class="footer__link">{{ git_tag }}</a> <a href="https://git.flash.moe/flashii/misuzu/src/tag/{{ git_tag }}" target="_blank" rel="noreferrer noopener" class="footer__link">{{ git_tag }}</a>
{% endif %} {% endif %}
# <a href="https://git.flash.moe/flashii/misuzu/commit/{{ git_commit_hash(true) }}" target="_blank" rel="noreferrer noopener" class="footer__link">{{ git_commit_hash() }}</a> # <a href="https://git.flash.moe/flashii/misuzu/commit/{{ git_commit_hash(true) }}" target="_blank" rel="noreferrer noopener" class="footer__link">{{ git_commit_hash() }}</a>
{% if constant('MSZ_DEBUG') or current_user.super|default(false) %} {% if display_debug_info %}
{% set sql_query_count = sql_query_count() %} {% set sql_query_count = sql_query_count() %}
/ Index {{ ndx_version() }} / Index {{ ndx_version() }}
/ SQL Queries: {{ sql_query_count.total|number_format }} (<span title="{{ sql_query_count.ndx|number_format }} queries processed through Index">{{ sql_query_count.ndx|number_format }}</span> + <span title="{{ sql_query_count.pdo|number_format }} queries processed through PDO">{{ sql_query_count.pdo|number_format }}</span>) / SQL Queries: {{ sql_query_count.total|number_format }} (<span title="{{ sql_query_count.ndx|number_format }} queries processed through Index">{{ sql_query_count.ndx|number_format }}</span> + <span title="{{ sql_query_count.pdo|number_format }} queries processed through PDO">{{ sql_query_count.pdo|number_format }}</span>)

View file

@ -4,10 +4,10 @@
{% if current_user_real is defined %} {% if current_user_real is defined %}
<div class="impersonate"> <div class="impersonate">
<div class="impersonate-content"> <div class="impersonate-content">
<div class="impersonate-user" style="--user-colour: {{ current_user_real.colour }}"> <div class="impersonate-user" style="--user-colour: {{ current_user_real_colour }}">
You are <a href="{{ url('user-profile', {'user': current_user_real.id}) }}" class="impersonate-user-link"> You are <a href="{{ url('user-profile', {'user': current_user_real.id}) }}" class="impersonate-user-link">
<div class="avatar impersonate-user-avatar">{{ avatar(current_user_real.id, 20, current_user_real.username) }}</div> <div class="avatar impersonate-user-avatar">{{ avatar(current_user_real.id, 20, current_user_real.name) }}</div>
{{ current_user_real.username }} {{ current_user_real.name }}
</a> </a>
</div> </div>
<div class="impersonate-options"> <div class="impersonate-options">
@ -26,40 +26,34 @@
</a> </a>
<div class="header__desktop__menus"> <div class="header__desktop__menus">
{% for item in site_menu %} {% for item in header_menu %}
{% if item.display is not defined or item.display %} <div class="header__desktop__menu">
<div class="header__desktop__menu"> <a href="{{ item.url }}" class="header__desktop__link header__desktop__menu__link">{{ item.title }}</a>
<a href="{{ item.url }}" class="header__desktop__link header__desktop__menu__link">{{ item.title }}</a>
{% if item.menu is defined and item.menu is iterable %} {% if item.menu is defined and item.menu is iterable %}
<div class="header__desktop__submenu"> <div class="header__desktop__submenu">
<div class="header__desktop__submenu__background"></div> <div class="header__desktop__submenu__background"></div>
<div class="header__desktop__submenu__content"> <div class="header__desktop__submenu__content">
{% for subitem in item.menu %} {% for subitem in item.menu %}
{% if subitem.display is not defined or subitem.display %} <a href="{{ subitem.url }}" class="header__desktop__link header__desktop__submenu__link">{{ subitem.title }}</a>
<a href="{{ subitem.url }}" class="header__desktop__link header__desktop__submenu__link">{{ subitem.title }}</a> {% endfor %}
{% endif %}
{% endfor %}
</div>
</div> </div>
{% endif %} </div>
</div> {% endif %}
{% endif %} </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="header__desktop__user"> <div class="header__desktop__user">
{% for item in user_menu %} {% for item in user_menu %}
{% if item.display is not defined or item.display %} <a href="{{ item.url }}" title="{{ item.title }}" class="header__desktop__user__button {{ item.class|default('') }}">
<a href="{{ item.url }}" title="{{ item.title }}" class="header__desktop__user__button {{ item.class|default('') }}"> <i class="{{ item.icon }}"></i>
<i class="{{ item.icon }}"></i> </a>
</a>
{% endif %}
{% endfor %} {% endfor %}
{% if current_user is defined %} {% if current_user is defined %}
<a href="{{ url('user-profile', {'user': current_user.id}) }}" class="avatar header__desktop__user__avatar" title="{{ current_user.username }}" style="--user-colour: {{ current_user.colour }}"> <a href="{{ url('user-profile', {'user': current_user.id}) }}" class="avatar header__desktop__user__avatar" title="{{ current_user.name }}">
{{ avatar(current_user.id, 60, current_user.username) }} {{ avatar(current_user.id, 60, current_user.name) }}
</a> </a>
{% else %} {% else %}
<a href="{{ url('auth-login') }}" class="avatar header__desktop__user__avatar"> <a href="{{ url('auth-login') }}" class="avatar header__desktop__user__avatar">
@ -80,7 +74,7 @@
</a> </a>
<label class="header__mobile__icon header__mobile__avatar" for="toggle-mobile-header"> <label class="header__mobile__icon header__mobile__avatar" for="toggle-mobile-header">
{{ avatar(current_user.id|default(0), 40, current_user.username|default('Log in')) }} {{ avatar(current_user.id|default(0), 40, current_user.name|default('Log in')) }}
</label> </label>
</div> </div>
@ -90,26 +84,20 @@
<div class="header__mobile__user"> <div class="header__mobile__user">
{% for item in user_menu %} {% for item in user_menu %}
{% if item.display is not defined or item.display %} <a href="{{ item.url }}" class="header__mobile__link header__mobile__link--user {{ item.class|default('') }}">
<a href="{{ item.url }}" class="header__mobile__link header__mobile__link--user {{ item.class|default('') }}"> <i class="{{ item.icon }}"></i> {{ item.title }}
<i class="{{ item.icon }}"></i> {{ item.title }} </a>
</a>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
<div class="header__mobile__navigation"> <div class="header__mobile__navigation">
{% for item in site_menu %} {% for item in header_menu %}
{% if item.display is not defined or item.display %} <a href="{{ item.url }}" class="header__mobile__link header__mobile__link--primary">{{ item.title }}</a>
<a href="{{ item.url }}" class="header__mobile__link header__mobile__link--primary">{{ item.title }}</a>
{% if item.menu is defined and item.menu is iterable %} {% if item.menu is defined and item.menu is iterable %}
{% for subitem in item.menu %} {% for subitem in item.menu %}
{% if subitem.display is not defined or subitem.display %} <a href="{{ subitem.url }}" class="header__mobile__link">{{ subitem.title }}</a>
<a href="{{ subitem.url }}" class="header__mobile__link">{{ subitem.title }}</a> {% endfor %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>

View file

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<form class="container auth__container auth__password" method="post" action="{{ url('auth-reset') }}"> <form class="container auth__container auth__password" method="post" action="{{ url('auth-reset') }}">
{{ container_title('<i class="fas fa-user-lock fa-fw"></i> Resetting password for ' ~ password_user.username) }} {{ container_title('<i class="fas fa-user-lock fa-fw"></i> Resetting password for ' ~ password_user.name) }}
{{ input_hidden('reset[user]', password_user.id) }} {{ input_hidden('reset[user]', password_user.id) }}
{{ input_csrf() }} {{ input_csrf() }}

View file

@ -18,18 +18,18 @@
</div> </div>
</div> </div>
<div class="container changelog__change"{% if change_user_info is not null %} style="--accent-colour: {{ change_user_info.colour }}"{% endif %}> <div class="container changelog__change"{% if change_user_colour is not null %} style="--accent-colour: {{ change_user_colour }}"{% endif %}>
<div class="changelog__change__info"> <div class="changelog__change__info">
<div class="changelog__change__info__background"></div> <div class="changelog__change__info__background"></div>
<div class="changelog__change__info__content"> <div class="changelog__change__info__content">
{% if change_user_info.id|default(null) is not null %} {% if change_user_info.id|default(null) is not null %}
<div class="changelog__change__user"> <div class="changelog__change__user">
<a class="changelog__change__avatar" href="{{ url('user-profile', {'user': change_user_info.id}) }}"> <a class="changelog__change__avatar" href="{{ url('user-profile', {'user': change_user_info.id}) }}">
{{ avatar(change_user_info.id, 60, change_user_info.username) }} {{ avatar(change_user_info.id, 60, change_user_info.name) }}
</a> </a>
<div class="changelog__change__user__details"> <div class="changelog__change__user__details">
<a class="changelog__change__username" href="{{ url('user-profile', {'user': change_user_info.id}) }}">{{ change_user_info.username }}</a> <a class="changelog__change__username" href="{{ url('user-profile', {'user': change_user_info.id}) }}">{{ change_user_info.name }}</a>
<a class="changelog__change__userrole" href="{{ url('user-list', {'role': change_user_info.displayRoleId}) }}">{{ change_user_info.title }}</a> <a class="changelog__change__userrole" href="{{ url('user-list', {'role': change_user_info.displayRoleId}) }}">{{ change_user_info.title }}</a>
</div> </div>
</div> </div>

View file

@ -25,7 +25,7 @@
{% endif %} {% endif %}
{% if is_user %} {% if is_user %}
{% set title = title ~ ' by ' ~ first_change_info.user.username %} {% set title = title ~ ' by ' ~ first_change_info.user.name %}
{% endif %} {% endif %}
{% else %} {% else %}
{% set feeds = [ {% set feeds = [

View file

@ -26,6 +26,7 @@
{% macro changelog_entry(change, is_small, is_manage) %} {% macro changelog_entry(change, is_small, is_manage) %}
{% set user = change.user|default(null) %} {% set user = change.user|default(null) %}
{% set user_colour = change.user_colour|default(null) %}
{% if change.change is defined %} {% if change.change is defined %}
{% set change = change.change %} {% set change = change.change %}
{% endif %} {% endif %}
@ -57,9 +58,9 @@
{% if not is_small %} {% if not is_small %}
<a class="changelog__entry__user" <a class="changelog__entry__user"
href="{{ url(is_manage ? 'manage-user' : 'user-profile', {'user': user.id|default(0)}) }}" href="{{ url(is_manage ? 'manage-user' : 'user-profile', {'user': user.id|default(0)}) }}"
style="--user-colour: {{ user.colour|default('inherit') }}"> {% if user_colour is not null %}style="--user-colour: {{ user_colour }}"{% endif %}>
<div class="changelog__entry__user__text"> <div class="changelog__entry__user__text">
{{ user.username|default('Anonymous') }} {{ user.name|default('Anonymous') }}
</div> </div>
</a> </a>
{% endif %} {% endif %}

View file

@ -0,0 +1,8 @@
{% extends 'errors/master.twig' %}
{% set error_code = 401 %}
{% set error_text = 'Unauthorised!' %}
{% block error_message %}
<p>You must log in to view this page.</p>
{% endblock %}

View file

@ -12,7 +12,7 @@
{ {
'html': '<i class="far fa-check-circle"></i> Mark as Read', 'html': '<i class="far fa-check-circle"></i> Mark as Read',
'url': url('forum-mark-single', {'forum': forum_info.forum_id}), 'url': url('forum-mark-single', {'forum': forum_info.forum_id}),
'display': current_user is defined, 'display': forum_show_mark_as_read,
'method': 'POST', 'method': 'POST',
} }
]) }} ]) }}

View file

@ -21,7 +21,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if current_user is defined %} {% if forum_show_mark_as_read %}
<div class="container forum__actions"> <div class="container forum__actions">
<a href="{{ url('forum-mark-global') }}" class="input__button forum__actions__button">Mark All Read</a> <a href="{{ url('forum-mark-global') }}" class="input__button forum__actions__button">Mark All Read</a>
</div> </div>

View file

@ -43,13 +43,13 @@
</div> </div>
{% endif %} {% endif %}
<div class="container forum__post" style="{{ posting_post.poster_colour|default(current_user.colour)|html_colour('--accent-colour') }}"> <div class="container forum__post" style="{{ posting_post.poster_colour|default(posting_info.colour)|html_colour('--accent-colour') }}">
<div class="forum__post__info"> <div class="forum__post__info">
<div class="forum__post__info__background"></div> <div class="forum__post__info__background"></div>
<div class="forum__post__info__content"> <div class="forum__post__info__content">
<span class="forum__post__avatar">{{ avatar(posting_post.poster_id|default(current_user.id), 120, posting_post.poster_name|default(current_user.username)) }}</span> <span class="forum__post__avatar">{{ avatar(posting_post.poster_id|default(posting_info.user_id), 120, posting_post.poster_name|default(posting_info.username)) }}</span>
<span class="forum__post__username">{{ posting_post.poster_name|default(current_user.username) }}</span> <span class="forum__post__username">{{ posting_post.poster_name|default(posting_info.username) }}</span>
<div class="forum__post__icons"> <div class="forum__post__icons">
<div class="flag flag--{{ posting_post.poster_country|default(posting_info.user_country)|lower }}" title="{{ posting_post.poster_country|default(posting_info.user_country)|country_name }}"></div> <div class="flag flag--{{ posting_post.poster_country|default(posting_info.user_country)|lower }}" title="{{ posting_post.poster_country|default(posting_info.user_country)|country_name }}"></div>

View file

@ -57,7 +57,7 @@
{{ forum_header(topic_info.topic_title, topic_breadcrumbs, false, canonical_url, topic_actions) }} {{ forum_header(topic_info.topic_title, topic_breadcrumbs, false, canonical_url, topic_actions) }}
{{ topic_notice|raw }} {{ topic_notice|raw }}
{{ topic_tools }} {{ topic_tools }}
{{ forum_post_listing(topic_posts, current_user.id|default(0), topic_perms) }} {{ forum_post_listing(topic_posts, topic_user_id, topic_perms) }}
{{ topic_tools }} {{ topic_tools }}
{{ topic_notice|raw }} {{ topic_notice|raw }}
{{ forum_header('', topic_breadcrumbs) }} {{ forum_header('', topic_breadcrumbs) }}

View file

@ -64,8 +64,8 @@
<div class="landing__online"> <div class="landing__online">
{% for user in online_users %} {% for user in online_users %}
<a href="{{ url('user-profile', {'user': user.user_id}) }}" class="landing__online__user" title="{{ user.username }}"> <a href="{{ url('user-profile', {'user': user.id}) }}" class="landing__online__user" title="{{ user.name }}">
{{ avatar(user.user_id, 30, user.username) }} {{ avatar(user.id, 30, user.name) }}
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
@ -77,33 +77,34 @@
{{ container_title('<i class="fas fa-birthday-cake fa-fw"></i> Happy Birthday!') }} {{ container_title('<i class="fas fa-birthday-cake fa-fw"></i> Happy Birthday!') }}
{% for birthday in birthdays %} {% for birthday in birthdays %}
<a class="landing__latest" style="--user-colour: {{ birthday.colour }}" href="{{ url('user-profile', {'user': birthday.id}) }}"> {% set age = birthday.info.age %}
<div class="landing__latest__avatar">{{ avatar(birthday.id, 50, birthday.username) }}</div> <a class="landing__latest" style="--user-colour: {{ birthday.colour }}" href="{{ url('user-profile', {'user': birthday.info.id}) }}">
<div class="landing__latest__avatar">{{ avatar(birthday.info.id, 50, birthday.info.name) }}</div>
<div class="landing__latest__content"> <div class="landing__latest__content">
<div class="landing__latest__username"> <div class="landing__latest__username">
{{ birthday.username }} {{ birthday.info.name }}
</div> </div>
{% if birthday.hasAge %} {% if age > 0 %}
<div class="landing__latest__joined"> <div class="landing__latest__joined">
Turned {{ birthday.age }} today! Turned {{ age }} today!
</div> </div>
{% endif %} {% endif %}
</div> </div>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
{% elseif latest_user is not null %} {% elseif newest_member is not empty %}
<div class="container landing__container"> <div class="container landing__container">
{{ container_title('<i class="fas fa-user-plus fa-fw"></i> Latest Member') }} {{ container_title('<i class="fas fa-user-plus fa-fw"></i> Newest Member') }}
<a class="landing__latest" style="--user-colour: {{ latest_user.colour }}" href="{{ url('user-profile', {'user': latest_user.id}) }}"> <a class="landing__latest" style="--user-colour: {{ newest_member.colour }}" href="{{ url('user-profile', {'user': newest_member.info.id}) }}">
<div class="landing__latest__avatar">{{ avatar(latest_user.id, 50, latest_user.username) }}</div> <div class="landing__latest__avatar">{{ avatar(newest_member.info.id, 50, newest_member.info.name) }}</div>
<div class="landing__latest__content"> <div class="landing__latest__content">
<div class="landing__latest__username"> <div class="landing__latest__username">
{{ latest_user.username }} {{ newest_member.info.name }}
</div> </div>
<div class="landing__latest__joined"> <div class="landing__latest__joined">
Joined <time datetime="{{ latest_user.createdTime|date('c') }}" title="{{ latest_user.createdTime|date('r') }}">{{ latest_user.createdTime|time_format }}</time> Joined <time datetime="{{ newest_member.info.createdTime|date('c') }}" title="{{ newest_member.info.createdTime|date('r') }}">{{ newest_member.info.createdTime|time_format }}</time>
</div> </div>
</div> </div>
</a> </a>

Some files were not shown because too many files have changed in this diff Show more