Rewrote roles code to be OOP.

This commit is contained in:
flash 2020-06-04 18:48:01 +00:00
parent 3521d06cef
commit 0bf8fde6c2
24 changed files with 652 additions and 573 deletions

View file

@ -80,7 +80,6 @@ require_once 'src/Forum/topic.php';
require_once 'src/Forum/validate.php';
require_once 'src/Users/avatar.php';
require_once 'src/Users/background.php';
require_once 'src/Users/role.php';
require_once 'src/Users/user_legacy.php';
$dbConfig = parse_ini_file(MSZ_ROOT . '/config/config.ini', true, INI_SCANNER_TYPED);

View file

@ -4,7 +4,9 @@ namespace Misuzu;
use Misuzu\Net\IPAddress;
use Misuzu\Net\IPAddressBlacklist;
use Misuzu\Users\User;
use Misuzu\Users\UserCreationFailedException;
use Misuzu\Users\UserLoginAttempt;
use Misuzu\Users\UserRole;
use Misuzu\Users\UserSession;
use Misuzu\Users\UserWarning;
@ -50,42 +52,38 @@ while(!$restricted && !empty($register)) {
}
$usernameValidation = User::validateUsername($register['username']);
if($usernameValidation !== '') {
if($usernameValidation !== '')
$notices[] = User::usernameValidationErrorString($usernameValidation);
}
$emailValidation = User::validateEMailAddress($register['email']);
if($emailValidation !== '') {
if($emailValidation !== '')
$notices[] = $emailValidation === 'in-use'
? 'This e-mail address has already been used!'
: 'The e-mail address you entered is invalid!';
}
if($register['password_confirm'] !== $register['password']) {
if($register['password_confirm'] !== $register['password'])
$notices[] = 'The given passwords don\'t match.';
}
if(User::validatePassword($register['password']) !== '') {
if(User::validatePassword($register['password']) !== '')
$notices[] = 'Your password is too weak!';
}
if(!empty($notices)) {
if(!empty($notices))
break;
}
$createUser = User::create(
$register['username'],
$register['password'],
$register['email'],
$ipAddress
);
if($createUser === null) {
try {
$createUser = User::create(
$register['username'],
$register['password'],
$register['email'],
$ipAddress
);
} catch(UserCreationFailedException $ex) {
$notices[] = 'Something went wrong while creating your account, please alert an administrator or a developer about this!';
break;
}
user_role_add($createUser->getId(), MSZ_ROLE_MAIN);
$createUser->addRole(UserRole::byDefault());
url_redirect('auth-login-welcome', ['username' => $createUser->getUsername()]);
return;
}

View file

@ -16,7 +16,7 @@ $emoteInfo = !$isNew ? Emoticon::byId($emoteId) : new Emoticon;
if(CSRF::validateRequest() && isset($_POST['emote_order']) && isset($_POST['emote_hierarchy']) && !empty($_POST['emote_url']) && !empty($_POST['emote_strings'])) {
$emoteInfo->setUrl($_POST['emote_url'])
->setHierarchy($_POST['emote_hierarchy'])
->setRank($_POST['emote_hierarchy'])
->setOrder($_POST['emote_order'])
->save();

View file

@ -1,8 +1,9 @@
<?php
// TODO: UNFUCK THIS FILE
namespace Misuzu;
use Misuzu\Users\User;
use Misuzu\Users\UserRole;
use Misuzu\Users\UserRoleNotFoundException;
require_once '../../../misuzu.php';
@ -11,23 +12,28 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->
return;
}
$roleId = $_GET['r'] ?? null;
$currentUserId = User::getCurrent()->getId();
/*$isSuperUser = user_check_super($currentUserId);
$canEdit = $isSuperUser || user_check_authority($currentUserId, $userId);*/
$canEditPerms = /*$canEdit && */perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
$roleId = (int)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT);
if($canEditPerms) {
if($roleId > 0)
try {
$roleInfo = UserRole::byId($roleId);
} catch(UserRoleNotFoundException $ex) {
echo render_error(404);
return;
}
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
if($canEditPerms)
$permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0));
}
if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()) {
$roleHierarchy = (int)($_POST['role']['hierarchy'] ?? -1);
if(!user_check_super($currentUserId) && ($roleId === null
? (user_get_hierarchy($currentUserId) <= $roleHierarchy)
: !user_role_check_authority($currentUserId, $roleId))) {
echo 'Your hierarchy is too low to do this.';
if(!$currentUser->isSuper() && (isset($roleInfo) ? $roleInfo->hasAuthorityOver($currentUser) : $currentUser->getRank() <= $roleHierarchy)) {
echo 'You don\'t hold authority over this role.';
return;
}
@ -63,15 +69,13 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
}
}
$roleDescription = $_POST['role']['description'] ?? null;
$roleTitle = $_POST['role']['title'] ?? null;
$roleDescription = $_POST['role']['description'] ?? '';
$roleTitle = $_POST['role']['title'] ?? '';
if($roleDescription !== null) {
$rdLength = strlen($roleDescription);
if($rdLength < 1) {
$roleDescription = null;
} elseif($rdLength > 1000) {
if($rdLength > 1000) {
echo 'description is too long';
return;
}
@ -80,52 +84,22 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
if($roleTitle !== null) {
$rtLength = strlen($roleTitle);
if($rtLength < 1) {
$roleTitle = null;
} elseif($rtLength > 64) {
if($rtLength > 64) {
echo 'title is too long';
return;
}
}
if($roleId < 1) {
$updateRole = DB::prepare('
INSERT INTO `msz_roles`
(
`role_name`, `role_hierarchy`, `role_hidden`, `role_colour`,
`role_description`, `role_title`
)
VALUES
(
:role_name, :role_hierarchy, :role_hidden, :role_colour,
:role_description, :role_title
)
');
} else {
$updateRole = DB::prepare('
UPDATE `msz_roles`
SET `role_name` = :role_name,
`role_hierarchy` = :role_hierarchy,
`role_hidden` = :role_hidden,
`role_colour` = :role_colour,
`role_description` = :role_description,
`role_title` = :role_title
WHERE `role_id` = :role_id
');
$updateRole->bind('role_id', $roleId);
}
if(!isset($roleInfo))
$roleInfo = new UserRole;
$updateRole->bind('role_name', $roleName);
$updateRole->bind('role_hierarchy', $roleHierarchy);
$updateRole->bind('role_hidden', $roleSecret ? 1 : 0);
$updateRole->bind('role_colour', $roleColour->getRaw());
$updateRole->bind('role_description', $roleDescription);
$updateRole->bind('role_title', $roleTitle);
$updateRole->execute();
if($roleId < 1) {
$roleId = DB::lastId();
}
$roleInfo->setName($roleName)
->setRank($roleHierarchy)
->setHidden($roleSecret)
->setColour($roleColour)
->setDescription($roleDescription)
->setTitle($roleTitle)
->save();
if(!empty($permissions) && !empty($_POST['perms']) && is_array($_POST['perms'])) {
$perms = manage_perms_apply($permissions, $_POST['perms']);
@ -138,7 +112,7 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
VALUES
(:role_id, NULL, :' . implode(', :', $permKeys) . ')
');
$setPermissions->bind('role_id', $roleId);
$setPermissions->bind('role_id', $roleInfo->getId());
foreach($perms as $key => $value) {
$setPermissions->bind($key, $value);
@ -151,41 +125,17 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
WHERE `role_id` = :role_id
AND `user_id` IS NULL
');
$deletePermissions->bind('role_id', $roleId);
$deletePermissions->bind('role_id', $roleInfo->getId());
$deletePermissions->execute();
}
}
url_redirect('manage-role', ['role' => $roleId]);
url_redirect('manage-role', ['role' => $roleInfo->getId()]);
return;
}
if($roleId !== null) {
if($roleId < 1) {
echo 'no';
return;
}
$getEditRole = DB::prepare('
SELECT *
FROM `msz_roles`
WHERE `role_id` = :role_id
');
$getEditRole->bind('role_id', $roleId);
$editRole = $getEditRole->fetch();
if(empty($editRole)) {
echo 'invalid role';
return;
}
Template::set([
'edit_role' => $editRole,
'role_colour' => new Colour($editRole['role_colour']),
]);
}
Template::render('manage.users.role', [
'role_info' => $roleInfo ?? null,
'can_manage_perms' => $canEditPerms,
'permissions' => $permissions ?? [],
]);

View file

@ -2,6 +2,7 @@
namespace Misuzu;
use Misuzu\Users\User;
use Misuzu\Users\UserRole;
require_once '../../../misuzu.php';
@ -10,34 +11,14 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->
return;
}
$manageRolesCount = (int)DB::query('
SELECT COUNT(`role_id`)
FROM `msz_roles`
')->fetchColumn();
$pagination = new Pagination(UserRole::countAll(true), 10);
$rolesPagination = new Pagination($manageRolesCount, 10);
if(!$rolesPagination->hasValidOffset()) {
if(!$pagination->hasValidOffset()) {
echo render_error(404);
return;
}
$getManageRoles = DB::prepare('
SELECT
`role_id`, `role_colour`, `role_name`, `role_title`,
(
SELECT COUNT(`user_id`)
FROM `msz_user_roles` as ur
WHERE ur.`role_id` = r.`role_id`
) as `users`
FROM `msz_roles` as r
LIMIT :offset, :take
');
$getManageRoles->bind('offset', $rolesPagination->getOffset());
$getManageRoles->bind('take', $rolesPagination->getRange());
$manageRoles = $getManageRoles->fetchAll();
Template::render('manage.users.roles', [
'manage_roles' => $manageRoles,
'manage_roles_pagination' => $rolesPagination,
'manage_roles' => UserRole::all(true, $pagination),
'manage_roles_pagination' => $pagination,
]);

View file

@ -2,6 +2,9 @@
namespace Misuzu;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\UserRole;
use Misuzu\Users\UserRoleNotFoundException;
require_once '../../../misuzu.php';
@ -11,32 +14,28 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->
}
$notices = [];
$userId = (int)($_GET['u'] ?? 0);
$currentUserId = User::getCurrent()->getId();
$userId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
if($userId < 1) {
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {
echo render_error(404);
return;
}
$isSuperUser = user_check_super($currentUserId);
$canEdit = $isSuperUser || user_check_authority($currentUserId, $userId);
$canEdit = $currentUser->hasAuthorityOver($userInfo);
$canEditPerms = $canEdit && perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
$permissions = manage_perms_list(perms_get_user_raw($userId));
if(CSRF::validateRequest() && $canEdit) {
if(!empty($_POST['roles']) && is_array($_POST['roles']) && array_test($_POST['roles'], 'ctype_digit')) {
// Fetch existing roles
$existingRoles = DB::prepare('
SELECT `role_id`
FROM `msz_user_roles`
WHERE `user_id` = :user_id
');
$existingRoles->bind('user_id', $userId);
$existingRoles = $existingRoles->fetchAll();
$existingRoles = $userInfo->getRoles();
// Initialise set array with existing role ids
$setRoles = array_column($existingRoles, 'role_id');
// Initialise set array with existing roles
$setRoles = $existingRoles;
// Read user input array and throw intval on em
$applyRoles = array_apply($_POST['roles'], 'intval');
@ -48,53 +47,33 @@ if(CSRF::validateRequest() && $canEdit) {
// Roles that the current users isn't allowed to touch (hierarchy) will stay.
foreach($setRoles as $role) {
// Also prevent the main role from being removed.
if($role === MSZ_ROLE_MAIN || (!$isSuperUser && !user_role_check_authority($currentUserId, $role))) {
if($role->isDefault() || !$currentUser->hasAuthorityOver($role))
continue;
}
if(!in_array($role, $applyRoles)) {
if(!in_array($role->getId(), $applyRoles))
$removeRoles[] = $role;
}
}
// STEP 2: Purge the ones marked for removal.
$setRoles = array_diff($setRoles, $removeRoles);
// STEP 3: Add roles to the set array from the user input, if the user has authority over the given roles.
foreach($applyRoles as $role) {
if(!$isSuperUser && !user_role_check_authority($currentUserId, $role)) {
foreach($applyRoles as $roleId) {
try {
$role = $existingRoles[$roleId] ?? UserRole::byId($roleId);
} catch(UserRoleNotFoundException $ex) {
continue;
}
if(!in_array($role, $setRoles)) {
if(!$currentUser->hasAuthorityOver($role))
continue;
if(!in_array($role, $setRoles))
$setRoles[] = $role;
}
}
if(!empty($setRoles)) {
// The implode here probably sets off alarm bells, but the array is
// guaranteed to only contain integers so it's probably fine.
$removeRanks = DB::prepare(sprintf('
DELETE FROM `msz_user_roles`
WHERE `user_id` = :user_id
AND `role_id` NOT IN (%s)
', implode(',', $setRoles)));
$removeRanks->bind('user_id', $userId);
$removeRanks->execute();
foreach($removeRoles as $role)
$userInfo->removeRole($role);
$addRank = DB::prepare('
INSERT IGNORE INTO `msz_user_roles`
(`user_id`, `role_id`)
VALUES
(:user_id, :role_id)
');
$addRank->bind('user_id', $userId);
foreach($setRoles as $role) {
$addRank->bind('role_id', $role);
$addRank->execute();
}
}
foreach($setRoles as $role)
$userInfo->addRole($role);
}
$setUserInfo = [];
@ -107,9 +86,9 @@ if(CSRF::validateRequest() && $canEdit) {
$displayRole = (int)($_POST['user']['display_role'] ?? 0);
if(user_role_has($userId, $displayRole)) {
$setUserInfo['display_role'] = $displayRole;
}
try {
$userInfo->setDisplayRole(UserRole::byId($displayRole));
} catch(UserRoleNotFoundException $ex) {}
$usernameValidation = User::validateUsername($setUserInfo['username']);
$emailValidation = User::validateEMailAddress($setUserInfo['email']);
@ -124,19 +103,16 @@ if(CSRF::validateRequest() && $canEdit) {
$notices[] = $emailValidation === 'in-use'
? 'This e-mail address has already been used!'
: 'This e-mail address is invalid!';
} else {
} else
$setUserInfo['email'] = mb_strtolower($setUserInfo['email']);
}
if(!$countryValidation) {
if(!$countryValidation)
$notices[] = 'Country code was invalid.';
}
if(strlen($setUserInfo['user_title']) < 1) {
if(strlen($setUserInfo['user_title']) < 1)
$setUserInfo['user_title'] = null;
} elseif(strlen($setUserInfo['user_title']) > 64) {
elseif(strlen($setUserInfo['user_title']) > 64)
$notices[] = 'User title was invalid.';
}
}
if(!empty($_POST['colour']) && is_array($_POST['colour'])) {
@ -160,13 +136,12 @@ if(CSRF::validateRequest() && $canEdit) {
$passwordConfirmValue = (string)($_POST['password']['confirm'] ?? '');
if(!empty($passwordNewValue)) {
if($passwordNewValue !== $passwordConfirmValue) {
if($passwordNewValue !== $passwordConfirmValue)
$notices[] = 'Confirm password does not match.';
} elseif(!empty(User::validatePassword($passwordNewValue))) {
elseif(!empty(User::validatePassword($passwordNewValue)))
$notices[] = 'New password is too weak.';
} else {
else
$setUserInfo['password'] = User::hashPassword($passwordNewValue);
}
}
}
@ -181,26 +156,22 @@ if(CSRF::validateRequest() && $canEdit) {
));
$userUpdate->bind('set_user_id', $userId);
foreach($setUserInfo as $key => $value) {
foreach($setUserInfo as $key => $value)
$userUpdate->bind($key, $value);
}
if(!$userUpdate->execute()) {
if(!$userUpdate->execute())
$notices[] = 'Something went wrong while updating the user.';
}
}
if($canEditPerms && !empty($_POST['perms']) && is_array($_POST['perms'])) {
$perms = manage_perms_apply($permissions, $_POST['perms']);
if($perms !== null) {
if(!perms_set_user_raw($userId, $perms)) {
if(!perms_set_user_raw($userId, $perms))
$notices[] = 'Failed to update permissions.';
}
} else {
if(!perms_delete_user($userId)) {
if(!perms_delete_user($userId))
$notices[] = 'Failed to remove permissions.';
}
}
// this smells, make it refresh/apply in a non-retarded way
@ -208,45 +179,10 @@ if(CSRF::validateRequest() && $canEdit) {
}
}
$getUser = DB::prepare('
SELECT
u.*,
INET6_NTOA(u.`register_ip`) as `register_ip_decoded`,
INET6_NTOA(u.`last_ip`) as `last_ip_decoded`,
COALESCE(u.`user_colour`, r.`role_colour`) as `colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`
WHERE `user_id` = :user_id
ORDER BY `user_id`
');
$getUser->bind('user_id', $userId);
$manageUser = $getUser->fetch();
if(empty($manageUser)) {
echo render_error(404);
return;
}
$getRoles = DB::prepare('
SELECT
r.`role_id`, r.`role_name`, r.`role_hierarchy`, r.`role_colour`,
(
SELECT COUNT(`user_id`) > 0
FROM `msz_user_roles`
WHERE `role_id` = r.`role_id`
AND `user_id` = :user_id
) AS `has_role`
FROM `msz_roles` AS r
');
$getRoles->bind('user_id', $manageUser['user_id']);
$roles = $getRoles->fetchAll();
Template::render('manage.users.user', [
'manage_user' => $manageUser,
'user_colour' => empty($manageUser['user_colour']) ? Colour::none() : new Colour($manageUser['user_colour']),
'user_info' => $userInfo,
'manage_notices' => $notices,
'manage_roles' => $roles,
'manage_roles' => UserRole::all(true),
'can_edit_user' => $canEdit,
'can_edit_perms' => $canEdit && $canEditPerms,
'permissions' => $permissions ?? [],

View file

@ -17,7 +17,8 @@ if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->
}
$notices = [];
$currentUserId = User::getCurrent()->getId();
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
if(!empty($_POST['lookup']) && is_string($_POST['lookup'])) {
url_redirect('manage-users-warnings', ['user' => user_id_from_username($_POST['lookup'])]);
@ -67,19 +68,19 @@ if(!empty($_POST['warning']) && is_array($_POST['warning'])) {
try {
$warningsUserInfo = User::byId((int)($_POST['warning']['user'] ?? 0));
$warningsUser = $warningsUserInfo->getId();
if(!$currentUser->hasAuthorityOver($warningsUserInfo))
$notices[] = 'You do not have authority over this user.';
} catch(UserNotFoundException $ex) {
$warningsUserInfo = null;
$notices[] = 'This user doesn\'t exist.';
}
if(!user_check_super($currentUserId) && !user_check_authority($currentUserId, $warningsUser)) {
$notices[] = 'You do not have authority over this user.';
}
if(empty($notices) && $warningsUser > 0) {
if(empty($notices) && !empty($warningsUserInfo)) {
try {
$warningInfo = UserWarning::create(
$warningsUserInfo,
User::getCurrent(),
$currentUser,
$warningType,
$warningDuration,
$_POST['warning']['note'],

View file

@ -2,10 +2,12 @@
namespace Misuzu;
use Misuzu\Users\User;
use Misuzu\Users\UserRole;
use Misuzu\Users\UserRoleNotFoundException;
require_once '../misuzu.php';
$roleId = !empty($_GET['r']) && is_string($_GET['r']) ? (int)$_GET['r'] : MSZ_ROLE_MAIN;
$roleId = !empty($_GET['r']) && is_string($_GET['r']) ? (int)$_GET['r'] : UserRole::DEFAULT;
$orderBy = !empty($_GET['ss']) && is_string($_GET['ss']) ? mb_strtolower($_GET['ss']) : '';
$orderDir = !empty($_GET['sd']) && is_string($_GET['sd']) ? mb_strtolower($_GET['sd']) : '';
@ -79,16 +81,16 @@ if(empty($orderDir)) {
$canManageUsers = perms_check_user(MSZ_PERMS_USER, User::hasCurrent() ? User::getCurrent()->getId() : 0, MSZ_PERM_USER_MANAGE_USERS);
$role = user_role_get($roleId);
if(empty($role)) {
try {
$roleInfo = UserRole::byId($roleId);
} catch(UserRoleNotFoundException $ex) {
echo render_error(404);
return;
}
$usersPagination = new Pagination($role['role_user_count'], 15);
$pagination = new Pagination($roleInfo->getUserCount(), 15);
$roles = user_role_all();
$roles = UserRole::all();
$getUsers = DB::prepare(sprintf(
'
@ -148,20 +150,19 @@ $getUsers = DB::prepare(sprintf(
$orderFields[$orderBy]['column'],
$orderDir,
\Misuzu\Users\UserRelation::TYPE_FOLLOW,
$usersPagination->getOffset(),
$usersPagination->getRange()
$pagination->getOffset(),
$pagination->getRange()
));
$getUsers->bind('role_id', $role['role_id']);
$getUsers->bind('role_id', $roleInfo->getId());
$getUsers->bind('current_user_id', User::hasCurrent() ? User::getCurrent()->getId() : 0);
$users = $getUsers->fetchAll();
if(empty($users)) {
if(empty($users))
http_response_code(404);
}
Template::render('user.listing', [
'roles' => $roles,
'role' => $role,
'role' => $roleInfo,
'users' => $users,
'order_fields' => $orderFields,
'order_directions' => $orderDirs,
@ -169,5 +170,5 @@ Template::render('user.listing', [
'order_direction' => $orderDir,
'order_default' => $defaultOrder,
'can_manage_users' => $canManageUsers,
'users_pagination' => $usersPagination,
'users_pagination' => $pagination,
]);

View file

@ -26,20 +26,15 @@ $currentUser = User::getCurrent();
$viewingAsGuest = $currentUser === null;
$currentUserId = $viewingAsGuest ? 0 : $currentUser->getId();
$viewingOwnProfile = $currentUserId === $profileUser->getId();
define('DUMB_SHIT', true);
$isBanned = $profileUser->hasActiveWarning();
$userPerms = perms_get_user($currentUserId)[MSZ_PERMS_USER];
$canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS);
$canEdit = !$isBanned
&& UserSession::hasCurrent()
&& (
$viewingOwnProfile
|| user_check_super($currentUserId)
|| (
perms_check($userPerms, MSZ_PERM_USER_MANAGE_USERS)
&& user_check_authority($currentUserId, $profileUser->getId())
)
);
&& ($viewingOwnProfile || $currentUserId->isSuper() || (
perms_check($userPerms, MSZ_PERM_USER_MANAGE_USERS)
&& $currentUserId->hasAuthorityOver($profileUser)
));
if($isEditing) {
if(!$canEdit) {

View file

@ -4,6 +4,8 @@ namespace Misuzu;
use Misuzu\AuditLog;
use Misuzu\Config;
use Misuzu\Users\User;
use Misuzu\Users\UserRole;
use Misuzu\Users\UserRoleNotFoundException;
use Misuzu\Users\UserSession;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
@ -23,24 +25,25 @@ $twoFactorInfo = user_totp_info($currentUserId);
$isVerifiedRequest = CSRF::validateRequest();
if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
$roleId = (int)($_POST['role']['id'] ?? 0);
try {
$roleInfo = UserRole::byId((int)($_POST['role']['id'] ?? 0));
} catch(UserRoleNotFoundException $ex) {}
if($roleId > 0 && user_role_has($currentUserId, $roleId)) {
if(empty($roleInfo) || !$currentUser->hasRole($roleInfo))
$errors[] = "You're trying to modify a role that hasn't been assigned to you.";
else {
switch($_POST['role']['mode'] ?? '') {
case 'display':
user_role_set_display($currentUserId, $roleId);
$currentUser->setDisplayRole($roleInfo);
break;
case 'leave':
if(user_role_can_leave($roleId)) {
user_role_remove($currentUserId, $roleId);
} else {
if($roleInfo->getCanLeave())
$currentUser->removeRole($roleInfo);
else
$errors[] = "You're not allow to leave this role, an administrator has to remove it for you.";
}
break;
}
} else {
$errors[] = "You're trying to modify a role that hasn't been assigned to you.";
}
}
@ -127,13 +130,9 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
}
}
$userRoles = user_role_all_user($currentUserId);
Template::render('settings.account', [
'errors' => $errors,
'current_email' => $currentUser->getEMailAddress(),
'user_roles' => $userRoles,
'user_display_role' => user_role_get_display($currentUserId),
'settings_user' => $currentUser,
'is_restricted' => $isRestricted,
'settings_2fa_enabled' => $twoFactorInfo['totp_enabled'],
]);

View file

@ -14,7 +14,7 @@ class Colour {
private int $raw = 0;
public function __construct(?int $raw = 0) {
$this->setRaw($raw);
$this->setRaw($raw ?? 0);
}
public static function none(): self {

View file

@ -36,11 +36,11 @@ final class Emoticon {
return $this;
}
public function getHierarchy(): int {
public function getRank(): int {
return $this->emote_hierarchy ?? 0;
}
public function setHierarchy(int $hierarchy): self {
$this->emote_hierarchy = $hierarchy;
public function setRank(int $rank): self {
$this->emote_hierarchy = $rank;
return $this;
}
@ -111,7 +111,7 @@ final class Emoticon {
}
$saved = $save->bind('order', $this->getOrder())
->bind('hierarchy', $this->getHierarchy())
->bind('hierarchy', $this->getRank())
->bind('url', $this->getUrl())
->execute();

7
src/HasRankInterface.php Normal file
View file

@ -0,0 +1,7 @@
<?php
namespace Misuzu;
interface HasRankInterface {
public function getRank(): int;
public function HasAuthorityOver(self $other): bool;
}

View file

@ -107,7 +107,7 @@ final class SockChatHandler extends Handler {
$out[] = [
'Text' => $strings,
'Image' => $emote->getUrl(),
'Hierarchy' => $emote->getHierarchy(),
'Hierarchy' => $emote->getRank(),
];
}
@ -275,7 +275,7 @@ final class SockChatHandler extends Handler {
'user_id' => $userInfo->getId(),
'username' => $userInfo->getUsername(),
'colour_raw' => $userInfo->getColourRaw(),
'hierarchy' => $userInfo->getHierarchy(),
'hierarchy' => $userInfo->getRank(),
'is_silenced' => date('c', $userInfo->isSilenced() || $userInfo->isBanned() ? ($userInfo->isActiveWarningPermanent() ? strtotime('10 years') : $userInfo->getActiveWarningExpiration()) : 0),
'perms' => $perms,
];

View file

@ -3,6 +3,7 @@ namespace Misuzu\Users;
use Misuzu\Colour;
use Misuzu\DB;
use Misuzu\HasRankInterface;
use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\TOTP;
@ -10,8 +11,9 @@ use Misuzu\Net\IPAddress;
class UserException extends UsersException {} // this naming definitely won't lead to confusion down the line!
class UserNotFoundException extends UserException {}
class UserCreationFailedException extends UserException {}
class User {
class User implements HasRankInterface {
public const NAME_MIN_LENGTH = 3; // Minimum username length
public const NAME_MAX_LENGTH = 16; // Maximum username length, unless your name is Flappyzor(WorldwideOnline2018through2019through2020)
public const NAME_REGEX = '[A-Za-z0-9-_]+'; // Username character constraint
@ -22,6 +24,17 @@ class User {
// Password hashing algorithm
public const PASSWORD_ALGO = PASSWORD_ARGON2ID;
// Order constants for ::all function
public const ORDER_ID = 'id';
public const ORDER_NAME = 'name';
public const ORDER_COUNTRY = 'country';
public const ORDER_CREATED = 'registered';
public const ORDER_ACTIVE = 'last-online';
public const ORDER_FORUM_TOPICS = 'forum-topics';
public const ORDER_FORUM_POSTS = 'forum-posts';
public const ORDER_FOLLOWING = 'following';
public const ORDER_FOLLOWERS = 'followers';
// Database fields
// TODO: update all references to use getters and setters and mark all of these as private
public $user_id = -1;
@ -52,7 +65,7 @@ class User {
public const TABLE = 'users';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`user_id`, %1$s.`username`, %1$s.`password`, %1$s.`email`, %1$s.`user_super`, %1$s.`user_title`, '
private const SELECT = '%1$s.`user_id`, %1$s.`username`, %1$s.`password`, %1$s.`email`, %1$s.`user_super`, %1$s.`user_title`'
. ', %1$s.`user_country`, %1$s.`user_colour`, %1$s.`display_role`, %1$s.`user_totp_key`'
. ', %1$s.`user_about_content`, %1$s.`user_about_parser`'
. ', %1$s.`user_signature_content`, %1$s.`user_signature_parser`'
@ -119,7 +132,11 @@ class User {
return get_country_name($this->getCountry());
}
public function getColour(): Colour {
public function getColour(): Colour { // Swaps role colour in if user has no personal colour
// TODO: Check inherit flag and grab role colour instead
return new Colour($this->getColourRaw());
}
public function getUserColour(): Colour { // Only ever gets the user's actual colour
return new Colour($this->getColourRaw());
}
public function getColourRaw(): int {
@ -137,8 +154,23 @@ class User {
return $this->user_active === null ? -1 : $this->user_active;
}
public function getHierarchy(): int {
return ($userId = $this->getId()) < 1 ? 0 : user_get_hierarchy($userId);
private $userRank = null;
public function getRank(): int {
if($this->userRank === null)
$this->userRank = (int)DB::prepare(
'SELECT MAX(`role_hierarchy`)'
. ' FROM `' . DB::PREFIX . UserRole::TABLE . '`'
. ' WHERE `role_id` IN (SELECT `role_id` FROM `' . DB::PREFIX . UserRoleRelation::TABLE . '` WHERE `user_id` = :user)'
)->bind('user', $this->getId())->fetchColumn();
return $this->userRank;
}
public function hasAuthorityOver(HasRankInterface $other): bool {
// Don't even bother checking if we're a super user
if($this->isSuper())
return true;
if($other instanceof self && $other->getId() === $this->getId())
return true;
return $this->getRank() > $other->getRank();
}
public function hasPassword(): bool {
@ -164,6 +196,29 @@ class User {
public function getDisplayRoleId(): int {
return $this->display_role < 1 ? -1 : $this->display_role;
}
public function setDisplayRoleId(int $roleId): self {
$this->display_role = $roleId < 1 ? -1 : $roleId;
// TEMPORARY!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// This DB update statement should be removed when a global update/save/whatever call exists
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `display_role` = :role WHERE `user_id` = :user')
->bind('role', $this->display_role)
->bind('user', $this->user_id)
->execute();
return $this;
}
public function getDisplayRole(): UserRole {
return $this->getRoleRelations()[$this->getDisplayRoleId()]->getRole();
}
public function setDisplayRole(UserRole $role): self {
if($this->hasRole($role))
$this->setDisplayRoleId($role->getId());
return $this;
}
public function isDisplayRole(UserRole $role): bool {
return $this->getDisplayRoleId() === $role->getId();
}
public function hasTOTP(): bool {
return !empty($this->user_totp_key);
@ -225,6 +280,54 @@ class User {
return $this->commentPermsArray;
}
/*********
* ROLES *
*********/
private $roleRelations = null;
public function addRole(UserRole $role, bool $display = false): void {
if(!$this->hasRole($role))
$this->roleRelations[$role->getId()] = UserRoleRelation::create($this, $role);
if($display && $this->isDisplayRole($role))
$this->setDisplayRole($role);
}
public function removeRole(UserRole $role): void {
if(!$this->hasRole($role))
return;
UserRoleRelation::destroy($this, $role);
unset($this->roleRelations[$role->getId()]);
if($this->isDisplayRole($role))
$this->setDisplayRoleId(UserRole::DEFAULT);
}
public function getRoleRelations(): array {
if($this->roleRelations === null) {
$this->roleRelations = [];
foreach(UserRoleRelation::byUser($this) as $rel)
$this->roleRelations[$rel->getRoleId()] = $rel;
}
return $this->roleRelations;
}
public function getRoles(): array {
$roles = [];
foreach($this->getRoleRelations() as $rel)
$roles[$rel->getRoleId()] = $rel->getRole();
return $roles;
}
public function hasRole(UserRole $role): bool {
return array_key_exists($role->getId(), $this->getRoleRelations());
}
/*************
* RELATIONS *
*************/
private $relationCache = [];
private $relationFollowingCount = -1;
private $relationFollowersCount = -1;
@ -283,6 +386,10 @@ class User {
return $this->relationFollowingCount;
}
/***************
* FORUM STATS *
***************/
private $forumTopicCount = -1;
private $forumPostCount = -1;
@ -327,6 +434,10 @@ class User {
return UserWarning::byProfile($this, $viewer);
}
/**************
* LOCAL USER *
**************/
public function setCurrent(): void {
self::$localUser = $this;
}
@ -340,6 +451,10 @@ class User {
return self::$localUser !== null;
}
/**************
* VALIDATION *
**************/
public static function validateUsername(string $name): string {
if($name !== trim($name))
return 'trim';
@ -418,25 +533,22 @@ class User {
string $password,
string $email,
string $ipAddress
): ?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)
): self {
$createUser = DB::prepare(
'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`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', IPAddress::country($ipAddress))
->executeGetId();
if($createUser < 1)
return null;
throw new UserCreationFailedException;
return static::byId($createUser);
return self::byId($createUser);
}
private static function getMemoizer() {

225
src/Users/UserRole.php Normal file
View file

@ -0,0 +1,225 @@
<?php
namespace Misuzu\Users;
use ArrayAccess;
use Misuzu\Colour;
use Misuzu\DB;
use Misuzu\HasRankInterface;
use Misuzu\Memoizer;
use Misuzu\Pagination;
class UserRoleException extends UsersException {}
class UserRoleNotFoundException extends UserRoleException {}
class UserRoleCreationFailedException extends UserRoleException {}
class UserRole implements ArrayAccess, HasRankInterface {
public const DEFAULT = 1;
// Database fields
private $role_id = -1;
private $role_hierarchy = 1;
private $role_name = '';
private $role_title = null;
private $role_description = null;
private $role_hidden = 0;
private $role_can_leave = 0;
private $role_colour = null;
private $role_created = null;
public const TABLE = 'roles';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`role_id`, %1$s.`role_hierarchy`, %1$s.`role_name`, %1$s.`role_title`, %1$s.`role_description`'
. ', %1$s.`role_hidden`, %1$s.`role_can_leave`, %1$s.`role_colour`'
. ', UNIX_TIMESTAMP(%1$s.`role_created`) AS `role_created`';
private $colour = null;
private $users = [];
private $userCount = -1;
public function getId(): int {
return $this->role_id < 1 ? -1 : $this->role_id;
}
public function getRank(): int {
return $this->role_hierarchy;
}
public function setRank(int $rank): self {
$this->role_hierarchy = $rank;
return $this;
}
public function getName(): string {
return $this->role_name;
}
public function setName(string $name): self {
$this->role_name = $name;
return $this;
}
public function getTitle(): string {
return $this->role_title ?? '';
}
public function setTitle(string $title): self {
$this->role_title = empty($title) ? null : $title;
return $this;
}
public function getDescription(): string {
return $this->role_description ?? '';
}
public function setDescription(string $description): self {
$this->role_description = empty($description) ? null : $description;
return $this;
}
public function isHidden(): bool {
return boolval($this->role_hidden);
}
public function setHidden(bool $hidden): self {
$this->role_hidden = $hidden ? 1 : 0;
return $this;
}
public function getCanLeave(): bool {
return boolval($this->role_can_leave);
}
public function setCanLeave(bool $canLeave): bool {
$this->role_can_leave = $canLeave ? 1 : 0;
return $this;
}
// Provided just because, avoid using these for validations sake
public function getColourRaw(): ?int {
return $this->role_colour;
}
public function setColourRaw(?int $colour): self {
$this->role_colour = $colour;
return $this;
}
public function getColour(): Colour {
if($this->colour === null || ($this->getColourRaw() ?? 0x40000000) !== $this->colour->getRaw())
$this->colour = new Colour($this->role_colour ?? 0x40000000);
return $this->colour;
}
public function setColour(Colour $colour): self {
$this->role_colour = $colour->getInherit() ? null : $colour->getRaw();
$this->colour = $this->colour;
return $this;
}
public function getCreatedTime(): int {
return $this->role_created === null ? -1 : $this->role_created;
}
public function getUserCount(): int {
if($this->userCount < 0)
$this->userCount = UserRoleRelation::countUsers($this);
return $this->userCount;
}
public function isDefault(): bool {
return $this->getId() === self::DEFAULT;
}
public function hasAuthorityOver(HasRankInterface $other): bool {
if($other instanceof User && $other->isSuper())
return false;
return $this->getRank() > $other->getRank();
}
public function save(): void {
$isInsert = $this->role_id < 1;
if($isInsert) {
$set = DB::prepare(
'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`role_hierarchy`, `role_name`, `role_title`, `role_description`, `role_hidden`, `role_can_leave`, `role_colour`)'
. ' VALUES (:rank, :name, :title, :desc, :hide, :can_leave, :colour)'
);
} else {
$set = DB::prepare(
'UPDATE `' . DB::PREFIX . self::TABLE . '` SET'
. ' `role_hierarchy` = :rank, `role_name` = :name, `role_title` = :title,'
. ' `role_description` = :desc, `role_hidden` = :hide, `role_can_leave` = :can_leave, `role_colour` = :colour'
. ' WHERE `role_id` = :role'
)->bind('role', $this->role_id);
}
$set->bind('rank', $this->role_hierarchy)
->bind('name', $this->role_name)
->bind('title', $this->role_title)
->bind('desc', $this->role_description)
->bind('hide', $this->role_hidden)
->bind('can_leave', $this->role_can_leave)
->bind('colour', $this->role_colour);
if($isInsert) {
$this->role_id = $set->executeGetId();
$this->role_created = time();
} else $set->execute();
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
}
public static function countAll(bool $showHidden = false): int {
return (int)DB::prepare(
self::countQueryBase()
. ($showHidden ? '' : ' WHERE `role_hidden` = 0')
)->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, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $roleId): self {
$object = DB::prepare(
self::byQueryBase() . ' WHERE `role_id` = :role'
) ->bind('role', $roleId)
->fetchObject(self::class);
if(!$object)
throw new UserRoleNotFoundException;
return $object;
}
public static function byDefault(): self {
return self::byId(self::DEFAULT);
}
public static function all(bool $showHidden = false, ?Pagination $pagination = null): array {
$query = self::byQueryBase();
if(!$showHidden)
$query .= ' WHERE `role_hidden` = 0';
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);
}
// to satisfy the fucked behaviour array_diff has
public function __toString() {
return md5($this->getId() . '#' . $this->getName());
}
// Twig shim for the roles list on the members page, don't use this class as an array normally.
public function offsetExists($offset): bool {
return $offset === 'name' || $offset === 'id';
}
public function offsetGet($offset) {
return $this->{'get' . ucfirst($offset)}();
}
public function offsetSet($offset, $value) {}
public function offsetUnset($offset) {}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Misuzu\Users;
use Misuzu\DB;
use Misuzu\Pagination;
class UserRoleRelation {
// Database fields
private $user_id = -1;
private $role_id = -1;
public const TABLE = 'user_roles';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`user_id`, %1$s.`role_id`';
private $user = null;
private $role = null;
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function getUser(): User {
if($this->user === null)
$this->user = User::byId($this->getUserId());
return $this->user;
}
public function getRoleId(): int {
return $this->role_id < 1 ? -1 : $this->role_id;
}
public function getRole(): UserRole {
if($this->role === null)
$this->role = UserRole::byId($this->getRoleId());
return $this->role;
}
public function delete(): void {
self::destroy($this->getUser(), $this->getRole());
}
public static function destroy(User $user, UserRole $role): void {
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `user_id` = :user AND `role_id` = :role')
->bind('user', $user->getId())
->bind('role', $role->getId())
->execute();
}
public static function purge(User $user): void {
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `user_id` = :user')
->bind('user', $user->getId())
->execute();
}
public static function create(User $user, UserRole $role): self {
$create = DB::prepare(
'REPLACE INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `role_id`)'
. ' VALUES (:user, :role)'
) ->bind('user', $user->getId())
->bind('role', $role->getId())
->execute();
// data is predictable, just create a "fake"
$object = new static;
$object->user = $user;
$object->user_id = $user->getId();
$object->role = $role;
$object->role_id = $role->getId();
return $object;
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
}
public static function countUsers(UserRole $role): int {
return (int)DB::prepare(self::countQueryBase() . ' WHERE `role_id` = :role')
->bind('role', $role->getId())
->fetchColumn();
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byUser(User $user): array {
return DB::prepare(self::byQueryBase() . ' WHERE `user_id` = :user')
->bind('user', $user->getId())
->fetchObjects(self::class);
}
}

View file

@ -1,145 +0,0 @@
<?php
define('MSZ_ROLE_MAIN', 1);
function user_role_add(int $userId, int $roleId): bool {
$addRole = \Misuzu\DB::prepare('
INSERT INTO `msz_user_roles`
(`user_id`, `role_id`)
VALUES
(:user_id, :role_id)
');
$addRole->bind('user_id', $userId);
$addRole->bind('role_id', $roleId);
return $addRole->execute();
}
function user_role_remove(int $userId, int $roleId): bool {
$removeRole = \Misuzu\DB::prepare('
DELETE FROM `msz_user_roles`
WHERE `user_id` = :user_id
AND `role_id` = :role_id
');
$removeRole->bind('user_id', $userId);
$removeRole->bind('role_id', $roleId);
return $removeRole->execute();
}
function user_role_can_leave(int $roleId): bool {
$canLeaveRole = \Misuzu\DB::prepare('
SELECT `role_can_leave` != 0
FROM `msz_roles`
WHERE `role_id` = :role_id
');
$canLeaveRole->bind('role_id', $roleId);
return (bool)$canLeaveRole->fetchColumn();
}
function user_role_has(int $userId, int $roleId): bool {
$hasRole = \Misuzu\DB::prepare('
SELECT COUNT(`role_id`) > 0
FROM `msz_user_roles`
WHERE `user_id` = :user_id
AND `role_id` = :role_id
');
$hasRole->bind('user_id', $userId);
$hasRole->bind('role_id', $roleId);
return (bool)$hasRole->fetchColumn();
}
function user_role_set_display(int $userId, int $roleId): bool {
if(!user_role_has($userId, $roleId))
return false;
$setDisplay = \Misuzu\DB::prepare('
UPDATE `msz_users`
SET `display_role` = :role_id
WHERE `user_id` = :user_id
');
$setDisplay->bind('user_id', $userId);
$setDisplay->bind('role_id', $roleId);
return $setDisplay->execute();
}
function user_role_get_display(int $userId): int {
if($userId < 1)
return MSZ_ROLE_MAIN;
$fetchRole = \Misuzu\DB::prepare('
SELECT `display_role`
FROM `msz_users`
WHERE `user_id` = :user_id
');
$fetchRole->bind('user_id', $userId);
return (int)$fetchRole->fetchColumn(0, MSZ_ROLE_MAIN);
}
function user_role_all_user(int $userId): array {
$getUserRoles = \Misuzu\DB::prepare('
SELECT
r.`role_id`, r.`role_name`, r.`role_description`,
r.`role_colour`, r.`role_can_leave`, r.`role_created`
FROM `msz_user_roles` AS ur
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = ur.`role_id`
WHERE ur.`user_id` = :user_id
ORDER BY r.`role_hierarchy` DESC
');
$getUserRoles->bind('user_id', $userId);
return $getUserRoles->fetchAll();
}
function user_role_all(bool $withHidden = false) {
return \Misuzu\DB::query(sprintf(
'
SELECT
r.`role_id`, r.`role_name`, r.`role_description`,
r.`role_colour`, r.`role_can_leave`, r.`role_created`,
(
SELECT COUNT(`user_id`)
FROM `msz_user_roles`
WHERE `role_id` = r.`role_id`
) AS `role_user_count`
FROM `msz_roles` AS r
%s
ORDER BY `role_id`
',
$withHidden ? '' : 'WHERE `role_hidden` = 0'
))->fetchAll();
}
function user_role_get(int $roleId): array {
$getRole = \Misuzu\DB::prepare('
SELECT
r.`role_id`, r.`role_name`, r.`role_description`,
r.`role_colour`, r.`role_can_leave`, r.`role_created`,
(
SELECT COUNT(`user_id`)
FROM `msz_user_roles`
WHERE `role_id` = r.`role_id`
) AS `role_user_count`
FROM `msz_roles` AS r
WHERE `role_id` = :role_id
');
$getRole->bind('role_id', $roleId);
return $getRole->fetch();
}
function user_role_check_authority(int $userId, int $roleId): bool {
$checkHierarchy = \Misuzu\DB::prepare('
SELECT (
SELECT MAX(r.`role_hierarchy`)
FROM `msz_roles` AS r
LEFT JOIN `msz_user_roles` AS ur
ON ur.`role_id` = r.`role_id`
WHERE ur.`user_id` = :user_id
) > (
SELECT `role_hierarchy`
FROM `msz_roles`
WHERE `role_id` = :role_id
)
');
$checkHierarchy->bind('user_id', $userId);
$checkHierarchy->bind('role_id', $roleId);
return (bool)$checkHierarchy->fetchColumn();
}

View file

@ -52,12 +52,6 @@ function user_id_from_username(string $username): int {
return (int)$getId->fetchColumn(0, 0);
}
function user_username_from_id(int $userId): string {
$getName = \Misuzu\DB::prepare('SELECT `username` FROM `msz_users` WHERE `user_id` = :user_id');
$getName->bind('user_id', $userId);
return $getName->fetchColumn(0, '');
}
function user_bump_last_active(int $userId, string $ipAddress = null): void {
$bumpUserLast = \Misuzu\DB::prepare('
UPDATE `msz_users`
@ -70,68 +64,6 @@ function user_bump_last_active(int $userId, string $ipAddress = null): void {
$bumpUserLast->execute();
}
function user_get_last_ip(int $userId): string {
$getAddress = \Misuzu\DB::prepare('
SELECT INET6_NTOA(`last_ip`)
FROM `msz_users`
WHERE `user_id` = :user_id
');
$getAddress->bind('user_id', $userId);
return $getAddress->fetchColumn(0, '');
}
function user_check_super(int $userId): bool {
static $superUsers = [];
if(!isset($superUsers[$userId])) {
$checkSuperUser = \Misuzu\DB::prepare("
SELECT `user_super`
FROM `msz_users`
WHERE `user_id` = :user_id
");
$checkSuperUser->bind('user_id', $userId);
$superUsers[$userId] = (bool)$checkSuperUser->fetchColumn(0, false);
}
return $superUsers[$userId];
}
function user_check_authority(int $userId, int $subjectId, bool $canManageSelf = true): bool {
if($canManageSelf && $userId === $subjectId)
return true;
$checkHierarchy = \Misuzu\DB::prepare('
SELECT (
SELECT MAX(r.`role_hierarchy`)
FROM `msz_roles` AS r
LEFT JOIN `msz_user_roles` AS ur
ON ur.`role_id` = r.`role_id`
WHERE ur.`user_id` = :user_id
) > (
SELECT MAX(r.`role_hierarchy`)
FROM `msz_roles` AS r
LEFT JOIN `msz_user_roles` AS ur
ON ur.`role_id` = r.`role_id`
WHERE ur.`user_id` = :subject_id
)
');
$checkHierarchy->bind('user_id', $userId);
$checkHierarchy->bind('subject_id', $subjectId);
return (bool)$checkHierarchy->fetchColumn(0, false);
}
function user_get_hierarchy(int $userId): int {
$getHierarchy = \Misuzu\DB::prepare('
SELECT MAX(r.`role_hierarchy`)
FROM `msz_roles` AS r
LEFT JOIN `msz_user_roles` AS ur
ON ur.`role_id` = r.`role_id`
WHERE ur.`user_id` = :user_id
');
$getHierarchy->bind('user_id', $userId);
return (int)$getHierarchy->fetchColumn(0, 0);
}
define('MSZ_E_USER_BIRTHDATE_OK', 0);
define('MSZ_E_USER_BIRTHDATE_USER', 1);
define('MSZ_E_USER_BIRTHDATE_DATE', 2);

View file

@ -4,37 +4,37 @@
{% from '_layout/input.twig' import input_csrf, input_text, input_checkbox %}
{% block manage_content %}
<form action="?v=role{{ edit_role is defined ? '&r=' ~ edit_role.role_id : '' }}" method="post"{% if edit_role is defined %} style="{{ edit_role.role_colour|html_colour('--accent-colour') }}"{% endif %}>
<form action="?v=role{{ role_info is not null ? '&r=' ~ role_info.id : '' }}" method="post"{% if role_info is not null %} style="--accent-colour: {{ role_info.colour }}"{% endif %}>
{{ input_csrf() }}
<div class="container">
{{ container_title(edit_role is defined ? 'Editing ' ~ edit_role.role_name ~ ' (' ~ edit_role.role_id ~ ')' : 'Creating a new role') }}
{{ container_title(role_info is not null ? 'Editing ' ~ role_info.name ~ ' (' ~ role_info.id ~ ')' : 'Creating a new role') }}
<label class="form__label">
<div class="form__label__text">Role Name</div>
<div class="form__label__input">
{{ input_text('role[name]', '', edit_role.role_name|default(''), 'text', '', true, {'maxlength':255}) }}
{{ input_text('role[name]', '', role_info.name|default(''), 'text', '', true, {'maxlength':255}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Hide Rank</div>
<div class="form__label__input">
{{ input_checkbox('role[secret]', '', edit_role is defined and edit_role.role_hidden) }}
{{ input_checkbox('role[secret]', '', role_info is not null and role_info.hidden) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Hierarchy</div>
<div class="form__label__input">
{{ input_text('role[hierarchy]', '', edit_role.role_hierarchy|default(1), 'number', '', true) }}
{{ input_text('role[hierarchy]', '', role_info.rank|default(1), 'number', '', true) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Title</div>
<div class="form__label__input">
{{ input_text('role[title]', '', edit_role.role_title|default(''), 'text', '', false, {'maxlength':64}) }}
{{ input_text('role[title]', '', role_info.title|default(''), 'text', '', false, {'maxlength':64}) }}
</div>
</label>
@ -46,28 +46,28 @@
<label class="form__label">
<div class="form__label__text">Inherit Colour</div>
<div class="form__label__input">
{{ input_checkbox('role[colour][inherit]', '', role_colour is defined ? role_colour.inherit : true) }}
{{ input_checkbox('role[colour][inherit]', '', role_info is not null ? role_info.colour.inherit : true) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Red</div>
<div class="form__label__input">
{{ input_text('role[colour][red]', '', role_colour.red|default(0), 'number', '', false, {'min':0,'max':255}) }}
{{ input_text('role[colour][red]', '', role_info.colour.red|default(0), 'number', '', false, {'min':0,'max':255}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Green</div>
<div class="form__label__input">
{{ input_text('role[colour][green]', '', role_colour.green|default(0), 'number', '', false, {'min':0,'max':255}) }}
{{ input_text('role[colour][green]', '', role_info.colour.green|default(0), 'number', '', false, {'min':0,'max':255}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Blue</div>
<div class="form__label__input">
{{ input_text('role[colour][blue]', '', role_colour.blue|default(0), 'number', '', false, {'min':0,'max':255}) }}
{{ input_text('role[colour][blue]', '', role_info.colour.blue|default(0), 'number', '', false, {'min':0,'max':255}) }}
</div>
</label>
@ -79,7 +79,7 @@
<label class="form__label">
<div class="form__label__text">Description</div>
<div class="form__label__input">
<textarea class="input__textarea" name="role[description]" maxlength="1000">{{ edit_role is defined ? edit_role.role_description : '' }}</textarea>
<textarea class="input__textarea" name="role[description]" maxlength="1000">{{ role_info.description|default('') }}</textarea>
</div>
</label>
</div>
@ -89,6 +89,6 @@
{{ permissions_table(permissions, not can_manage_perms) }}
</div>
<button class="input__button">{{ edit_role is defined ? 'Update role' : 'Create role' }}</button>
<button class="input__button">{{ role_info is not null ? 'Update role' : 'Create role' }}</button>
</form>
{% endblock %}

View file

@ -32,8 +32,8 @@
</div>
{% for role in manage_roles %}
<div class="manage__role-item" style="{{ role.role_colour|html_colour('--accent-colour') }}">
<a href="{{ url('manage-role', {'role': role.role_id}) }}" class="manage__role-item__background"></a>
<div class="manage__role-item" style="--accent-colour: {{ role.colour }}">
<a href="{{ url('manage-role', {'role': role.id}) }}" class="manage__role-item__background"></a>
<div class="manage__role-item__container">
<div class="manage__role-item__icon">
@ -44,23 +44,23 @@
</div>
<div class="manage__role-item__info">
<div class="manage__role-item__name">
{{ role.role_name }}
{{ role.name }}
</div>
<div class="manage__role-item__details">
{% if role.users > 0 %}
{% if role.userCount > 0 %}
<div class="manage__role-item__users">
<i class="fas fa-users fa-fw"></i> {{ role.users|number_format }}
<i class="fas fa-users fa-fw"></i> {{ role.userCount|number_format }}
</div>
{% endif %}
{% if role.role_title|default('')|length > 0 %}
{% if role.title is not empty %}
<div class="manage__role-item__title">
{{ role.role_title }}
{{ role.title }}
</div>
{% endif %}
</div>
</div>
<div class="manage__role-item__actions">
<a href="{{ url('user-list', {'role': role.role_id}) }}" class="manage__role-item__action" title="Members">
<a href="{{ url('user-list', {'role': role.id}) }}" class="manage__role-item__action" title="Members">
<i class="fas fa-users fa-fw"></i>
</a>
</div>

View file

@ -3,10 +3,10 @@
{% from 'manage/macros.twig' import permissions_table %}
{% from '_layout/input.twig' import input_csrf, input_text, input_checkbox, input_file, input_select, input_colour %}
{% set site_link = url('user-profile', {'user': manage_user.user_id|default(0)}) %}
{% set site_link = url('user-profile', {'user': user_info.id}) %}
{% block manage_content %}
<div class="manage__user"{% if manage_user is defined %} style="{{ manage_user.user_colour|html_colour('--accent-colour') }}"{% endif %}>
<div class="manage__user" style="--accent-colour: {{ user_info.userColour }}">
{% if manage_notices|length > 0 %}
<div class="warning">
<div class="warning__content">
@ -17,64 +17,64 @@
</div>
{% endif %}
<form method="post" action="{{ url('manage-user', {'user': manage_user.user_id|default(0)}) }}" class="container manage__user__container">
{{ container_title('Editing ' ~ manage_user.username ~ ' (' ~ manage_user.user_id ~ ')') }}
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
{{ container_title('Editing ' ~ user_info.username ~ ' (' ~ user_info.id ~ ')') }}
{{ input_csrf() }}
<div class="manage__user__details">
<label class="form__label">
<div class="form__label__text">Username</div>
<div class="form__label__input">
{{ input_text(can_edit_user ? 'user[username]' : '', 'manage__user__input', manage_user.username, 'text', '', true, {'maxlength':16}) }}
{{ input_text(can_edit_user ? 'user[username]' : '', 'manage__user__input', user_info.username, 'text', '', true, {'maxlength':16}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">E-mail address</div>
<div class="form__label__input">
{{ input_text(can_edit_user ? 'user[email]' : '', 'manage__user__input', manage_user.email, 'text', '', true, {'maxlength':255}) }}
{{ input_text(can_edit_user ? 'user[email]' : '', 'manage__user__input', user_info.emailAddress, 'text', '', true, {'maxlength':255}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Title</div>
<div class="form__label__input">
{{ input_text(can_edit_user ? 'user[title]' : '', 'manage__user__input', manage_user.user_title, 'text', '', false, {'maxlength':64}) }}
{{ input_text(can_edit_user ? 'user[title]' : '', 'manage__user__input', user_info.title, 'text', '', false, {'maxlength':64}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Joined</div>
<div class="form__label__input">
{{ input_text('', 'manage__user__input', manage_user.user_created) }}
{{ input_text('', 'manage__user__input', user_info.createdTime|date('c')) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Last online</div>
<div class="form__label__input">
{{ input_text('', 'manage__user__input', manage_user.user_active) }}
{{ input_text('', 'manage__user__input', user_info.activeTime|date('c')) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Register IP</div>
<div class="form__label__input">
{{ input_text('', 'manage__user__input', manage_user.register_ip_decoded) }}
{{ input_text('', 'manage__user__input', user_info.registerRemoteAddress) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Last IP</div>
<div class="form__label__input">
{{ input_text('', 'manage__user__input', manage_user.last_ip_decoded) }}
{{ input_text('', 'manage__user__input', user_info.lastRemoteAddress) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Country</div>
<div class="form__label__input">
{{ input_text(can_edit_user ? 'user[country]' : '', 'manage__user__input', manage_user.user_country, 'text', 'XX', true, {'maxlength':2,'minlength':2}) }}
{{ input_text(can_edit_user ? 'user[country]' : '', 'manage__user__input', user_info.country, 'text', 'XX', true, {'maxlength':2,'minlength':2}) }}
</div>
</label>
@ -99,25 +99,25 @@
<label class="form__label">
<div class="form__label__text">Custom Colour</div>
<div class="form__label__input">
{{ input_checkbox('colour[enable]', '', not user_colour.inherit, '', '', false, null, not can_edit_user) }}
{{ input_checkbox('colour[enable]', '', not user_info.userColour.inherit, '', '', false, null, not can_edit_user) }}
</div>
</label>
{{ input_colour(can_edit_user ? 'colour[hex]' : '', '', '#%s'|format(user_colour.hex)) }}
{{ input_colour(can_edit_user ? 'colour[hex]' : '', '', '#%s'|format(user_info.userColour.hex)) }}
</div>
{# TODO: if the hierarchy of the current user is too low to touch the role then opacity should be lowered and input disabled #}
<div class="manage__user__details">
<div class="manage__tags manage__tags--fixed">
{% for role in manage_roles %}
<label class="manage__tag" style="{{ role.role_colour|html_colour('--accent-colour') }}">
<label class="manage__tag" style="--accent-colour: {{ role.colour }}">
<div class="manage__tag__background"></div>
<div class="manage__tag__content">
{{ input_checkbox('roles[]', '', role.has_role, 'manage__tag__checkbox', role.role_id, false, null, not can_edit_user) }}
{{ input_checkbox('roles[]', '', user_info.hasRole(role), 'manage__tag__checkbox', role.id, false, null, not can_edit_user) }}
<div class="manage__tag__title">
{{ role.role_name }}
{{ role.name }}
</div>
{{ input_checkbox('user[display_role]', '', role.role_id == manage_user.display_role, 'manage__tag__checkbox', role.role_id, true, null, not can_edit_user) }}
{{ input_checkbox('user[display_role]', '', role.id == user_info.displayRoleId, 'manage__tag__checkbox', role.id, true, null, not can_edit_user) }}
</div>
</label>
{% endfor %}
@ -132,8 +132,8 @@
{% endif %}
</form>
<form method="post" action="{{ url('manage-user', {'user': manage_user.user_id|default(0)}) }}" class="container manage__user__container">
{{ container_title('Permissions for ' ~ manage_user.username ~ ' (' ~ manage_user.user_id ~ ')') }}
<form method="post" action="{{ url('manage-user', {'user': user_info.id}) }}" class="container manage__user__container">
{{ container_title('Permissions for ' ~ user_info.username ~ ' (' ~ user_info.id ~ ')') }}
{{ permissions_table(permissions, not can_edit_perms) }}

View file

@ -19,7 +19,7 @@
<div class="settings__account__title">
New e-mail address
</div>
{{ input_text('email[new]', 'settings__account__input', '', 'email', current_email) }}
{{ input_text('email[new]', 'settings__account__input', '', 'email', settings_user.emailAddress) }}
</label>
<label class="settings__account__input">
@ -71,22 +71,22 @@
</div>
<div class="settings__role__collection">
{% for role in user_roles %}
{% set is_display_role = user_display_role == role.role_id %}
{% for role in settings_user.roles %}
{% set is_display_role = settings_user.isDisplayRole(role) %}
<div class="settings__role" style="{{ role.role_colour|html_colour('--accent-colour') }}">
<div class="settings__role" style="--accent-colour: {{ role.colour }}">
<div class="settings__role__content">
<div class="settings__role__name">
{{ role.role_name }}
{{ role.name }}
</div>
<div class="settings__role__description">
{{ role.role_description }}
{{ role.description }}
</div>
<form class="settings__role__options" method="post" action="{{ url('settings-account') }}">
{{ input_csrf() }}
{{ input_hidden('role[id]', role.role_id) }}
{{ input_hidden('role[id]', role.id) }}
<button class="settings__role__option{% if is_display_role %} settings__role__option--disabled{% endif %}"
name="role[mode]" value="display" title="Set this as your display role"
@ -94,10 +94,10 @@
<i class="far {{ is_display_role ? 'fa-check-square' : 'fa-square' }}"></i>
</button>
<button class="settings__role__option{% if not role.role_can_leave %} settings__role__option--disabled{% endif %}"
<button class="settings__role__option{% if not role.canLeave %} settings__role__option--disabled{% endif %}"
name="role[mode]" value="leave" title="Leave this role"
onclick="return confirm('Are you sure you want to remove {{ role.role_name|replace({"'": "\'"}) }} from your account?')"
{% if not role.role_can_leave %}disabled{% endif %}>
onclick="return confirm('Are you sure you want to remove {{ role.name|replace({"'": "\'"}) }} from your account?')"
{% if not role.canLeave %}disabled{% endif %}>
<i class="fas fa-times-circle"></i>
</button>
</form>

View file

@ -2,7 +2,7 @@
{% from 'macros.twig' import container_title %}
{% from 'user/macros.twig' import user_card %}
{% set url_role = role.role_id != 1 ? role.role_id : 0 %}
{% set url_role = role.id > 1 ? role.id : 0 %}
{% set url_sort = order_field == order_default ? '' : order_field %}
{% set url_direction = order_fields[order_field]['default-dir'] == order_direction ? '' : order_direction %}
{% set canonical_url = url('user-list', {
@ -11,7 +11,7 @@
'direction': url_direction,
'page': users_pagination.page|default(0) > 2 ? users_pagination.page : 0,
}) %}
{% set title = role.role_id == 1 ? 'Members' : 'Role » ' ~ role.role_name %}
{% set title = role.id == 1 ? 'Members' : 'Role » ' ~ role.name %}
{% set manage_link = url('manage-users') %}
{% macro member_nav(roles, role_id, orders, order, directions, direction, users_pagination, url_role, url_sort, url_direction) %}
@ -20,7 +20,7 @@
<div class="userlist__navigation">
<form onchange="this.submit()" class="userlist__sorting">
{{ input_select('r', roles, role_id, 'role_name', 'role_id', false, 'userlist__select') }}
{{ input_select('r', roles, role_id, 'name', 'id', false, 'userlist__select') }}
{{ input_select('ss', orders, order, 'title', null, false, 'userlist__select') }}
{{ input_select('sd', directions, direction, null, null, false, 'userlist__select') }}
@ -38,13 +38,13 @@
{% block content %}
{% from _self import member_nav %}
{% set member_nav = member_nav(
roles, role.role_id,
roles, role.id,
order_fields, order_field,
order_directions, order_direction,
users_pagination, url_role, url_sort, url_direction
) %}
<div class="container userlist__container" style="{{ role.role_colour|html_colour('--accent-colour') }}">
<div class="container userlist__container" style="--accent-colour: {{ role.colour }}">
{{ member_nav }}
</div>
@ -62,7 +62,7 @@
</div>
{% endif %}
<div class="container userlist__container" style="{{ role.role_colour|html_colour('--accent-colour') }}">
<div class="container userlist__container" style="--accent-colour: {{ role.colour }}">
{{ member_nav }}
</div>
{% endblock %}