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; } }