mirror of https://github.com/joomla/joomla-cms.git
1169 lines
34 KiB
PHP
1169 lines
34 KiB
PHP
<?php
|
|
/**
|
|
* Joomla! Content Management System
|
|
*
|
|
* @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
|
|
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
|
*/
|
|
|
|
namespace Joomla\CMS\Access;
|
|
|
|
\defined('JPATH_PLATFORM') or die;
|
|
|
|
use Joomla\CMS\Component\ComponentHelper;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Helper\UserGroupsHelper;
|
|
use Joomla\CMS\Log\Log;
|
|
use Joomla\CMS\Profiler\Profiler;
|
|
use Joomla\CMS\Table\Asset;
|
|
use Joomla\CMS\User\User;
|
|
use Joomla\Database\ParameterType;
|
|
use Joomla\Utilities\ArrayHelper;
|
|
|
|
/**
|
|
* Class that handles all access authorisation routines.
|
|
*
|
|
* @since 1.7.0
|
|
*/
|
|
class Access
|
|
{
|
|
/**
|
|
* Array of view levels
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $viewLevels = array();
|
|
|
|
/**
|
|
* Array of rules for the asset
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $assetRules = array();
|
|
|
|
/**
|
|
* Array of identities for asset rules
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $assetRulesIdentities = array();
|
|
|
|
/**
|
|
* Array of the permission parent ID mappings
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $assetPermissionsParentIdMapping = array();
|
|
|
|
/**
|
|
* Array of asset types that have been preloaded
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $preloadedAssetTypes = array();
|
|
|
|
/**
|
|
* Array of loaded user identities
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $identities = array();
|
|
|
|
/**
|
|
* Array of user groups.
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $userGroups = array();
|
|
|
|
/**
|
|
* Array of user group paths.
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $userGroupPaths = array();
|
|
|
|
/**
|
|
* Array of cached groups by user.
|
|
*
|
|
* @var array
|
|
* @since 1.7.0
|
|
*/
|
|
protected static $groupsByUser = array();
|
|
|
|
/**
|
|
* Array of preloaded asset names and ids (key is the asset id).
|
|
*
|
|
* @var array
|
|
* @since 3.7.0
|
|
*/
|
|
protected static $preloadedAssets = array();
|
|
|
|
/**
|
|
* The root asset id.
|
|
*
|
|
* @var integer
|
|
* @since 3.7.0
|
|
*/
|
|
protected static $rootAssetId = null;
|
|
|
|
/**
|
|
* Method for clearing static caches.
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 1.7.3
|
|
*/
|
|
public static function clearStatics()
|
|
{
|
|
self::$viewLevels = array();
|
|
self::$assetRules = array();
|
|
self::$assetRulesIdentities = array();
|
|
self::$assetPermissionsParentIdMapping = array();
|
|
self::$preloadedAssetTypes = array();
|
|
self::$identities = array();
|
|
self::$userGroups = array();
|
|
self::$userGroupPaths = array();
|
|
self::$groupsByUser = array();
|
|
self::$preloadedAssets = array();
|
|
self::$rootAssetId = null;
|
|
}
|
|
|
|
/**
|
|
* Method to check if a user is authorised to perform an action, optionally on an asset.
|
|
*
|
|
* @param integer $userId Id of the user for which to check authorisation.
|
|
* @param string $action The name of the action to authorise.
|
|
* @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset.
|
|
* @param boolean $preload Indicates whether preloading should be used.
|
|
*
|
|
* @return boolean|null True if allowed, false for an explicit deny, null for an implicit deny.
|
|
*
|
|
* @since 1.7.0
|
|
*/
|
|
public static function check($userId, $action, $assetKey = null, $preload = true)
|
|
{
|
|
// Sanitise inputs.
|
|
$userId = (int) $userId;
|
|
$action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action)));
|
|
|
|
if (!isset(self::$identities[$userId]))
|
|
{
|
|
// Get all groups against which the user is mapped.
|
|
self::$identities[$userId] = self::getGroupsByUser($userId);
|
|
array_unshift(self::$identities[$userId], $userId * -1);
|
|
}
|
|
|
|
return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::$identities[$userId]);
|
|
}
|
|
|
|
/**
|
|
* Method to preload the Rules object for the given asset type.
|
|
*
|
|
* @param integer|string|array $assetTypes The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2').
|
|
* Also accepts the asset id. An array of asset type or a special
|
|
* 'components' string to load all component assets.
|
|
* @param boolean $reload Set to true to reload from database.
|
|
*
|
|
* @return boolean True on success.
|
|
*
|
|
* @since 1.6
|
|
* @note This method will return void in 4.0.
|
|
*/
|
|
public static function preload($assetTypes = 'components', $reload = false)
|
|
{
|
|
// If sent an asset id, we first get the asset type for that asset id.
|
|
if (is_numeric($assetTypes))
|
|
{
|
|
$assetTypes = self::getAssetType($assetTypes);
|
|
}
|
|
|
|
// Check for default case:
|
|
$isDefault = \is_string($assetTypes) && \in_array($assetTypes, array('components', 'component'));
|
|
|
|
// Preload the rules for all of the components.
|
|
if ($isDefault)
|
|
{
|
|
self::preloadComponents();
|
|
|
|
return true;
|
|
}
|
|
|
|
// If we get to this point, this is a regular asset type and we'll proceed with the preloading process.
|
|
if (!\is_array($assetTypes))
|
|
{
|
|
$assetTypes = (array) $assetTypes;
|
|
}
|
|
|
|
foreach ($assetTypes as $assetType)
|
|
{
|
|
self::preloadPermissions($assetType, $reload);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Method to recursively retrieve the list of parent Asset IDs
|
|
* for a particular Asset.
|
|
*
|
|
* @param string $assetType The asset type, or the asset name, or the extension of the asset
|
|
* (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact').
|
|
* @param integer $assetId The numeric asset id.
|
|
*
|
|
* @return array List of ancestor ids (includes original $assetId).
|
|
*
|
|
* @since 1.6
|
|
*/
|
|
protected static function getAssetAncestors($assetType, $assetId)
|
|
{
|
|
// Get the extension name from the $assetType provided
|
|
$extensionName = self::getExtensionNameFromAsset($assetType);
|
|
|
|
// Holds the list of ancestors for the Asset ID:
|
|
$ancestors = array();
|
|
|
|
// Add in our starting Asset ID:
|
|
$ancestors[] = (int) $assetId;
|
|
|
|
// Initialize the variable we'll use in the loop:
|
|
$id = (int) $assetId;
|
|
|
|
while ($id !== 0)
|
|
{
|
|
if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id]))
|
|
{
|
|
$id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id;
|
|
|
|
if ($id !== 0)
|
|
{
|
|
$ancestors[] = $id;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Add additional case to break out of the while loop automatically in
|
|
// the case that the ID is non-existent in our mapping variable above.
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $ancestors;
|
|
}
|
|
|
|
/**
|
|
* Method to retrieve the Asset Rule strings for this particular
|
|
* Asset Type and stores them for later usage in getAssetRules().
|
|
* Stores 2 arrays: one where the list has the Asset ID as the key
|
|
* and a second one where the Asset Name is the key.
|
|
*
|
|
* @param string $assetType The asset type, or the asset name, or the extension of the asset
|
|
* (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact').
|
|
* @param boolean $reload Reload the preloaded assets.
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 1.6
|
|
*/
|
|
protected static function preloadPermissions($assetType, $reload = false)
|
|
{
|
|
// Get the extension name from the $assetType provided
|
|
$extensionName = self::getExtensionNameFromAsset($assetType);
|
|
|
|
// If asset is a component, make sure that all the component assets are preloaded.
|
|
if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload)
|
|
{
|
|
return;
|
|
}
|
|
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadPermissions (' . $extensionName . ')');
|
|
|
|
// Get the database connection object.
|
|
$db = Factory::getDbo();
|
|
$assetKey = $extensionName . '.%';
|
|
|
|
// Get a fresh query object.
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName(array('id', 'name', 'rules', 'parent_id')))
|
|
->from($db->quoteName('#__assets'))
|
|
->where(
|
|
[
|
|
$db->quoteName('name') . ' LIKE :asset',
|
|
$db->quoteName('name') . ' = :extension',
|
|
$db->quoteName('parent_id') . ' = 0',
|
|
],
|
|
'OR'
|
|
)
|
|
->bind(':extension', $extensionName)
|
|
->bind(':asset', $assetKey);
|
|
|
|
// Get the permission map for all assets in the asset extension.
|
|
$assets = $db->setQuery($query)->loadObjectList();
|
|
|
|
self::$assetPermissionsParentIdMapping[$extensionName] = array();
|
|
|
|
foreach ($assets as $asset)
|
|
{
|
|
self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset;
|
|
self::$preloadedAssets[$asset->id] = $asset->name;
|
|
}
|
|
|
|
// Mark asset type and it's extension name as preloaded.
|
|
self::$preloadedAssetTypes[$assetType] = true;
|
|
self::$preloadedAssetTypes[$extensionName] = true;
|
|
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadPermissions (' . $extensionName . ')');
|
|
}
|
|
|
|
/**
|
|
* Method to preload the Rules objects for all components.
|
|
*
|
|
* Note: This will only get the base permissions for the component.
|
|
* e.g. it will get 'com_content', but not 'com_content.article.1' or
|
|
* any more specific asset type rules.
|
|
*
|
|
* @return array Array of component names that were preloaded.
|
|
*
|
|
* @since 1.6
|
|
*/
|
|
protected static function preloadComponents()
|
|
{
|
|
// If the components already been preloaded do nothing.
|
|
if (isset(self::$preloadedAssetTypes['components']))
|
|
{
|
|
return array();
|
|
}
|
|
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadComponents (all components)');
|
|
|
|
// Add root to asset names list.
|
|
$components = array('root.1');
|
|
|
|
// Add enabled components to asset names list.
|
|
foreach (ComponentHelper::getComponents() as $component)
|
|
{
|
|
if ($component->enabled)
|
|
{
|
|
$components[] = $component->option;
|
|
}
|
|
}
|
|
|
|
// Get the database connection object.
|
|
$db = Factory::getDbo();
|
|
|
|
// Get the asset info for all assets in asset names list.
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName(array('id', 'name', 'rules', 'parent_id')))
|
|
->from($db->quoteName('#__assets'))
|
|
->whereIn($db->quoteName('name'), $components, ParameterType::STRING);
|
|
|
|
// Get the Name Permission Map List
|
|
$assets = $db->setQuery($query)->loadObjectList();
|
|
|
|
$rootAsset = null;
|
|
|
|
// First add the root asset and save it to preload memory and mark it as preloaded.
|
|
foreach ($assets as &$asset)
|
|
{
|
|
if ((int) $asset->parent_id === 0)
|
|
{
|
|
$rootAsset = $asset;
|
|
self::$rootAssetId = $asset->id;
|
|
self::$preloadedAssetTypes[$asset->name] = true;
|
|
self::$preloadedAssets[$asset->id] = $asset->name;
|
|
self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset;
|
|
|
|
unset($asset);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Now create save the components asset tree to preload memory.
|
|
foreach ($assets as $asset)
|
|
{
|
|
if (!isset(self::$assetPermissionsParentIdMapping[$asset->name]))
|
|
{
|
|
self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset);
|
|
self::$preloadedAssets[$asset->id] = $asset->name;
|
|
}
|
|
}
|
|
|
|
// Mark all components asset type as preloaded.
|
|
self::$preloadedAssetTypes['components'] = true;
|
|
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadComponents (all components)');
|
|
|
|
return $components;
|
|
}
|
|
|
|
/**
|
|
* Method to check if a group is authorised to perform an action, optionally on an asset.
|
|
*
|
|
* @param integer $groupId The path to the group for which to check authorisation.
|
|
* @param string $action The name of the action to authorise.
|
|
* @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset.
|
|
* @param boolean $preload Indicates whether preloading should be used.
|
|
*
|
|
* @return boolean True if authorised.
|
|
*
|
|
* @since 1.7.0
|
|
*/
|
|
public static function checkGroup($groupId, $action, $assetKey = null, $preload = true)
|
|
{
|
|
// Sanitize input.
|
|
$groupId = (int) $groupId;
|
|
$action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action)));
|
|
|
|
return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId));
|
|
}
|
|
|
|
/**
|
|
* Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree
|
|
* (including the leaf group id).
|
|
*
|
|
* @param mixed $groupId An integer or array of integers representing the identities to check.
|
|
*
|
|
* @return mixed True if allowed, false for an explicit deny, null for an implicit deny.
|
|
*
|
|
* @since 1.7.0
|
|
*/
|
|
protected static function getGroupPath($groupId)
|
|
{
|
|
// Load all the groups to improve performance on intensive groups checks
|
|
$groups = UserGroupsHelper::getInstance()->getAll();
|
|
|
|
if (!isset($groups[$groupId]))
|
|
{
|
|
return array();
|
|
}
|
|
|
|
return $groups[$groupId]->path;
|
|
}
|
|
|
|
/**
|
|
* Method to return the Rules object for an asset. The returned object can optionally hold
|
|
* only the rules explicitly set for the asset or the summation of all inherited rules from
|
|
* parent assets and explicit rules.
|
|
*
|
|
* @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset.
|
|
* @param boolean $recursive True to return the rules object with inherited rules.
|
|
* @param boolean $recursiveParentAsset True to calculate the rule also based on inherited component/extension rules.
|
|
* @param boolean $preload Indicates whether preloading should be used.
|
|
*
|
|
* @return Rules Rules object for the asset.
|
|
*
|
|
* @since 1.7.0
|
|
* @note The non preloading code will be removed in 4.0. All asset rules should use asset preloading.
|
|
*/
|
|
public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true)
|
|
{
|
|
// Auto preloads the components assets and root asset (if chosen).
|
|
if ($preload)
|
|
{
|
|
self::preload('components');
|
|
}
|
|
|
|
// When asset key is null fallback to root asset.
|
|
$assetKey = self::cleanAssetKey($assetKey);
|
|
|
|
// Auto preloads assets for the asset type (if chosen).
|
|
if ($preload)
|
|
{
|
|
self::preload(self::getAssetType($assetKey));
|
|
}
|
|
|
|
// Get the asset id and name.
|
|
$assetId = self::getAssetId($assetKey);
|
|
|
|
// If asset rules already cached em memory return it (only in full recursive mode).
|
|
if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId]))
|
|
{
|
|
return self::$assetRules[$assetId];
|
|
}
|
|
|
|
// Get the asset name and the extension name.
|
|
$assetName = self::getAssetName($assetKey);
|
|
$extensionName = self::getExtensionNameFromAsset($assetName);
|
|
|
|
// If asset id does not exist fallback to extension asset, then root asset.
|
|
if (!$assetId)
|
|
{
|
|
if ($extensionName && $assetName !== $extensionName)
|
|
{
|
|
Log::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, Log::WARNING, 'assets');
|
|
|
|
return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload);
|
|
}
|
|
|
|
if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId])
|
|
{
|
|
Log::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], Log::WARNING, 'assets');
|
|
|
|
return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload);
|
|
}
|
|
}
|
|
|
|
// Almost all calls can take advantage of preloading.
|
|
if ($assetId && isset(self::$preloadedAssets[$assetId]))
|
|
{
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')');
|
|
|
|
// Collects permissions for each asset
|
|
$collected = array();
|
|
|
|
// If not in any recursive mode. We only want the asset rules.
|
|
if (!$recursive && !$recursiveParentAsset)
|
|
{
|
|
$collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules);
|
|
}
|
|
// If there is any type of recursive mode.
|
|
else
|
|
{
|
|
$ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId));
|
|
|
|
foreach ($ancestors as $id)
|
|
{
|
|
// If full recursive mode, but not recursive parent mode, do not add the extension asset rules.
|
|
if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If not full recursive mode, but recursive parent mode, do not add other recursion rules.
|
|
if (!$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName
|
|
&& (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If empty asset to not add to rules.
|
|
if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hashing the collected rules allows us to store
|
|
* only one instance of the Rules object for
|
|
* Assets that have the same exact permissions...
|
|
* it's a great way to save some memory.
|
|
*/
|
|
$hash = md5(implode(',', $collected));
|
|
|
|
if (!isset(self::$assetRulesIdentities[$hash]))
|
|
{
|
|
$rules = new Rules;
|
|
$rules->mergeCollection($collected);
|
|
|
|
self::$assetRulesIdentities[$hash] = $rules;
|
|
}
|
|
|
|
// Save asset rules to memory cache(only in full recursive mode).
|
|
if ($recursive && $recursiveParentAsset)
|
|
{
|
|
self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash];
|
|
}
|
|
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')');
|
|
|
|
return self::$assetRulesIdentities[$hash];
|
|
}
|
|
|
|
// Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen.
|
|
Log::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', Log::INFO, 'assets');
|
|
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (assetKey:' . $assetKey . ')');
|
|
|
|
// There's no need to process it with the recursive method for the Root Asset ID.
|
|
if ((int) $assetKey === 1)
|
|
{
|
|
$recursive = false;
|
|
}
|
|
|
|
// Get the database connection object.
|
|
$db = Factory::getDbo();
|
|
|
|
// Build the database query to get the rules for the asset.
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName($recursive ? 'b.rules' : 'a.rules', 'rules'))
|
|
->from($db->quoteName('#__assets', 'a'));
|
|
|
|
// If the asset identifier is numeric assume it is a primary key, else lookup by name.
|
|
if (is_numeric($assetKey))
|
|
{
|
|
$query->where($db->quoteName('a.id') . ' = :asset', 'OR')
|
|
->bind(':asset', $assetKey, ParameterType::INTEGER);
|
|
}
|
|
else
|
|
{
|
|
$query->where($db->quoteName('a.name') . ' = :asset', 'OR')
|
|
->bind(':asset', $assetKey);
|
|
}
|
|
|
|
if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey)))
|
|
{
|
|
$query->where($db->quoteName('a.name') . ' = :extension')
|
|
->bind(':extension', $extensionName);
|
|
}
|
|
|
|
// If we want the rules cascading up to the global asset node we need a self-join.
|
|
if ($recursive)
|
|
{
|
|
$query->where($db->quoteName('a.parent_id') . ' = 0')
|
|
->join(
|
|
'LEFT',
|
|
$db->quoteName('#__assets', 'b'),
|
|
$db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt')
|
|
)
|
|
->order($db->quoteName('b.lft'));
|
|
}
|
|
|
|
// Execute the query and load the rules from the result.
|
|
$result = $db->setQuery($query)->loadColumn();
|
|
|
|
// Get the root even if the asset is not found and in recursive mode
|
|
if (empty($result))
|
|
{
|
|
$rootId = (new Asset($db))->getRootId();
|
|
|
|
$query->clear()
|
|
->select($db->quoteName('rules'))
|
|
->from($db->quoteName('#__assets'))
|
|
->where($db->quoteName('id') . ' = :rootId')
|
|
->bind(':rootId', $rootId, ParameterType::INTEGER);
|
|
|
|
$result = $db->setQuery($query)->loadColumn();
|
|
}
|
|
|
|
// Instantiate and return the Rules object for the asset rules.
|
|
$rules = new Rules;
|
|
$rules->mergeCollection($result);
|
|
|
|
!JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules <strong>Slower</strong> (assetKey:' . $assetKey . ')');
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/**
|
|
* Method to clean the asset key to make sure we always have something.
|
|
*
|
|
* @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset.
|
|
*
|
|
* @return integer|string Asset id or asset name.
|
|
*
|
|
* @since 3.7.0
|
|
*/
|
|
protected static function cleanAssetKey($assetKey = null)
|
|
{
|
|
// If it's a valid asset key, clean it and return it.
|
|
if ($assetKey)
|
|
{
|
|
return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey)));
|
|
}
|
|
|
|
// Return root asset id if already preloaded.
|
|
if (self::$rootAssetId !== null)
|
|
{
|
|
return self::$rootAssetId;
|
|
}
|
|
|
|
// No preload. Return root asset id from Assets.
|
|
$assets = new Asset(Factory::getDbo());
|
|
|
|
return $assets->getRootId();
|
|
}
|
|
|
|
/**
|
|
* Method to get the asset id from the asset key.
|
|
*
|
|
* @param integer|string $assetKey The asset key (asset id or asset name).
|
|
*
|
|
* @return integer The asset id.
|
|
*
|
|
* @since 3.7.0
|
|
*/
|
|
protected static function getAssetId($assetKey)
|
|
{
|
|
static $loaded = array();
|
|
|
|
// If the asset is already an id return it.
|
|
if (is_numeric($assetKey))
|
|
{
|
|
return (int) $assetKey;
|
|
}
|
|
|
|
if (!isset($loaded[$assetKey]))
|
|
{
|
|
// It's the root asset.
|
|
if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId])
|
|
{
|
|
$loaded[$assetKey] = self::$rootAssetId;
|
|
}
|
|
else
|
|
{
|
|
$preloadedAssetsByName = array_flip(self::$preloadedAssets);
|
|
|
|
// If we already have the asset name stored in preloading, example, a component, no need to fetch it from table.
|
|
if (isset($preloadedAssetsByName[$assetKey]))
|
|
{
|
|
$loaded[$assetKey] = $preloadedAssetsByName[$assetKey];
|
|
}
|
|
// Else we have to do an extra db query to fetch it from the table fetch it from table.
|
|
else
|
|
{
|
|
$table = new Asset(Factory::getDbo());
|
|
$table->load(array('name' => $assetKey));
|
|
$loaded[$assetKey] = $table->id;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (int) $loaded[$assetKey];
|
|
}
|
|
|
|
/**
|
|
* Method to get the asset name from the asset key.
|
|
*
|
|
* @param integer|string $assetKey The asset key (asset id or asset name).
|
|
*
|
|
* @return string The asset name (ex: com_content.article.8).
|
|
*
|
|
* @since 3.7.0
|
|
*/
|
|
protected static function getAssetName($assetKey)
|
|
{
|
|
static $loaded = array();
|
|
|
|
// If the asset is already a string return it.
|
|
if (!is_numeric($assetKey))
|
|
{
|
|
return $assetKey;
|
|
}
|
|
|
|
if (!isset($loaded[$assetKey]))
|
|
{
|
|
// It's the root asset.
|
|
if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId)
|
|
{
|
|
$loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId];
|
|
}
|
|
// If we already have the asset name stored in preloading, example, a component, no need to fetch it from table.
|
|
elseif (isset(self::$preloadedAssets[$assetKey]))
|
|
{
|
|
$loaded[$assetKey] = self::$preloadedAssets[$assetKey];
|
|
}
|
|
// Else we have to do an extra db query to fetch it from the table fetch it from table.
|
|
else
|
|
{
|
|
$table = new Asset(Factory::getDbo());
|
|
$table->load($assetKey);
|
|
$loaded[$assetKey] = $table->name;
|
|
}
|
|
}
|
|
|
|
return $loaded[$assetKey];
|
|
}
|
|
|
|
/**
|
|
* Method to get the extension name from the asset name.
|
|
*
|
|
* @param integer|string $assetKey The asset key (asset id or asset name).
|
|
*
|
|
* @return string The extension name (ex: com_content).
|
|
*
|
|
* @since 1.6
|
|
*/
|
|
public static function getExtensionNameFromAsset($assetKey)
|
|
{
|
|
static $loaded = array();
|
|
|
|
if (!isset($loaded[$assetKey]))
|
|
{
|
|
$assetName = self::getAssetName($assetKey);
|
|
$firstDot = strpos($assetName, '.');
|
|
|
|
if ($assetName !== 'root.1' && $firstDot !== false)
|
|
{
|
|
$assetName = substr($assetName, 0, $firstDot);
|
|
}
|
|
|
|
$loaded[$assetKey] = $assetName;
|
|
}
|
|
|
|
return $loaded[$assetKey];
|
|
}
|
|
|
|
/**
|
|
* Method to get the asset type from the asset name.
|
|
*
|
|
* For top level components this returns "components":
|
|
* 'com_content' returns 'components'
|
|
*
|
|
* For other types:
|
|
* 'com_content.article.1' returns 'com_content.article'
|
|
* 'com_content.category.1' returns 'com_content.category'
|
|
*
|
|
* @param integer|string $assetKey The asset key (asset id or asset name).
|
|
*
|
|
* @return string The asset type (ex: com_content.article).
|
|
*
|
|
* @since 1.6
|
|
*/
|
|
public static function getAssetType($assetKey)
|
|
{
|
|
// If the asset is already a string return it.
|
|
$assetName = self::getAssetName($assetKey);
|
|
$lastDot = strrpos($assetName, '.');
|
|
|
|
if ($assetName !== 'root.1' && $lastDot !== false)
|
|
{
|
|
return substr($assetName, 0, $lastDot);
|
|
}
|
|
|
|
return 'components';
|
|
}
|
|
|
|
/**
|
|
* Method to return the title of a user group
|
|
*
|
|
* @param integer $groupId Id of the group for which to get the title of.
|
|
*
|
|
* @return string Tthe title of the group
|
|
*
|
|
* @since 3.5
|
|
*/
|
|
public static function getGroupTitle($groupId)
|
|
{
|
|
// Cast as integer until method is typehinted.
|
|
$groupId = (int) $groupId;
|
|
|
|
// Fetch the group title from the database
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true);
|
|
$query->select($db->quoteName('title'))
|
|
->from($db->quoteName('#__usergroups'))
|
|
->where($db->quoteName('id') . ' = :groupId')
|
|
->bind(':groupId', $groupId, ParameterType::INTEGER);
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadResult();
|
|
}
|
|
|
|
/**
|
|
* Method to return a list of user groups mapped to a user. The returned list can optionally hold
|
|
* only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited
|
|
* by the user.
|
|
*
|
|
* @param integer $userId Id of the user for which to get the list of groups.
|
|
* @param boolean $recursive True to include inherited user groups.
|
|
*
|
|
* @return array List of user group ids to which the user is mapped.
|
|
*
|
|
* @since 1.7.0
|
|
*/
|
|
public static function getGroupsByUser($userId, $recursive = true)
|
|
{
|
|
// Cast as integer until method is typehinted.
|
|
$userId = (int) $userId;
|
|
|
|
// Creates a simple unique string for each parameter combination:
|
|
$storeId = $userId . ':' . (int) $recursive;
|
|
|
|
if (!isset(self::$groupsByUser[$storeId]))
|
|
{
|
|
// TODO: Uncouple this from ComponentHelper and allow for a configuration setting or value injection.
|
|
$guestUsergroup = (int) ComponentHelper::getParams('com_users')->get('guest_usergroup', 1);
|
|
|
|
// Guest user (if only the actually assigned group is requested)
|
|
if (empty($userId) && !$recursive)
|
|
{
|
|
$result = array($guestUsergroup);
|
|
}
|
|
// Registered user and guest if all groups are requested
|
|
else
|
|
{
|
|
$db = Factory::getDbo();
|
|
|
|
// Build the database query to get the rules for the asset.
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName($recursive ? 'b.id' : 'a.id'));
|
|
|
|
if (empty($userId))
|
|
{
|
|
$query->from($db->quoteName('#__usergroups', 'a'))
|
|
->where($db->quoteName('a.id') . ' = :guest')
|
|
->bind(':guest', $guestUsergroup, ParameterType::INTEGER);
|
|
}
|
|
else
|
|
{
|
|
$query->from($db->quoteName('#__user_usergroup_map', 'map'))
|
|
->where($db->quoteName('map.user_id') . ' = :userId')
|
|
->join('LEFT', $db->quoteName('#__usergroups', 'a'), $db->quoteName('a.id') . ' = ' . $db->quoteName('map.group_id'))
|
|
->bind(':userId', $userId, ParameterType::INTEGER);
|
|
}
|
|
|
|
// If we want the rules cascading up to the global asset node we need a self-join.
|
|
if ($recursive)
|
|
{
|
|
$query->join(
|
|
'LEFT',
|
|
$db->quoteName('#__usergroups', 'b'),
|
|
$db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt')
|
|
);
|
|
}
|
|
|
|
// Execute the query and load the rules from the result.
|
|
$db->setQuery($query);
|
|
$result = $db->loadColumn();
|
|
|
|
// Clean up any NULL or duplicate values, just in case
|
|
$result = ArrayHelper::toInteger($result);
|
|
|
|
if (empty($result))
|
|
{
|
|
$result = array(1);
|
|
}
|
|
else
|
|
{
|
|
$result = array_unique($result);
|
|
}
|
|
}
|
|
|
|
self::$groupsByUser[$storeId] = $result;
|
|
}
|
|
|
|
return self::$groupsByUser[$storeId];
|
|
}
|
|
|
|
/**
|
|
* Method to return a list of user Ids contained in a Group
|
|
*
|
|
* @param integer $groupId The group Id
|
|
* @param boolean $recursive Recursively include all child groups (optional)
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 1.7.0
|
|
* @todo This method should move somewhere else
|
|
*/
|
|
public static function getUsersByGroup($groupId, $recursive = false)
|
|
{
|
|
// Cast as integer until method is typehinted.
|
|
$groupId = (int) $groupId;
|
|
|
|
// Get a database object.
|
|
$db = Factory::getDbo();
|
|
|
|
$test = $recursive ? ' >= ' : ' = ';
|
|
|
|
// First find the users contained in the group
|
|
$query = $db->getQuery(true)
|
|
->select('DISTINCT(' . $db->quoteName('user_id') . ')')
|
|
->from($db->quoteName('#__usergroups', 'ug1'))
|
|
->join(
|
|
'INNER',
|
|
$db->quoteName('#__usergroups', 'ug2'),
|
|
$db->quoteName('ug2.lft') . $test . $db->quoteName('ug1.lft') . ' AND ' . $db->quoteName('ug1.rgt') . $test . $db->quoteName('ug2.rgt')
|
|
)
|
|
->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('ug2.id') . ' = ' . $db->quoteName('m.group_id'))
|
|
->where($db->quoteName('ug1.id') . ' = :groupId')
|
|
->bind(':groupId', $groupId, ParameterType::INTEGER);
|
|
|
|
$db->setQuery($query);
|
|
|
|
$result = $db->loadColumn();
|
|
|
|
// Clean up any NULL values, just in case
|
|
$result = ArrayHelper::toInteger($result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Method to return a list of view levels for which the user is authorised.
|
|
*
|
|
* @param integer $userId Id of the user for which to get the list of authorised view levels.
|
|
*
|
|
* @return array List of view levels for which the user is authorised.
|
|
*
|
|
* @since 1.7.0
|
|
*/
|
|
public static function getAuthorisedViewLevels($userId)
|
|
{
|
|
// Only load the view levels once.
|
|
if (empty(self::$viewLevels))
|
|
{
|
|
// Get a database object.
|
|
$db = Factory::getDbo();
|
|
|
|
// Build the base query.
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName(['id', 'rules']))
|
|
->from($db->quoteName('#__viewlevels'));
|
|
|
|
// Set the query for execution.
|
|
$db->setQuery($query);
|
|
|
|
// Build the view levels array.
|
|
foreach ($db->loadAssocList() as $level)
|
|
{
|
|
self::$viewLevels[$level['id']] = (array) json_decode($level['rules']);
|
|
}
|
|
}
|
|
|
|
// Initialise the authorised array.
|
|
$authorised = array(1);
|
|
|
|
// Check for the recovery mode setting and return early.
|
|
$user = User::getInstance($userId);
|
|
$root_user = Factory::getApplication()->get('root_user');
|
|
|
|
if (($user->username && $user->username == $root_user) || (is_numeric($root_user) && $user->id > 0 && $user->id == $root_user))
|
|
{
|
|
// Find the super user levels.
|
|
foreach (self::$viewLevels as $level => $rule)
|
|
{
|
|
foreach ($rule as $id)
|
|
{
|
|
if ($id > 0 && self::checkGroup($id, 'core.admin'))
|
|
{
|
|
$authorised[] = $level;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $authorised;
|
|
}
|
|
|
|
// Get all groups that the user is mapped to recursively.
|
|
$groups = self::getGroupsByUser($userId);
|
|
|
|
// Find the authorised levels.
|
|
foreach (self::$viewLevels as $level => $rule)
|
|
{
|
|
foreach ($rule as $id)
|
|
{
|
|
if (($id < 0) && (($id * -1) == $userId))
|
|
{
|
|
$authorised[] = $level;
|
|
break;
|
|
}
|
|
// Check to see if the group is mapped to the level.
|
|
elseif (($id >= 0) && \in_array($id, $groups))
|
|
{
|
|
$authorised[] = $level;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $authorised;
|
|
}
|
|
|
|
/**
|
|
* Method to return a list of actions from a file for which permissions can be set.
|
|
*
|
|
* @param string $file The path to the XML file.
|
|
* @param string $xpath An optional xpath to search for the fields.
|
|
*
|
|
* @return boolean|array False if case of error or the list of actions available.
|
|
*
|
|
* @since 3.0.0
|
|
*/
|
|
public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/")
|
|
{
|
|
if (!is_file($file) || !is_readable($file))
|
|
{
|
|
// If unable to find the file return false.
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Else return the actions from the xml.
|
|
$xml = simplexml_load_file($file);
|
|
|
|
return self::getActionsFromData($xml, $xpath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method to return a list of actions from a string or from an xml for which permissions can be set.
|
|
*
|
|
* @param string|\SimpleXMLElement $data The XML string or an XML element.
|
|
* @param string $xpath An optional xpath to search for the fields.
|
|
*
|
|
* @return boolean|array False if case of error or the list of actions available.
|
|
*
|
|
* @since 3.0.0
|
|
*/
|
|
public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/")
|
|
{
|
|
// If the data to load isn't already an XML element or string return false.
|
|
if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Attempt to load the XML if a string.
|
|
if (\is_string($data))
|
|
{
|
|
try
|
|
{
|
|
$data = new \SimpleXMLElement($data);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Make sure the XML loaded correctly.
|
|
if (!$data)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Initialise the actions array
|
|
$actions = array();
|
|
|
|
// Get the elements from the xpath
|
|
$elements = $data->xpath($xpath . 'action[@name][@title]');
|
|
|
|
// If there some elements, analyse them
|
|
if (!empty($elements))
|
|
{
|
|
foreach ($elements as $element)
|
|
{
|
|
// Add the action to the actions array
|
|
$action = array(
|
|
'name' => (string) $element['name'],
|
|
'title' => (string) $element['title'],
|
|
);
|
|
|
|
if (isset($element['description']))
|
|
{
|
|
$action['description'] = (string) $element['description'];
|
|
}
|
|
|
|
$actions[] = (object) $action;
|
|
}
|
|
}
|
|
|
|
// Finally return the actions array
|
|
return $actions;
|
|
}
|
|
}
|