misuzu/src/Perms/Permissions.php

382 lines
17 KiB
PHP

<?php
namespace Misuzu\Perms;
use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Environment;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Data\IDbStatement;
use Misuzu\Forum\ForumCategories;
use Misuzu\Forum\ForumCategoryInfo;
use Misuzu\Users\RoleInfo;
use Misuzu\Users\UserInfo;
class Permissions {
// limiting this to 53-bit in case it ever has to be sent to javascript or any other implicit float language
// For More Information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
// it's still a ways up from the 31-bit of the old permission system which only existed because i developed on a 32-bit laptop for a bit
public const PERMS_MIN = 0;
public const PERMS_MAX = 9007199254740991;
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
// this method is purely intended for getting the permission data for a single entity
// it should not be used to do actual permission checks
public function getPermissionInfo(
UserInfo|string|null $userInfo = null,
RoleInfo|string|null $roleInfo = null,
ForumCategoryInfo|string|null $forumCategoryInfo = null,
array|string|null $categoryNames = null,
): PermissionInfo|array|null {
$hasUserInfo = $userInfo !== null;
$hasRoleInfo = $roleInfo !== null;
if($hasUserInfo && $hasRoleInfo)
throw new InvalidArgumentException('$userInfo and $roleInfo may not be set at the same time.');
$hasForumCategoryInfo = $forumCategoryInfo !== null;
$hasCategoryName = $categoryNames !== null;
$categoryNamesIsArray = $hasCategoryName && is_array($categoryNames);
if($categoryNamesIsArray && empty($categoryNames))
throw new InvalidArgumentException('$categoryNames may not be empty if it is an array.');
$query = 'SELECT user_id, role_id, forum_id, perms_category, perms_allow, perms_deny FROM msz_perms';
$query .= sprintf(' WHERE user_id %s', $hasUserInfo ? '= ?' : 'IS NULL');
$query .= sprintf(' AND role_id %s', $hasRoleInfo ? '= ?' : 'IS NULL');
$query .= sprintf(' AND forum_id %s', $hasForumCategoryInfo ? '= ?' : 'IS NULL');
if($hasCategoryName)
$query .= ' AND perms_category ' . ($categoryNamesIsArray ? sprintf('IN (%s)', DbTools::prepareListString($categoryNames)) : '= ?');
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
if($hasRoleInfo)
$stmt->addParameter(++$args, $roleInfo instanceof RoleInfo ? $roleInfo->getId() : $roleInfo);
if($hasForumCategoryInfo)
$stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
if($hasCategoryName) {
if($categoryNamesIsArray) {
foreach($categoryNames as $name)
$stmt->addParameter(++$args, $name);
} else
$stmt->addParameter(++$args, $categoryNames);
}
$stmt->execute();
$result = $stmt->getResult();
if(is_string($categoryNames))
return $result->next() ? PermissionInfo::fromResult($result) : null;
$perms = [];
while($result->next())
$perms[$result->getString(3)] = PermissionInfo::fromResult($result);
return $perms;
}
public function setPermissions(
string $categoryName,
int $allow,
int $deny,
UserInfo|string|null $userInfo = null,
RoleInfo|string|null $roleInfo = null,
ForumCategoryInfo|string|null $forumCategoryInfo = null
): void {
if($allow < self::PERMS_MIN || $allow > self::PERMS_MAX)
throw new InvalidArgumentException('$allow must be an positive 53-bit integer.');
if($deny < self::PERMS_MIN || $deny > self::PERMS_MAX)
throw new InvalidArgumentException('$allow must be an positive 53-bit integer.');
if($userInfo !== null && $roleInfo !== null)
throw new InvalidArgumentException('$userInfo and $roleInfo may not be set at the same time.');
// because of funny technical reasons we have to delete separately
$this->removePermissions($categoryName, $userInfo, $roleInfo, $forumCategoryInfo);
// don't insert zeroes
if($allow === 0 && $deny === 0)
return;
$stmt = $this->cache->get('INSERT INTO msz_perms (user_id, role_id, forum_id, perms_category, perms_allow, perms_deny) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->addParameter(2, $roleInfo instanceof RoleInfo ? $roleInfo->getId() : $roleInfo);
$stmt->addParameter(3, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
$stmt->addParameter(4, $categoryName);
$stmt->addParameter(5, $allow);
$stmt->addParameter(6, $deny);
$stmt->execute();
}
public function removePermissions(
array|string|null $categoryNames,
UserInfo|string|null $userInfo = null,
RoleInfo|string|null $roleInfo = null,
ForumCategoryInfo|string|null $forumCategoryInfo = null
): void {
$hasUserInfo = $userInfo !== null;
$hasRoleInfo = $roleInfo !== null;
$hasForumCategoryInfo = $forumCategoryInfo !== null;
$hasCategoryNames = $categoryNames !== null;
$categoryNamesIsArray = $hasCategoryNames && is_array($categoryNames);
$query = 'DELETE FROM msz_perms';
$query .= sprintf(' WHERE user_id %s', $hasUserInfo ? '= ?' : 'IS NULL');
$query .= sprintf(' AND role_id %s', $hasRoleInfo ? '= ?' : 'IS NULL');
$query .= sprintf(' AND forum_id %s', $hasForumCategoryInfo ? '= ?' : 'IS NULL');
if($hasCategoryNames)
$query .= ' AND perms_category ' . ($categoryNamesIsArray ? sprintf('IN (%s)', DbTools::prepareListString($categoryNames)) : '= ?');
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
if($hasRoleInfo)
$stmt->addParameter(++$args, $roleInfo instanceof RoleInfo ? $roleInfo->getId() : $roleInfo);
if($hasForumCategoryInfo)
$stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
if($categoryNamesIsArray) {
foreach($categoryNames as $name)
$stmt->addParameter(++$args, $name);
} else
$stmt->addParameter(++$args, $categoryNames);
$stmt->execute();
}
public function checkPermissions(
string $categoryName,
int $perms,
UserInfo|string|null $userInfo = null,
ForumCategoryInfo|string|null $forumCategoryInfo = null
): int {
$hasUserInfo = $userInfo !== null;
$hasForumCategoryInfo = $forumCategoryInfo !== null;
$query = 'SELECT perms_calculated & ? FROM msz_perms_calculated WHERE perms_category = ?';
$query .= sprintf(' AND forum_id %s', $hasForumCategoryInfo ? '= ?' : 'IS NULL');
$query .= sprintf(' AND user_id %s', $hasUserInfo ? '= ?' : 'IS NULL');
$args = 0;
$stmt = $this->cache->get($query);
$stmt->addParameter(++$args, $perms);
$stmt->addParameter(++$args, $categoryName);
if($hasForumCategoryInfo)
$stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
public function getPermissions(
string|array $categoryNames,
UserInfo|string|null $userInfo = null,
ForumCategoryInfo|string|null $forumCategoryInfo = null
): PermissionResult|stdClass {
$categoryNamesIsArray = is_array($categoryNames);
if($categoryNamesIsArray && empty($categoryNames))
throw new InvalidArgumentException('$categoryNames may not be an empty array.');
$hasUserInfo = $userInfo !== null;
$hasForumCategoryInfo = $forumCategoryInfo !== null;
$query = 'SELECT perms_category, perms_calculated FROM msz_perms_calculated';
$query .= ' WHERE perms_category ' . ($categoryNamesIsArray ? sprintf('IN (%s)', DbTools::prepareListString($categoryNames)) : '= ?');
$query .= sprintf(' AND forum_id %s', $hasForumCategoryInfo ? '= ?' : 'IS NULL');
$query .= sprintf(' AND user_id %s', $hasUserInfo ? '= ?' : 'IS NULL');
$query .= ' GROUP BY perms_category';
$args = 0;
$stmt = $this->cache->get($query);
if($categoryNamesIsArray) {
foreach($categoryNames as $name)
$stmt->addParameter(++$args, $name);
} else
$stmt->addParameter(++$args, $categoryNames);
if($hasForumCategoryInfo)
$stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->execute();
$result = $stmt->getResult();
if(!$categoryNamesIsArray)
return new PermissionResult($result->next() ? $result->getInteger(1) : 0);
$results = [];
while($result->next())
$results[$result->getString(0)] = $result->getInteger(1);
$sets = new stdClass;
foreach($categoryNames as $categoryName)
$sets->{$categoryName} = new PermissionResult($results[$categoryName] ?? 0);
return $sets;
}
// precalculates all permissions for fast lookups
public function precalculatePermissions(ForumCategories $forumCategories, array $userIds = []): void {
$suppliedUsers = !empty($userIds);
$doGuest = !$suppliedUsers;
if($suppliedUsers) {
self::precalculatePermissionsLog('Removing calculations for given user ids...');
$stmt = $this->cache->get('DELETE FROM msz_perms_calculated WHERE user_id = ?');
foreach($userIds as $userId) {
$stmt->addParameter(1, $userId);
$stmt->execute();
}
} else {
self::precalculatePermissionsLog('Loading list of user IDs...');
$result = $this->dbConn->query('SELECT user_id FROM msz_users');
while($result->next())
$userIds[] = $result->getString(0);
self::precalculatePermissionsLog('Clearing existing precalculations...');
$this->dbConn->execute('TRUNCATE msz_perms_calculated');
}
self::precalculatePermissionsLog('Creating inserter statement...');
$insert = $this->cache->get('INSERT INTO msz_perms_calculated (user_id, forum_id, perms_category, perms_calculated) VALUES (?, ?, ?, ?)');
if($doGuest) {
self::precalculatePermissionsLog('Calculating guest permissions...');
$result = $this->dbConn->query('SELECT perms_category, BIT_OR(perms_allow) & ~BIT_OR(perms_deny) FROM msz_perms WHERE forum_id IS NULL AND user_id IS NULL AND role_id IS NULL GROUP BY perms_category');
$insert->addParameter(1, null);
$insert->addParameter(2, null);
while($result->next()) {
$category = $result->getString(0);
$perms = $result->getInteger(1);
if($perms === 0)
continue;
self::precalculatePermissionsLog('Inserting guest permissions for category %s with value %x...', $category, $perms);
$insert->addParameter(3, $category);
$insert->addParameter(4, $perms);
$insert->execute();
}
}
self::precalculatePermissionsLog('Calculating user permissions...');
$stmt = $this->cache->get('SELECT perms_category, BIT_OR(perms_allow) & ~BIT_OR(perms_deny) FROM msz_perms WHERE forum_id IS NULL AND (user_id = ? OR role_id IN (SELECT role_id FROM msz_users_roles WHERE user_id = ?)) GROUP BY perms_category');
foreach($userIds as $userId) {
$insert->reset();
$insert->addParameter(1, $userId);
$insert->addParameter(2, null);
$stmt->reset();
$stmt->addParameter(1, $userId);
$stmt->addParameter(2, $userId);
$stmt->execute();
$result = $stmt->getResult();
while($result->next()) {
$category = $result->getString(0);
$perms = $result->getInteger(1);
if($perms === 0)
continue;
self::precalculatePermissionsLog('Inserting user #%s permissions for category %s with value %x...', $userId, $category, $perms);
$insert->addParameter(3, $category);
$insert->addParameter(4, $perms);
$insert->execute();
}
}
self::precalculatePermissionsLog('Loading list of forum categories...');
$forumCats = $forumCategories->getCategories(asTree: true);
foreach($forumCats as $forumCat)
$this->precalculatePermissionsForForumCategory($insert, $userIds, $forumCat, $doGuest);
self::precalculatePermissionsLog('Finished permission precalculations!');
}
private function precalculatePermissionsForForumCategory(IDbStatement $insert, array $userIds, object $forumCat, bool $doGuest, array $catIds = []): void {
$catIds[] = $currentCatId = $forumCat->info->getId();
self::precalculatePermissionsLog('Precalcuting permissions for forum category #%s (%s)...', $currentCatId, implode(' <- ', $catIds));
if($doGuest) {
self::precalculatePermissionsLog('Calculating guest permission for forum category #%s...', $currentCatId);
$args = 0;
$stmt = $this->cache->get(sprintf(
'SELECT perms_category, BIT_OR(perms_allow) & ~BIT_OR(perms_deny) FROM msz_perms WHERE forum_id IN (%s) AND user_id IS NULL AND role_id IS NULL GROUP BY perms_category',
DbTools::prepareListString($catIds)
));
foreach($catIds as $catId)
$stmt->addParameter(++$args, $catId);
$stmt->execute();
$insert->reset();
$insert->addParameter(1, null);
$insert->addParameter(2, $currentCatId);
$result = $stmt->getResult();
while($result->next()) {
$category = $result->getString(0);
$perms = $result->getInteger(1);
if($perms === 0)
continue;
self::precalculatePermissionsLog('Inserting guest permissions for category %s with value %x for forum category #%s...', $category, $perms, $currentCatId);
$insert->addParameter(3, $category);
$insert->addParameter(4, $perms);
$insert->execute();
}
}
$args = 0;
$stmt = $this->cache->get(sprintf(
'SELECT perms_category, BIT_OR(perms_allow) & ~BIT_OR(perms_deny) FROM msz_perms WHERE forum_id IN (%s) AND (user_id = ? OR role_id IN (SELECT role_id FROM msz_users_roles WHERE user_id = ?)) GROUP BY perms_category',
DbTools::prepareListString($catIds)
));
foreach($catIds as $catId)
$stmt->addParameter(++$args, $catId);
$startArgs = $args;
foreach($userIds as $userId) {
$args = $startArgs;
$stmt->addParameter(++$args, $userId);
$stmt->addParameter(++$args, $userId);
$stmt->execute();
$insert->reset();
$insert->addParameter(1, $userId);
$insert->addParameter(2, $currentCatId);
$result = $stmt->getResult();
while($result->next()) {
$category = $result->getString(0);
$perms = $result->getInteger(1);
if($perms === 0)
continue;
self::precalculatePermissionsLog('Inserting user #%s permissions for category %s with value %x for forum category #%s...', $userId, $category, $perms, $currentCatId);
$insert->addParameter(3, $category);
$insert->addParameter(4, $perms);
$insert->execute();
}
}
foreach($forumCat->children as $forumChild)
$this->precalculatePermissionsForForumCategory($insert, $userIds, $forumChild, $doGuest, $catIds);
}
private static function precalculatePermissionsLog(string $fmt, ...$args): void {
if(!Environment::isConsole())
return;
echo DateTime::now()->format('[H:i:s.u] ');
vprintf($fmt, $args);
echo PHP_EOL;
}
}