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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,6 @@
namespace Misuzu;
use RuntimeException;
use Misuzu\Users\User;
// 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
@ -18,9 +17,8 @@ if(!CSRF::validateRequest()) {
return;
}
$currentUserInfo = User::getCurrent();
if($currentUserInfo === null) {
echo render_info('You must be logged in to manage comments.', 401);
if(!$msz->isLoggedIn()) {
echo render_info('You must be logged in to manage comments.', 403);
return;
}
@ -29,8 +27,9 @@ if($msz->hasActiveBan()) {
return;
}
$comments = $msz->getComments();
$currentUserInfo = $msz->getActiveUser();
$comments = $msz->getComments();
$commentPerms = perms_for_comments($currentUserInfo->getId());
$commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
@ -127,7 +126,7 @@ switch($commentMode) {
break;
}
$isOwnComment = $commentInfo->getUserId() === (string)$currentUserInfo->getId();
$isOwnComment = $commentInfo->getUserId() === $currentUserInfo->getId();
$isModAction = $commentPerms['can_delete_any'] && !$isOwnComment;
if(!$isModAction && !$isOwnComment) {

View file

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

View file

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

View file

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

View file

@ -1,21 +1,19 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
$postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
$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);
return;
}
$currentUser = User::getCurrent();
$currentUserId = $currentUser === null ? 0 : $currentUser->getId();
$currentUser = $msz->getActiveUser();
$currentUserId = $currentUser === null ? '0' : $currentUser->getId();
if($postMode !== '' && $msz->hasActiveBan()) {
echo render_info('You have been banned, check your profile for more information.', 403);

View file

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

View file

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

View file

@ -5,9 +5,8 @@ use DateTimeInterface;
use RuntimeException;
use Index\DateTime;
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);
return;
}
@ -115,4 +114,5 @@ Template::render('manage.changelog.change', [
'change_info' => $changeInfo ?? null,
'change_tags' => $changeTags,
'change_actions' => $changeActions,
'change_author_id' => $msz->getActiveUser()->getId(),
]);

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}
@ -20,6 +19,7 @@ if(!$changelogPagination->hasValidOffset()) {
$changeInfos = $changelog->getAllChanges(withTags: true, pagination: $changelogPagination);
$changes = [];
$userInfos = [];
$userColours = [];
foreach($changeInfos as $changeInfo) {
$userId = $changeInfo->getUserId();
@ -28,7 +28,8 @@ foreach($changeInfos as $changeInfo) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
$userInfo = $users->getUser($userId, 'id');
$userColours[$userId] = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
}
@ -39,6 +40,7 @@ foreach($changeInfos as $changeInfo) {
$changes[] = [
'change' => $changeInfo,
'user' => $userInfo,
'user_colour' => $userColours[$userId] ?? \Index\Colour\Colour::none(),
];
}

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}

View file

@ -1,9 +1,7 @@
<?php
namespace Misuzu;
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);
return;
}

View file

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

View file

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

View file

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

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}

View file

@ -2,13 +2,13 @@
namespace Misuzu;
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);
return;
}
$users = $msz->getUsers();
$auditLog = $msz->getAuditLog();
$pagination = new Pagination($auditLog->countLogs(), 50);
@ -19,15 +19,20 @@ if(!$pagination->hasValidOffset()) {
$logs = $auditLog->getLogs(pagination: $pagination);
$userInfos = [];
$userColours = [];
foreach($logs as $log)
if($log->hasUserId()) {
$userId = $log->getUserId();
if(!array_key_exists($userId, $userInfos))
$userInfos[$userId] = User::byId($userId);
if(!array_key_exists($userId, $userInfos)) {
$userInfos[$userId] = $users->getUser($userId, 'id');
$userColours[$userId] = $users->getUserColour($userInfos[$userId]);
}
}
Template::render('manage.general.logs', [
'global_logs' => $logs,
'global_logs_pagination' => $pagination,
'global_logs_users' => $userInfos,
'global_logs_users_colours' => $userColours,
]);

View file

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

View file

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

View file

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

View file

@ -1,9 +1,7 @@
<?php
namespace Misuzu;
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);
return;
}

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}
@ -40,7 +39,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$body = trim((string)filter_input(INPUT_POST, 'np_body'));
if($isNew) {
$postInfo = $news->createPost($category, $title, $body, $featured, User::getCurrent());
$postInfo = $news->createPost($category, $title, $body, $featured, $msz->getActiveUser());
} else {
if($category === $postInfo->getCategoryId())
$category = null;

View file

@ -1,9 +1,7 @@
<?php
namespace Misuzu;
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);
return;
}

View file

@ -4,9 +4,8 @@ namespace Misuzu;
use DateTimeInterface;
use RuntimeException;
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);
return;
}
@ -29,14 +28,16 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete'))
return;
}
$users = $msz->getUsers();
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) {
echo render_error(404);
return;
}
$modInfo = User::getCurrent();
$modInfo = $msz->getActiveUser();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT);

View file

@ -2,23 +2,28 @@
namespace Misuzu;
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);
return;
}
$users = $msz->getUsers();
$userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(),
$msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
];
$filterUser = null;
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 {
$filterUser = User::byId($filterUserId);
$userInfos[(string)$filterUser->getId()] = $filterUser;
$filterUser = $users->getUser($filterUserId, 'id');
$userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) {
echo render_error(404);
return;
@ -40,19 +45,34 @@ foreach($banInfos as $banInfo) {
if(array_key_exists($banInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$banInfo->getUserId()];
else
$userInfos[$banInfo->getUserId()] = $userInfo = User::byId((int)$banInfo->getUserId());
$userInfos[$banInfo->getUserId()] = $userInfo = $users->getUser($banInfo->getUserId(), 'id');
if(!$banInfo->hasModId())
$modInfo = null;
elseif(array_key_exists($banInfo->getModId(), $userInfos))
$modInfo = $userInfos[$banInfo->getModId()];
if(array_key_exists($userInfo->getId(), $userColours))
$userColour = $userColours[$userInfo->getId()];
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[] = [
'info' => $banInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'mod' => $modInfo,
'mod_colour' => $modColour,
];
}

View file

@ -1,21 +1,41 @@
<?php
namespace Misuzu;
use Misuzu\Users\User;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS)) {
if(!$msz->isLoggedIn() || !perms_check_user(MSZ_PERMS_USER, $msz->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_USERS)) {
echo render_error(403);
return;
}
$pagination = new Pagination(User::countAll(true), 30);
$users = $msz->getUsers();
$roles = $msz->getRoles();
$pagination = new Pagination($users->countUsers(), 30);
if(!$pagination->hasValidOffset()) {
echo render_error(404);
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', [
'manage_users' => User::all(true, $pagination),
'manage_users' => $userList,
'manage_users_pagination' => $pagination,
]);

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}
@ -17,19 +16,20 @@ if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId)) {
return;
}
$users = $msz->getUsers();
$modNotes = $msz->getModNotes();
if($hasUserId) {
$isNew = true;
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) {
echo render_error(404);
return;
}
$authorInfo = User::getCurrent();
$authorInfo = $msz->getActiveUser();
} elseif($hasNoteId) {
$isNew = false;
@ -49,8 +49,8 @@ if($hasUserId) {
return;
}
$userInfo = User::byId((int)$noteInfo->getUserId());
$authorInfo = $noteInfo->hasAuthorId() ? User::byId((int)$noteInfo->getAuthorId()) : null;
$userInfo = $users->getUser($noteInfo->getUserId(), 'id');
$authorInfo = $noteInfo->hasAuthorId() ? $users->getUser($noteInfo->getAuthorId(), 'id') : null;
}
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
@ -83,5 +83,7 @@ Template::render('manage.users.note', [
'note_new' => $isNew,
'note_info' => $noteInfo ?? null,
'note_user' => $userInfo,
'note_user_colour' => $users->getUserColour($userInfo),
'note_author' => $authorInfo,
'note_author_colour' => $users->getUserColour($authorInfo),
]);

View file

@ -2,23 +2,28 @@
namespace Misuzu;
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);
return;
}
$users = $msz->getUsers();
$userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(),
$msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
];
$filterUser = null;
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 {
$filterUser = User::byId($filterUserId);
$userInfos[(string)$filterUser->getId()] = $filterUser;
$filterUser = $users->getUser($filterUserId, 'id');
$userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) {
echo render_error(404);
return;
@ -40,19 +45,34 @@ foreach($noteInfos as $noteInfo) {
if(array_key_exists($noteInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$noteInfo->getUserId()];
else
$userInfos[$noteInfo->getUserId()] = $userInfo = User::byId((int)$noteInfo->getUserId());
$userInfos[$noteInfo->getUserId()] = $userInfo = $users->getUser($noteInfo->getUserId(), 'id');
if(!$noteInfo->hasAuthorId())
$authorInfo = null;
elseif(array_key_exists($noteInfo->getAuthorId(), $userInfos))
$authorInfo = $userInfos[$noteInfo->getAuthorId()];
if(array_key_exists($userInfo->getId(), $userColours))
$userColour = $userColours[$userInfo->getId()];
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[] = [
'info' => $noteInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'author' => $authorInfo,
'author_colour' => $authorColour,
];
}

View file

@ -4,13 +4,13 @@ namespace Misuzu;
use RuntimeException;
use Index\Colour\Colour;
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);
return;
}
$users = $msz->getUsers();
$roles = $msz->getRoles();
if(filter_has_var(INPUT_GET, 'r')) {
@ -25,15 +25,16 @@ if(filter_has_var(INPUT_GET, 'r')) {
}
} else $isNew = true;
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
$currentUser = $msz->getActiveUser();
$canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_PERMS);
if($canEditPerms)
$permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0));
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.';
break;
}
@ -62,7 +63,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
'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.';
break;
}

View file

@ -1,9 +1,7 @@
<?php
namespace Misuzu;
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);
return;
}

View file

@ -5,7 +5,7 @@ use RuntimeException;
use Index\Colour\Colour;
use Misuzu\Users\User;
if(!User::hasCurrent()) {
if(!$msz->isLoggedIn()) {
echo render_error(403);
return;
}
@ -13,15 +13,15 @@ if(!User::hasCurrent()) {
$users = $msz->getUsers();
$roles = $msz->getRoles();
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$currentUser = $msz->getActiveUser();
$canManageUsers = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_USERS);
$canManagePerms = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
$canManageNotes = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_NOTES);
$canManageWarnings = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_WARNINGS);
$canManageBans = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_BANS);
$canImpersonate = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_IMPERSONATE);
$canManageUsers = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_USERS);
$canManagePerms = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_PERMS);
$canManageNotes = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_NOTES);
$canManageWarnings = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_WARNINGS);
$canManageBans = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_MANAGE_BANS);
$canImpersonate = perms_check_user(MSZ_PERMS_USER, $currentUser->getId(), MSZ_PERM_USER_IMPERSONATE);
$canSendTestMail = $currentUser->isSuperUser();
$hasAccess = $canManageUsers || $canManageNotes || $canManageWarnings || $canManageBans;
if(!$hasAccess) {
@ -33,13 +33,16 @@ $notices = [];
$userId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
try {
$userInfo = User::byId($userId);
$userInfo = $users->getUser($userId, 'id');
} catch(RuntimeException $ex) {
echo render_error(404);
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;
$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') {
$notices[] = 'You didn\'t say the magic word.';
} else {
$allowToImpersonate = $currentUser->isSuper();
$allowToImpersonate = $currentUser->isSuperUser();
if(!$allowToImpersonate) {
$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) {
$msz->createAuditLog('USER_IMPERSONATE', [$userInfo->getId(), $userInfo->getUsername()]);
$msz->createAuditLog('USER_IMPERSONATE', [$userInfo->getId(), $userInfo->getName()]);
$authToken->setImpersonatedUserId($userInfo->getId());
$authToken->applyCookie();
url_redirect('index');
@ -68,13 +71,13 @@ if(CSRF::validateRequest() && $canEdit) {
}
if(!empty($_POST['send_test_email'])) {
if(!$currentUser->isSuper()) {
if(!$canSendTestMail) {
$notices[] = 'You must be a super user to do this.';
} elseif(!is_string($_POST['send_test_email']) || $_POST['send_test_email'] !== 'yes_send_it') {
$notices[] = 'Invalid request thing shut the fuck up.';
} else {
$testMail = Mailer::sendMessage(
[$userInfo->getEMailAddress() => $userInfo->getUsername()],
[$userInfo->getEMailAddress() => $userInfo->getName()],
'Flashii Test E-mail',
'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 = [];
foreach($existingRoles as $roleInfo) {
if($roleInfo->isDefault() || !($currentUser->isSuper() || $currentUser->getRank() > $roleInfo->getRank()))
if($roleInfo->isDefault() || !($currentUser->isSuperUser() || $userRank > $roleInfo->getRank()))
continue;
if(!in_array($roleInfo->getId(), $applyRoles))
@ -119,7 +122,7 @@ if(CSRF::validateRequest() && $canEdit) {
continue;
}
if(!$currentUser->isSuper() && $currentUser->getRank() <= $roleInfo->getRank())
if(!$currentUser->isSuperUser() && $userRank <= $roleInfo->getRank())
continue;
if(!in_array($roleInfo, $existingRoles))
@ -152,10 +155,9 @@ if(CSRF::validateRequest() && $canEdit) {
$users->updateUser(
userInfo: $userInfo,
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))
$userInfo->setColour($setColour);
$users->updateUser(userInfo: $userInfo, colour: $setColour);
}
if(!empty($_POST['password']) && is_array($_POST['password'])) {
@ -182,13 +184,10 @@ if(CSRF::validateRequest() && $canEdit) {
elseif(!empty(User::validatePassword($passwordNewValue)))
$notices[] = 'New password is too weak.';
else
$userInfo->setPassword($passwordNewValue);
$users->updateUser(userInfo: $userInfo, password: $passwordNewValue);
}
}
if(empty($notices))
$userInfo->save();
if($canEditPerms && !empty($_POST['perms']) && is_array($_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
$permissions = manage_perms_list(perms_get_user_raw($userId));
}
url_redirect('manage-user', ['user' => $userInfo->getId()]);
return;
}
$rolesAll = $roles->getRoles();
@ -219,5 +221,6 @@ Template::render('manage.users.user', [
'can_manage_warnings' => $canManageWarnings,
'can_manage_bans' => $canManageBans,
'can_impersonate' => $canImpersonate,
'can_send_test_mail' => $canSendTestMail,
'permissions' => $permissions ?? [],
]);

View file

@ -2,9 +2,8 @@
namespace Misuzu;
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);
return;
}
@ -27,14 +26,16 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete'))
return;
}
$users = $msz->getUsers();
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) {
echo render_error(404);
return;
}
$modInfo = User::getCurrent();
$modInfo = $msz->getActiveUser();
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$body = trim((string)filter_input(INPUT_POST, 'uw_body'));

View file

@ -2,23 +2,28 @@
namespace Misuzu;
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);
return;
}
$users = $msz->getUsers();
$userInfos = [
(string)User::getCurrent()->getId() => User::getCurrent(),
$msz->getActiveUser()->getId() => $msz->getActiveUser(),
];
$userColours = [
$msz->getActiveUser()->getId() => $users->getUserColour($msz->getActiveUser()),
];
$filterUser = null;
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 {
$filterUser = User::byId($filterUserId);
$userInfos[(string)$filterUser->getId()] = $filterUser;
$filterUser = $users->getUser($filterUserId, 'id');
$userInfos[$filterUserId] = $filterUser;
$userColours[$filterUserId] = $users->getUserColour($filterUser);
} catch(RuntimeException $ex) {
echo render_error(404);
return;
@ -40,19 +45,34 @@ foreach($warnInfos as $warnInfo) {
if(array_key_exists($warnInfo->getUserId(), $userInfos))
$userInfo = $userInfos[$warnInfo->getUserId()];
else
$userInfos[$warnInfo->getUserId()] = $userInfo = User::byId((int)$warnInfo->getUserId());
$userInfos[$warnInfo->getUserId()] = $userInfo = $users->getUser($warnInfo->getUserId(), 'id');
if(!$warnInfo->hasModId())
$modInfo = null;
elseif(array_key_exists($warnInfo->getModId(), $userInfos))
$modInfo = $userInfos[$warnInfo->getModId()];
if(array_key_exists($userInfo->getId(), $userColours))
$userColour = $userColours[$userInfo->getId()];
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[] = [
'info' => $warnInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'mod' => $modInfo,
'mod_colour' => $modColour,
];
}

View file

@ -2,8 +2,15 @@
namespace Misuzu;
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();
$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'));
$orderDirs = [
'asc' => 'Ascending',
'desc' => 'Descending',
'asc' => 'In Order',
'desc' => 'Reverse Order',
];
$defaultOrder = 'last-online';
$defaultOrder = 'active';
$orderFields = [
'id' => [
'column' => 'u.`user_id`',
'default-dir' => 'asc',
'title' => 'User ID',
],
'name' => [
'column' => 'u.`username`',
'default-dir' => 'asc',
'title' => 'Username',
],
'country' => [
'column' => 'u.`user_country`',
'default-dir' => 'asc',
'title' => 'Country',
],
'created' => [
'title' => 'Registration Date',
],
'active' => [
'title' => 'Last Online',
],
'registered' => [
'column' => 'u.`user_created`',
'default-dir' => 'desc',
'alt' => 'created',
'title' => 'Registration Date',
],
'last-online' => [
'column' => 'u.`user_active`',
'default-dir' => 'desc',
'alt' => 'active',
'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)) {
@ -61,14 +56,17 @@ if(empty($orderBy)) {
return;
}
if(array_key_exists('alt', $orderFields[$orderBy]))
$orderBy = $orderFields[$orderBy]['alt'];
if(empty($orderDir)) {
$orderDir = $orderFields[$orderBy]['default-dir'];
$orderDir = 'asc';
} elseif(!array_key_exists($orderDir, $orderDirs)) {
echo render_error(400);
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) {
$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);
$pagination = new Pagination($users->countUsers(roleInfo: $roleInfo, deleted: $deleted), 15);
$getUsers = DB::prepare(sprintf(
'
SELECT
:current_user_id AS `current_user_id`,
u.`user_id`, u.`username`, u.`user_country`,
u.`user_created`, u.`user_active`, r.`role_id`,
COALESCE(u.`user_title`, r.`role_title`) AS `user_title`,
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();
$userList = [];
$userInfos = $users->getUsers(
roleInfo: $roleInfo,
deleted: $deleted,
orderBy: $orderBy,
reverseOrder: $orderDir !== 'asc',
pagination: $pagination,
);
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);
Template::render('user.listing', [
'roles' => $rolesAll,
'role' => $roleInfo,
'users' => $users,
'users' => $userList,
'order_fields' => $orderFields,
'order_directions' => $orderDirs,
'order_field' => $orderBy,

View file

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

View file

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

View file

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

View file

@ -4,18 +4,19 @@ namespace Misuzu;
use ZipArchive;
use Index\XString;
use Index\IO\FileStream;
use Misuzu\Users\User;
use Misuzu\Users\UserInfo;
if(!User::hasCurrent()) {
if(!$msz->isLoggedIn()) {
echo render_error(401);
return;
}
$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;
$userId = $userInfo->getId();
$fields = [];
foreach($fieldInfos as $key => $fieldInfo) {
@ -41,7 +42,7 @@ function db_to_zip(ZipArchive $archive, int $userId, string $baseName, array $fi
$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);
try {
@ -99,18 +100,17 @@ function db_to_zip(ZipArchive $archive, int $userId, string $baseName, array $fi
}
$errors = [];
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$userInfo = $msz->getActiveUser();
if(isset($_POST['action']) && is_string($_POST['action'])) {
if(isset($_POST['password']) && is_string($_POST['password'])
&& ($currentUser->checkPassword($_POST['password'] ?? ''))) {
&& ($userInfo->verifyPassword($_POST['password'] ?? ''))) {
switch($_POST['action']) {
case 'data':
$msz->createAuditLog('PERSONAL_DATA_DOWNLOAD');
$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;
$archive = new ZipArchive;
@ -119,27 +119,27 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
$tmpFiles = [];
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, $currentUserId, '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, $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, $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, $currentUserId, '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, $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, $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, $currentUserId, '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, $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, $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, $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, $currentUserId, '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, $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, $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, $currentUserId, '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, $currentUserId, 'users_roles', ['user_id:s', 'role_id: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, $userInfo, 'auth_tfa', ['user_id:s', 'tfa_token:n', 'tfa_created:t']);
$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, $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, $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, $userInfo, 'comments_votes', ['comment_id:s', 'user_id:s', 'comment_vote: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, $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, $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, $userInfo, '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_track', ['user_id:s', 'topic_id:s', 'forum_id:s', 'track_last_read:t']);
$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, $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, $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, $userInfo, 'profile_fields_values', ['field_id:s', 'user_id:s', 'format_id:s', 'field_value:s']);
$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, $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, $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, $userInfo, 'users_password_resets', ['user_id:s', 'reset_ip:a', 'reset_requested:t', 'verification_code:n']);
$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, $userInfo, 'users_roles', ['user_id:s', 'role_id:s']);
$archive->close();
} finally {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ namespace Misuzu;
use Index\IO\MemoryStream;
use Index\Serialisation\UriBase64;
use Misuzu\Auth\SessionInfo;
use Misuzu\Users\User;
use Misuzu\Users\UserInfo;
/* Map of props
* u - User ID
@ -54,12 +54,11 @@ class AuthToken {
return true;
}
public function getUserId(): int {
$value = (int)$this->getProperty('u');
return $value < 1 ? -1 : $value;
public function getUserId(): string {
return $this->getProperty('u');
}
public function setUserId(int $userId): self {
$this->setProperty('u', (string)$userId);
public function setUserId(string $userId): self {
$this->setProperty('u', $userId);
return $this;
}
@ -177,7 +176,7 @@ class AuthToken {
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->setUserId($userInfo->getId());
$token->setSessionToken($sessionInfo->getToken());
@ -213,23 +212,4 @@ class AuthToken {
setcookie('msz_uid', '', -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\IDbResult;
use Misuzu\Pagination;
use Misuzu\Users\User;
use Misuzu\Users\UserInfo;
class Changelog {
// not a strict list but useful to have
@ -96,12 +96,12 @@ class Changelog {
}
public function countAllChanges(
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null,
?array $tags = null
): int {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds();
@ -149,13 +149,13 @@ class Changelog {
public function getAllChanges(
bool $withTags = false,
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
DateTime|int|null $dateTime = null,
?array $tags = null,
?Pagination $pagination = null
): array {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($dateTime instanceof DateTime)
$dateTime = $dateTime->getUnixTimeSeconds();
@ -224,13 +224,13 @@ class Changelog {
string|int $action,
string $summary,
string $body = '',
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null
): ChangeInfo {
if(is_string($action))
$action = self::convertToActionId($action);
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds();
@ -268,7 +268,7 @@ class Changelog {
?string $summary = null,
?string $body = null,
bool $updateUserInfo = false,
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
DateTime|int|null $createdAt = null
): void {
if($infoOrId instanceof ChangeInfo)
@ -276,8 +276,8 @@ class Changelog {
if(is_string($action))
$action = self::convertToActionId($action);
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($createdAt instanceof DateTime)
$createdAt = $createdAt->getUnixTimeSeconds();

View file

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

View file

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

View file

@ -3,24 +3,26 @@ namespace Misuzu\Comments;
use stdClass;
use RuntimeException;
use Misuzu\Users\User;
use Misuzu\MisuzuContext;
use Misuzu\Users\Users;
class CommentsEx {
private Comments $comments;
private array $userInfos;
public function __construct(Comments $comments, array $userInfos = []) {
$this->comments = $comments;
$this->userInfos = $userInfos;
}
public function __construct(
private MisuzuContext $context,
private Comments $comments,
private Users $users,
private array $userInfos = [],
private array $userColours = []
) {}
public function getCommentsForLayout(CommentsCategoryInfo|string $category): object {
$info = new stdClass;
if(is_string($category))
$category = $this->comments->ensureCategory($category);
$hasUser = User::hasCurrent();
$info->user = $hasUser ? User::getCurrent() : null;
$hasUser = $this->context->isLoggedIn();
$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->category = $category;
$info->posts = [];
@ -37,20 +39,28 @@ class CommentsEx {
$userId = $postInfo->getUserId();
if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId];
$userColour = $this->userColours[$userId];
} else {
try {
$userInfo = User::byId($userId);
$userInfo = $this->users->getUser($userId, 'id');
$userColour = $this->users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
$userColour = null;
}
$this->userInfos[$userId] = $userInfo;
$this->userColours[$userId] = $userColour;
}
} else $userInfo = null;
} else {
$userInfo = null;
$userColour = null;
}
$info = new stdClass;
$info->post = $postInfo;
$info->user = $userInfo;
$info->colour = $userColour;
$info->vote = $this->comments->getPostVote($postInfo, $userInfo);
$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 {
$getPostingInfo = \Misuzu\DB::prepare('
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`)
FROM `msz_forum_posts`
@ -440,6 +441,8 @@ function forum_posting_info(int $userId): array {
LIMIT 1
) AS `user_post_parse`
FROM `msz_users` as u
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
WHERE `user_id` = :user_id
');
$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');
}
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)
return null;
@ -541,3 +546,41 @@ function forum_get_user_most_active_category_info(int $userId): ?object {
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 Misuzu\GitInfo;
use Misuzu\Users\User;
use Misuzu\Users\UserInfo;
use Misuzu\Users\Assets\StaticUserImageAsset;
use Misuzu\Users\Assets\UserImageAssetInterface;
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 {
private function canViewAsset($request, User $assetUser): bool {
private function canViewAsset($request, UserInfo $assetUser): bool {
return !$this->context->hasActiveBan($assetUser) || (
User::hasCurrent()
$this->context->isLoggedIn()
&& 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);
try {
$userInfo = User::byId($userId);
$userInfo = $this->context->getUsers()->getUser($userId, 'id');
if(!$this->canViewAsset($request, $userInfo)) {
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/banned-avatar.png', MSZ_PUBLIC);
} elseif($userInfo->hasAvatar()) {
$assetInfo = $userInfo->getAvatarInfo();
} else {
$userAssetInfo = new UserAvatarAsset($userInfo);
if($userAssetInfo->isPresent())
$assetInfo = $userAssetInfo;
}
} catch(RuntimeException $ex) {}
@ -68,15 +72,21 @@ final class AssetsHandler extends Handler {
return 404;
try {
$userInfo = User::byId($userId);
$userInfo = $this->context->getUsers()->getUser($userId, 'id');
} 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('');
return 404;
}
$this->serveUserAsset($response, $request, $userInfo->getBackgroundInfo());
$this->serveUserAsset($response, $request, $assetInfo);
}
public function serveLegacy($response, $request) {

View file

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

View file

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

View file

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

View file

@ -13,14 +13,15 @@ use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\News\NewsCategoryInfo;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
final class NewsHandler extends Handler {
private function fetchPostInfo(array $postInfos, array $categoryInfos = []): array {
$news = $this->context->getNews();
$users = $this->context->getUsers();
$comments = $this->context->getComments();
$posts = [];
$userInfos = [];
$userColours = [];
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
@ -28,14 +29,18 @@ final class NewsHandler extends Handler {
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
$userColour = $userColours[$userId];
} else {
try {
$userInfo = User::byId($userId);
$userInfo = $users->getUser($userId, 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
$userColour = null;
}
$userInfos[$userId] = $userInfo;
$userColours[$userId] = $userColour;
}
if(array_key_exists($categoryId, $categoryInfos))
@ -50,6 +55,7 @@ final class NewsHandler extends Handler {
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'comments_count' => $commentsCount,
];
}
@ -111,6 +117,7 @@ final class NewsHandler extends Handler {
public function viewPost($response, $request, string $postId) {
$news = $this->context->getNews();
$users = $this->context->getUsers();
$comments = $this->context->getComments();
try {
@ -137,17 +144,20 @@ final class NewsHandler extends Handler {
}
$userInfo = null;
$userColour = null;
if($postInfo->hasUserId())
try {
$userInfo = User::byId($postInfo->getUserId());
$userInfo = $users->getUser($postInfo->getUserId(), 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {}
$comments = new CommentsEx($comments);
$comments = new CommentsEx($this->context, $comments, $users);
$response->setContent(Template::renderRaw('news.post', [
'post_info' => $postInfo,
'post_category_info' => $categoryInfo,
'post_user_info' => $userInfo,
'post_user_colour' => $userColour,
'comments_info' => $comments->getCommentsForLayout($commentsCategory),
]));
}
@ -170,7 +180,7 @@ final class NewsHandler extends Handler {
$userName = 'Author';
if($userInfo !== null) {
$userId = $userInfo->getId();
$userName = $userInfo->getUsername();
$userName = $userInfo->getName();
}
$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 {
$news = $this->context->getNews();
$users = $this->context->getUsers();
$posts = [];
$userInfos = [];
@ -209,7 +220,7 @@ final class NewsHandler extends Handler {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = User::byId($userId);
$userInfo = $users->getUser($userId, 'id');
} catch(RuntimeException $ex) {
$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\ModNotes;
use Misuzu\Users\Roles;
use Misuzu\Users\User;
use Misuzu\Users\Users;
use Misuzu\Users\UserInfo;
use Misuzu\Users\Warnings;
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigrationRepo;
@ -168,12 +168,61 @@ class MisuzuContext {
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 = [];
public function tryGetActiveBan(User|string|null $userInfo = null): ?BanInfo {
public function tryGetActiveBan(UserInfo|string|null $userInfo = null): ?BanInfo {
if($userInfo === null) {
if(User::hasCurrent())
$userInfo = User::getCurrent();
if($this->isLoggedIn())
$userInfo = $this->getActiveUser();
else return null;
}
@ -184,13 +233,13 @@ class MisuzuContext {
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;
}
public function createAuditLog(string $action, array $params = [], User|string|null $userInfo = null): void {
if($userInfo === null && User::hasCurrent())
$userInfo = User::getCurrent();
public function createAuditLog(string $action, array $params = [], UserInfo|string|null $userInfo = null): void {
if($userInfo === null && $this->isLoggedIn())
$userInfo = $this->getActiveUser();
$this->auditLog->createLog(
$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 {
$this->router = new HttpFx;
$this->router->use('/', function($response) {
@ -263,8 +430,8 @@ class MisuzuContext {
$this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET'));
$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 SatoriRoutes($this->dbConn, $this->config->scopeTo('satori'), $this->router, $this->profileFields);
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->users, $this->profileFields);
}
private function registerLegacyRedirects(): void {

View file

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

View file

@ -7,7 +7,7 @@ use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\Users\User;
use Misuzu\Users\UserInfo;
class ProfileFields {
private DbStatementCache $cache;
@ -141,8 +141,8 @@ class ProfileFields {
return new ProfileFieldFormatInfo($result);
}
public function getFieldValues(User|string $userInfo): array {
if($userInfo instanceof User)
public function getFieldValues(UserInfo|string $userInfo): array {
if($userInfo instanceof UserInfo)
$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
@ -162,11 +162,11 @@ class ProfileFields {
public function getFieldValue(
ProfileFieldInfo|string $fieldInfo,
User|string $userInfo
UserInfo|string $userInfo
): ProfileFieldValueInfo {
if($fieldInfo instanceof ProfileFieldInfo)
$fieldInfo = $fieldInfo->getId();
if($userInfo instanceof User)
if($userInfo instanceof UserInfo)
$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 = ?');
@ -183,7 +183,7 @@ class ProfileFields {
}
public function setFieldValues(
User|string $userInfo,
UserInfo|string $userInfo,
ProfileFieldInfo|string|array $fieldInfos,
string|array $values
): void {
@ -202,7 +202,7 @@ class ProfileFields {
if($fieldsCount !== count($values))
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();
$rows = [];
@ -241,12 +241,12 @@ class ProfileFields {
}
public function removeFieldValues(
User|string $userInfo,
UserInfo|string $userInfo,
ProfileFieldInfo|string|array $fieldInfos
): void {
if(empty($fieldInfos))
return;
if($userInfo instanceof User)
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if(!is_array($fieldInfos))

View file

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

View file

@ -1,7 +1,7 @@
<?php
namespace Misuzu\SharpChat;
use Misuzu\Users\User;
use Misuzu\Users\UserInfo;
final class SharpChatPerms {
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
| 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;
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;
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;
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;
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;
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;
return $perms;

View file

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

View file

@ -5,7 +5,6 @@ use Index\ByteFormat;
use Index\DateTime;
use Index\Environment;
use Misuzu\MisuzuContext;
use Misuzu\Comments\CommentsParser;
use Misuzu\Parsers\Parser;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
@ -24,7 +23,6 @@ final class TwigMisuzu extends AbstractExtension {
new TwigFilter('html_colour', 'html_colour'),
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_comment', fn(string $text): string => CommentsParser::parseForDisplay($text)),
new TwigFilter('perms_check', 'perms_check'),
new TwigFilter('time_format', [$this, 'timeFormat']),
];

View file

@ -2,7 +2,6 @@
namespace Misuzu\Users\Assets;
use Misuzu\Imaging\Image;
use Misuzu\Users\User;
class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterface {
private const FORMAT = 'avatars/%s/%d.msz';
@ -30,10 +29,10 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
}
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 {
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 {
@ -45,10 +44,10 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
}
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 {
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 {
@ -64,11 +63,11 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
}
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 {
$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 {

View file

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

View file

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

View file

@ -8,7 +8,6 @@ use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Misuzu\Pagination;
use Misuzu\Users\User;
class Bans {
public const SEVERITY_MAX = 10;
@ -24,11 +23,11 @@ class Bans {
}
public function countBans(
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
?bool $activeOnly = null
): int {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null;
$hasActiveOnly = $activeOnly !== null;
@ -61,13 +60,13 @@ class Bans {
}
public function getBans(
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
?bool $activeOnly = null,
?bool $activeFirst = null,
?Pagination $pagination = null
): array {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null;
$hasActiveOnly = $activeOnly !== null;
@ -124,11 +123,11 @@ class Bans {
}
public function tryGetActiveBan(
User|string $userInfo,
UserInfo|string $userInfo,
int $minimumSeverity = self::SEVERITY_MIN
): ?BanInfo {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
// 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');
@ -141,19 +140,19 @@ class Bans {
}
public function createBan(
User|string $userInfo,
UserInfo|string $userInfo,
DateTime|int|null $expires,
string $publicReason,
string $privateReason,
int $severity = self::SEVERITY_DEFAULT,
User|string|null $modInfo = null
UserInfo|string|null $modInfo = null
): BanInfo {
if($severity < self::SEVERITY_MIN || $severity > self::SEVERITY_MAX)
throw new InvalidArgumentException('$severity may not be less than -10 or more than 10.');
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($modInfo instanceof User)
$modInfo = (string)$modInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($modInfo instanceof UserInfo)
$modInfo = $modInfo->getId();
if($expires instanceof DateTime)
$expires = $expires->getUnixTimeSeconds();

View file

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

View file

@ -73,6 +73,10 @@ class RoleInfo implements Stringable {
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);
}

View file

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

View file

@ -1,29 +1,10 @@
<?php
namespace Misuzu\Users;
use DateTime;
use DateTimeZone;
use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Colour\Colour;
use Misuzu\DateCheck;
use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\TOTPGenerator;
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 {
public const NAME_MIN_LENGTH = 3; // Minimum username length
@ -33,374 +14,12 @@ class User {
// Minimum amount of unique characters for passwords
public const PASSWORD_UNIQUE = 6;
// Password hashing algorithm
public const PASSWORD_ALGO = PASSWORD_ARGON2ID;
// Maximum length of profile about section
public const PROFILE_ABOUT_MAX_LENGTH = 50000;
// Maximum length of forum signature
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 {
if($name !== trim($name))
return 'trim';
@ -508,217 +127,4 @@ class User {
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;
use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Colour\Colour;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Misuzu\Pagination;
class Users {
//private IDbConnection $dbConn;
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
//$this->dbConn = $dbConn;
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
public function updateUser(
User|string $userInfo,
private const PASSWORD_ALGO = PASSWORD_ARGON2ID;
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
): 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 {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($displayRoleInfo instanceof RoleInfo)
$displayRoleInfo = $displayRoleInfo->getId();
$stmt = $this->cache->get('UPDATE msz_users SET display_role = COALESCE(?, display_role) WHERE user_id = ?');
$stmt->addParameter(1, $displayRoleInfo);
// do sanity checks on values at some point lol
$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->execute();
}
public function hasRole(
User|string $userInfo,
UserInfo|string $userInfo,
RoleInfo|string $roleInfo
): bool {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
@ -43,11 +409,11 @@ class Users {
}
public function hasRoles(
User|string $userInfo,
UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos
): array {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if(!is_array($roleInfos))
$roleInfos = [$roleInfos];
elseif(empty($roleInfos))
@ -81,11 +447,11 @@ class Users {
}
public function addRoles(
User|string $userInfo,
UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos
): void {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if(!is_array($roleInfos))
$roleInfos = [$roleInfos];
elseif(empty($roleInfos))
@ -111,11 +477,11 @@ class Users {
}
public function removeRoles(
User|string $userInfo,
UserInfo|string $userInfo,
RoleInfo|string|array $roleInfos
): void {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if(!is_array($roleInfos))
$roleInfos = [$roleInfos];
elseif(empty($roleInfos))
@ -139,4 +505,39 @@ class Users {
$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\IDbConnection;
use Misuzu\Pagination;
use Misuzu\Users\User;
// 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
@ -24,11 +23,11 @@ class Warnings {
}
public function countWarnings(
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
?int $backlog = null
): int {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null;
$hasBacklog = $backlog !== null;
@ -63,7 +62,7 @@ class Warnings {
}
public function getWarningsWithDefaultBacklog(
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
?Pagination $pagination = null
): array {
return $this->getWarnings(
@ -74,12 +73,12 @@ class Warnings {
}
public function getWarnings(
User|string|null $userInfo = null,
UserInfo|string|null $userInfo = null,
?int $backlog = null,
?Pagination $pagination = null
): array {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null;
$hasBacklog = $backlog !== null;
@ -135,14 +134,14 @@ class Warnings {
}
public function createWarning(
User|string $userInfo,
UserInfo|string $userInfo,
string $body,
User|string|null $modInfo
UserInfo|string|null $modInfo
): WarningInfo {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($modInfo instanceof User)
$modInfo = (string)$modInfo->getId();
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($modInfo instanceof UserInfo)
$modInfo = $modInfo->getId();
$stmt = $this->cache->get('INSERT INTO msz_users_warnings (user_id, mod_id, warn_body) VALUES (?, ?, ?)');
$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);
}
function perms_get_user(int $user): array {
if($user < 1) {
function perms_get_user(int|string $user): array {
if(is_string($user))
$user = (int)$user;
if($user < 1)
return perms_get_blank();
}
static $memo = [];
@ -128,10 +129,11 @@ function perms_get_user(int $user): array {
return $memo[$user] = $getPerms->fetch();
}
function perms_delete_user(int $user): bool {
if($user < 1) {
function perms_delete_user(int|string $user): bool {
if(is_string($user))
$user = (int)$user;
if($user < 1)
return false;
}
$deletePermissions = \Misuzu\DB::prepare('
DELETE FROM `msz_permissions`
@ -167,10 +169,11 @@ function perms_get_role(int $role): array {
return $memo[$role] = $getPerms->fetch();
}
function perms_get_user_raw(int $user): array {
if($user < 1) {
function perms_get_user_raw(int|string $user): array {
if(is_string($user))
$user = (int)$user;
if($user < 1)
return perms_create();
}
$getPerms = \Misuzu\DB::prepare(sprintf('
SELECT `%s`
@ -188,10 +191,11 @@ function perms_get_user_raw(int $user): array {
return $perms;
}
function perms_set_user_raw(int $user, array $perms): bool {
if($user < 1) {
function perms_set_user_raw(int|string $user, array $perms): bool {
if(is_string($user))
$user = (int)$user;
if($user < 1)
return false;
}
$realPerms = perms_create();
$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;
}
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);
}
@ -257,13 +263,13 @@ function perms_check_bulk(int $perms, array $set, bool $strict = false): array {
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;
return perms_check_bulk($perms, $set, $strict);
}
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_delete' => MSZ_PERM_COMMENTS_DELETE_OWN | 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="avatar comment__avatar">
{{ avatar(user.id, reply_mode ? 40 : 50, user.username) }}
{{ avatar(user.id, reply_mode ? 40 : 50, user.name) }}
</div>
<div class="comment__content">
<textarea
@ -40,7 +40,7 @@
</form>
{% endmacro %}
{% macro comments_entry(comment, indent, category, user, perms) %}
{% macro comments_entry(comment, indent, category, user, colour, perms) %}
{% from 'macros.twig' import avatar %}
{% from '_layout/input.twig' import input_checkbox_raw %}
@ -72,7 +72,7 @@
</div>
{% else %}
<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>
{% endif %}
<div class="comment__content">
@ -80,7 +80,7 @@
{% if not hide_details %}
<a class="comment__user comment__user--link"
href="{{ url('user-profile', {'user': poster.id}) }}"
style="--user-colour: {{ poster.colour}}">{{ poster.username }}</a>
style="--user-colour: {{ colour }}">{{ poster.name }}</a>
{% endif %}
<a class="comment__link" href="#comment-{{ comment.id }}">
<time class="comment__date"
@ -102,7 +102,7 @@
{% endif %}
</div>
<div class="comment__text">
{{ hide_details ? '(deleted)' : body|parse_comment|raw }}
{{ hide_details ? '(deleted)' : body }}
</div>
<div class="comment__actions">
{% if not comment.deleted and user is not null %}
@ -152,7 +152,7 @@
{% endif %}
{% if replies|length > 0 %}
{% for reply in replies %}
{{ comments_entry(reply, indent + 1, category, user, perms) }}
{{ comments_entry(reply, indent + 1, category, user, colour, perms) }}
{% endfor %}
{% endif %}
</div>
@ -162,6 +162,7 @@
{% macro comments_section(category) %}
{% set user = category.user %}
{% set colour = category.colour %}
{% set posts = category.posts %}
{% set perms = category.perms %}
{% set category = category.category %}
@ -206,7 +207,7 @@
{% if posts|length > 0 %}
{% from _self import comments_entry %}
{% for comment in posts %}
{{ comments_entry(comment, 1, category, user, perms) }}
{{ comments_entry(comment, 1, category, user, colour, perms) }}
{% endfor %}
{% else %}
<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>
{% 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>
{% if constant('MSZ_DEBUG') or current_user.super|default(false) %}
{% if display_debug_info %}
{% set sql_query_count = sql_query_count() %}
/ 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>)

View file

@ -4,10 +4,10 @@
{% if current_user_real is defined %}
<div class="impersonate">
<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">
<div class="avatar impersonate-user-avatar">{{ avatar(current_user_real.id, 20, current_user_real.username) }}</div>
{{ current_user_real.username }}
<div class="avatar impersonate-user-avatar">{{ avatar(current_user_real.id, 20, current_user_real.name) }}</div>
{{ current_user_real.name }}
</a>
</div>
<div class="impersonate-options">
@ -26,40 +26,34 @@
</a>
<div class="header__desktop__menus">
{% for item in site_menu %}
{% if item.display is not defined or item.display %}
<div class="header__desktop__menu">
<a href="{{ item.url }}" class="header__desktop__link header__desktop__menu__link">{{ item.title }}</a>
{% for item in header_menu %}
<div class="header__desktop__menu">
<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 %}
<div class="header__desktop__submenu">
<div class="header__desktop__submenu__background"></div>
<div class="header__desktop__submenu__content">
{% 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>
{% endif %}
{% endfor %}
</div>
{% if item.menu is defined and item.menu is iterable %}
<div class="header__desktop__submenu">
<div class="header__desktop__submenu__background"></div>
<div class="header__desktop__submenu__content">
{% for subitem in item.menu %}
<a href="{{ subitem.url }}" class="header__desktop__link header__desktop__submenu__link">{{ subitem.title }}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<div class="header__desktop__user">
{% 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('') }}">
<i class="{{ item.icon }}"></i>
</a>
{% endif %}
<a href="{{ item.url }}" title="{{ item.title }}" class="header__desktop__user__button {{ item.class|default('') }}">
<i class="{{ item.icon }}"></i>
</a>
{% endfor %}
{% 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 }}">
{{ avatar(current_user.id, 60, current_user.username) }}
<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.name) }}
</a>
{% else %}
<a href="{{ url('auth-login') }}" class="avatar header__desktop__user__avatar">
@ -80,7 +74,7 @@
</a>
<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>
</div>
@ -90,26 +84,20 @@
<div class="header__mobile__user">
{% 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('') }}">
<i class="{{ item.icon }}"></i> {{ item.title }}
</a>
{% endif %}
<a href="{{ item.url }}" class="header__mobile__link header__mobile__link--user {{ item.class|default('') }}">
<i class="{{ item.icon }}"></i> {{ item.title }}
</a>
{% endfor %}
</div>
<div class="header__mobile__navigation">
{% for item in site_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>
{% for item in header_menu %}
<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 %}
{% 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>
{% endif %}
{% endfor %}
{% endif %}
{% if item.menu is defined and item.menu is iterable %}
{% for subitem in item.menu %}
<a href="{{ subitem.url }}" class="header__mobile__link">{{ subitem.title }}</a>
{% endfor %}
{% endif %}
{% endfor %}
</div>

View file

@ -6,7 +6,7 @@
{% block content %}
<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_csrf() }}

View file

@ -18,18 +18,18 @@
</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__background"></div>
<div class="changelog__change__info__content">
{% if change_user_info.id|default(null) is not null %}
<div class="changelog__change__user">
<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>
<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>
</div>
</div>

View file

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

View file

@ -26,6 +26,7 @@
{% macro changelog_entry(change, is_small, is_manage) %}
{% set user = change.user|default(null) %}
{% set user_colour = change.user_colour|default(null) %}
{% if change.change is defined %}
{% set change = change.change %}
{% endif %}
@ -57,9 +58,9 @@
{% if not is_small %}
<a class="changelog__entry__user"
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">
{{ user.username|default('Anonymous') }}
{{ user.name|default('Anonymous') }}
</div>
</a>
{% 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',
'url': url('forum-mark-single', {'forum': forum_info.forum_id}),
'display': current_user is defined,
'display': forum_show_mark_as_read,
'method': 'POST',
}
]) }}

View file

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

View file

@ -43,13 +43,13 @@
</div>
{% 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__background"></div>
<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="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) }}
{{ topic_notice|raw }}
{{ 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_notice|raw }}
{{ forum_header('', topic_breadcrumbs) }}

View file

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

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