Files
2025-12-19 19:17:57 -08:00

203 lines
5.7 KiB
PHP

<?php
/**
* @brief Permissions
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 4/16/2025
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Application;
use IPS\Application\Module;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Extensions\PermissionsAbstract;
use IPS\IPS;
use IPS\Member;
use IPS\Member\Group;
use IPS\Node\Model;
use IPS\Settings;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
class Permissions
{
/**
* @brief Used for Permissions extensions; override default permissions
*/
const PERM_ALLOW = 1;
/**
* @brief Used for Permissions extensions; override default permissions
*/
const PERM_DENY = 2;
/**
* @brief Used for Permissions extensions; revert to default permission checks
*/
const PERM_DEFAULT = 0;
/**
* @var array
*/
protected static array $nodeIncludesMap = [];
/**
* @var array
*/
protected static array $nodeExcludeMap = [];
/**
* Load all the inclusion/exclusion mappings from extensions
*
* @param string $permission
* @param string $nodeClass
* @param Member $member
* @return void
*/
protected static function loadMappings( string $permission, string $nodeClass, Member $member ) : void
{
if( !isset( static::$nodeIncludesMap[ $nodeClass ][ $permission ] ) )
{
$include = [];
$exclude = [];
foreach( Application::allExtensions( 'core', 'Permissions' ) as $ext )
{
/* @var PermissionsAbstract $ext */
$include = array_merge( $include, $ext->nodeIdsToAllow( $permission, $nodeClass, $member ) );
$exclude = array_merge( $exclude, $ext->nodeIdsToDeny( $permission, $nodeClass, $member ) );
}
static::$nodeIncludesMap[ $nodeClass ][ $permission ] = $include;
static::$nodeExcludeMap[ $nodeClass ][ $permission ] = $exclude;
}
}
/**
* Easily load included IDs, for my sanity
*
* @param string $permission
* @param string $nodeClass
* @param Member|null $member
* @param int $permType
* @return array|null
*/
protected static function getMapping( string $permission, string $nodeClass, ?Member $member, int $permType ) : ?array
{
/* Only allow these extensions to run on the front-end or from the API */
if( !Dispatcher::hasInstance() or ( Dispatcher::i()->controllerLocation != 'front' and Dispatcher::i()->controllerLocation != 'api' ) )
{
return null;
}
$member = $member ?: Member::loggedIn();
static::loadMappings( $permission, $nodeClass, $member );
switch( $permType )
{
case static::PERM_ALLOW:
return count( static::$nodeIncludesMap[ $nodeClass ][ $permission ] ) ? static::$nodeIncludesMap[ $nodeClass ][ $permission ] : null;
case static::PERM_DENY:
return count( static::$nodeExcludeMap[ $nodeClass ][ $permission ] ) ? static::$nodeExcludeMap[ $nodeClass ][ $permission ] : null;
}
return null;
}
/**
* Wrapper method to call extension
*
* @param string $permission
* @param object $object
* @param Member|Group|null $member
* @return int
*/
public static function can( string $permission, object $object, Member|Group|null $member=null ) : int
{
/* Rather than do a breaking change, skip this check if we pass in a group */
if( $member instanceof Group )
{
return static::PERM_DEFAULT;
}
/* Only allow these extensions to run on the front-end or from the API */
if( !Dispatcher::hasInstance() or ( Dispatcher::i()->controllerLocation != 'front' and Dispatcher::i()->controllerLocation != 'api' ) )
{
return static::PERM_DEFAULT;
}
foreach( Application::allExtensions( 'core', 'Permissions' ) as $ext )
{
/* @var PermissionsAbstract $ext */
$result = $ext->checkPermission( $permission, $object, $member );
if( $result !== static::PERM_DEFAULT )
{
return $result;
}
}
return static::PERM_DEFAULT;
}
/**
* Build a query clause to pull node IDs from the permissions table.
*
* @param string $permission
* @param string $nodeClass
* @param Member|null $member
* @param bool $includeClubs
* @return array
*/
public static function nodePermissionClause( string $permission, string $nodeClass, ?Member $member=null, bool $includeClubs=true ) : array
{
$member = $member ?: Member::loggedIn();
/* If clubs are disabled, or we don't have permission, force it to exclude them */
if( !Settings::i()->clubs or !$member->canAccessModule( Module::get( 'core', 'clubs', 'front' ) ) or !IPS::classUsesTrait( $nodeClass, ClubContainer::class ) )
{
$includeClubs = false;
}
/* @var Model $nodeClass */
/* @var array $permissionMap */
$clause = [
Db::i()->findInSet( 'core_permission_index.perm_' . $nodeClass::$permissionMap[ $permission ], ( $includeClubs ? $member->permissionArray() : $member->groups ) ),
'core_permission_index.perm_' . $nodeClass::$permissionMap[ $permission ] . '=?'
];
$binds = [
'*'
];
/* If we have any specific inclusions, put them in */
if( $includes = static::getMapping( $permission, $nodeClass, $member, static::PERM_ALLOW ) )
{
$clause[] = 'core_permission_index.perm_type_id in (' . implode( ",", $includes ) . ')';
}
$return = [
array_merge( array( '(' . implode( ' OR ', $clause ) . ')' ), $binds )
];
/* If we have any specific exclusions, we need it separate so that we make it an AND */
if( $excludes = static::getMapping( $permission, $nodeClass, $member, static::PERM_DENY ) )
{
$return[] = [ Db::i()->in( 'core_permission_index.perm_type_id', $excludes, true ) ];
}
return $return;
}
}