Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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

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