382 lines
17 KiB
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;
|
|
}
|
|
}
|