Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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 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          'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
 100          'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
 101          'IMSGlobal\LTI' => 'lib/ltiprovider/src',
 102          'Packback\\Lti1p3' => 'lib/lti1p3/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 = [];
 557              $subpluginsjson = json_decode(file_get_contents("$ownerdir/db/subplugins.json"));
 558              if (json_last_error() === JSON_ERROR_NONE) {
 559                  if (!empty($subpluginsjson->plugintypes)) {
 560                      $subplugins = (array) $subpluginsjson->plugintypes;
 561                  } else {
 562                      error_log("No plugintypes defined in $ownerdir/db/subplugins.json");
 563                  }
 564              } else {
 565                  $jsonerror = json_last_error_msg();
 566                  error_log("$ownerdir/db/subplugins.json is invalid ($jsonerror)");
 567              }
 568          } else if (file_exists("$ownerdir/db/subplugins.php")) {
 569              error_log('Use of subplugins.php has been deprecated. ' .
 570                  "Please update your '$ownerdir' plugin to provide a subplugins.json file instead.");
 571              include("$ownerdir/db/subplugins.php");
 572          }
 573  
 574          foreach ($subplugins as $subtype => $dir) {
 575              if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
 576                  error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
 577                  continue;
 578              }
 579              if (isset(self::$subsystems[$subtype])) {
 580                  error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
 581                  continue;
 582              }
 583              if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
 584                  $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
 585              }
 586              if (!is_dir("$CFG->dirroot/$dir")) {
 587                  error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
 588                  continue;
 589              }
 590              $types[$subtype] = "$CFG->dirroot/$dir";
 591          }
 592  
 593          return $types;
 594      }
 595  
 596      /**
 597       * Returns list of plugins of given type in given directory.
 598       * @param string $plugintype
 599       * @param string $fulldir
 600       * @return array
 601       */
 602      protected static function fetch_plugins($plugintype, $fulldir) {
 603          global $CFG;
 604  
 605          $fulldirs = (array)$fulldir;
 606          if ($plugintype === 'theme') {
 607              if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
 608                  // Include themes in standard location too.
 609                  array_unshift($fulldirs, $CFG->dirroot.'/theme');
 610              }
 611          }
 612  
 613          $result = array();
 614  
 615          foreach ($fulldirs as $fulldir) {
 616              if (!is_dir($fulldir)) {
 617                  continue;
 618              }
 619              $items = new \DirectoryIterator($fulldir);
 620              foreach ($items as $item) {
 621                  if ($item->isDot() or !$item->isDir()) {
 622                      continue;
 623                  }
 624                  $pluginname = $item->getFilename();
 625                  if ($plugintype === 'auth' and $pluginname === 'db') {
 626                      // Special exception for this wrong plugin name.
 627                  } else if (isset(self::$ignoreddirs[$pluginname])) {
 628                      continue;
 629                  }
 630                  if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
 631                      // Always ignore plugins with problematic names here.
 632                      continue;
 633                  }
 634                  $result[$pluginname] = $fulldir.'/'.$pluginname;
 635                  unset($item);
 636              }
 637              unset($items);
 638          }
 639  
 640          ksort($result);
 641          return $result;
 642      }
 643  
 644      /**
 645       * Find all classes that can be autoloaded including frankenstyle namespaces.
 646       */
 647      protected static function fill_classmap_cache() {
 648          global $CFG;
 649  
 650          self::$classmap = array();
 651  
 652          self::load_classes('core', "$CFG->dirroot/lib/classes");
 653  
 654          foreach (self::$subsystems as $subsystem => $fulldir) {
 655              if (!$fulldir) {
 656                  continue;
 657              }
 658              self::load_classes('core_'.$subsystem, "$fulldir/classes");
 659          }
 660  
 661          foreach (self::$plugins as $plugintype => $plugins) {
 662              foreach ($plugins as $pluginname => $fulldir) {
 663                  self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
 664              }
 665          }
 666          ksort(self::$classmap);
 667      }
 668  
 669      /**
 670       * Fills up the cache defining what plugins have certain files.
 671       *
 672       * @see self::get_plugin_list_with_file
 673       * @return void
 674       */
 675      protected static function fill_filemap_cache() {
 676          global $CFG;
 677  
 678          self::$filemap = array();
 679  
 680          foreach (self::$filestomap as $file) {
 681              if (!isset(self::$filemap[$file])) {
 682                  self::$filemap[$file] = array();
 683              }
 684              foreach (self::$plugins as $plugintype => $plugins) {
 685                  if (!isset(self::$filemap[$file][$plugintype])) {
 686                      self::$filemap[$file][$plugintype] = array();
 687                  }
 688                  foreach ($plugins as $pluginname => $fulldir) {
 689                      if (file_exists("$fulldir/$file")) {
 690                          self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
 691                      }
 692                  }
 693              }
 694          }
 695      }
 696  
 697      /**
 698       * Find classes in directory and recurse to subdirs.
 699       * @param string $component
 700       * @param string $fulldir
 701       * @param string $namespace
 702       */
 703      protected static function load_classes($component, $fulldir, $namespace = '') {
 704          if (!is_dir($fulldir)) {
 705              return;
 706          }
 707  
 708          if (!is_readable($fulldir)) {
 709              // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
 710              // because its pretty likely to lead to a missing class error further down the line.
 711              // But our early setup code can't handle errors this early at the moment.
 712              return;
 713          }
 714  
 715          $items = new \DirectoryIterator($fulldir);
 716          foreach ($items as $item) {
 717              if ($item->isDot()) {
 718                  continue;
 719              }
 720              if ($item->isDir()) {
 721                  $dirname = $item->getFilename();
 722                  self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
 723                  continue;
 724              }
 725  
 726              $filename = $item->getFilename();
 727              $classname = preg_replace('/\.php$/', '', $filename);
 728  
 729              if ($filename === $classname) {
 730                  // Not a php file.
 731                  continue;
 732              }
 733              if ($namespace === '') {
 734                  // Legacy long frankenstyle class name.
 735                  self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
 736              }
 737              // New namespaced classes.
 738              self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
 739          }
 740          unset($item);
 741          unset($items);
 742      }
 743  
 744  
 745      /**
 746       * List all core subsystems and their location
 747       *
 748       * This is a list of components that are part of the core and their
 749       * language strings are defined in /lang/en/<<subsystem>>.php. If a given
 750       * plugin is not listed here and it does not have proper plugintype prefix,
 751       * then it is considered as course activity module.
 752       *
 753       * The location is absolute file path to dir. NULL means there is no special
 754       * directory for this subsystem. If the location is set, the subsystem's
 755       * renderer.php is expected to be there.
 756       *
 757       * @return array of (string)name => (string|null)full dir location
 758       */
 759      public static function get_core_subsystems() {
 760          self::init();
 761          return self::$subsystems;
 762      }
 763  
 764      /**
 765       * Get list of available plugin types together with their location.
 766       *
 767       * @return array as (string)plugintype => (string)fulldir
 768       */
 769      public static function get_plugin_types() {
 770          self::init();
 771          return self::$plugintypes;
 772      }
 773  
 774      /**
 775       * Get list of plugins of given type.
 776       *
 777       * @param string $plugintype
 778       * @return array as (string)pluginname => (string)fulldir
 779       */
 780      public static function get_plugin_list($plugintype) {
 781          self::init();
 782  
 783          if (!isset(self::$plugins[$plugintype])) {
 784              return array();
 785          }
 786          return self::$plugins[$plugintype];
 787      }
 788  
 789      /**
 790       * Get a list of all the plugins of a given type that define a certain class
 791       * in a certain file. The plugin component names and class names are returned.
 792       *
 793       * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
 794       * @param string $class the part of the name of the class after the
 795       *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
 796       *      names like report_courselist_thing. If you are looking for classes with
 797       *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
 798       *      Frankenstyle namespaces are also supported.
 799       * @param string $file the name of file within the plugin that defines the class.
 800       * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
 801       *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
 802       */
 803      public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
 804          global $CFG; // Necessary in case it is referenced by included PHP scripts.
 805  
 806          if ($class) {
 807              $suffix = '_' . $class;
 808          } else {
 809              $suffix = '';
 810          }
 811  
 812          $pluginclasses = array();
 813          $plugins = self::get_plugin_list($plugintype);
 814          foreach ($plugins as $plugin => $fulldir) {
 815              // Try class in frankenstyle namespace.
 816              if ($class) {
 817                  $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
 818                  if (class_exists($classname, true)) {
 819                      $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 820                      continue;
 821                  }
 822              }
 823  
 824              // Try autoloading of class with frankenstyle prefix.
 825              $classname = $plugintype . '_' . $plugin . $suffix;
 826              if (class_exists($classname, true)) {
 827                  $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 828                  continue;
 829              }
 830  
 831              // Fall back to old file location and class name.
 832              if ($file and file_exists("$fulldir/$file")) {
 833                  include_once("$fulldir/$file");
 834                  if (class_exists($classname, false)) {
 835                      $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 836                      continue;
 837                  }
 838              }
 839          }
 840  
 841          return $pluginclasses;
 842      }
 843  
 844      /**
 845       * Get a list of all the plugins of a given type that contain a particular file.
 846       *
 847       * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
 848       * @param string $file the name of file that must be present in the plugin.
 849       *                     (e.g. 'view.php', 'db/install.xml').
 850       * @param bool $include if true (default false), the file will be include_once-ed if found.
 851       * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
 852       *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
 853       */
 854      public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
 855          global $CFG; // Necessary in case it is referenced by included PHP scripts.
 856          $pluginfiles = array();
 857  
 858          if (isset(self::$filemap[$file])) {
 859              // If the file was supposed to be mapped, then it should have been set in the array.
 860              if (isset(self::$filemap[$file][$plugintype])) {
 861                  $pluginfiles = self::$filemap[$file][$plugintype];
 862              }
 863          } else {
 864              // Old-style search for non-cached files.
 865              $plugins = self::get_plugin_list($plugintype);
 866              foreach ($plugins as $plugin => $fulldir) {
 867                  $path = $fulldir . '/' . $file;
 868                  if (file_exists($path)) {
 869                      $pluginfiles[$plugin] = $path;
 870                  }
 871              }
 872          }
 873  
 874          if ($include) {
 875              foreach ($pluginfiles as $path) {
 876                  include_once($path);
 877              }
 878          }
 879  
 880          return $pluginfiles;
 881      }
 882  
 883      /**
 884       * Returns all classes in a component matching the provided namespace.
 885       *
 886       * It checks that the class exists.
 887       *
 888       * e.g. get_component_classes_in_namespace('mod_forum', 'event')
 889       *
 890       * @param string|null $component A valid moodle component (frankenstyle) or null if searching all components
 891       * @param string $namespace Namespace from the component name or empty string if all $component classes.
 892       * @return array The full class name as key and the class path as value, empty array if $component is `null`
 893       * and $namespace is empty.
 894       */
 895      public static function get_component_classes_in_namespace($component = null, $namespace = '') {
 896  
 897          $classes = array();
 898  
 899          // Only look for components if a component name is set or a namespace is set.
 900          if (isset($component) || !empty($namespace)) {
 901  
 902              // If a component parameter value is set we only want to look in that component.
 903              // Otherwise we want to check all components.
 904              $component = (isset($component)) ? self::normalize_componentname($component) : '\w+';
 905              if ($namespace) {
 906  
 907                  // We will add them later.
 908                  $namespace = trim($namespace, '\\');
 909  
 910                  // We need add double backslashes as it is how classes are stored into self::$classmap.
 911                  $namespace = implode('\\\\', explode('\\', $namespace));
 912                  $namespace = $namespace . '\\\\';
 913              }
 914              $regex = '|^' . $component . '\\\\' . $namespace . '|';
 915              $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
 916  
 917              // We want to be sure that they exist.
 918              foreach ($it as $classname => $classpath) {
 919                  if (class_exists($classname)) {
 920                      $classes[$classname] = $classpath;
 921                  }
 922              }
 923          }
 924  
 925          return $classes;
 926      }
 927  
 928      /**
 929       * Returns the exact absolute path to plugin directory.
 930       *
 931       * @param string $plugintype type of plugin
 932       * @param string $pluginname name of the plugin
 933       * @return string full path to plugin directory; null if not found
 934       */
 935      public static function get_plugin_directory($plugintype, $pluginname) {
 936          if (empty($pluginname)) {
 937              // Invalid plugin name, sorry.
 938              return null;
 939          }
 940  
 941          self::init();
 942  
 943          if (!isset(self::$plugins[$plugintype][$pluginname])) {
 944              return null;
 945          }
 946          return self::$plugins[$plugintype][$pluginname];
 947      }
 948  
 949      /**
 950       * Returns the exact absolute path to plugin directory.
 951       *
 952       * @param string $subsystem type of core subsystem
 953       * @return string full path to subsystem directory; null if not found
 954       */
 955      public static function get_subsystem_directory($subsystem) {
 956          self::init();
 957  
 958          if (!isset(self::$subsystems[$subsystem])) {
 959              return null;
 960          }
 961          return self::$subsystems[$subsystem];
 962      }
 963  
 964      /**
 965       * This method validates a plug name. It is much faster than calling clean_param.
 966       *
 967       * @param string $plugintype type of plugin
 968       * @param string $pluginname a string that might be a plugin name.
 969       * @return bool if this string is a valid plugin name.
 970       */
 971      public static function is_valid_plugin_name($plugintype, $pluginname) {
 972          if ($plugintype === 'mod') {
 973              // Modules must not have the same name as core subsystems.
 974              if (!isset(self::$subsystems)) {
 975                  // Watch out, this is called from init!
 976                  self::init();
 977              }
 978              if (isset(self::$subsystems[$pluginname])) {
 979                  return false;
 980              }
 981              // Modules MUST NOT have any underscores,
 982              // component normalisation would break very badly otherwise!
 983              return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
 984  
 985          } else {
 986              return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
 987          }
 988      }
 989  
 990      /**
 991       * Normalize the component name.
 992       *
 993       * Note: this does not verify the validity of the plugin or component.
 994       *
 995       * @param string $component
 996       * @return string
 997       */
 998      public static function normalize_componentname($componentname) {
 999          list($plugintype, $pluginname) = self::normalize_component($componentname);
1000          if ($plugintype === 'core' && is_null($pluginname)) {
1001              return $plugintype;
1002          }
1003          return $plugintype . '_' . $pluginname;
1004      }
1005  
1006      /**
1007       * Normalize the component name using the "frankenstyle" rules.
1008       *
1009       * Note: this does not verify the validity of plugin or type names.
1010       *
1011       * @param string $component
1012       * @return array two-items list of [(string)type, (string|null)name]
1013       */
1014      public static function normalize_component($component) {
1015          if ($component === 'moodle' or $component === 'core' or $component === '') {
1016              return array('core', null);
1017          }
1018  
1019          if (strpos($component, '_') === false) {
1020              self::init();
1021              if (array_key_exists($component, self::$subsystems)) {
1022                  $type   = 'core';
1023                  $plugin = $component;
1024              } else {
1025                  // Everything else without underscore is a module.
1026                  $type   = 'mod';
1027                  $plugin = $component;
1028              }
1029  
1030          } else {
1031              list($type, $plugin) = explode('_', $component, 2);
1032              if ($type === 'moodle') {
1033                  $type = 'core';
1034              }
1035              // Any unknown type must be a subplugin.
1036          }
1037  
1038          return array($type, $plugin);
1039      }
1040  
1041      /**
1042       * Return exact absolute path to a plugin directory.
1043       *
1044       * @param string $component name such as 'moodle', 'mod_forum'
1045       * @return string full path to component directory; NULL if not found
1046       */
1047      public static function get_component_directory($component) {
1048          global $CFG;
1049  
1050          list($type, $plugin) = self::normalize_component($component);
1051  
1052          if ($type === 'core') {
1053              if ($plugin === null) {
1054                  return $path = $CFG->libdir;
1055              }
1056              return self::get_subsystem_directory($plugin);
1057          }
1058  
1059          return self::get_plugin_directory($type, $plugin);
1060      }
1061  
1062      /**
1063       * Returns list of plugin types that allow subplugins.
1064       * @return array as (string)plugintype => (string)fulldir
1065       */
1066      public static function get_plugin_types_with_subplugins() {
1067          self::init();
1068  
1069          $return = array();
1070          foreach (self::$supportsubplugins as $type) {
1071              $return[$type] = self::$plugintypes[$type];
1072          }
1073          return $return;
1074      }
1075  
1076      /**
1077       * Returns parent of this subplugin type.
1078       *
1079       * @param string $type
1080       * @return string parent component or null
1081       */
1082      public static function get_subtype_parent($type) {
1083          self::init();
1084  
1085          if (isset(self::$parents[$type])) {
1086              return self::$parents[$type];
1087          }
1088  
1089          return null;
1090      }
1091  
1092      /**
1093       * Return all subplugins of this component.
1094       * @param string $component.
1095       * @return array $subtype=>array($component, ..), null if no subtypes defined
1096       */
1097      public static function get_subplugins($component) {
1098          self::init();
1099  
1100          if (isset(self::$subplugins[$component])) {
1101              return self::$subplugins[$component];
1102          }
1103  
1104          return null;
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 string sha1 hash
1113       */
1114      public static function get_all_versions_hash() {
1115          return sha1(serialize(self::get_all_versions()));
1116      }
1117  
1118      /**
1119       * Returns hash of all versions including core and all plugins.
1120       *
1121       * This is relatively slow and not fully cached, use with care!
1122       *
1123       * @return array as (string)plugintype_pluginname => (int)version
1124       */
1125      public static function get_all_versions() : array {
1126          global $CFG;
1127  
1128          self::init();
1129  
1130          $versions = array();
1131  
1132          // Main version first.
1133          $versions['core'] = self::fetch_core_version();
1134  
1135          // The problem here is tha the component cache might be stable,
1136          // we want this to work also on frontpage without resetting the component cache.
1137          $usecache = false;
1138          if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1139              $usecache = true;
1140          }
1141  
1142          // Now all plugins.
1143          $plugintypes = core_component::get_plugin_types();
1144          foreach ($plugintypes as $type => $typedir) {
1145              if ($usecache) {
1146                  $plugs = core_component::get_plugin_list($type);
1147              } else {
1148                  $plugs = self::fetch_plugins($type, $typedir);
1149              }
1150              foreach ($plugs as $plug => $fullplug) {
1151                  $plugin = new stdClass();
1152                  $plugin->version = null;
1153                  $module = $plugin;
1154                  include ($fullplug.'/version.php');
1155                  $versions[$type.'_'.$plug] = $plugin->version;
1156              }
1157          }
1158  
1159          return $versions;
1160      }
1161  
1162      /**
1163       * Invalidate opcode cache for given file, this is intended for
1164       * php files that are stored in dataroot.
1165       *
1166       * Note: we need it here because this class must be self-contained.
1167       *
1168       * @param string $file
1169       */
1170      public static function invalidate_opcode_php_cache($file) {
1171          if (function_exists('opcache_invalidate')) {
1172              if (!file_exists($file)) {
1173                  return;
1174              }
1175              opcache_invalidate($file, true);
1176          }
1177      }
1178  
1179      /**
1180       * Return true if subsystemname is core subsystem.
1181       *
1182       * @param string $subsystemname name of the subsystem.
1183       * @return bool true if core subsystem.
1184       */
1185      public static function is_core_subsystem($subsystemname) {
1186          return isset(self::$subsystems[$subsystemname]);
1187      }
1188  
1189      /**
1190       * Records all class renames that have been made to facilitate autoloading.
1191       */
1192      protected static function fill_classmap_renames_cache() {
1193          global $CFG;
1194  
1195          self::$classmaprenames = array();
1196  
1197          self::load_renamed_classes("$CFG->dirroot/lib/");
1198  
1199          foreach (self::$subsystems as $subsystem => $fulldir) {
1200              self::load_renamed_classes($fulldir);
1201          }
1202  
1203          foreach (self::$plugins as $plugintype => $plugins) {
1204              foreach ($plugins as $pluginname => $fulldir) {
1205                  self::load_renamed_classes($fulldir);
1206              }
1207          }
1208      }
1209  
1210      /**
1211       * Loads the db/renamedclasses.php file from the given directory.
1212       *
1213       * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1214       * and the value is the new class name.
1215       * It is only included when we are populating the component cache. After that is not needed.
1216       *
1217       * @param string|null $fulldir The directory to the renamed classes.
1218       */
1219      protected static function load_renamed_classes(?string $fulldir) {
1220          if (is_null($fulldir)) {
1221              return;
1222          }
1223  
1224          $file = $fulldir . '/db/renamedclasses.php';
1225          if (is_readable($file)) {
1226              $renamedclasses = null;
1227              require($file);
1228              if (is_array($renamedclasses)) {
1229                  foreach ($renamedclasses as $oldclass => $newclass) {
1230                      self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1231                  }
1232              }
1233          }
1234      }
1235  
1236      /**
1237       * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1238       *
1239       * E.g.
1240       *  [
1241       *      'mod' => [
1242       *          'mod_forum' => FORUM_PLUGIN_PATH,
1243       *          ...
1244       *      ],
1245       *      ...
1246       *      'core' => [
1247       *          'core_comment' => COMMENT_SUBSYSTEM_PATH,
1248       *          ...
1249       *      ]
1250       * ]
1251       *
1252       * @return array an associative array of components and their corresponding paths.
1253       */
1254      public static function get_component_list() : array {
1255          $components = [];
1256          // Get all plugins.
1257          foreach (self::get_plugin_types() as $plugintype => $typedir) {
1258              $components[$plugintype] = [];
1259              foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1260                  $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1261              }
1262          }
1263          // Get all subsystems.
1264          foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1265              $components['core']['core_' . $subsystemname] = $subsystempath;
1266          }
1267          return $components;
1268      }
1269  
1270      /**
1271       * Returns a list of frankenstyle component names.
1272       *
1273       * E.g.
1274       *  [
1275       *      'core_course',
1276       *      'core_message',
1277       *      'mod_assign',
1278       *      ...
1279       *  ]
1280       * @return array the list of frankenstyle component names.
1281       */
1282      public static function get_component_names() : array {
1283          $componentnames = [];
1284          // Get all plugins.
1285          foreach (self::get_plugin_types() as $plugintype => $typedir) {
1286              foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1287                  $componentnames[] = $plugintype . '_' . $pluginname;
1288              }
1289          }
1290          // Get all subsystems.
1291          foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1292              $componentnames[] = 'core_' . $subsystemname;
1293          }
1294          return $componentnames;
1295      }
1296  
1297      /**
1298       * Checks for the presence of monologo icons within a plugin.
1299       *
1300       * Only checks monologo icons in PNG and SVG formats as they are
1301       * formats that can have transparent background.
1302       *
1303       * @param string $plugintype The plugin type.
1304       * @param string $pluginname The plugin name.
1305       * @return bool True if the plugin has a monologo icon
1306       */
1307      public static function has_monologo_icon(string $plugintype, string $pluginname): bool {
1308          $plugindir = core_component::get_plugin_directory($plugintype, $pluginname);
1309          if ($plugindir === null) {
1310              return false;
1311          }
1312          return file_exists("$plugindir/pix/monologo.svg") || file_exists("$plugindir/pix/monologo.png");
1313      }
1314  }