2023-08-30 22:37:21 +00:00
< ? php
namespace Misuzu\Perms ;
use stdClass ;
use InvalidArgumentException ;
use RuntimeException ;
use Index\DateTime ;
2023-09-08 00:54:19 +00:00
use Index\Environment ;
2023-08-30 22:37:21 +00:00
use Index\Data\DbStatementCache ;
use Index\Data\DbTools ;
use Index\Data\IDbConnection ;
use Index\Data\IDbStatement ;
2023-09-08 13:22:46 +00:00
use Misuzu\Forum\ForumCategories ;
2023-08-30 22:37:21 +00:00
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 () ? new PermissionInfo ( $result ) : null ;
$perms = [];
while ( $result -> next ())
$perms [ $result -> getString ( 3 )] = new PermissionInfo ( $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 ;
}
2023-08-30 23:56:30 +00:00
// precalculates all permissions for fast lookups
2023-09-08 13:22:46 +00:00
public function precalculatePermissions ( ForumCategories $forumCategories , array $userIds = []) : void {
2023-08-30 23:56:30 +00:00
$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' );
}
2023-08-30 22:37:21 +00:00
self :: precalculatePermissionsLog ( 'Creating inserter statement...' );
$insert = $this -> cache -> get ( 'INSERT INTO msz_perms_calculated (user_id, forum_id, perms_category, perms_calculated) VALUES (?, ?, ?, ?)' );
2023-08-30 23:56:30 +00:00
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 ();
}
2023-08-30 22:37:21 +00:00
}
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...' );
2023-09-08 13:22:46 +00:00
$forumCats = $forumCategories -> getCategories ( asTree : true );
2023-08-30 22:37:21 +00:00
foreach ( $forumCats as $forumCat )
2023-08-30 23:56:30 +00:00
$this -> precalculatePermissionsForForumCategory ( $insert , $userIds , $forumCat , $doGuest );
2023-08-30 22:37:21 +00:00
self :: precalculatePermissionsLog ( 'Finished permission precalculations!' );
}
2023-08-30 23:56:30 +00:00
private function precalculatePermissionsForForumCategory ( IDbStatement $insert , array $userIds , object $forumCat , bool $doGuest , array $catIds = []) : void {
2023-08-30 22:37:21 +00:00
$catIds [] = $currentCatId = $forumCat -> info -> getId ();
self :: precalculatePermissionsLog ( 'Precalcuting permissions for forum category #%s (%s)...' , $currentCatId , implode ( ' <- ' , $catIds ));
2023-08-30 23:56:30 +00:00
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 ();
2023-08-30 22:37:21 +00:00
2023-08-30 23:56:30 +00:00
$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 ();
}
2023-08-30 22:37:21 +00:00
}
$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 )
2023-08-30 23:56:30 +00:00
$this -> precalculatePermissionsForForumCategory ( $insert , $userIds , $forumChild , $doGuest , $catIds );
2023-08-30 22:37:21 +00:00
}
private static function precalculatePermissionsLog ( string $fmt , ... $args ) : void {
2023-09-08 00:54:19 +00:00
if ( ! Environment :: isConsole ())
2023-08-30 23:56:30 +00:00
return ;
2023-08-30 22:37:21 +00:00
echo DateTime :: now () -> format ( '[H:i:s.u] ' );
vprintf ( $fmt , $args );
echo PHP_EOL ;
}
}