diff --git a/libraries/joomla/access/access.php b/libraries/joomla/access/access.php index 0640e0262db3a..445e3eab876b9 100644 --- a/libraries/joomla/access/access.php +++ b/libraries/joomla/access/access.php @@ -34,6 +34,57 @@ class JAccess */ protected static $assetRules = array(); + /** + * Array of identities for asset rules + * + * @var array + * @since 11.1 + */ + protected static $assetRulesIdentities = array(); + + /** + * Array of permissions for an asset type + * (Array Key = Asset ID) + * Also includes the rules string for the asset + * + * @var array + * @since 11.1 + */ + protected static $assetPermissionsById = array(); + + /** + * Array of permissions for an asset type + * (Array Key = Asset Name) + * + * @var array + * @since 11.1 + */ + protected static $assetPermissionsByName = array(); + + /** + * Array of the permission parent ID mappings + * + * @var array + * @since 11.1 + */ + protected static $assetPermissionsParentIdMapping = array(); + + /** + * Array of asset types that have been preloaded + * + * @var array + * @since 11.1 + */ + protected static $preloadedAssetTypes = array(); + + /** + * Array of loaded user identities + * + * @var array + * @since 11.1 + */ + protected static $identities = array(); + /** * Array of user groups. * @@ -68,6 +119,11 @@ class JAccess public static function clearStatics() { self::$viewLevels = array(); + self::$assetPermissionsById = array(); + self::$assetPermissionsByName = array(); + self::$assetPermissionsParentIdMapping = array(); + self::$preloadedAssetTypes = array(); + self::$identities = array(); self::$assetRules = array(); self::$userGroups = array(); self::$userGroupPaths = array(); @@ -77,15 +133,16 @@ public static function clearStatics() /** * 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 mixed $asset Integer asset id or the name of the asset as a string. Defaults to the global asset node. + * @param integer $userId Id of the user for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param mixed $asset Integer asset id or the name of the asset as a string. Defaults to the global asset node. + * @param boolean $preload Indicates whether preloading should be used * * @return boolean True if authorised. * * @since 11.1 */ - public static function check($userId, $action, $asset = null) + public static function check($userId, $action, $asset = null, $preload = true) { // Sanitise inputs. $userId = (int) $userId; @@ -101,17 +158,316 @@ public static function check($userId, $action, $asset = null) $asset = $assets->getRootId(); } + // Auto preloads assets for the asset type: + if (!is_numeric($asset) && $preload) + { + $assetType = self::getAssetType($asset); + + if (!isset(self::$preloadedAssetTypes[$assetType])) + { + self::preload($assetType); + self::$preloadedAssetTypes[$assetType] = true; + } + } + // Get the rules for the asset recursively to root if not already retrieved. if (empty(self::$assetRules[$asset])) { self::$assetRules[$asset] = self::getAssetRules($asset, true); } - // Get all groups against which the user is mapped. - $identities = self::getGroupsByUser($userId); - array_unshift($identities, $userId * -1); + 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::$assetRules[$asset]->allow($action, $identities); + return self::$assetRules[$asset]->allow($action, self::$identities[$userId]); + } + + /** + * Method to preload the JAccessRules object for the given asset type. + * + * @param string|array $assetTypes e.g. 'com_content.article' + * @param boolean $reload Set to true to reload from database. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public static function preload($assetTypes = 'components', $reload = false) + { + // Get instance of the Profiler: + $_PROFILER = JProfiler::getInstance('Application'); + + // Check for default case: + $isDefault = (is_string($assetTypes) && in_array($assetTypes, array('components', 'component'))); + + // Preload the rules for all of the components: + if ($isDefault) + { + // Mark in the profiler. + JDEBUG ? $_PROFILER->mark('Start JAccess::preload(components)') : null; + + $components = self::preloadComponents(); + self::$preloadedAssetTypes = array_merge(self::$preloadedAssetTypes, array_flip($components)); + + // Mark in the profiler. + JDEBUG ? $_PROFILER->mark('Finish JAccess::preload(components)') : null; + + // Quick short circuit for default case: + if ($isDefault) + { + 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) + { + if (!isset(self::$preloadedAssetTypes[$assetType]) || $reload) + { + JDEBUG ? $_PROFILER->mark('New JAccess Preloading Process(' . $assetType . ')') : null; + + self::preloadPermissionsParentIdMapping($assetType); + JDEBUG ? $_PROFILER->mark('After preloadPermissionsParentIdMapping (' . $assetType . ')') : null; + + self::preloadPermissions($assetType); + JDEBUG ? $_PROFILER->mark('After preloadPermissions (' . $assetType . ')') : null; + + JDEBUG ? $_PROFILER->mark('End New JAccess Preloading Process(' . $assetType . ')') : null; + + self::$preloadedAssetTypes[$assetType] = true; + } + } + + return true; + } + + /** + * Method to recursively retrieve the list of parent Asset IDs + * for a particular Asset. + * + * @param string $assetType e.g. 'com_content.article' + * @param string|int $assetId 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 list of Asset IDs and their Parent Asset IDs + * and store them for later usage in getAssetRules(). + * + * @param string $assetType e.g. 'com_content.article' + * + * @return array List of Asset IDs (includes Parent Asset ID Info) + * + * @since 1.6 + */ + protected static function &preloadPermissionsParentIdMapping($assetType) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + if (!isset(self::$assetPermissionsParentIdMapping[$extensionName])) + { + // Get the database connection object. + $db = JFactory::getDbo(); + + // Get a fresh query object: + $query = $db->getQuery(true); + + // Build the database query: + $query->select('a.id, a.parent_id'); + $query->from('#__assets AS a'); + $query->where('(a.name LIKE ' . $db->quote($extensionName . '.%') . ' OR a.name = ' . $db->quote($extensionName) . ' OR a.id = 1)'); + + // Get the Name Permission Map List + $db->setQuery($query); + $parentIdMapping = $db->loadObjectList('id'); + + self::$assetPermissionsParentIdMapping[$extensionName] = &$parentIdMapping; + } + + return self::$assetPermissionsParentIdMapping[$extensionName]; + } + + /** + * 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 e.g. 'com_content.article' + * + * @return bool True + * + * @since 1.6 + */ + protected static function preloadPermissions($assetType) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + if (!isset(self::$assetPermissionsById[$extensionName]) && !isset(self::$assetPermissionsByName[$extensionName])) + { + // Get the database connection object. + $db = JFactory::getDbo(); + + // Get a fresh query object: + $query = $db->getQuery(true); + + // Build the database query: + $query->select('a.id, a.name, a.rules'); + $query->from('#__assets AS a'); + $query->where('(a.name LIKE ' . $db->quote($extensionName . '.%') . ' OR a.name = ' . $db->quote($extensionName) . ' OR a.id = 1 )'); + + // Get the Name Permission Map List + $db->setQuery($query); + + $iterator = $db->getIterator(); + + self::$assetPermissionsById[$extensionName] = array(); + self::$assetPermissionsByName[$extensionName] = array(); + foreach ($iterator as $row) + { + self::$assetPermissionsById[$extensionName][$row->id] = $row; + self::$assetPermissionsByName[$extensionName][$row->name] = $row; + } + + } + + return true; + } + + /** + * Method to preload the JAccessRules 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() + { + // Get the database connection object. + $db = JFactory::getDbo(); + + // Build the database query: + $query = $db->getQuery(true); + $query->select('element'); + $query->from('#__extensions'); + $query->where('type = ' . $db->quote('component')); + $query->where('enabled = ' . $db->quote(1)); + + // Set the query and get the list of active components: + $db->setQuery($query); + $components = $db->loadColumn(); + + // Get a fresh query object: + $query = $db->getQuery(true); + + // Build the in clause for the queries: + $inClause = ''; + $last = end($components); + foreach ($components as $component) + { + if ($component === $last) + { + $inClause .= $db->quote($component); + } + else + { + $inClause .= $db->quote($component) . ','; + } + } + + // Build the database query: + $query->select('a.name, a.rules'); + $query->from('#__assets AS a'); + $query->where('(a.name IN (' . $inClause . ') OR a.name = ' . $db->quote('root.1') . ')'); + + // Get the Name Permission Map List + $db->setQuery($query); + $namePermissionMap = $db->loadAssocList('name'); + + $root = array(); + $root['rules'] = ''; + if (isset($namePermissionMap['root.1'])) + { + $root = $namePermissionMap['root.1']; + unset($namePermissionMap['root.1']); + } + + // Container for all of the JAccessRules for this $assetType + $rulesList = array(); + + // Collects permissions for each $assetName and adds + // into the $assetRules class variable. + foreach ($namePermissionMap as $assetName => &$permissions) + { + // Instantiate and return the JAccessRules object for the asset rules. + $rules = new JAccessRules; + $rules->mergeCollection(array($root['rules'], $permissions['rules'])); + + $rulesList[$assetName] = $rules; + } + + unset($assetName); + unset($permissions); + + // Merge our rules list with self::$assetRules + self::$assetRules = self::$assetRules + $rulesList; + unset($rulesList); + + return $components; } /** @@ -213,57 +569,196 @@ protected static function getGroupPath($groupId) */ public static function getAssetRules($asset, $recursive = false) { - // Get the database connection object. - $db = JFactory::getDbo(); + // Get instance of the Profiler: + $_PROFILER = JProfiler::getInstance('Application'); - // Build the database query to get the rules for the asset. - $query = $db->getQuery(true) - ->select($recursive ? 'b.rules' : 'a.rules') - ->from('#__assets AS a'); - - // SQLsrv change - $query->group($recursive ? 'b.id, b.rules, b.lft' : 'a.id, a.rules, a.lft'); + $extensionName = self::getExtensionNameFromAsset($asset); - // If the asset identifier is numeric assume it is a primary key, else lookup by name. - if (is_numeric($asset)) + // Almost all calls should have recursive set to true + // so we'll get to take advantage of preloading: + if ($recursive && isset(self::$assetPermissionsByName[$extensionName]) && isset(self::$assetPermissionsByName[$extensionName][$asset])) { - $query->where('(a.id = ' . (int) $asset . ')'); + // Mark in the profiler. + JDEBUG ? $_PROFILER->mark('Start JAccess::getAssetRules New (' . $asset . ')') : null; + + $assetType = self::getAssetType($asset); + $assetId = self::$assetPermissionsByName[$extensionName][$asset]->id; + + $ancestors = array_reverse(self::getAssetAncestors($assetType, $assetId)); + + // Collects permissions for each $asset + $collected = array(); + foreach ($ancestors as $id) + { + $collected[] = self::$assetPermissionsById[$extensionName][$id]->rules; + } + + /** + * Hashing the collected rules allows us to store + * only one instance of the JAccessRules 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 JAccessRules; + $rules->mergeCollection($collected); + + self::$assetRulesIdentities[$hash] = $rules; + } + + // Mark in the profiler. + JDEBUG ? $_PROFILER->mark('Finish JAccess::getAssetRules New (' . $asset . ')') : null; + + return self::$assetRulesIdentities[$hash]; } else { - $query->where('(a.name = ' . $db->quote($asset) . ')'); + // Mark in the profiler. + JDEBUG ? $_PROFILER->mark('Start JAccess::getAssetRules Old (' . $asset . ')') : null; + + if ($asset === "1") + { + // There's no need to process it with the + // recursive method for the Root Asset ID. + $recursive = false; + } + + // Get the database connection object. + $db = JFactory::getDbo(); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(true) + ->select($recursive ? 'b.rules' : 'a.rules') + ->from('#__assets AS a'); + + $extensionString = ''; + if ($extensionName !== $asset || is_numeric($asset)) + { + $extensionString = ' OR a.name = ' . $db->quote($extensionName); + } + + $recursiveString = ''; + if ($recursive) + { + $recursiveString = ' OR a.parent_id=0'; + } + + // If the asset identifier is numeric assume it is a primary key, else lookup by name. + if (is_numeric($asset)) + { + $query->where('(a.id = ' . (int) $asset . $extensionString . $recursiveString . ')'); + } + else + { + $query->where('(a.name = ' . $db->quote($asset) . $extensionString . $recursiveString . ')'); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) + { + $query->join('LEFT', '#__assets AS b ON b.lft <= a.lft AND b.rgt >= a.rgt') + ->order('b.lft'); + } + + // Execute the query and load the rules from the result. + $db->setQuery($query); + $result = $db->loadColumn(); + + // Get the root even if the asset is not found and in recursive mode + if (empty($result)) + { + $db = JFactory::getDbo(); + $assets = JTable::getInstance('Asset', 'JTable', array('dbo' => $db)); + $rootId = $assets->getRootId(); + $query->clear() + ->select('rules') + ->from('#__assets') + ->where('id = ' . $db->quote($rootId)); + $db->setQuery($query); + $result = $db->loadResult(); + $result = array($result); + } + // Instantiate and return the JAccessRules object for the asset rules. + $rules = new JAccessRules; + $rules->mergeCollection($result); + + JDEBUG ? $_PROFILER->mark('Finish JAccess::getAssetRules Old (' . $asset . ')') : null; + + return $rules; } + } + + /** + * Method to get the extension name from the asset name. + * + * @param string $asset Asset Name + * + * @return string Extension Name. + * + * @since 1.6 + */ + public static function getExtensionNameFromAsset($asset) + { + static $loaded = array(); - // If we want the rules cascading up to the global asset node we need a self-join. - if ($recursive) + if (!isset($loaded[$asset])) { - $query->join('LEFT', '#__assets AS b ON b.lft <= a.lft AND b.rgt >= a.rgt') - ->order('b.lft'); + if (is_numeric($asset)) + { + $table = JTable::getInstance('Asset'); + $table->load($asset); + $assetName = $table->name; + } + else + { + $assetName = $asset; + } + + $firstDot = strpos($assetName, '.'); + if ($assetName !== 'root.1' && $firstDot !== false) + { + $assetName = substr($assetName, 0, $firstDot); + } + + $loaded[$asset] = $assetName; } - // Execute the query and load the rules from the result. - $db->setQuery($query); - $result = $db->loadColumn(); + return $loaded[$asset]; + } + + /** + * 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 string $asset Asset Name + * + * @return string Asset Type. + * + * @since 1.6 + */ + public static function getAssetType($asset) + { + $lastDot = strrpos($asset, '.'); - // Get the root even if the asset is not found and in recursive mode - if (empty($result)) + if ($asset !== 'root.1' && $lastDot !== false) { - $db = JFactory::getDbo(); - $assets = JTable::getInstance('Asset', 'JTable', array('dbo' => $db)); - $rootId = $assets->getRootId(); - $query->clear() - ->select('rules') - ->from('#__assets') - ->where('id = ' . $db->quote($rootId)); - $db->setQuery($query); - $result = $db->loadResult(); - $result = array($result); + $assetType = substr($asset, 0, $lastDot); + } + else + { + $assetType = 'components'; } - // Instantiate and return the JAccessRules object for the asset rules. - $rules = new JAccessRules; - $rules->mergeCollection($result); - return $rules; + return $assetType; } /**