Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Components (core subsystems + plugins) related code.
  19   *
  20   * @package    core
  21   * @copyright  2013 Petr Skoda {@link http://skodak.org}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  // Constants used in version.php files, these must exist when core_component executes.
  28  
  29  /** Software maturity level - internals can be tested using white box techniques. */
  30  define('MATURITY_ALPHA',    50);
  31  /** Software maturity level - feature complete, ready for preview and testing. */
  32  define('MATURITY_BETA',     100);
  33  /** Software maturity level - tested, will be released unless there are fatal bugs. */
  34  define('MATURITY_RC',       150);
  35  /** Software maturity level - ready for production deployment. */
  36  define('MATURITY_STABLE',   200);
  37  /** Any version - special value that can be used in $plugin->dependencies in version.php files. */
  38  define('ANY_VERSION', 'any');
  39  
  40  
  41  /**
  42   * Collection of components related methods.
  43   */
  44  class core_component {
  45      /** @var array list of ignored directories in plugin type roots - watch out for auth/db exception */
  46      protected static $ignoreddirs = [
  47          'CVS' => true,
  48          '_vti_cnf' => true,
  49          'amd' => true,
  50          'classes' => true,
  51          'db' => true,
  52          'fonts' => true,
  53          'lang' => true,
  54          'pix' => true,
  55          'simpletest' => true,
  56          'templates' => true,
  57          'tests' => true,
  58          'yui' => true,
  59      ];
  60      /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
  61      protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
  62  
  63      /** @var object JSON source of the component data */
  64      protected static $componentsource = null;
  65      /** @var array cache of plugin types */
  66      protected static $plugintypes = null;
  67      /** @var array cache of plugin locations */
  68      protected static $plugins = null;
  69      /** @var array cache of core subsystems */
  70      protected static $subsystems = null;
  71      /** @var array subplugin type parents */
  72      protected static $parents = null;
  73      /** @var array subplugins */
  74      protected static $subplugins = null;
  75      /** @var array list of all known classes that can be autoloaded */
  76      protected static $classmap = null;
  77      /** @var array list of all classes that have been renamed to be autoloaded */
  78      protected static $classmaprenames = null;
  79      /** @var array list of some known files that can be included. */
  80      protected static $filemap = null;
  81      /** @var int|float core version. */
  82      protected static $version = null;
  83      /** @var array list of the files to map. */
  84      protected static $filestomap = array('lib.php', 'settings.php');
  85      /** @var array associative array of PSR-0 namespaces and corresponding paths. */
  86      protected static $psr0namespaces = array(
  87          'Horde' => 'lib/horde/framework/Horde',
  88          'Mustache' => 'lib/mustache/src/Mustache',
  89          'CFPropertyList' => 'lib/plist/classes/CFPropertyList',
  90      );
  91      /** @var array associative array of PRS-4 namespaces and corresponding paths. */
  92      protected static $psr4namespaces = array(
  93          'MaxMind' => 'lib/maxmind/MaxMind',
  94          'GeoIp2' => 'lib/maxmind/GeoIp2',
  95          'Sabberworm\\CSS' => 'lib/php-css-parser',
  96          'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
  97          'ScssPhp\\ScssPhp' => 'lib/scssphp',
  98          'Box\\Spout' => 'lib/spout/src/Spout',
  99          'BirknerAlex\\XMPPHP' => 'lib/jabber/XMPP',
 100          'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
 101          'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
 102          'IMSGlobal\LTI' => 'lib/ltiprovider/src',
 103          'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
 104          'PHPMailer\\PHPMailer' => 'lib/phpmailer/src',
 105          'RedeyeVentures\\GeoPattern' => 'lib/geopattern-php/GeoPattern',
 106          'MongoDB' => 'cache/stores/mongodb/MongoDB',
 107          'Firebase\\JWT' => 'lib/php-jwt/src',
 108          'ZipStream' => 'lib/zipstream/src/',
 109          'MyCLabs\\Enum' => 'lib/php-enum/src',
 110          'Psr\\Http\\Message' => 'lib/http-message/src',
 111      );
 112  
 113      /**
 114       * Class loader for Frankenstyle named classes in standard locations.
 115       * Frankenstyle namespaces are supported.
 116       *
 117       * The expected location for core classes is:
 118       *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
 119       *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
 120       *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
 121       *
 122       * The expected location for plugin classes is:
 123       *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
 124       *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
 125       *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
 126       *
 127       * @param string $classname
 128       */
 129      public static function classloader($classname) {
 130          self::init();
 131  
 132          if (isset(self::$classmap[$classname])) {
 133              // Global $CFG is expected in included scripts.
 134              global $CFG;
 135              // Function include would be faster, but for BC it is better to include only once.
 136              include_once(self::$classmap[$classname]);
 137              return;
 138          }
 139          if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
 140              $newclassname = self::$classmaprenames[$classname];
 141              $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
 142              debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
 143              if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
 144                  throw new \coding_exception("Cannot alias $classname to $newclassname");
 145              }
 146              class_alias($newclassname, $classname);
 147              return;
 148          }
 149  
 150          $file = self::psr_classloader($classname);
 151          // If the file is found, require it.
 152          if (!empty($file)) {
 153              require($file);
 154              return;
 155          }
 156      }
 157  
 158      /**
 159       * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
 160       * demand. Only returns paths to files that exist.
 161       *
 162       * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
 163       * compatible.
 164       *
 165       * @param string $class the name of the class.
 166       * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
 167       */
 168      protected static function psr_classloader($class) {
 169          // Iterate through each PSR-4 namespace prefix.
 170          foreach (self::$psr4namespaces as $prefix => $path) {
 171              $file = self::get_class_file($class, $prefix, $path, array('\\'));
 172              if (!empty($file) && file_exists($file)) {
 173                  return $file;
 174              }
 175          }
 176  
 177          // Iterate through each PSR-0 namespace prefix.
 178          foreach (self::$psr0namespaces as $prefix => $path) {
 179              $file = self::get_class_file($class, $prefix, $path, array('\\', '_'));
 180              if (!empty($file) && file_exists($file)) {
 181                  return $file;
 182              }
 183          }
 184  
 185          return false;
 186      }
 187  
 188      /**
 189       * Return the path to the class based on the given namespace prefix and path it corresponds to.
 190       *
 191       * Will return the path even if the file does not exist. Check the file esists before requiring.
 192       *
 193       * @param string $class the name of the class.
 194       * @param string $prefix The namespace prefix used to identify the base directory of the source files.
 195       * @param string $path The relative path to the base directory of the source files.
 196       * @param string[] $separators The characters that should be used for separating.
 197       * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
 198       */
 199      protected static function get_class_file($class, $prefix, $path, $separators) {
 200          global $CFG;
 201  
 202          // Does the class use the namespace prefix?
 203          $len = strlen($prefix);
 204          if (strncmp($prefix, $class, $len) !== 0) {
 205              // No, move to the next prefix.
 206              return false;
 207          }
 208          $path = $CFG->dirroot . '/' . $path;
 209  
 210          // Get the relative class name.
 211          $relativeclass = substr($class, $len);
 212  
 213          // Replace the namespace prefix with the base directory, replace namespace
 214          // separators with directory separators in the relative class name, append
 215          // with .php.
 216          $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
 217  
 218          return $file;
 219      }
 220  
 221  
 222      /**
 223       * Initialise caches, always call before accessing self:: caches.
 224       */
 225      protected static function init() {
 226          global $CFG;
 227  
 228          // Init only once per request/CLI execution, we ignore changes done afterwards.
 229          if (isset(self::$plugintypes)) {
 230              return;
 231          }
 232  
 233          if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
 234              self::fill_all_caches();
 235              return;
 236          }
 237  
 238          if (!empty($CFG->alternative_component_cache)) {
 239              // Hack for heavily clustered sites that want to manage component cache invalidation manually.
 240              $cachefile = $CFG->alternative_component_cache;
 241  
 242              if (file_exists($cachefile)) {
 243                  if (CACHE_DISABLE_ALL) {
 244                      // Verify the cache state only on upgrade pages.
 245                      $content = self::get_cache_content();
 246                      if (sha1_file($cachefile) !== sha1($content)) {
 247                          die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
 248                      }
 249                      return;
 250                  }
 251                  $cache = array();
 252                  include($cachefile);
 253                  self::$plugintypes      = $cache['plugintypes'];
 254                  self::$plugins          = $cache['plugins'];
 255                  self::$subsystems       = $cache['subsystems'];
 256                  self::$parents          = $cache['parents'];
 257                  self::$subplugins       = $cache['subplugins'];
 258                  self::$classmap         = $cache['classmap'];
 259                  self::$classmaprenames  = $cache['classmaprenames'];
 260                  self::$filemap          = $cache['filemap'];
 261                  return;
 262              }
 263  
 264              if (!is_writable(dirname($cachefile))) {
 265                  die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
 266              }
 267  
 268              // Lets try to create the file, it might be in some writable directory or a local cache dir.
 269  
 270          } else {
 271              // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
 272              //       use $CFG->alternative_component_cache if you do not like it.
 273              $cachefile = "$CFG->cachedir/core_component.php";
 274          }
 275  
 276          if (!CACHE_DISABLE_ALL and !self::is_developer()) {
 277              // 1/ Use the cache only outside of install and upgrade.
 278              // 2/ Let developers add/remove classes in developer mode.
 279              if (is_readable($cachefile)) {
 280                  $cache = false;
 281                  include($cachefile);
 282                  if (!is_array($cache)) {
 283                      // Something is very wrong.
 284                  } else if (!isset($cache['version'])) {
 285                      // Something is very wrong.
 286                  } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
 287                      // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
 288                      error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
 289                  } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
 290                      // $CFG->dirroot was changed.
 291                  } else {
 292                      // The cache looks ok, let's use it.
 293                      self::$plugintypes      = $cache['plugintypes'];
 294                      self::$plugins          = $cache['plugins'];
 295                      self::$subsystems       = $cache['subsystems'];
 296                      self::$parents          = $cache['parents'];
 297                      self::$subplugins       = $cache['subplugins'];
 298                      self::$classmap         = $cache['classmap'];
 299                      self::$classmaprenames  = $cache['classmaprenames'];
 300                      self::$filemap          = $cache['filemap'];
 301                      return;
 302                  }
 303                  // Note: we do not verify $CFG->admin here intentionally,
 304                  //       they must visit admin/index.php after any change.
 305              }
 306          }
 307  
 308          if (!isset(self::$plugintypes)) {
 309              // This needs to be atomic and self-fixing as much as possible.
 310  
 311              $content = self::get_cache_content();
 312              if (file_exists($cachefile)) {
 313                  if (sha1_file($cachefile) === sha1($content)) {
 314                      return;
 315                  }
 316                  // Stale cache detected!
 317                  unlink($cachefile);
 318              }
 319  
 320              // Permissions might not be setup properly in installers.
 321              $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
 322              $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
 323  
 324              clearstatcache();
 325              $cachedir = dirname($cachefile);
 326              if (!is_dir($cachedir)) {
 327                  mkdir($cachedir, $dirpermissions, true);
 328              }
 329  
 330              if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
 331                  fwrite($fp, $content);
 332                  fclose($fp);
 333                  @rename($cachefile.'.tmp', $cachefile);
 334                  @chmod($cachefile, $filepermissions);
 335              }
 336              @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
 337              self::invalidate_opcode_php_cache($cachefile);
 338          }
 339      }
 340  
 341      /**
 342       * Are we in developer debug mode?
 343       *
 344       * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
 345       *       the reason is we need to use this before we setup DB connection or caches for CFG.
 346       *
 347       * @return bool
 348       */
 349      protected static function is_developer() {
 350          global $CFG;
 351  
 352          // Note we can not rely on $CFG->debug here because DB is not initialised yet.
 353          if (isset($CFG->config_php_settings['debug'])) {
 354              $debug = (int)$CFG->config_php_settings['debug'];
 355          } else {
 356              return false;
 357          }
 358  
 359          if ($debug & E_ALL and $debug & E_STRICT) {
 360              return true;
 361          }
 362  
 363          return false;
 364      }
 365  
 366      /**
 367       * Create cache file content.
 368       *
 369       * @private this is intended for $CFG->alternative_component_cache only.
 370       *
 371       * @return string
 372       */
 373      public static function get_cache_content() {
 374          if (!isset(self::$plugintypes)) {
 375              self::fill_all_caches();
 376          }
 377  
 378          $cache = array(
 379              'subsystems'        => self::$subsystems,
 380              'plugintypes'       => self::$plugintypes,
 381              'plugins'           => self::$plugins,
 382              'parents'           => self::$parents,
 383              'subplugins'        => self::$subplugins,
 384              'classmap'          => self::$classmap,
 385              'classmaprenames'   => self::$classmaprenames,
 386              'filemap'           => self::$filemap,
 387              'version'           => self::$version,
 388          );
 389  
 390          return '<?php
 391  $cache = '.var_export($cache, true).';
 392  ';
 393      }
 394  
 395      /**
 396       * Fill all caches.
 397       */
 398      protected static function fill_all_caches() {
 399          self::$subsystems = self::fetch_subsystems();
 400  
 401          list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
 402  
 403          self::$plugins = array();
 404          foreach (self::$plugintypes as $type => $fulldir) {
 405              self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
 406          }
 407  
 408          self::fill_classmap_cache();
 409          self::fill_classmap_renames_cache();
 410          self::fill_filemap_cache();
 411          self::fetch_core_version();
 412      }
 413  
 414      /**
 415       * Get the core version.
 416       *
 417       * In order for this to work properly, opcache should be reset beforehand.
 418       *
 419       * @return float core version.
 420       */
 421      protected static function fetch_core_version() {
 422          global $CFG;
 423          if (self::$version === null) {
 424              $version = null; // Prevent IDE complaints.
 425              require($CFG->dirroot . '/version.php');
 426              self::$version = $version;
 427          }
 428          return self::$version;
 429      }
 430  
 431      /**
 432       * Returns list of core subsystems.
 433       * @return array
 434       */
 435      protected static function fetch_subsystems() {
 436          global $CFG;
 437  
 438          // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
 439          $info = [];
 440          foreach (self::fetch_component_source('subsystems') as $subsystem => $path) {
 441              // Replace admin/ directory with the config setting.
 442              if ($CFG->admin !== 'admin') {
 443                  if ($path === 'admin') {
 444                      $path = $CFG->admin;
 445                  }
 446                  if (strpos($path, 'admin/') === 0) {
 447                      $path = $CFG->admin . substr($path, 5);
 448                  }
 449              }
 450  
 451              $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
 452          }
 453  
 454          return $info;
 455      }
 456  
 457      /**
 458       * Returns list of known plugin types.
 459       * @return array
 460       */
 461      protected static function fetch_plugintypes() {
 462          global $CFG;
 463  
 464          $types = [];
 465          foreach (self::fetch_component_source('plugintypes') as $plugintype => $path) {
 466              // Replace admin/ with the config setting.
 467              if ($CFG->admin !== 'admin' && strpos($path, 'admin/') === 0) {
 468                  $path = $CFG->admin . substr($path, 5);
 469              }
 470              $types[$plugintype] = "{$CFG->dirroot}/{$path}";
 471          }
 472  
 473          $parents = array();
 474          $subplugins = array();
 475  
 476          if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
 477              $types['theme'] = $CFG->themedir;
 478          } else {
 479              $types['theme'] = $CFG->dirroot.'/theme';
 480          }
 481  
 482          foreach (self::$supportsubplugins as $type) {
 483              if ($type === 'local') {
 484                  // Local subplugins must be after local plugins.
 485                  continue;
 486              }
 487              $plugins = self::fetch_plugins($type, $types[$type]);
 488              foreach ($plugins as $plugin => $fulldir) {
 489                  $subtypes = self::fetch_subtypes($fulldir);
 490                  if (!$subtypes) {
 491                      continue;
 492                  }
 493                  $subplugins[$type.'_'.$plugin] = array();
 494                  foreach($subtypes as $subtype => $subdir) {
 495                      if (isset($types[$subtype])) {
 496                          error_log("Invalid subtype '$subtype', duplicate detected.");
 497                          continue;
 498                      }
 499                      $types[$subtype] = $subdir;
 500                      $parents[$subtype] = $type.'_'.$plugin;
 501                      $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
 502                  }
 503              }
 504          }
 505          // Local is always last!
 506          $types['local'] = $CFG->dirroot.'/local';
 507  
 508          if (in_array('local', self::$supportsubplugins)) {
 509              $type = 'local';
 510              $plugins = self::fetch_plugins($type, $types[$type]);
 511              foreach ($plugins as $plugin => $fulldir) {
 512                  $subtypes = self::fetch_subtypes($fulldir);
 513                  if (!$subtypes) {
 514                      continue;
 515                  }
 516                  $subplugins[$type.'_'.$plugin] = array();
 517                  foreach($subtypes as $subtype => $subdir) {
 518                      if (isset($types[$subtype])) {
 519                          error_log("Invalid subtype '$subtype', duplicate detected.");
 520                          continue;
 521                      }
 522                      $types[$subtype] = $subdir;
 523                      $parents[$subtype] = $type.'_'.$plugin;
 524                      $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
 525                  }
 526              }
 527          }
 528  
 529          return array($types, $parents, $subplugins);
 530      }
 531  
 532      /**
 533       * Returns the component source content as loaded from /lib/components.json.
 534       *
 535       * @return array
 536       */
 537      protected static function fetch_component_source(string $key) {
 538          if (null === self::$componentsource) {
 539              self::$componentsource = (array) json_decode(file_get_contents(__DIR__ . '/../components.json'));
 540          }
 541  
 542          return (array) self::$componentsource[$key];
 543      }
 544  
 545      /**
 546       * Returns list of subtypes.
 547       * @param string $ownerdir
 548       * @return array
 549       */
 550      protected static function fetch_subtypes($ownerdir) {
 551          global $CFG;
 552  
 553          $types = array();
 554          $subplugins = array();
 555          if (file_exists("$ownerdir/db/subplugins.json")) {
 556              $subplugins = (array) json_decode(file_get_contents("$ownerdir/db/subplugins.json"))->plugintypes;
 557          } else if (file_exists("$ownerdir/db/subplugins.php")) {
 558              error_log('Use of subplugins.php has been deprecated. ' .
 559                  "Please update your '$ownerdir' plugin to provide a subplugins.json file instead.");
 560              include("$ownerdir/db/subplugins.php");
 561          }
 562  
 563          foreach ($subplugins as $subtype => $dir) {
 564              if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
 565                  error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
 566                  continue;
 567              }
 568              if (isset(self::$subsystems[$subtype])) {
 569                  error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
 570                  continue;
 571              }
 572              if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
 573                  $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
 574              }
 575              if (!is_dir("$CFG->dirroot/$dir")) {
 576                  error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
 577                  continue;
 578              }
 579              $types[$subtype] = "$CFG->dirroot/$dir";
 580          }
 581  
 582          return $types;
 583      }
 584  
 585      /**
 586       * Returns list of plugins of given type in given directory.
 587       * @param string $plugintype
 588       * @param string $fulldir
 589       * @return array
 590       */
 591      protected static function fetch_plugins($plugintype, $fulldir) {
 592          global $CFG;
 593  
 594          $fulldirs = (array)$fulldir;
 595          if ($plugintype === 'theme') {
 596              if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
 597                  // Include themes in standard location too.
 598                  array_unshift($fulldirs, $CFG->dirroot.'/theme');
 599              }
 600          }
 601  
 602          $result = array();
 603  
 604          foreach ($fulldirs as $fulldir) {
 605              if (!is_dir($fulldir)) {
 606                  continue;
 607              }
 608              $items = new \DirectoryIterator($fulldir);
 609              foreach ($items as $item) {
 610                  if ($item->isDot() or !$item->isDir()) {
 611                      continue;
 612                  }
 613                  $pluginname = $item->getFilename();
 614                  if ($plugintype === 'auth' and $pluginname === 'db') {
 615                      // Special exception for this wrong plugin name.
 616                  } else if (isset(self::$ignoreddirs[$pluginname])) {
 617                      continue;
 618                  }
 619                  if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
 620                      // Always ignore plugins with problematic names here.
 621                      continue;
 622                  }
 623                  $result[$pluginname] = $fulldir.'/'.$pluginname;
 624                  unset($item);
 625              }
 626              unset($items);
 627          }
 628  
 629          ksort($result);
 630          return $result;
 631      }
 632  
 633      /**
 634       * Find all classes that can be autoloaded including frankenstyle namespaces.
 635       */
 636      protected static function fill_classmap_cache() {
 637          global $CFG;
 638  
 639          self::$classmap = array();
 640  
 641          self::load_classes('core', "$CFG->dirroot/lib/classes");
 642  
 643          foreach (self::$subsystems as $subsystem => $fulldir) {
 644              if (!$fulldir) {
 645                  continue;
 646              }
 647              self::load_classes('core_'.$subsystem, "$fulldir/classes");
 648          }
 649  
 650          foreach (self::$plugins as $plugintype => $plugins) {
 651              foreach ($plugins as $pluginname => $fulldir) {
 652                  self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
 653              }
 654          }
 655          ksort(self::$classmap);
 656      }
 657  
 658      /**
 659       * Fills up the cache defining what plugins have certain files.
 660       *
 661       * @see self::get_plugin_list_with_file
 662       * @return void
 663       */
 664      protected static function fill_filemap_cache() {
 665          global $CFG;
 666  
 667          self::$filemap = array();
 668  
 669          foreach (self::$filestomap as $file) {
 670              if (!isset(self::$filemap[$file])) {
 671                  self::$filemap[$file] = array();
 672              }
 673              foreach (self::$plugins as $plugintype => $plugins) {
 674                  if (!isset(self::$filemap[$file][$plugintype])) {
 675                      self::$filemap[$file][$plugintype] = array();
 676                  }
 677                  foreach ($plugins as $pluginname => $fulldir) {
 678                      if (file_exists("$fulldir/$file")) {
 679                          self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
 680                      }
 681                  }
 682              }
 683          }
 684      }
 685  
 686      /**
 687       * Find classes in directory and recurse to subdirs.
 688       * @param string $component
 689       * @param string $fulldir
 690       * @param string $namespace
 691       */
 692      protected static function load_classes($component, $fulldir, $namespace = '') {
 693          if (!is_dir($fulldir)) {
 694              return;
 695          }
 696  
 697          if (!is_readable($fulldir)) {
 698              // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
 699              // because its pretty likely to lead to a missing class error further down the line.
 700              // But our early setup code can't handle errors this early at the moment.
 701              return;
 702          }
 703  
 704          $items = new \DirectoryIterator($fulldir);
 705          foreach ($items as $item) {
 706              if ($item->isDot()) {
 707                  continue;
 708              }
 709              if ($item->isDir()) {
 710                  $dirname = $item->getFilename();
 711                  self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
 712                  continue;
 713              }
 714  
 715              $filename = $item->getFilename();
 716              $classname = preg_replace('/\.php$/', '', $filename);
 717  
 718              if ($filename === $classname) {
 719                  // Not a php file.
 720                  continue;
 721              }
 722              if ($namespace === '') {
 723                  // Legacy long frankenstyle class name.
 724                  self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
 725              }
 726              // New namespaced classes.
 727              self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
 728          }
 729          unset($item);
 730          unset($items);
 731      }
 732  
 733  
 734      /**
 735       * List all core subsystems and their location
 736       *
 737       * This is a list of components that are part of the core and their
 738       * language strings are defined in /lang/en/<<subsystem>>.php. If a given
 739       * plugin is not listed here and it does not have proper plugintype prefix,
 740       * then it is considered as course activity module.
 741       *
 742       * The location is absolute file path to dir. NULL means there is no special
 743       * directory for this subsystem. If the location is set, the subsystem's
 744       * renderer.php is expected to be there.
 745       *
 746       * @return array of (string)name => (string|null)full dir location
 747       */
 748      public static function get_core_subsystems() {
 749          self::init();
 750          return self::$subsystems;
 751      }
 752  
 753      /**
 754       * Get list of available plugin types together with their location.
 755       *
 756       * @return array as (string)plugintype => (string)fulldir
 757       */
 758      public static function get_plugin_types() {
 759          self::init();
 760          return self::$plugintypes;
 761      }
 762  
 763      /**
 764       * Get list of plugins of given type.
 765       *
 766       * @param string $plugintype
 767       * @return array as (string)pluginname => (string)fulldir
 768       */
 769      public static function get_plugin_list($plugintype) {
 770          self::init();
 771  
 772          if (!isset(self::$plugins[$plugintype])) {
 773              return array();
 774          }
 775          return self::$plugins[$plugintype];
 776      }
 777  
 778      /**
 779       * Get a list of all the plugins of a given type that define a certain class
 780       * in a certain file. The plugin component names and class names are returned.
 781       *
 782       * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
 783       * @param string $class the part of the name of the class after the
 784       *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
 785       *      names like report_courselist_thing. If you are looking for classes with
 786       *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
 787       *      Frankenstyle namespaces are also supported.
 788       * @param string $file the name of file within the plugin that defines the class.
 789       * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
 790       *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
 791       */
 792      public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
 793          global $CFG; // Necessary in case it is referenced by included PHP scripts.
 794  
 795          if ($class) {
 796              $suffix = '_' . $class;
 797          } else {
 798              $suffix = '';
 799          }
 800  
 801          $pluginclasses = array();
 802          $plugins = self::get_plugin_list($plugintype);
 803          foreach ($plugins as $plugin => $fulldir) {
 804              // Try class in frankenstyle namespace.
 805              if ($class) {
 806                  $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
 807                  if (class_exists($classname, true)) {
 808                      $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 809                      continue;
 810                  }
 811              }
 812  
 813              // Try autoloading of class with frankenstyle prefix.
 814              $classname = $plugintype . '_' . $plugin . $suffix;
 815              if (class_exists($classname, true)) {
 816                  $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 817                  continue;
 818              }
 819  
 820              // Fall back to old file location and class name.
 821              if ($file and file_exists("$fulldir/$file")) {
 822                  include_once("$fulldir/$file");
 823                  if (class_exists($classname, false)) {
 824                      $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 825                      continue;
 826                  }
 827              }
 828          }
 829  
 830          return $pluginclasses;
 831      }
 832  
 833      /**
 834       * Get a list of all the plugins of a given type that contain a particular file.
 835       *
 836       * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
 837       * @param string $file the name of file that must be present in the plugin.
 838       *                     (e.g. 'view.php', 'db/install.xml').
 839       * @param bool $include if true (default false), the file will be include_once-ed if found.
 840       * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
 841       *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
 842       */
 843      public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
 844          global $CFG; // Necessary in case it is referenced by included PHP scripts.
 845          $pluginfiles = array();
 846  
 847          if (isset(self::$filemap[$file])) {
 848              // If the file was supposed to be mapped, then it should have been set in the array.
 849              if (isset(self::$filemap[$file][$plugintype])) {
 850                  $pluginfiles = self::$filemap[$file][$plugintype];
 851              }
 852          } else {
 853              // Old-style search for non-cached files.
 854              $plugins = self::get_plugin_list($plugintype);
 855              foreach ($plugins as $plugin => $fulldir) {
 856                  $path = $fulldir . '/' . $file;
 857                  if (file_exists($path)) {
 858                      $pluginfiles[$plugin] = $path;
 859                  }
 860              }
 861          }
 862  
 863          if ($include) {
 864              foreach ($pluginfiles as $path) {
 865                  include_once($path);
 866              }
 867          }
 868  
 869          return $pluginfiles;
 870      }
 871  
 872      /**
 873       * Returns all classes in a component matching the provided namespace.
 874       *
 875       * It checks that the class exists.
 876       *
 877       * e.g. get_component_classes_in_namespace('mod_forum', 'event')
 878       *
 879       * @param string|null $component A valid moodle component (frankenstyle) or null if searching all components
 880       * @param string $namespace Namespace from the component name or empty string if all $component classes.
 881       * @return array The full class name as key and the class path as value, empty array if $component is `null`
 882       * and $namespace is empty.
 883       */
 884      public static function get_component_classes_in_namespace($component = null, $namespace = '') {
 885  
 886          $classes = array();
 887  
 888          // Only look for components if a component name is set or a namespace is set.
 889          if (isset($component) || !empty($namespace)) {
 890  
 891              // If a component parameter value is set we only want to look in that component.
 892              // Otherwise we want to check all components.
 893              $component = (isset($component)) ? self::normalize_componentname($component) : '\w+';
 894              if ($namespace) {
 895  
 896                  // We will add them later.
 897                  $namespace = trim($namespace, '\\');
 898  
 899                  // We need add double backslashes as it is how classes are stored into self::$classmap.
 900                  $namespace = implode('\\\\', explode('\\', $namespace));
 901                  $namespace = $namespace . '\\\\';
 902              }
 903              $regex = '|^' . $component . '\\\\' . $namespace . '|';
 904              $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
 905  
 906              // We want to be sure that they exist.
 907              foreach ($it as $classname => $classpath) {
 908                  if (class_exists($classname)) {
 909                      $classes[$classname] = $classpath;
 910                  }
 911              }
 912          }
 913  
 914          return $classes;
 915      }
 916  
 917      /**
 918       * Returns the exact absolute path to plugin directory.
 919       *
 920       * @param string $plugintype type of plugin
 921       * @param string $pluginname name of the plugin
 922       * @return string full path to plugin directory; null if not found
 923       */
 924      public static function get_plugin_directory($plugintype, $pluginname) {
 925          if (empty($pluginname)) {
 926              // Invalid plugin name, sorry.
 927              return null;
 928          }
 929  
 930          self::init();
 931  
 932          if (!isset(self::$plugins[$plugintype][$pluginname])) {
 933              return null;
 934          }
 935          return self::$plugins[$plugintype][$pluginname];
 936      }
 937  
 938      /**
 939       * Returns the exact absolute path to plugin directory.
 940       *
 941       * @param string $subsystem type of core subsystem
 942       * @return string full path to subsystem directory; null if not found
 943       */
 944      public static function get_subsystem_directory($subsystem) {
 945          self::init();
 946  
 947          if (!isset(self::$subsystems[$subsystem])) {
 948              return null;
 949          }
 950          return self::$subsystems[$subsystem];
 951      }
 952  
 953      /**
 954       * This method validates a plug name. It is much faster than calling clean_param.
 955       *
 956       * @param string $plugintype type of plugin
 957       * @param string $pluginname a string that might be a plugin name.
 958       * @return bool if this string is a valid plugin name.
 959       */
 960      public static function is_valid_plugin_name($plugintype, $pluginname) {
 961          if ($plugintype === 'mod') {
 962              // Modules must not have the same name as core subsystems.
 963              if (!isset(self::$subsystems)) {
 964                  // Watch out, this is called from init!
 965                  self::init();
 966              }
 967              if (isset(self::$subsystems[$pluginname])) {
 968                  return false;
 969              }
 970              // Modules MUST NOT have any underscores,
 971              // component normalisation would break very badly otherwise!
 972              return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
 973  
 974          } else {
 975              return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
 976          }
 977      }
 978  
 979      /**
 980       * Normalize the component name.
 981       *
 982       * Note: this does not verify the validity of the plugin or component.
 983       *
 984       * @param string $component
 985       * @return string
 986       */
 987      public static function normalize_componentname($componentname) {
 988          list($plugintype, $pluginname) = self::normalize_component($componentname);
 989          if ($plugintype === 'core' && is_null($pluginname)) {
 990              return $plugintype;
 991          }
 992          return $plugintype . '_' . $pluginname;
 993      }
 994  
 995      /**
 996       * Normalize the component name using the "frankenstyle" rules.
 997       *
 998       * Note: this does not verify the validity of plugin or type names.
 999       *
1000       * @param string $component
1001       * @return array two-items list of [(string)type, (string|null)name]
1002       */
1003      public static function normalize_component($component) {
1004          if ($component === 'moodle' or $component === 'core' or $component === '') {
1005              return array('core', null);
1006          }
1007  
1008          if (strpos($component, '_') === false) {
1009              self::init();
1010              if (array_key_exists($component, self::$subsystems)) {
1011                  $type   = 'core';
1012                  $plugin = $component;
1013              } else {
1014                  // Everything else without underscore is a module.
1015                  $type   = 'mod';
1016                  $plugin = $component;
1017              }
1018  
1019          } else {
1020              list($type, $plugin) = explode('_', $component, 2);
1021              if ($type === 'moodle') {
1022                  $type = 'core';
1023              }
1024              // Any unknown type must be a subplugin.
1025          }
1026  
1027          return array($type, $plugin);
1028      }
1029  
1030      /**
1031       * Return exact absolute path to a plugin directory.
1032       *
1033       * @param string $component name such as 'moodle', 'mod_forum'
1034       * @return string full path to component directory; NULL if not found
1035       */
1036      public static function get_component_directory($component) {
1037          global $CFG;
1038  
1039          list($type, $plugin) = self::normalize_component($component);
1040  
1041          if ($type === 'core') {
1042              if ($plugin === null) {
1043                  return $path = $CFG->libdir;
1044              }
1045              return self::get_subsystem_directory($plugin);
1046          }
1047  
1048          return self::get_plugin_directory($type, $plugin);
1049      }
1050  
1051      /**
1052       * Returns list of plugin types that allow subplugins.
1053       * @return array as (string)plugintype => (string)fulldir
1054       */
1055      public static function get_plugin_types_with_subplugins() {
1056          self::init();
1057  
1058          $return = array();
1059          foreach (self::$supportsubplugins as $type) {
1060              $return[$type] = self::$plugintypes[$type];
1061          }
1062          return $return;
1063      }
1064  
1065      /**
1066       * Returns parent of this subplugin type.
1067       *
1068       * @param string $type
1069       * @return string parent component or null
1070       */
1071      public static function get_subtype_parent($type) {
1072          self::init();
1073  
1074          if (isset(self::$parents[$type])) {
1075              return self::$parents[$type];
1076          }
1077  
1078          return null;
1079      }
1080  
1081      /**
1082       * Return all subplugins of this component.
1083       * @param string $component.
1084       * @return array $subtype=>array($component, ..), null if no subtypes defined
1085       */
1086      public static function get_subplugins($component) {
1087          self::init();
1088  
1089          if (isset(self::$subplugins[$component])) {
1090              return self::$subplugins[$component];
1091          }
1092  
1093          return null;
1094      }
1095  
1096      /**
1097       * Returns hash of all versions including core and all plugins.
1098       *
1099       * This is relatively slow and not fully cached, use with care!
1100       *
1101       * @return string sha1 hash
1102       */
1103      public static function get_all_versions_hash() {
1104          return sha1(serialize(self::get_all_versions()));
1105      }
1106  
1107      /**
1108       * Returns hash of all versions including core and all plugins.
1109       *
1110       * This is relatively slow and not fully cached, use with care!
1111       *
1112       * @return array as (string)plugintype_pluginname => (int)version
1113       */
1114      public static function get_all_versions() : array {
1115          global $CFG;
1116  
1117          self::init();
1118  
1119          $versions = array();
1120  
1121          // Main version first.
1122          $versions['core'] = self::fetch_core_version();
1123  
1124          // The problem here is tha the component cache might be stable,
1125          // we want this to work also on frontpage without resetting the component cache.
1126          $usecache = false;
1127          if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1128              $usecache = true;
1129          }
1130  
1131          // Now all plugins.
1132          $plugintypes = core_component::get_plugin_types();
1133          foreach ($plugintypes as $type => $typedir) {
1134              if ($usecache) {
1135                  $plugs = core_component::get_plugin_list($type);
1136              } else {
1137                  $plugs = self::fetch_plugins($type, $typedir);
1138              }
1139              foreach ($plugs as $plug => $fullplug) {
1140                  $plugin = new stdClass();
1141                  $plugin->version = null;
1142                  $module = $plugin;
1143                  include ($fullplug.'/version.php');
1144                  $versions[$type.'_'.$plug] = $plugin->version;
1145              }
1146          }
1147  
1148          return $versions;
1149      }
1150  
1151      /**
1152       * Invalidate opcode cache for given file, this is intended for
1153       * php files that are stored in dataroot.
1154       *
1155       * Note: we need it here because this class must be self-contained.
1156       *
1157       * @param string $file
1158       */
1159      public static function invalidate_opcode_php_cache($file) {
1160          if (function_exists('opcache_invalidate')) {
1161              if (!file_exists($file)) {
1162                  return;
1163              }
1164              opcache_invalidate($file, true);
1165          }
1166      }
1167  
1168      /**
1169       * Return true if subsystemname is core subsystem.
1170       *
1171       * @param string $subsystemname name of the subsystem.
1172       * @return bool true if core subsystem.
1173       */
1174      public static function is_core_subsystem($subsystemname) {
1175          return isset(self::$subsystems[$subsystemname]);
1176      }
1177  
1178      /**
1179       * Records all class renames that have been made to facilitate autoloading.
1180       */
1181      protected static function fill_classmap_renames_cache() {
1182          global $CFG;
1183  
1184          self::$classmaprenames = array();
1185  
1186          self::load_renamed_classes("$CFG->dirroot/lib/");
1187  
1188          foreach (self::$subsystems as $subsystem => $fulldir) {
1189              self::load_renamed_classes($fulldir);
1190          }
1191  
1192          foreach (self::$plugins as $plugintype => $plugins) {
1193              foreach ($plugins as $pluginname => $fulldir) {
1194                  self::load_renamed_classes($fulldir);
1195              }
1196          }
1197      }
1198  
1199      /**
1200       * Loads the db/renamedclasses.php file from the given directory.
1201       *
1202       * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1203       * and the value is the new class name.
1204       * It is only included when we are populating the component cache. After that is not needed.
1205       *
1206       * @param string|null $fulldir The directory to the renamed classes.
1207       */
1208      protected static function load_renamed_classes(?string $fulldir) {
1209          if (is_null($fulldir)) {
1210              return;
1211          }
1212  
1213          $file = $fulldir . '/db/renamedclasses.php';
1214          if (is_readable($file)) {
1215              $renamedclasses = null;
1216              require($file);
1217              if (is_array($renamedclasses)) {
1218                  foreach ($renamedclasses as $oldclass => $newclass) {
1219                      self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1220                  }
1221              }
1222          }
1223      }
1224  
1225      /**
1226       * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1227       *
1228       * E.g.
1229       *  [
1230       *      'mod' => [
1231       *          'mod_forum' => FORUM_PLUGIN_PATH,
1232       *          ...
1233       *      ],
1234       *      ...
1235       *      'core' => [
1236       *          'core_comment' => COMMENT_SUBSYSTEM_PATH,
1237       *          ...
1238       *      ]
1239       * ]
1240       *
1241       * @return array an associative array of components and their corresponding paths.
1242       */
1243      public static function get_component_list() : array {
1244          $components = [];
1245          // Get all plugins.
1246          foreach (self::get_plugin_types() as $plugintype => $typedir) {
1247              $components[$plugintype] = [];
1248              foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1249                  $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1250              }
1251          }
1252          // Get all subsystems.
1253          foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1254              $components['core']['core_' . $subsystemname] = $subsystempath;
1255          }
1256          return $components;
1257      }
1258  
1259      /**
1260       * Returns a list of frankenstyle component names.
1261       *
1262       * E.g.
1263       *  [
1264       *      'core_course',
1265       *      'core_message',
1266       *      'mod_assign',
1267       *      ...
1268       *  ]
1269       * @return array the list of frankenstyle component names.
1270       */
1271      public static function get_component_names() : array {
1272          $componentnames = [];
1273          // Get all plugins.
1274          foreach (self::get_plugin_types() as $plugintype => $typedir) {
1275              foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1276                  $componentnames[] = $plugintype . '_' . $pluginname;
1277              }
1278          }
1279          // Get all subsystems.
1280          foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1281              $componentnames[] = 'core_' . $subsystemname;
1282          }
1283          return $componentnames;
1284      }
1285  }