1 <?php
   2 /**
   3  * @package     Joomla.Platform
   4  * @subpackage  Access
   5  *
   6  * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
   7  * @license     GNU General Public License version 2 or later; see LICENSE
   8  */
   9 
  10 defined('JPATH_PLATFORM') or die;
  11 
  12 use Joomla\Utilities\ArrayHelper;
  13 
  14 /**
  15  * Class that handles all access authorisation routines.
  16  *
  17  * @since  11.1
  18  */
  19 class JAccess
  20 {
  21     /**
  22      * Array of view levels
  23      *
  24      * @var    array
  25      * @since  11.1
  26      */
  27     protected static $viewLevels = array();
  28 
  29     /**
  30      * Array of rules for the asset
  31      *
  32      * @var    array
  33      * @since  11.1
  34      */
  35     protected static $assetRules = array();
  36 
  37     /**
  38      * Array of identities for asset rules
  39      *
  40      * @var    array
  41      * @since  11.1
  42      */
  43     protected static $assetRulesIdentities = array();
  44 
  45     /**
  46      * Array of permissions for an asset type
  47      * (Array Key = Asset ID)
  48      * Also includes the rules string for the asset
  49      *
  50      * @var    array
  51      * @since  11.1
  52      * @deprecated  3.7.0  No replacement. Will be removed in 4.0.
  53      */
  54     protected static $assetPermissionsById = array();
  55 
  56     /**
  57      * Array of permissions for an asset type
  58      * (Array Key = Asset Name)
  59      *
  60      * @var    array
  61      * @since  11.1
  62      * @deprecated  3.7.0  No replacement. Will be removed in 4.0.
  63      */
  64     protected static $assetPermissionsByName = array();
  65 
  66     /**
  67      * Array of the permission parent ID mappings
  68      *
  69      * @var    array
  70      * @since  11.1
  71      */
  72     protected static $assetPermissionsParentIdMapping = array();
  73 
  74     /**
  75      * Array of asset types that have been preloaded
  76      *
  77      * @var    array
  78      * @since  11.1
  79      */
  80     protected static $preloadedAssetTypes = array();
  81 
  82     /**
  83      * Array of loaded user identities
  84      *
  85      * @var    array
  86      * @since  11.1
  87      */
  88     protected static $identities = array();
  89 
  90     /**
  91      * Array of user groups.
  92      *
  93      * @var    array
  94      * @since  11.1
  95      */
  96     protected static $userGroups = array();
  97 
  98     /**
  99      * Array of user group paths.
 100      *
 101      * @var    array
 102      * @since  11.1
 103      */
 104     protected static $userGroupPaths = array();
 105 
 106     /**
 107      * Array of cached groups by user.
 108      *
 109      * @var    array
 110      * @since  11.1
 111      */
 112     protected static $groupsByUser = array();
 113 
 114     /**
 115      * Array of preloaded asset names and ids (key is the asset id).
 116      *
 117      * @var    array
 118      * @since  3.7.0
 119      */
 120     protected static $preloadedAssets = array();
 121 
 122     /**
 123      * The root asset id.
 124      *
 125      * @var    integer
 126      * @since  3.7.0
 127      */
 128     protected static $rootAssetId = null;
 129 
 130     /**
 131      * Method for clearing static caches.
 132      *
 133      * @return  void
 134      *
 135      * @since   11.3
 136      */
 137     public static function clearStatics()
 138     {
 139         self::$viewLevels                      = array();
 140         self::$assetRules                      = array();
 141         self::$assetRulesIdentities            = array();
 142         self::$assetPermissionsParentIdMapping = array();
 143         self::$preloadedAssetTypes             = array();
 144         self::$identities                      = array();
 145         self::$userGroups                      = array();
 146         self::$userGroupPaths                  = array();
 147         self::$groupsByUser                    = array();
 148         self::$preloadedAssets                 = array();
 149         self::$rootAssetId                     = null;
 150 
 151         // The following properties are deprecated since 3.7.0 and will be removed in 4.0.
 152         self::$assetPermissionsById   = array();
 153         self::$assetPermissionsByName = array();
 154     }
 155 
 156     /**
 157      * Method to check if a user is authorised to perform an action, optionally on an asset.
 158      *
 159      * @param   integer         $userId    Id of the user for which to check authorisation.
 160      * @param   string          $action    The name of the action to authorise.
 161      * @param   integer|string  $assetKey  The asset key (asset id or asset name). null fallback to root asset.
 162      * @param   boolean         $preload   Indicates whether preloading should be used.
 163      *
 164      * @return  boolean|null  True if allowed, false for an explicit deny, null for an implicit deny.
 165      *
 166      * @since   11.1
 167      */
 168     public static function check($userId, $action, $assetKey = null, $preload = true)
 169     {
 170         // Sanitise inputs.
 171         $userId = (int) $userId;
 172         $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action)));
 173 
 174         if (!isset(self::$identities[$userId]))
 175         {
 176             // Get all groups against which the user is mapped.
 177             self::$identities[$userId] = self::getGroupsByUser($userId);
 178             array_unshift(self::$identities[$userId], $userId * -1);
 179         }
 180 
 181         return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::$identities[$userId]);
 182     }
 183 
 184     /**
 185      * Method to preload the JAccessRules object for the given asset type.
 186      *
 187      * @param   integer|string|array  $assetTypes  The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2').
 188      *                                             Also accepts the asset id. An array of asset type or a special
 189      *                                             'components' string to load all component assets.
 190      * @param   boolean               $reload      Set to true to reload from database.
 191      *
 192      * @return  boolean  True on success.
 193      *
 194      * @since   1.6
 195      * @note    This method will return void in 4.0.
 196      */
 197     public static function preload($assetTypes = 'components', $reload = false)
 198     {
 199         // If sent an asset id, we first get the asset type for that asset id.
 200         if (is_numeric($assetTypes))
 201         {
 202             $assetTypes = self::getAssetType($assetTypes);
 203         }
 204 
 205         // Check for default case:
 206         $isDefault = is_string($assetTypes) && in_array($assetTypes, array('components', 'component'));
 207 
 208         // Preload the rules for all of the components.
 209         if ($isDefault)
 210         {
 211             self::preloadComponents();
 212 
 213             return true;
 214         }
 215 
 216         // If we get to this point, this is a regular asset type and we'll proceed with the preloading process.
 217         if (!is_array($assetTypes))
 218         {
 219             $assetTypes = (array) $assetTypes;
 220         }
 221 
 222         foreach ($assetTypes as $assetType)
 223         {
 224             self::preloadPermissions($assetType, $reload);
 225         }
 226 
 227         return true;
 228     }
 229 
 230     /**
 231      * Method to recursively retrieve the list of parent Asset IDs
 232      * for a particular Asset.
 233      *
 234      * @param   string   $assetType  The asset type, or the asset name, or the extension of the asset
 235      *                               (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact').
 236      * @param   integer  $assetId    The numeric asset id.
 237      *
 238      * @return  array  List of ancestor ids (includes original $assetId).
 239      *
 240      * @since   1.6
 241      */
 242     protected static function getAssetAncestors($assetType, $assetId)
 243     {
 244         // Get the extension name from the $assetType provided
 245         $extensionName = self::getExtensionNameFromAsset($assetType);
 246 
 247         // Holds the list of ancestors for the Asset ID:
 248         $ancestors = array();
 249 
 250         // Add in our starting Asset ID:
 251         $ancestors[] = (int) $assetId;
 252 
 253         // Initialize the variable we'll use in the loop:
 254         $id = (int) $assetId;
 255 
 256         while ($id !== 0)
 257         {
 258             if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id]))
 259             {
 260                 $id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id;
 261 
 262                 if ($id !== 0)
 263                 {
 264                     $ancestors[] = $id;
 265                 }
 266             }
 267             else
 268             {
 269                 // Add additional case to break out of the while loop automatically in
 270                 // the case that the ID is non-existent in our mapping variable above.
 271                 break;
 272             }
 273         }
 274 
 275         return $ancestors;
 276     }
 277 
 278     /**
 279      * Method to retrieve the list of Asset IDs and their Parent Asset IDs
 280      * and store them for later usage in getAssetRules().
 281      *
 282      * @param   string  $assetType  The asset type, or the asset name, or the extension of the asset
 283      *                              (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact').
 284      *
 285      * @return  array  List of asset ids (includes parent asset id information).
 286      *
 287      * @since   1.6
 288      * @deprecated  3.7.0  No replacement. Will be removed in 4.0.
 289      */
 290     protected static function &preloadPermissionsParentIdMapping($assetType)
 291     {
 292         // Get the extension name from the $assetType provided
 293         $extensionName = self::getExtensionNameFromAsset($assetType);
 294 
 295         if (!isset(self::$assetPermissionsParentIdMapping[$extensionName]))
 296         {
 297             // Get the database connection object.
 298             $db = JFactory::getDbo();
 299 
 300             // Get a fresh query object:
 301             $query    = $db->getQuery(true);
 302 
 303             // Build the database query:
 304             $query->select('a.id, a.parent_id');
 305             $query->from('#__assets AS a');
 306             $query->where('(a.name LIKE ' . $db->quote($extensionName . '.%') . ' OR a.name = ' . $db->quote($extensionName) . ' OR a.id = 1)');
 307 
 308             // Get the Name Permission Map List
 309             $db->setQuery($query);
 310             $parentIdMapping = $db->loadObjectList('id');
 311 
 312             self::$assetPermissionsParentIdMapping[$extensionName] = &$parentIdMapping;
 313         }
 314 
 315         return self::$assetPermissionsParentIdMapping[$extensionName];
 316     }
 317 
 318     /**
 319      * Method to retrieve the Asset Rule strings for this particular
 320      * Asset Type and stores them for later usage in getAssetRules().
 321      * Stores 2 arrays: one where the list has the Asset ID as the key
 322      * and a second one where the Asset Name is the key.
 323      *
 324      * @param   string   $assetType  The asset type, or the asset name, or the extension of the asset
 325      *                               (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact').
 326      * @param   boolean  $reload     Reload the preloaded assets.
 327      *
 328      * @return  bool  True
 329      *
 330      * @since   1.6
 331      * @note    This function will return void in 4.0.
 332      */
 333     protected static function preloadPermissions($assetType, $reload = false)
 334     {
 335         // Get the extension name from the $assetType provided
 336         $extensionName = self::getExtensionNameFromAsset($assetType);
 337 
 338         // If asset is a component, make sure that all the component assets are preloaded.
 339         if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload)
 340         {
 341             return true;
 342         }
 343 
 344         !JDEBUG ?: JProfiler::getInstance('Application')->mark('Before JAccess::preloadPermissions (' . $extensionName . ')');
 345 
 346         // Get the database connection object.
 347         $db         = JFactory::getDbo();
 348         $extraQuery = $db->qn('name') . ' = ' . $db->q($extensionName) . ' OR ' . $db->qn('parent_id') . ' = 0';
 349 
 350         // Get a fresh query object.
 351         $query = $db->getQuery(true)
 352             ->select($db->qn(array('id', 'name', 'rules', 'parent_id')))
 353             ->from($db->qn('#__assets'))
 354             ->where($db->qn('name') . ' LIKE ' . $db->q($extensionName . '.%') . ' OR ' . $extraQuery);
 355 
 356         // Get the permission map for all assets in the asset extension.
 357         $assets = $db->setQuery($query)->loadObjectList();
 358 
 359         self::$assetPermissionsParentIdMapping[$extensionName] = array();
 360 
 361         // B/C Populate the old class properties. They are deprecated since 3.7.0 and will be removed in 4.0.
 362         self::$assetPermissionsById[$assetType]   = array();
 363         self::$assetPermissionsByName[$assetType] = array();
 364 
 365         foreach ($assets as $asset)
 366         {
 367             self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset;
 368             self::$preloadedAssets[$asset->id]                                 = $asset->name;
 369 
 370             // B/C Populate the old class properties. They are deprecated since 3.7.0 and will be removed in 4.0.
 371             self::$assetPermissionsById[$assetType][$asset->id]     = $asset;
 372             self::$assetPermissionsByName[$assetType][$asset->name] = $asset;
 373         }
 374 
 375         // Mark asset type and it's extension name as preloaded.
 376         self::$preloadedAssetTypes[$assetType]     = true;
 377         self::$preloadedAssetTypes[$extensionName] = true;
 378 
 379         !JDEBUG ?: JProfiler::getInstance('Application')->mark('After JAccess::preloadPermissions (' . $extensionName . ')');
 380 
 381         return true;
 382     }
 383 
 384     /**
 385      * Method to preload the JAccessRules objects for all components.
 386      *
 387      * Note: This will only get the base permissions for the component.
 388      * e.g. it will get 'com_content', but not 'com_content.article.1' or
 389      * any more specific asset type rules.
 390      *
 391      * @return   array  Array of component names that were preloaded.
 392      *
 393      * @since    1.6
 394      */
 395     protected static function preloadComponents()
 396     {
 397         // If the components already been preloaded do nothing.
 398         if (isset(self::$preloadedAssetTypes['components']))
 399         {
 400             return array();
 401         }
 402 
 403         !JDEBUG ?: JProfiler::getInstance('Application')->mark('Before JAccess::preloadComponents (all components)');
 404 
 405         // Add root to asset names list.
 406         $components = array();
 407 
 408         // Add enabled components to asset names list.
 409         foreach (JComponentHelper::getComponents() as $component)
 410         {
 411             if ($component->enabled)
 412             {
 413                 $components[] = $component->option;
 414             }
 415         }
 416 
 417         // Get the database connection object.
 418         $db = JFactory::getDbo();
 419 
 420         // Get the asset info for all assets in asset names list.
 421         $query = $db->getQuery(true)
 422             ->select($db->qn(array('id', 'name', 'rules', 'parent_id')))
 423             ->from($db->qn('#__assets'))
 424             ->where($db->qn('name') . ' IN (' . implode(',', $db->quote($components)) . ') OR ' . $db->qn('parent_id') . ' = 0');
 425 
 426         // Get the Name Permission Map List
 427         $assets = $db->setQuery($query)->loadObjectList();
 428 
 429         $rootAsset = null;
 430 
 431         // First add the root asset and save it to preload memory and mark it as preloaded.
 432         foreach ($assets as &$asset)
 433         {
 434             if ((int) $asset->parent_id === 0)
 435             {
 436                 $rootAsset                                                       = $asset;
 437                 self::$rootAssetId                                               = $asset->id;
 438                 self::$preloadedAssetTypes[$asset->name]                         = true;
 439                 self::$preloadedAssets[$asset->id]                               = $asset->name;
 440                 self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset;
 441 
 442                 unset($asset);
 443                 break;
 444             }
 445         }
 446 
 447         // Now create save the components asset tree to preload memory.
 448         foreach ($assets as $asset)
 449         {
 450             if (!isset(self::$assetPermissionsParentIdMapping[$asset->name]))
 451             {
 452                 self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset);
 453                 self::$preloadedAssets[$asset->id]                   = $asset->name;
 454             }
 455         }
 456 
 457         // Mark all components asset type as preloaded.
 458         self::$preloadedAssetTypes['components'] = true;
 459 
 460         !JDEBUG ?: JProfiler::getInstance('Application')->mark('After JAccess::preloadComponents (all components)');
 461 
 462         return $components;
 463     }
 464 
 465     /**
 466      * Method to check if a group is authorised to perform an action, optionally on an asset.
 467      *
 468      * @param   integer         $groupId   The path to the group for which to check authorisation.
 469      * @param   string          $action    The name of the action to authorise.
 470      * @param   integer|string  $assetKey  The asset key (asset id or asset name). null fallback to root asset.
 471      * @param   boolean         $preload   Indicates whether preloading should be used.
 472      *
 473      * @return  boolean  True if authorised.
 474      *
 475      * @since   11.1
 476      */
 477     public static function checkGroup($groupId, $action, $assetKey = null, $preload = true)
 478     {
 479         // Sanitize input.
 480         $groupId = (int) $groupId;
 481         $action  = strtolower(preg_replace('#[\s\-]+#', '.', trim($action)));
 482 
 483         return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId));
 484     }
 485 
 486     /**
 487      * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree
 488      * (including the leaf group id).
 489      *
 490      * @param   mixed  $groupId  An integer or array of integers representing the identities to check.
 491      *
 492      * @return  mixed  True if allowed, false for an explicit deny, null for an implicit deny.
 493      *
 494      * @since   11.1
 495      */
 496     protected static function getGroupPath($groupId)
 497     {
 498         // Load all the groups to improve performance on intensive groups checks
 499         $groups = JHelperUsergroups::getInstance()->getAll();
 500 
 501         if (!isset($groups[$groupId]))
 502         {
 503             return array();
 504         }
 505 
 506         return $groups[$groupId]->path;
 507     }
 508 
 509     /**
 510      * Method to return the JAccessRules object for an asset.  The returned object can optionally hold
 511      * only the rules explicitly set for the asset or the summation of all inherited rules from
 512      * parent assets and explicit rules.
 513      *
 514      * @param   integer|string  $assetKey              The asset key (asset id or asset name). null fallback to root asset.
 515      * @param   boolean         $recursive             True to return the rules object with inherited rules.
 516      * @param   boolean         $recursiveParentAsset  True to calculate the rule also based on inherited component/extension rules.
 517      * @param   boolean         $preload               Indicates whether preloading should be used.
 518      *
 519      * @return  JAccessRules  JAccessRules object for the asset.
 520      *
 521      * @since   11.1
 522      * @note    The non preloading code will be removed in 4.0. All asset rules should use asset preloading.
 523      */
 524     public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true)
 525     {
 526         // Auto preloads the components assets and root asset (if chosen).
 527         if ($preload)
 528         {
 529             self::preload('components');
 530         }
 531 
 532         // When asset key is null fallback to root asset.
 533         $assetKey = self::cleanAssetKey($assetKey);
 534 
 535         // Auto preloads assets for the asset type (if chosen).
 536         if ($preload)
 537         {
 538             self::preload(self::getAssetType($assetKey));
 539         }
 540 
 541         // Get the asset id and name.
 542         $assetId = self::getAssetId($assetKey);
 543 
 544         // If asset rules already cached em memory return it (only in full recursive mode).
 545         if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId]))
 546         {
 547             return self::$assetRules[$assetId];
 548         }
 549 
 550         // Get the asset name and the extension name.
 551         $assetName     = self::getAssetName($assetKey);
 552         $extensionName = self::getExtensionNameFromAsset($assetName);
 553 
 554         // If asset id does not exist fallback to extension asset, then root asset.
 555         if (!$assetId)
 556         {
 557             if ($extensionName && $assetName !== $extensionName)
 558             {
 559                 JLog::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, JLog::WARNING, 'assets');
 560 
 561                 return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload);
 562             }
 563 
 564             if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId])
 565             {
 566                 JLog::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], JLog::WARNING, 'assets');
 567 
 568                 return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload);
 569             }
 570         }
 571 
 572         // Almost all calls can take advantage of preloading.
 573         if ($assetId && isset(self::$preloadedAssets[$assetId]))
 574         {
 575             !JDEBUG ?: JProfiler::getInstance('Application')->mark('Before JAccess::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')');
 576 
 577             // Collects permissions for each asset
 578             $collected = array();
 579 
 580             // If not in any recursive mode. We only want the asset rules.
 581             if (!$recursive && !$recursiveParentAsset)
 582             {
 583                 $collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules);
 584             }
 585             // If there is any type of recursive mode.
 586             else
 587             {
 588                 $ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId));
 589 
 590                 foreach ($ancestors as $id)
 591                 {
 592                     // If full recursive mode, but not recursive parent mode, do not add the extension asset rules.
 593                     if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName)
 594                     {
 595                         continue;
 596                     }
 597 
 598                     // If not full recursive mode, but recursive parent mode, do not add other recursion rules.
 599                     if (!$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName
 600                         && self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId)
 601                     {
 602                         continue;
 603                     }
 604 
 605                     // If empty asset to not add to rules.
 606                     if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}')
 607                     {
 608                         continue;
 609                     }
 610 
 611                     $collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules;
 612                 }
 613             }
 614 
 615             /**
 616             * Hashing the collected rules allows us to store
 617             * only one instance of the JAccessRules object for
 618             * Assets that have the same exact permissions...
 619             * it's a great way to save some memory.
 620             */
 621             $hash = md5(implode(',', $collected));
 622 
 623             if (!isset(self::$assetRulesIdentities[$hash]))
 624             {
 625                 $rules = new JAccessRules;
 626                 $rules->mergeCollection($collected);
 627 
 628                 self::$assetRulesIdentities[$hash] = $rules;
 629             }
 630 
 631             // Save asset rules to memory cache(only in full recursive mode).
 632             if ($recursive && $recursiveParentAsset)
 633             {
 634                 self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash];
 635             }
 636 
 637             !JDEBUG ?: JProfiler::getInstance('Application')->mark('After JAccess::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')');
 638 
 639             return self::$assetRulesIdentities[$hash];
 640         }
 641 
 642         // Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen.
 643         JLog::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', JLog::INFO, 'assets');
 644 
 645         !JDEBUG ?: JProfiler::getInstance('Application')->mark('Before JAccess::getAssetRules (assetKey:' . $assetKey . ')');
 646 
 647         // There's no need to process it with the recursive method for the Root Asset ID.
 648         if ((int) $assetKey === 1)
 649         {
 650             $recursive = false;
 651         }
 652 
 653         // Get the database connection object.
 654         $db = JFactory::getDbo();
 655 
 656         // Build the database query to get the rules for the asset.
 657         $query = $db->getQuery(true)
 658             ->select($db->qn(($recursive ? 'b.rules' : 'a.rules'), 'rules'))
 659             ->select($db->qn(($recursive ? array('b.id', 'b.name', 'b.parent_id') : array('a.id', 'a.name', 'a.parent_id'))))
 660             ->from($db->qn('#__assets', 'a'));
 661 
 662         // If the asset identifier is numeric assume it is a primary key, else lookup by name.
 663         $assetString     = is_numeric($assetKey) ? $db->qn('a.id') . ' = ' . $assetKey : $db->qn('a.name') . ' = ' . $db->q($assetKey);
 664         $extensionString = '';
 665 
 666         if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey)))
 667         {
 668             $extensionString = ' OR ' . $db->qn('a.name') . ' = ' . $db->q($extensionName);
 669         }
 670 
 671         $recursiveString = $recursive ? ' OR ' . $db->qn('a.parent_id') . ' = 0' : '';
 672 
 673         $query->where('(' . $assetString . $extensionString . $recursiveString . ')');
 674 
 675         // If we want the rules cascading up to the global asset node we need a self-join.
 676         if ($recursive)
 677         {
 678             $query->join('LEFT', $db->qn('#__assets', 'b') . ' ON b.lft <= a.lft AND b.rgt >= a.rgt')
 679                 ->order($db->qn('b.lft'));
 680         }
 681 
 682         // Execute the query and load the rules from the result.
 683         $result = $db->setQuery($query)->loadObjectList();
 684 
 685         // Get the root even if the asset is not found and in recursive mode
 686         if (empty($result))
 687         {
 688             $assets = JTable::getInstance('Asset', 'JTable', array('dbo' => $db));
 689 
 690             $query->clear()
 691                 ->select($db->qn(array('id', 'name', 'parent_id', 'rules')))
 692                 ->from($db->qn('#__assets'))
 693                 ->where($db->qn('id') . ' = ' . $db->q($assets->getRootId()));
 694 
 695             $result = $db->setQuery($query)->loadObjectList();
 696         }
 697 
 698         $collected = array();
 699 
 700         foreach ($result as $asset)
 701         {
 702             $collected[] = $asset->rules;
 703         }
 704 
 705         // Instantiate and return the JAccessRules object for the asset rules.
 706         $rules = new JAccessRules;
 707         $rules->mergeCollection($collected);
 708 
 709         !JDEBUG ?: JProfiler::getInstance('Application')->mark('Before JAccess::getAssetRules <strong>Slower</strong> (assetKey:' . $assetKey . ')');
 710 
 711         return $rules;
 712     }
 713 
 714     /**
 715      * Method to clean the asset key to make sure we always have something.
 716      *
 717      * @param   integer|string  $assetKey  The asset key (asset id or asset name). null fallback to root asset.
 718      *
 719      * @return  integer|string  Asset id or asset name.
 720      *
 721      * @since   3.7.0
 722      */
 723     protected static function cleanAssetKey($assetKey = null)
 724     {
 725         // If it's a valid asset key, clean it and return it.
 726         if ($assetKey)
 727         {
 728             return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey)));
 729         }
 730 
 731         // Return root asset id if already preloaded.
 732         if (self::$rootAssetId !== null)
 733         {
 734             return self::$rootAssetId;
 735         }
 736 
 737         // No preload. Return root asset id from JTableAssets.
 738         $assets = JTable::getInstance('Asset', 'JTable', array('dbo' => JFactory::getDbo()));
 739 
 740         return $assets->getRootId();
 741     }
 742 
 743     /**
 744      * Method to get the asset id from the asset key.
 745      *
 746      * @param   integer|string  $assetKey  The asset key (asset id or asset name).
 747      *
 748      * @return  integer  The asset id.
 749      *
 750      * @since   3.7.0
 751      */
 752     protected static function getAssetId($assetKey)
 753     {
 754         static $loaded = array();
 755 
 756         // If the asset is already an id return it.
 757         if (is_numeric($assetKey))
 758         {
 759             return (int) $assetKey;
 760         }
 761 
 762         if (!isset($loaded[$assetKey]))
 763         {
 764             // It's the root asset.
 765             if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId])
 766             {
 767                 $loaded[$assetKey] = self::$rootAssetId;
 768             }
 769             else
 770             {
 771                 $preloadedAssetsByName = array_flip(self::$preloadedAssets);
 772 
 773                 // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table.
 774                 if (isset($preloadedAssetsByName[$assetKey]))
 775                 {
 776                     $loaded[$assetKey] = $preloadedAssetsByName[$assetKey];
 777                 }
 778                 // Else we have to do an extra db query to fetch it from the table fetch it from table.
 779                 else
 780                 {
 781                     $table = JTable::getInstance('Asset');
 782                     $table->load(array('name' => $assetKey));
 783                     $loaded[$assetKey] = $table->id;
 784                 }
 785             }
 786         }
 787 
 788         return (int) $loaded[$assetKey];
 789     }
 790 
 791     /**
 792      * Method to get the asset name from the asset key.
 793      *
 794      * @param   integer|string  $assetKey  The asset key (asset id or asset name).
 795      *
 796      * @return  string  The asset name (ex: com_content.article.8).
 797      *
 798      * @since   3.7.0
 799      */
 800     protected static function getAssetName($assetKey)
 801     {
 802         static $loaded = array();
 803 
 804         // If the asset is already a string return it.
 805         if (!is_numeric($assetKey))
 806         {
 807             return $assetKey;
 808         }
 809 
 810         if (!isset($loaded[$assetKey]))
 811         {
 812             // It's the root asset.
 813             if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId)
 814             {
 815                 $loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId];
 816             }
 817             // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table.
 818             elseif (isset(self::$preloadedAssets[$assetKey]))
 819             {
 820                 $loaded[$assetKey] = self::$preloadedAssets[$assetKey];
 821             }
 822             // Else we have to do an extra db query to fetch it from the table fetch it from table.
 823             else
 824             {
 825                 $table = JTable::getInstance('Asset');
 826                 $table->load($assetKey);
 827                 $loaded[$assetKey] = $table->name;
 828             }
 829         }
 830 
 831         return $loaded[$assetKey];
 832     }
 833 
 834     /**
 835      * Method to get the extension name from the asset name.
 836      *
 837      * @param   integer|string  $assetKey  The asset key (asset id or asset name).
 838      *
 839      * @return  string  The extension name (ex: com_content).
 840      *
 841      * @since    1.6
 842      */
 843     public static function getExtensionNameFromAsset($assetKey)
 844     {
 845         static $loaded = array();
 846 
 847         if (!isset($loaded[$assetKey]))
 848         {
 849             $assetName = self::getAssetName($assetKey);
 850             $firstDot  = strpos($assetName, '.');
 851 
 852             if ($assetName !== 'root.1' && $firstDot !== false)
 853             {
 854                 $assetName = substr($assetName, 0, $firstDot);
 855             }
 856 
 857             $loaded[$assetKey] = $assetName;
 858         }
 859 
 860         return $loaded[$assetKey];
 861     }
 862 
 863     /**
 864      * Method to get the asset type from the asset name.
 865      *
 866      * For top level components this returns "components":
 867      * 'com_content' returns 'components'
 868      *
 869      * For other types:
 870      * 'com_content.article.1' returns 'com_content.article'
 871      * 'com_content.category.1' returns 'com_content.category'
 872      *
 873      * @param   integer|string  $assetKey  The asset key (asset id or asset name).
 874      *
 875      * @return  string  The asset type (ex: com_content.article).
 876      *
 877      * @since    1.6
 878      */
 879     public static function getAssetType($assetKey)
 880     {
 881         // If the asset is already a string return it.
 882         $assetName = self::getAssetName($assetKey);
 883         $lastDot   = strrpos($assetName, '.');
 884 
 885         if ($assetName !== 'root.1' && $lastDot !== false)
 886         {
 887             return substr($assetName, 0, $lastDot);
 888         }
 889 
 890         return 'components';
 891     }
 892 
 893     /**
 894      * Method to return the title of a user group
 895      *
 896      * @param   integer  $groupId  Id of the group for which to get the title of.
 897      *
 898      * @return  string  Tthe title of the group
 899      *
 900      * @since   3.5
 901      */
 902     public static function getGroupTitle($groupId)
 903     {
 904         // Fetch the group title from the database
 905         $db    = JFactory::getDbo();
 906         $query = $db->getQuery(true);
 907         $query->select('title')
 908             ->from('#__usergroups')
 909             ->where('id = ' . $db->quote($groupId));
 910         $db->setQuery($query);
 911 
 912         return $db->loadResult();
 913     }
 914 
 915     /**
 916      * Method to return a list of user groups mapped to a user. The returned list can optionally hold
 917      * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited
 918      * by the user.
 919      *
 920      * @param   integer  $userId     Id of the user for which to get the list of groups.
 921      * @param   boolean  $recursive  True to include inherited user groups.
 922      *
 923      * @return  array    List of user group ids to which the user is mapped.
 924      *
 925      * @since   11.1
 926      */
 927     public static function getGroupsByUser($userId, $recursive = true)
 928     {
 929         // Creates a simple unique string for each parameter combination:
 930         $storeId = $userId . ':' . (int) $recursive;
 931 
 932         if (!isset(self::$groupsByUser[$storeId]))
 933         {
 934             // TODO: Uncouple this from JComponentHelper and allow for a configuration setting or value injection.
 935             if (class_exists('JComponentHelper'))
 936             {
 937                 $guestUsergroup = JComponentHelper::getParams('com_users')->get('guest_usergroup', 1);
 938             }
 939             else
 940             {
 941                 $guestUsergroup = 1;
 942             }
 943 
 944             // Guest user (if only the actually assigned group is requested)
 945             if (empty($userId) && !$recursive)
 946             {
 947                 $result = array($guestUsergroup);
 948             }
 949             // Registered user and guest if all groups are requested
 950             else
 951             {
 952                 $db = JFactory::getDbo();
 953 
 954                 // Build the database query to get the rules for the asset.
 955                 $query = $db->getQuery(true)
 956                     ->select($recursive ? 'b.id' : 'a.id');
 957 
 958                 if (empty($userId))
 959                 {
 960                     $query->from('#__usergroups AS a')
 961                         ->where('a.id = ' . (int) $guestUsergroup);
 962                 }
 963                 else
 964                 {
 965                     $query->from('#__user_usergroup_map AS map')
 966                         ->where('map.user_id = ' . (int) $userId)
 967                         ->join('LEFT', '#__usergroups AS a ON a.id = map.group_id');
 968                 }
 969 
 970                 // If we want the rules cascading up to the global asset node we need a self-join.
 971                 if ($recursive)
 972                 {
 973                     $query->join('LEFT', '#__usergroups AS b ON b.lft <= a.lft AND b.rgt >= a.rgt');
 974                 }
 975 
 976                 // Execute the query and load the rules from the result.
 977                 $db->setQuery($query);
 978                 $result = $db->loadColumn();
 979 
 980                 // Clean up any NULL or duplicate values, just in case
 981                 $result = ArrayHelper::toInteger($result);
 982 
 983                 if (empty($result))
 984                 {
 985                     $result = array('1');
 986                 }
 987                 else
 988                 {
 989                     $result = array_unique($result);
 990                 }
 991             }
 992 
 993             self::$groupsByUser[$storeId] = $result;
 994         }
 995 
 996         return self::$groupsByUser[$storeId];
 997     }
 998 
 999     /**
1000      * Method to return a list of user Ids contained in a Group
1001      *
1002      * @param   integer  $groupId    The group Id
1003      * @param   boolean  $recursive  Recursively include all child groups (optional)
1004      *
1005      * @return  array
1006      *
1007      * @since   11.1
1008      * @todo    This method should move somewhere else
1009      */
1010     public static function getUsersByGroup($groupId, $recursive = false)
1011     {
1012         // Get a database object.
1013         $db = JFactory::getDbo();
1014 
1015         $test = $recursive ? '>=' : '=';
1016 
1017         // First find the users contained in the group
1018         $query = $db->getQuery(true)
1019             ->select('DISTINCT(user_id)')
1020             ->from('#__usergroups as ug1')
1021             ->join('INNER', '#__usergroups AS ug2 ON ug2.lft' . $test . 'ug1.lft AND ug1.rgt' . $test . 'ug2.rgt')
1022             ->join('INNER', '#__user_usergroup_map AS m ON ug2.id=m.group_id')
1023             ->where('ug1.id=' . $db->quote($groupId));
1024 
1025         $db->setQuery($query);
1026 
1027         $result = $db->loadColumn();
1028 
1029         // Clean up any NULL values, just in case
1030         $result = ArrayHelper::toInteger($result);
1031 
1032         return $result;
1033     }
1034 
1035     /**
1036      * Method to return a list of view levels for which the user is authorised.
1037      *
1038      * @param   integer  $userId  Id of the user for which to get the list of authorised view levels.
1039      *
1040      * @return  array    List of view levels for which the user is authorised.
1041      *
1042      * @since   11.1
1043      */
1044     public static function getAuthorisedViewLevels($userId)
1045     {
1046         // Only load the view levels once.
1047         if (empty(self::$viewLevels))
1048         {
1049             // Get a database object.
1050             $db = JFactory::getDbo();
1051 
1052             // Build the base query.
1053             $query = $db->getQuery(true)
1054                 ->select('id, rules')
1055                 ->from($db->quoteName('#__viewlevels'));
1056 
1057             // Set the query for execution.
1058             $db->setQuery($query);
1059 
1060             // Build the view levels array.
1061             foreach ($db->loadAssocList() as $level)
1062             {
1063                 self::$viewLevels[$level['id']] = (array) json_decode($level['rules']);
1064             }
1065         }
1066 
1067         // Initialise the authorised array.
1068         $authorised = array(1);
1069 
1070         // Check for the recovery mode setting and return early.
1071         $user      = JUser::getInstance($userId);
1072         $root_user = JFactory::getConfig()->get('root_user');
1073 
1074         if ($root_user && ($root_user == $user->username || $root_user == $user->id))
1075         {
1076             // Find the super user levels.
1077             foreach (self::$viewLevels as $level => $rule)
1078             {
1079                 foreach ($rule as $id)
1080                 {
1081                     if ($id > 0 && self::checkGroup($id, 'core.admin'))
1082                     {
1083                         $authorised[] = $level;
1084                         break;
1085                     }
1086                 }
1087             }
1088 
1089             return $authorised;
1090         }
1091 
1092         // Get all groups that the user is mapped to recursively.
1093         $groups = self::getGroupsByUser($userId);
1094 
1095         // Find the authorised levels.
1096         foreach (self::$viewLevels as $level => $rule)
1097         {
1098             foreach ($rule as $id)
1099             {
1100                 if (($id < 0) && (($id * -1) == $userId))
1101                 {
1102                     $authorised[] = $level;
1103                     break;
1104                 }
1105                 // Check to see if the group is mapped to the level.
1106                 elseif (($id >= 0) && in_array($id, $groups))
1107                 {
1108                     $authorised[] = $level;
1109                     break;
1110                 }
1111             }
1112         }
1113 
1114         return $authorised;
1115     }
1116 
1117     /**
1118      * Method to return a list of actions for which permissions can be set given a component and section.
1119      *
1120      * @param   string  $component  The component from which to retrieve the actions.
1121      * @param   string  $section    The name of the section within the component from which to retrieve the actions.
1122      *
1123      * @return  array  List of actions available for the given component and section.
1124      *
1125      * @since       11.1
1126      * @deprecated  12.3 (Platform) & 4.0 (CMS)  Use JAccess::getActionsFromFile or JAccess::getActionsFromData instead.
1127      * @codeCoverageIgnore
1128      */
1129     public static function getActions($component, $section = 'component')
1130     {
1131         JLog::add(__METHOD__ . ' is deprecated. Use JAccess::getActionsFromFile or JAccess::getActionsFromData instead.', JLog::WARNING, 'deprecated');
1132 
1133         $actions = self::getActionsFromFile(
1134             JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml',
1135             "/access/section[@name='" . $section . "']/"
1136         );
1137 
1138         if (empty($actions))
1139         {
1140             return array();
1141         }
1142         else
1143         {
1144             return $actions;
1145         }
1146     }
1147 
1148     /**
1149      * Method to return a list of actions from a file for which permissions can be set.
1150      *
1151      * @param   string  $file   The path to the XML file.
1152      * @param   string  $xpath  An optional xpath to search for the fields.
1153      *
1154      * @return  boolean|array   False if case of error or the list of actions available.
1155      *
1156      * @since   12.1
1157      */
1158     public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/")
1159     {
1160         if (!is_file($file) || !is_readable($file))
1161         {
1162             // If unable to find the file return false.
1163             return false;
1164         }
1165         else
1166         {
1167             // Else return the actions from the xml.
1168             $xml = simplexml_load_file($file);
1169 
1170             return self::getActionsFromData($xml, $xpath);
1171         }
1172     }
1173 
1174     /**
1175      * Method to return a list of actions from a string or from an xml for which permissions can be set.
1176      *
1177      * @param   string|SimpleXMLElement  $data   The XML string or an XML element.
1178      * @param   string                   $xpath  An optional xpath to search for the fields.
1179      *
1180      * @return  boolean|array   False if case of error or the list of actions available.
1181      *
1182      * @since   12.1
1183      */
1184     public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/")
1185     {
1186         // If the data to load isn't already an XML element or string return false.
1187         if ((!($data instanceof SimpleXMLElement)) && (!is_string($data)))
1188         {
1189             return false;
1190         }
1191 
1192         // Attempt to load the XML if a string.
1193         if (is_string($data))
1194         {
1195             try
1196             {
1197                 $data = new SimpleXMLElement($data);
1198             }
1199             catch (Exception $e)
1200             {
1201                 return false;
1202             }
1203 
1204             // Make sure the XML loaded correctly.
1205             if (!$data)
1206             {
1207                 return false;
1208             }
1209         }
1210 
1211         // Initialise the actions array
1212         $actions = array();
1213 
1214         // Get the elements from the xpath
1215         $elements = $data->xpath($xpath . 'action[@name][@title][@description]');
1216 
1217         // If there some elements, analyse them
1218         if (!empty($elements))
1219         {
1220             foreach ($elements as $action)
1221             {
1222                 // Add the action to the actions array
1223                 $actions[] = (object) array(
1224                     'name' => (string) $action['name'],
1225                     'title' => (string) $action['title'],
1226                     'description' => (string) $action['description'],
1227                 );
1228             }
1229         }
1230 
1231         // Finally return the actions array
1232         return $actions;
1233     }
1234 }
1235