Rewrote user role handling.

This commit is contained in:
flash 2023-07-27 23:26:05 +00:00
parent 26a0e11253
commit 461ffbf73b
22 changed files with 710 additions and 536 deletions

View file

@ -3,7 +3,6 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserRole;
use Misuzu\Users\UserSession; use Misuzu\Users\UserSession;
if(UserSession::hasCurrent()) { if(UserSession::hasCurrent()) {
@ -11,6 +10,9 @@ if(UserSession::hasCurrent()) {
return; return;
} }
$users = $msz->getUsers();
$roles = $msz->getRoles();
$register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : []; $register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : [];
$notices = []; $notices = [];
$ipAddress = $_SERVER['REMOTE_ADDR']; $ipAddress = $_SERVER['REMOTE_ADDR'];
@ -95,7 +97,7 @@ while(!$restricted && !empty($register)) {
break; break;
} }
$createUser->addRole(UserRole::byDefault()); $users->addRoles($createUser, $roles->getDefaultRole());
url_redirect('auth-login-welcome', ['username' => $createUser->getUsername()]); url_redirect('auth-login-welcome', ['username' => $createUser->getUsername()]);
return; return;

View file

@ -5,22 +5,25 @@ use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Index\Colour\ColourRGB; use Index\Colour\ColourRGB;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserRole;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) { if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$roleId = (int)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT); $roles = $msz->getRoles();
if(filter_has_var(INPUT_GET, 'r')) {
$roleId = (string)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT);
if($roleId > 0)
try { try {
$roleInfo = UserRole::byId($roleId); $isNew = false;
$roleInfo = $roles->getRole($roleId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
} }
} else $isNew = true;
$currentUser = User::getCurrent(); $currentUser = User::getCurrent();
$currentUserId = $currentUser->getId(); $currentUserId = $currentUser->getId();
@ -29,70 +32,92 @@ $canEditPerms = perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_M
if($canEditPerms) if($canEditPerms)
$permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0)); $permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0));
if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
$roleHierarchy = (int)($_POST['role']['hierarchy'] ?? -1); if(!$isNew && !$currentUser->isSuper() && $roleInfo->getRank() >= $currentUser->getRank()) {
echo 'You aren\'t allowed to edit this role.';
if(!$currentUser->isSuper() && (isset($roleInfo) ? $roleInfo->hasAuthorityOver($currentUser) : $currentUser->getRank() <= $roleHierarchy)) { break;
echo 'You don\'t hold authority over this role.';
return;
} }
$roleName = $_POST['role']['name'] ?? ''; $roleName = (string)filter_input(INPUT_POST, 'ur_name');
$roleNameLength = strlen($roleName); $roleHide = !empty($_POST['ur_hidden']);
$roleLeavable = !empty($_POST['ur_leavable']);
$roleRank = (int)filter_input(INPUT_POST, 'ur_rank', FILTER_SANITIZE_NUMBER_INT);
$roleTitle = (string)filter_input(INPUT_POST, 'ur_title');
$roleDesc = (string)filter_input(INPUT_POST, 'ur_desc');
$colourInherit = !empty($_POST['ur_col_inherit']);
$colourRed = (int)filter_input(INPUT_POST, 'ur_col_red', FILTER_SANITIZE_NUMBER_INT);
$colourGreen = (int)filter_input(INPUT_POST, 'ur_col_green', FILTER_SANITIZE_NUMBER_INT);
$colourBlue = (int)filter_input(INPUT_POST, 'ur_col_blue', FILTER_SANITIZE_NUMBER_INT);
if($roleNameLength < 1 || $roleNameLength > 255) { Template::set([
echo 'invalid name length'; 'role_ur_name' => $roleName,
return; 'role_ur_hidden' => $roleHide,
'role_ur_leavable' => $roleLeavable,
'role_ur_rank' => $roleRank,
'role_ur_title' => $roleTitle,
'role_ur_desc' => $roleDesc,
'role_ur_col_inherit' => $colourInherit,
'role_ur_col_red' => $colourRed,
'role_ur_col_green' => $colourGreen,
'role_ur_col_blue' => $colourBlue,
]);
if(!$currentUser->isSuper() && $roleRank >= $currentUser->getRank()) {
echo 'You aren\'t allowed to make a role with equal rank to your own.';
break;
} }
$roleSecret = !empty($_POST['role']['secret']); $roleNameLength = mb_strlen($roleName);
if($roleNameLength < 1 || $roleNameLength > 100) {
if($roleHierarchy < 1 || $roleHierarchy > 100) { echo 'Provided role name is either too long or too short.';
echo 'Invalid hierarchy value.'; break;
return;
} }
if(!empty($_POST['role']['colour']['inherit'])) { if($roleRank < 1 || $roleRank > 100) {
$roleColour = \Index\Colour\Colour::none(); echo 'Role rank may not be less than 1 or more than 100.';
break;
}
$roleColour = $colourInherit
? Colour::none()
: new ColourRGB($colourRed, $colourGreen, $colourBlue);
if(mb_strlen($roleDesc) > 1000) {
echo 'Description may not be longer than 1000 characters.';
break;
}
if(mb_strlen($roleTitle) > 64) {
echo 'Role title may not be longer than 64 characters.';
break;
}
if($isNew) {
$roleInfo = $roles->createRole($roleName, $roleRank, $roleColour, $roleTitle, $roleDesc, $roleHide, $roleLeavable);
} else { } else {
$roleColour = new ColourRGB( if($roleName === $roleInfo->getName())
(int)($_POST['role']['colour']['red'] ?? -1), $roleName = null;
(int)($_POST['role']['colour']['green'] ?? -1), if($roleHide === $roleInfo->isHidden())
(int)($_POST['role']['colour']['blue'] ?? -1) $roleHide = null;
if($roleLeavable === $roleInfo->isLeavable())
$roleLeavable = null;
if($roleRank === $roleInfo->getRank())
$roleRank = null;
if($roleTitle === $roleInfo->getTitle())
$roleTitle = null;
if($roleDesc === $roleInfo->getDescription())
$roleDesc = null;
// local genius did not implement colour comparison
if((string)$roleColour === (string)$roleInfo->getColour())
$roleColour = null;
$roles->updateRole($roleInfo, $roleName, $roleRank, $roleColour, $roleTitle, $roleDesc, $roleHide, $roleLeavable);
}
$msz->createAuditLog(
$isNew ? 'ROLE_CREATE' : 'ROLE_UPDATE',
[$roleInfo->getId()]
); );
}
$roleDescription = $_POST['role']['description'] ?? '';
$roleTitle = $_POST['role']['title'] ?? '';
if($roleDescription !== null) {
$rdLength = strlen($roleDescription);
if($rdLength > 1000) {
echo 'description is too long';
return;
}
}
if($roleTitle !== null) {
$rtLength = strlen($roleTitle);
if($rtLength > 64) {
echo 'title is too long';
return;
}
}
if(!isset($roleInfo))
$roleInfo = new UserRole;
$roleInfo->setName($roleName)
->setRank($roleHierarchy)
->setHidden($roleSecret)
->setColour($roleColour)
->setDescription($roleDescription)
->setTitle($roleTitle)
->save();
if(!empty($permissions) && !empty($_POST['perms']) && is_array($_POST['perms'])) { if(!empty($permissions) && !empty($_POST['perms']) && is_array($_POST['perms'])) {
$perms = manage_perms_apply($permissions, $_POST['perms']); $perms = manage_perms_apply($permissions, $_POST['perms']);
@ -100,10 +125,8 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
if($perms !== null) { if($perms !== null) {
$permKeys = array_keys($perms); $permKeys = array_keys($perms);
$setPermissions = DB::prepare(' $setPermissions = DB::prepare('
REPLACE INTO `msz_permissions` REPLACE INTO `msz_permissions` (`role_id`, `user_id`, `' . implode('`, `', $permKeys) . '`)
(`role_id`, `user_id`, `' . implode('`, `', $permKeys) . '`) VALUES (:role_id, NULL, :' . implode(', :', $permKeys) . ')
VALUES
(:role_id, NULL, :' . implode(', :', $permKeys) . ')
'); ');
$setPermissions->bind('role_id', $roleInfo->getId()); $setPermissions->bind('role_id', $roleInfo->getId());
@ -113,11 +136,7 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
$setPermissions->execute(); $setPermissions->execute();
} else { } else {
$deletePermissions = DB::prepare(' $deletePermissions = DB::prepare('DELETE FROM `msz_permissions` WHERE `role_id` = :role_id AND `user_id` IS NULL');
DELETE FROM `msz_permissions`
WHERE `role_id` = :role_id
AND `user_id` IS NULL
');
$deletePermissions->bind('role_id', $roleInfo->getId()); $deletePermissions->bind('role_id', $roleInfo->getId());
$deletePermissions->execute(); $deletePermissions->execute();
} }
@ -128,6 +147,7 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
} }
Template::render('manage.users.role', [ Template::render('manage.users.role', [
'role_new' => $isNew,
'role_info' => $roleInfo ?? null, 'role_info' => $roleInfo ?? null,
'can_manage_perms' => $canEditPerms, 'can_manage_perms' => $canEditPerms,
'permissions' => $permissions ?? [], 'permissions' => $permissions ?? [],

View file

@ -2,21 +2,30 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserRole;
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) { if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
echo render_error(403); echo render_error(403);
return; return;
} }
$pagination = new Pagination(UserRole::countAll(true), 10); $roles = $msz->getRoles();
$pagination = new Pagination($roles->countRoles(), 10);
if(!$pagination->hasValidOffset()) { if(!$pagination->hasValidOffset()) {
echo render_error(404); echo render_error(404);
return; return;
} }
$rolesAll = [];
$roleInfos = $roles->getRoles(pagination: $pagination);
foreach($roleInfos as $roleInfo)
$rolesAll[] = [
'info' => $roleInfo,
'members' => $roles->countRoleUsers($roleInfo),
];
Template::render('manage.users.roles', [ Template::render('manage.users.roles', [
'manage_roles' => UserRole::all(true, $pagination), 'manage_roles' => $rolesAll,
'manage_roles_pagination' => $pagination, 'manage_roles_pagination' => $pagination,
]); ]);

View file

@ -4,13 +4,15 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Index\Colour\Colour; use Index\Colour\Colour;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserRole;
if(!User::hasCurrent()) { if(!User::hasCurrent()) {
echo render_error(403); echo render_error(403);
return; return;
} }
$users = $msz->getUsers();
$roles = $msz->getRoles();
$currentUser = User::getCurrent(); $currentUser = User::getCurrent();
$currentUserId = $currentUser->getId(); $currentUserId = $currentUser->getId();
@ -77,60 +79,53 @@ if(CSRF::validateRequest() && $canEdit) {
foreach($_POST['roles'] as $item) { foreach($_POST['roles'] as $item) {
if(!ctype_digit($item)) if(!ctype_digit($item))
die('Invalid item encountered in roles list.'); die('Invalid item encountered in roles list.');
$applyRoles[] = (int)$item; $applyRoles[] = (string)$item;
} }
// Fetch existing roles $existingRoles = [];
$existingRoles = $userInfo->getRoles(); foreach($roles->getRoles(userInfo: $userInfo) as $roleInfo)
$existingRoles[$roleInfo->getId()] = $roleInfo;
// Initialise set array with existing roles
$setRoles = $existingRoles;
// Storage array for roles to dump
$removeRoles = []; $removeRoles = [];
// STEP 1: Check for roles to be removed in the existing set. foreach($existingRoles as $roleInfo) {
// Roles that the current users isn't allowed to touch (hierarchy) will stay. if($roleInfo->isDefault() || !($currentUser->isSuper() || $currentUser->getRank() > $roleInfo->getRank()))
foreach($setRoles as $role) {
// Also prevent the main role from being removed.
if($role->isDefault() || !$currentUser->hasAuthorityOver($role))
continue; continue;
if(!in_array($role->getId(), $applyRoles))
$removeRoles[] = $role; if(!in_array($roleInfo->getId(), $applyRoles))
$removeRoles[] = $roleInfo;
} }
// STEP 2: Purge the ones marked for removal. if(!empty($removeRoles))
$setRoles = array_diff($setRoles, $removeRoles); $users->removeRoles($userInfo, $removeRoles);
$addRoles = [];
// STEP 3: Add roles to the set array from the user input, if the user has authority over the given roles.
foreach($applyRoles as $roleId) { foreach($applyRoles as $roleId) {
try { try {
$role = $existingRoles[$roleId] ?? UserRole::byId($roleId); $roleInfo = $existingRoles[$roleId] ?? $roles->getRole($roleId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
continue; continue;
} }
if(!$currentUser->hasAuthorityOver($role))
if(!$currentUser->isSuper() && $currentUser->getRank() <= $roleInfo->getRank())
continue; continue;
if(!in_array($role, $setRoles))
$setRoles[] = $role; if(!in_array($roleInfo, $existingRoles))
$addRoles[] = $roleInfo;
} }
foreach($removeRoles as $role) if(!empty($addRoles))
$userInfo->removeRole($role); $users->addRoles($userInfo, $addRoles);
foreach($setRoles as $role)
$userInfo->addRole($role);
} }
if(!empty($_POST['user']) && is_array($_POST['user'])) { if(!empty($_POST['user']) && is_array($_POST['user'])) {
$setCountry = (string)($_POST['user']['country'] ?? ''); $setCountry = (string)($_POST['user']['country'] ?? '');
$setTitle = (string)($_POST['user']['title'] ?? ''); $setTitle = (string)($_POST['user']['title'] ?? '');
$displayRole = (int)($_POST['user']['display_role'] ?? 0); $displayRole = (string)($_POST['user']['display_role'] ?? 0);
if(!$users->hasRole($userInfo, $displayRole))
try { $notices[] = 'User does not have the role you\'re trying to assign as primary.';
$userInfo->setDisplayRole(UserRole::byId($displayRole));
} catch(RuntimeException $ex) {}
$countryValidation = strlen($setCountry) === 2 $countryValidation = strlen($setCountry) === 2
&& ctype_alpha($setCountry) && ctype_alpha($setCountry)
@ -142,10 +137,15 @@ if(CSRF::validateRequest() && $canEdit) {
if(strlen($setTitle) > 64) if(strlen($setTitle) > 64)
$notices[] = 'User title was invalid.'; $notices[] = 'User title was invalid.';
if(empty($notices)) if(empty($notices)) {
$users->updateUser(
userInfo: $userInfo,
displayRoleInfo: $displayRole,
);
$userInfo->setCountry((string)($_POST['user']['country'] ?? '')) $userInfo->setCountry((string)($_POST['user']['country'] ?? ''))
->setTitle((string)($_POST['user']['title'] ?? '')) ->setTitle((string)($_POST['user']['title'] ?? ''));
->setDisplayRole(UserRole::byId((int)($_POST['user']['display_role'] ?? 0))); }
} }
if(!empty($_POST['colour']) && is_array($_POST['colour'])) { if(!empty($_POST['colour']) && is_array($_POST['colour'])) {
@ -194,10 +194,14 @@ if(CSRF::validateRequest() && $canEdit) {
} }
} }
$rolesAll = $roles->getRoles();
$userRoleIds = $users->hasRoles($userInfo, $rolesAll);
Template::render('manage.users.user', [ Template::render('manage.users.user', [
'user_info' => $userInfo, 'user_info' => $userInfo,
'manage_notices' => $notices, 'manage_notices' => $notices,
'manage_roles' => UserRole::all(true), 'manage_roles' => $rolesAll,
'manage_user_has_roles' => $userRoleIds,
'can_edit_user' => $canEdit, 'can_edit_user' => $canEdit,
'can_edit_perms' => $canEdit && $canEditPerms, 'can_edit_perms' => $canEdit && $canEditPerms,
'can_manage_notes' => $canManageNotes, 'can_manage_notes' => $canManageNotes,

View file

@ -3,11 +3,12 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserRole;
$roleId = !empty($_GET['r']) && is_string($_GET['r']) ? (int)$_GET['r'] : UserRole::DEFAULT; $roles = $msz->getRoles();
$orderBy = !empty($_GET['ss']) && is_string($_GET['ss']) ? mb_strtolower($_GET['ss']) : '';
$orderDir = !empty($_GET['sd']) && is_string($_GET['sd']) ? mb_strtolower($_GET['sd']) : ''; $roleId = filter_has_var(INPUT_GET, 'r') ? (string)filter_input(INPUT_GET, 'r') : null;
$orderBy = strtolower((string)filter_input(INPUT_GET, 'ss'));
$orderDir = strtolower((string)filter_input(INPUT_GET, 'sd'));
$orderDirs = [ $orderDirs = [
'asc' => 'Ascending', 'asc' => 'Ascending',
@ -69,16 +70,20 @@ if(empty($orderDir)) {
$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, User::hasCurrent() ? User::getCurrent()->getId() : 0, MSZ_PERM_USER_MANAGE_USERS);
if($roleId === null) {
$roleInfo = $roles->getDefaultRole();
} else {
try { try {
$roleInfo = UserRole::byId($roleId); $roleInfo = $roles->getRole($roleId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
echo render_error(404); echo render_error(404);
return; return;
} }
}
$pagination = new Pagination($roleInfo->getUserCount(), 15);
$roles = UserRole::all(); $pagination = new Pagination($roles->countRoleUsers($roleInfo), 15);
$rolesAll = $roles->getRoles(hidden: false);
$getUsers = DB::prepare(sprintf( $getUsers = DB::prepare(sprintf(
' '
@ -124,7 +129,7 @@ if(empty($users))
http_response_code(404); http_response_code(404);
Template::render('user.listing', [ Template::render('user.listing', [
'roles' => $roles, 'roles' => $rolesAll,
'role' => $roleInfo, 'role' => $roleInfo,
'users' => $users, 'users' => $users,
'order_fields' => $orderFields, 'order_fields' => $orderFields,

View file

@ -3,7 +3,6 @@ namespace Misuzu;
use RuntimeException; use RuntimeException;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserRole;
use Misuzu\Users\UserSession; use Misuzu\Users\UserSession;
use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions; use chillerlan\QRCode\QROptions;
@ -14,6 +13,8 @@ if(!UserSession::hasCurrent()) {
} }
$errors = []; $errors = [];
$users = $msz->getUsers();
$roles = $msz->getRoles();
$currentUser = User::getCurrent(); $currentUser = User::getCurrent();
$currentUserId = $currentUser->getId(); $currentUserId = $currentUser->getId();
$isRestricted = $msz->hasActiveBan(); $isRestricted = $msz->hasActiveBan();
@ -21,20 +22,23 @@ $isVerifiedRequest = CSRF::validateRequest();
if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) { if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
try { try {
$roleInfo = UserRole::byId((int)($_POST['role']['id'] ?? 0)); $roleInfo = $roles->getRole(($_POST['role']['id'] ?? 0));
} catch(RuntimeException $ex) {} } catch(RuntimeException $ex) {}
if(empty($roleInfo) || !$currentUser->hasRole($roleInfo)) if(empty($roleInfo) || !$users->hasRole($userInfo, $roleInfo))
$errors[] = "You're trying to modify a role that hasn't been assigned to you."; $errors[] = "You're trying to modify a role that hasn't been assigned to you.";
else { else {
switch($_POST['role']['mode'] ?? '') { switch($_POST['role']['mode'] ?? '') {
case 'display': case 'display':
$currentUser->setDisplayRole($roleInfo); $users->updateUser(
$currentUser,
displayRoleInfo: $roleInfo
);
break; break;
case 'leave': case 'leave':
if($roleInfo->getCanLeave()) if($roleInfo->isLeavable())
$currentUser->removeRole($roleInfo); $users->removeRoles($currentUser, $roleInfo);
else else
$errors[] = "You're not allow to leave this role, an administrator has to remove it for you."; $errors[] = "You're not allow to leave this role, an administrator has to remove it for you.";
break; break;
@ -124,11 +128,19 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
} }
// THIS FUCKING SUCKS AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA // THIS FUCKING SUCKS AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest) if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest) {
$currentUser->save(); $currentUser->save();
// 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);
Template::render('settings.account', [ Template::render('settings.account', [
'errors' => $errors, 'errors' => $errors,
'settings_user' => $currentUser, 'settings_user' => $currentUser,
'settings_roles' => $userRoles,
'is_restricted' => $isRestricted, 'is_restricted' => $isRestricted,
]); ]);

View file

@ -132,5 +132,8 @@ class AuditLogInfo {
'WARN_CREATE' => 'Added warning #%d to user #%d.', 'WARN_CREATE' => 'Added warning #%d to user #%d.',
'WARN_DELETE' => 'Removed warning #%d from user #%d.', 'WARN_DELETE' => 'Removed warning #%d from user #%d.',
'ROLE_CREATE' => 'Created role #%d.',
'ROLE_UPDATE' => 'Updated role #%d.',
]; ];
} }

View file

@ -15,7 +15,9 @@ use Misuzu\SharpChat\SharpChatRoutes;
use Misuzu\Users\Bans; use Misuzu\Users\Bans;
use Misuzu\Users\BanInfo; use Misuzu\Users\BanInfo;
use Misuzu\Users\ModNotes; use Misuzu\Users\ModNotes;
use Misuzu\Users\Roles;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\Users;
use Misuzu\Users\Warnings; use Misuzu\Users\Warnings;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigrationRepo; use Index\Data\Migration\IDbMigrationRepo;
@ -46,6 +48,8 @@ class MisuzuContext {
private Bans $bans; private Bans $bans;
private Warnings $warnings; private Warnings $warnings;
private TwoFactorAuthSessions $tfaSessions; private TwoFactorAuthSessions $tfaSessions;
private Roles $roles;
private Users $users;
public function __construct(IDbConnection $dbConn, IConfig $config) { public function __construct(IDbConnection $dbConn, IConfig $config) {
$this->dbConn = $dbConn; $this->dbConn = $dbConn;
@ -61,6 +65,8 @@ class MisuzuContext {
$this->bans = new Bans($this->dbConn); $this->bans = new Bans($this->dbConn);
$this->warnings = new Warnings($this->dbConn); $this->warnings = new Warnings($this->dbConn);
$this->tfaSessions = new TwoFactorAuthSessions($this->dbConn); $this->tfaSessions = new TwoFactorAuthSessions($this->dbConn);
$this->roles = new Roles($this->dbConn);
$this->users = new Users($this->dbConn);
} }
public function getDbConn(): IDbConnection { public function getDbConn(): IDbConnection {
@ -132,6 +138,14 @@ class MisuzuContext {
return $this->tfaSessions; return $this->tfaSessions;
} }
public function getRoles(): Roles {
return $this->roles;
}
public function getUsers(): Users {
return $this->users;
}
private array $activeBansCache = []; private array $activeBansCache = [];
public function tryGetActiveBan(User|string|null $userInfo = null): ?BanInfo { public function tryGetActiveBan(User|string|null $userInfo = null): ?BanInfo {

View file

@ -172,6 +172,8 @@ class Bans {
public function deleteBans(BanInfo|string|array $banInfos): void { public function deleteBans(BanInfo|string|array $banInfos): void {
if(!is_array($banInfos)) if(!is_array($banInfos))
$banInfos = [$banInfos]; $banInfos = [$banInfos];
elseif(empty($banInfos))
return;
$stmt = $this->cache->get(sprintf( $stmt = $this->cache->get(sprintf(
'DELETE FROM msz_users_bans WHERE ban_id IN (%s)', 'DELETE FROM msz_users_bans WHERE ban_id IN (%s)',

View file

@ -139,6 +139,8 @@ class ModNotes {
public function deleteNotes(ModNoteInfo|string|array $noteInfos): void { public function deleteNotes(ModNoteInfo|string|array $noteInfos): void {
if(!is_array($noteInfos)) if(!is_array($noteInfos))
$noteInfos = [$noteInfos]; $noteInfos = [$noteInfos];
elseif(empty($noteInfos))
return;
$stmt = $this->cache->get(sprintf( $stmt = $this->cache->get(sprintf(
'DELETE FROM msz_users_modnotes WHERE note_id IN (%s)', 'DELETE FROM msz_users_modnotes WHERE note_id IN (%s)',

91
src/Users/RoleInfo.php Normal file
View file

@ -0,0 +1,91 @@
<?php
namespace Misuzu\Users;
use Stringable;
use Index\DateTime;
use Index\Colour\Colour;
use Index\Data\IDbResult;
class RoleInfo implements Stringable {
private string $id;
private int $rank;
private string $name;
private ?string $title;
private ?string $description;
private bool $hidden;
private bool $leavable;
private ?int $colour;
private int $created;
public function __construct(IDbResult $result) {
$this->id = (string)$result->getInteger(0);
$this->rank = $result->getInteger(1);
$this->name = $result->getString(2);
$this->title = $result->isNull(3) ? null : $result->getString(3);
$this->description = $result->isNull(4) ? null : $result->getString(4);
$this->hidden = $result->getInteger(5) !== 0;
$this->leavable = $result->getInteger(6) !== 0;
$this->colour = $result->isNull(7) ? null : $result->getInteger(7);
$this->created = $result->getInteger(8);
}
public function getId(): string {
return $this->id;
}
public function isDefault(): bool {
return $this->id === Roles::DEFAULT_ROLE;
}
public function getRank(): int {
return $this->rank;
}
public function getName(): string {
return $this->name;
}
public function hasTitle(): bool {
return $this->title !== null && $this->title !== '';
}
public function getTitle(): ?string {
return $this->title;
}
public function hasDescription(): bool {
return $this->description !== null && $this->description !== '';
}
public function getDescription(): ?string {
return $this->description;
}
public function isHidden(): bool {
return $this->hidden;
}
public function isLeavable(): bool {
return $this->leavable;
}
public function hasColour(): bool {
return $this->colour !== null && ($this->colour & 0x40000000) === 0;
}
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 __toString(): string {
return 'r' . $this->id;
}
}

226
src/Users/Roles.php Normal file
View file

@ -0,0 +1,226 @@
<?php
namespace Misuzu\Users;
use InvalidArgumentException;
use RuntimeException;
use Index\Colour\Colour;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Misuzu\Pagination;
class Roles {
public const DEFAULT_ROLE = '1';
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
public function countRoles(
User|string|null $userInfo = null,
?bool $hidden = null
): int {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
$hasUserInfo = $userInfo !== null;
$hasHidden = $hidden !== null;
$args = 0;
$query = 'SELECT COUNT(*) FROM msz_roles';
if($hasUserInfo) {
++$args;
$query .= ' WHERE role_id IN (SELECT role_id FROM msz_users_roles WHERE user_id = ?)';
}
if($hasHidden)
$query .= sprintf(' %s role_hidden %s 0', ++$args > 0 ? 'AND' : 'WHERE', $hidden ? '=' : '<>');
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
$stmt->execute();
$count = 0;
$result = $stmt->getResult();
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function getRoles(
User|string|null $userInfo = null,
?bool $hidden = null,
?Pagination $pagination = null
): array {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
$hasUserInfo = $userInfo !== null;
$hasHidden = $hidden !== null;
$hasPagination = $pagination !== null;
$args = 0;
$query = 'SELECT role_id, role_hierarchy, role_name, role_title, role_description, role_hidden, role_can_leave, role_colour, UNIX_TIMESTAMP(role_created) FROM msz_roles';
if($hasUserInfo) {
++$args;
$query .= ' WHERE role_id IN (SELECT role_id FROM msz_users_roles WHERE user_id = ?)';
}
if($hasHidden)
$query .= sprintf(' %s role_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '=');
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
$roles = [];
$result = $stmt->getResult();
while($result->next())
$roles[] = new RoleInfo($result);
return $roles;
}
public function getRole(string $roleId): RoleInfo {
$stmt = $this->cache->get('SELECT role_id, role_hierarchy, role_name, role_title, role_description, role_hidden, role_can_leave, role_colour, UNIX_TIMESTAMP(role_created) FROM msz_roles WHERE role_id = ?');
$stmt->addParameter(1, $roleId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Could not find role with ID $roleId.');
return new RoleInfo($result);
}
public function createRole(
string $name,
int $rank,
Colour $colour,
string $title = '',
string $description = '',
bool $hidden = false,
bool $leavable = false
): RoleInfo {
$colour = $colour->shouldInherit() ? null : Colour::toMisuzu($colour);
// should these continue to accept NULL?
if($title === '') $title = null;
if($description === '') $description = null;
$stmt = $this->cache->get('INSERT INTO msz_roles (role_hierarchy, role_name, role_title, role_description, role_hidden, role_can_leave, role_colour) VALUES (?, ?, ?, ?, ?, ?, ?)');
$stmt->addParameter(1, $rank);
$stmt->addParameter(2, $name);
$stmt->addParameter(3, $title);
$stmt->addParameter(4, $description);
$stmt->addParameter(5, $hidden ? 1 : 0);
$stmt->addParameter(6, $leavable ? 1 : 0);
$stmt->addParameter(7, $colour);
$stmt->execute();
return $this->getRole((string)$this->dbConn->getLastInsertId());
}
public function deleteRoles(RoleInfo|string|array $roleInfos): void {
if(!is_array($roleInfos))
$roleInfos = [$roleInfos];
elseif(empty($roleInfos))
return;
$stmt = $this->cache->get(sprintf(
'DELETE FROM msz_roles WHERE role_id IN (%s)',
DbTools::prepareListString($roleInfos)
));
$args = 0;
foreach($roleInfos as $roleInfo) {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
elseif(!is_string($roleInfo))
throw new InvalidArgumentException('$roleInfos must be strings of instances of RoleInfo.');
$stmt->addParameter(++$args, $roleInfo);
}
$stmt->execute();
}
public function updateRole(
RoleInfo|string $roleInfo,
?string $name = null,
?int $rank = null,
?Colour $colour = null,
?string $title = null,
?string $description = null,
?bool $hidden = null,
?bool $leavable = null
): void {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
$applyTitle = $title !== null;
$applyDescription = $description !== null;
$applyColour = $colour !== null;
$applyHidden = $hidden !== null;
$applyLeavable = $leavable !== null;
if($applyColour)
$colour = $colour->shouldInherit() ? null : Colour::toMisuzu($colour);
// should these continue to accept NULL?
if($title === '') $title = null;
if($description === '') $description = null;
$stmt = $this->cache->get('UPDATE msz_roles SET role_hierarchy = COALESCE(?, role_hierarchy), role_name = COALESCE(?, role_name), role_title = IF(?, ?, role_title), role_description = IF(?, ?, role_description), role_hidden = IF(?, ?, role_hidden), role_can_leave = IF(?, ?, role_can_leave), role_colour = IF(?, ?, role_colour) WHERE role_id = ?');
$stmt->addParameter(1, $rank);
$stmt->addParameter(2, $name);
$stmt->addParameter(3, $applyTitle ? 1 : 0);
$stmt->addParameter(4, $title);
$stmt->addParameter(5, $applyDescription ? 1 : 0);
$stmt->addParameter(6, $description);
$stmt->addParameter(7, $applyHidden ? 1 : 0);
$stmt->addParameter(8, $hidden ? 1 : 0);
$stmt->addParameter(9, $applyLeavable ? 1 : 0);
$stmt->addParameter(10, $leavable ? 1 : 0);
$stmt->addParameter(11, $applyColour ? 1 : 0);
$stmt->addParameter(12, $colour);
$stmt->addParameter(13, $roleInfo);
$stmt->execute();
}
public function getDefaultRole(): RoleInfo {
return $this->getRole(self::DEFAULT_ROLE);
}
public function countRoleUsers(RoleInfo|string $roleInfo): int {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_users_roles WHERE role_id = ?');
$stmt->addParameter(1, $roleInfo);
$stmt->execute();
$count = 0;
$result = $stmt->getResult();
if($result->next())
$count = $result->getInteger(0);
return $count;
}
}

View file

@ -126,8 +126,12 @@ class User implements HasRankInterface {
public function getColour(): Colour { // Swaps role colour in if user has no personal colour public function getColour(): Colour { // Swaps role colour in if user has no personal colour
if($this->realColour === null) { if($this->realColour === null) {
$this->realColour = $this->getUserColour(); $this->realColour = $this->getUserColour();
if($this->realColour->shouldInherit()) if($this->realColour->shouldInherit()) {
$this->realColour = $this->getDisplayRole()->getColour(); $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; return $this->realColour;
} }
@ -165,38 +169,20 @@ class User implements HasRankInterface {
if($this->userRank === null) if($this->userRank === null)
$this->userRank = (int)DB::prepare( $this->userRank = (int)DB::prepare(
'SELECT MAX(`role_hierarchy`)' 'SELECT MAX(`role_hierarchy`)'
. ' FROM `' . DB::PREFIX . UserRole::TABLE . '`' . ' FROM `msz_roles`'
. ' WHERE `role_id` IN (SELECT `role_id` FROM `' . DB::PREFIX . UserRoleRelation::TABLE . '` WHERE `user_id` = :user)' . ' WHERE `role_id` IN (SELECT `role_id` FROM `msz_users_roles` WHERE `user_id` = :user)'
)->bind('user', $this->getId())->fetchColumn(); )->bind('user', $this->getId())->fetchColumn();
return $this->userRank; return $this->userRank;
} }
public function hasAuthorityOver(HasRankInterface $other): bool { public function hasAuthorityOver(HasRankInterface $other): bool {
// Don't even bother checking if we're a super user return $this->isSuper()
if($this->isSuper()) || $other instanceof self && $other->getId() === $this->getId()
return true; || $this->getRank() > $other->getRank();
if($other instanceof self && $other->getId() === $this->getId())
return true;
return $this->getRank() > $other->getRank();
} }
public function getDisplayRoleId(): int { public function getDisplayRoleId(): int {
return $this->display_role < 1 ? -1 : $this->display_role; return $this->display_role < 1 ? -1 : $this->display_role;
} }
public function setDisplayRoleId(int $roleId): self {
$this->display_role = $roleId < 1 ? -1 : $roleId;
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 { public function hasTOTP(): bool {
return !empty($this->user_totp_key); return !empty($this->user_totp_key);
@ -417,50 +403,6 @@ class User implements HasRankInterface {
return $this->getBackgroundInfo()->isPresent(); return $this->getBackgroundInfo()->isPresent();
} }
/*********
* 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());
}
/*************** /***************
* FORUM STATS * * FORUM STATS *
***************/ ***************/
@ -621,7 +563,7 @@ class User implements HasRankInterface {
'UPDATE `' . DB::PREFIX . self::TABLE . '`' 'UPDATE `' . DB::PREFIX . self::TABLE . '`'
. ' SET `username` = :username, `email` = :email, `password` = :password' . ' SET `username` = :username, `email` = :email, `password` = :password'
. ', `user_super` = :is_super, `user_country` = :country, `user_colour` = :colour, `user_title` = :title' . ', `user_super` = :is_super, `user_country` = :country, `user_colour` = :colour, `user_title` = :title'
. ', `display_role` = :display_role, `user_totp_key` = :totp' . ', `user_totp_key` = :totp'
. ' WHERE `user_id` = :user' . ' WHERE `user_id` = :user'
) ->bind('user', $this->user_id) ) ->bind('user', $this->user_id)
->bind('username', $this->username) ->bind('username', $this->username)
@ -630,7 +572,6 @@ class User implements HasRankInterface {
->bind('is_super', $this->user_super) ->bind('is_super', $this->user_super)
->bind('country', $this->user_country) ->bind('country', $this->user_country)
->bind('colour', $this->user_colour) ->bind('colour', $this->user_colour)
->bind('display_role', $this->display_role)
->bind('totp', $this->user_totp_key) ->bind('totp', $this->user_totp_key)
->bind('title', $this->user_title) ->bind('title', $this->user_title)
->execute(); ->execute();

View file

@ -1,223 +0,0 @@
<?php
namespace Misuzu\Users;
use ArrayAccess;
use RuntimeException;
use Index\Colour\Colour;
use Misuzu\DB;
use Misuzu\HasRankInterface;
use Misuzu\Memoizer;
use Misuzu\Pagination;
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 $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): self {
$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->role_colour ?? 0x40000000) !== Colour::toMisuzu($this->colour))
$this->colour = Colour::fromMisuzu($this->role_colour ?? 0x40000000);
return $this->colour;
}
public function setColour(Colour $colour): self {
$this->role_colour = $colour->shouldInherit() ? null : Colour::toMisuzu($colour);
$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, 'COUNT(*)');
}
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 {
return self::memoizer()->find($roleId, function() use ($roleId) {
$object = DB::prepare(
self::byQueryBase() . ' WHERE `role_id` = :role'
) ->bind('role', $roleId)
->fetchObject(self::class);
if(!$object)
throw new RuntimeException('No role found with that ID.');
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): mixed {
return $this->{'get' . ucfirst($offset)}();
}
public function offsetSet($offset, $value): void {}
public function offsetUnset($offset): void {}
}

View file

@ -1,88 +0,0 @@
<?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 = 'users_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 UserRoleRelation;
$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, 'COUNT(*)');
}
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);
}
}

142
src/Users/Users.php Normal file
View file

@ -0,0 +1,142 @@
<?php
namespace Misuzu\Users;
use InvalidArgumentException;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
class Users {
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
public function updateUser(
User|string $userInfo,
RoleInfo|string|null $displayRoleInfo = null
): void {
if($userInfo instanceof User)
$userInfo = (string)$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);
$stmt->addParameter(2, $userInfo);
$stmt->execute();
}
public function hasRole(
User|string $userInfo,
RoleInfo|string $roleInfo
): bool {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
return in_array($roleInfo, $this->hasRoles($userInfo, $roleInfo));
}
public function hasRoles(
User|string $userInfo,
RoleInfo|string|array $roleInfos
): array {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if(!is_array($roleInfos))
$roleInfos = [$roleInfos];
elseif(empty($roleInfos))
return [];
$args = 0;
$stmt = $this->cache->get(sprintf(
'SELECT role_id FROM msz_users_roles WHERE user_id = ? AND role_id IN (%s)',
DbTools::prepareListString($roleInfos)
));
$stmt->addParameter(++$args, $userInfo);
foreach($roleInfos as $roleInfo) {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
elseif(!is_string($roleInfo))
throw new InvalidArgumentException('$roleInfos must be strings of instances of RoleInfo.');
$stmt->addParameter(++$args, $roleInfo);
}
$stmt->execute();
$roleIds = [];
$result = $stmt->getResult();
while($result->next())
$roleIds[] = (string)$result->getInteger(0);
return $roleIds;
}
public function addRoles(
User|string $userInfo,
RoleInfo|string|array $roleInfos
): void {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if(!is_array($roleInfos))
$roleInfos = [$roleInfos];
elseif(empty($roleInfos))
return;
$stmt = $this->cache->get(sprintf(
'REPLACE INTO msz_users_roles (user_id, role_id) VALUES %s',
DbTools::prepareListString($roleInfos, '(?, ?)')
));
$args = 0;
foreach($roleInfos as $roleInfo) {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
elseif(!is_string($roleInfo))
throw new InvalidArgumentException('$roleInfos must be strings of instances of RoleInfo.');
$stmt->addParameter(++$args, $userInfo);
$stmt->addParameter(++$args, $roleInfo);
}
$stmt->execute();
}
public function removeRoles(
User|string $userInfo,
RoleInfo|string|array $roleInfos
): void {
if($userInfo instanceof User)
$userInfo = (string)$userInfo->getId();
if(!is_array($roleInfos))
$roleInfos = [$roleInfos];
elseif(empty($roleInfos))
return;
$args = 0;
$stmt = $this->cache->get(sprintf(
'DELETE FROM msz_users_roles WHERE user_id = ? AND role_id IN (%s)',
DbTools::prepareListString($roleInfos)
));
$stmt->addParameter(++$args, $userInfo);
foreach($roleInfos as $roleInfo) {
if($roleInfo instanceof RoleInfo)
$roleInfo = $roleInfo->getId();
elseif(!is_string($roleInfo))
throw new InvalidArgumentException('$roleInfos must be strings of instances of RoleInfo.');
$stmt->addParameter(++$args, $roleInfo);
}
$stmt->execute();
}
}

View file

@ -156,6 +156,8 @@ class Warnings {
public function deleteWarnings(WarningInfo|string|array $warnInfos): void { public function deleteWarnings(WarningInfo|string|array $warnInfos): void {
if(!is_array($warnInfos)) if(!is_array($warnInfos))
$warnInfos = [$warnInfos]; $warnInfos = [$warnInfos];
elseif(empty($warnInfos))
return;
$stmt = $this->cache->get(sprintf( $stmt = $this->cache->get(sprintf(
'DELETE FROM msz_users_warnings WHERE warn_id IN (%s)', 'DELETE FROM msz_users_warnings WHERE warn_id IN (%s)',

View file

@ -4,7 +4,7 @@
{% from '_layout/input.twig' import input_csrf, input_text, input_checkbox %} {% from '_layout/input.twig' import input_csrf, input_text, input_checkbox %}
{% block manage_content %} {% block manage_content %}
<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 %}> <form action="{{ url('manage-role', {'role': role_info.id|default(0)}) }}" method="post"{% if role_info is not null %} style="--accent-colour: {{ role_info.colour }}"{% endif %}>
{{ input_csrf() }} {{ input_csrf() }}
<div class="container"> <div class="container">
@ -13,28 +13,35 @@
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Role Name</div> <div class="form__label__text">Role Name</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_text('role[name]', '', role_info.name|default(''), 'text', '', true, {'maxlength':255}) }} {{ input_text('ur_name', '', role_ur_name|default(role_info.name|default()), 'text', '', true, {'maxlength':255}) }}
</div> </div>
</label> </label>
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Hide Rank</div> <div class="form__label__text">Hide Role</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_checkbox('role[secret]', '', role_info is not null and role_info.hidden) }} {{ input_checkbox('ur_hidden', '', role_ur_hidden is defined ? role_ur_hidden : (role_info is not null ? role_info.hidden : false)) }}
</div> </div>
</label> </label>
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Hierarchy</div> <div class="form__label__text">Role can be removed by the user themselves</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_text('role[hierarchy]', '', role_info.rank|default(1), 'number', '', true) }} {{ input_checkbox('ur_leavable', '', role_ur_leavable is defined ? role_ur_leavable : (role_info is not null ? role_info.leavable : false)) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Rank</div>
<div class="form__label__input">
{{ input_text('ur_rank', '', role_ur_rank|default(role_info.rank|default(1)), 'number', '', true) }}
</div> </div>
</label> </label>
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Title</div> <div class="form__label__text">Title</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_text('role[title]', '', role_info.title|default(''), 'text', '', false, {'maxlength':64}) }} {{ input_text('ur_title', '', role_ur_title|default(role_info.title|default()), 'text', '', false, {'maxlength':64}) }}
</div> </div>
</label> </label>
@ -46,28 +53,28 @@
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Inherit Colour</div> <div class="form__label__text">Inherit Colour</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_checkbox('role[colour][inherit]', '', role_info is not null ? role_info.colour.shouldInherit : true) }} {{ input_checkbox('ur_col_inherit', '', role_ur_col_inherit is defined ? role_ur_col_inherit : (role_info is not null and role_info.hasColour ? role_info.colour.shouldInherit : true)) }}
</div> </div>
</label> </label>
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Red</div> <div class="form__label__text">Red</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_text('role[colour][red]', '', role_info.colour.red|default(0), 'number', '', false, {'min':0,'max':255}) }} {{ input_text('ur_col_red', '', role_ur_col_red|default(role_info.colour.red|default(0)), 'number', '', false, {'min':0,'max':255}) }}
</div> </div>
</label> </label>
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Green</div> <div class="form__label__text">Green</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_text('role[colour][green]', '', role_info.colour.green|default(0), 'number', '', false, {'min':0,'max':255}) }} {{ input_text('ur_col_green', '', role_ur_col_green|default(role_info.colour.green|default(0)), 'number', '', false, {'min':0,'max':255}) }}
</div> </div>
</label> </label>
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Blue</div> <div class="form__label__text">Blue</div>
<div class="form__label__input"> <div class="form__label__input">
{{ input_text('role[colour][blue]', '', role_info.colour.blue|default(0), 'number', '', false, {'min':0,'max':255}) }} {{ input_text('ur_col_blue', '', role_ur_col_blue|default(role_info.colour.blue|default(0)), 'number', '', false, {'min':0,'max':255}) }}
</div> </div>
</label> </label>
@ -79,7 +86,7 @@
<label class="form__label"> <label class="form__label">
<div class="form__label__text">Description</div> <div class="form__label__text">Description</div>
<div class="form__label__input"> <div class="form__label__input">
<textarea class="input__textarea" name="role[description]" maxlength="1000">{{ role_info.description|default('') }}</textarea> <textarea class="input__textarea" name="ur_desc" maxlength="1000">{{ role_ur_desc|default(role_info.description|default()) }}</textarea>
</div> </div>
</label> </label>
</div> </div>

View file

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

View file

@ -106,14 +106,13 @@
{{ input_colour(can_edit_user ? 'colour[hex]' : '', '', user_info.userColour) }} {{ input_colour(can_edit_user ? 'colour[hex]' : '', '', user_info.userColour) }}
</div> </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__user__details">
<div class="manage__tags manage__tags--fixed"> <div class="manage__tags manage__tags--fixed">
{% for role in manage_roles %} {% for role in manage_roles %}
<label class="manage__tag" style="--accent-colour: {{ role.colour }}"> <label class="manage__tag" style="--accent-colour: {{ role.colour }}">
<div class="manage__tag__background"></div> <div class="manage__tag__background"></div>
<div class="manage__tag__content"> <div class="manage__tag__content">
{{ input_checkbox('roles[]', '', user_info.hasRole(role), 'manage__tag__checkbox', role.id, false, null, not can_edit_user) }} {{ input_checkbox('roles[]', '', role.id in manage_user_has_roles, 'manage__tag__checkbox', role.id, false, null, not can_edit_user) }}
<div class="manage__tag__title"> <div class="manage__tag__title">
{{ role.name }} {{ role.name }}
</div> </div>

View file

@ -71,8 +71,8 @@
</div> </div>
<div class="settings__role__collection"> <div class="settings__role__collection">
{% for role in settings_user.roles %} {% for role in settings_roles %}
{% set is_display_role = settings_user.isDisplayRole(role) %} {% set is_display_role = role.id == settings_user.displayRoleId %}
<div class="settings__role" style="--accent-colour: {{ role.colour }}"> <div class="settings__role" style="--accent-colour: {{ role.colour }}">
<div class="settings__role__content"> <div class="settings__role__content">
@ -94,10 +94,10 @@
<i class="far {{ is_display_role ? 'fa-check-square' : 'fa-square' }}"></i> <i class="far {{ is_display_role ? 'fa-check-square' : 'fa-square' }}"></i>
</button> </button>
<button class="settings__role__option{% if not role.canLeave %} settings__role__option--disabled{% endif %}" <button class="settings__role__option{% if not role.leavable %} settings__role__option--disabled{% endif %}"
name="role[mode]" value="leave" title="Leave this role" name="role[mode]" value="leave" title="Leave this role"
onclick="return confirm('Are you sure you want to remove {{ role.name|replace({"'": "\'"}) }} from your account?')" onclick="return confirm('Are you sure you want to remove {{ role.name|replace({"'": "\'"}) }} from your account?')"
{% if not role.canLeave %}disabled{% endif %}> {% if not role.leavable %}disabled{% endif %}>
<i class="fas fa-times-circle"></i> <i class="fas fa-times-circle"></i>
</button> </button>
</form> </form>

View file

@ -20,7 +20,11 @@
<div class="userlist__navigation"> <div class="userlist__navigation">
<form onchange="this.submit()" class="userlist__sorting"> <form onchange="this.submit()" class="userlist__sorting">
{{ input_select('r', roles, role_id, 'name', 'id', false, 'userlist__select') }} <select class="input__select userlist__select" name="r">
{% for role in roles %}
<option value="{{ role.id }}"{% if role.id == role_id %}selected{% endif %} style="background-color: #222; {% if role.hasColour %}color: {{ role.colour }}{% endif %}">{{ role.name }}</option>
{% endfor %}
</select>
{{ input_select('ss', orders, order, 'title', null, false, 'userlist__select') }} {{ input_select('ss', orders, order, 'title', null, false, 'userlist__select') }}
{{ input_select('sd', directions, direction, null, null, false, 'userlist__select') }} {{ input_select('sd', directions, direction, null, null, false, 'userlist__select') }}