Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [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 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          'PhpXmlRpc' => 'lib/phpxmlrpc',
 112      );
 113  
 114      /**
 115       * Class loader for Frankenstyle named classes in standard locations.
 116       * Frankenstyle namespaces are supported.
 117       *
 118       * The expected location for core classes is:
 119       *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
 120       *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
 121       *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
 122       *
 123       * The expected location for plugin classes is:
 124       *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
 125       *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
 126       *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
 127       *
 128       * @param string $classname
 129       */
 130      public static function classloader($classname) {
 131          self::init();
 132  
 133          if (isset(self::$classmap[$classname])) {
 134              // Global $CFG is expected in included scripts.
 135              global $CFG;
 136              // Function include would be faster, but for BC it is better to include only once.
 137              include_once(self::$classmap[$classname]);
 138              return;
 139          }
 140          if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
 141              $newclassname = self::$classmaprenames[$classname];
 142              $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
 143              debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
 144              if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
 145                  throw new \coding_exception("Cannot alias $classname to $newclassname");
 146              }
 147              class_alias($newclassname, $classname);
 148              return;
 149          }
 150  
 151          $file = self::psr_classloader($classname);
 152          // If the file is found, require it.
 153          if (!empty($file)) {
 154              require($file);
 155              return;
 156          }
 157      }
 158  
 159      /**
 160       * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
 161       * demand. Only returns paths to files that exist.
 162       *
 163       * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
 164       * compatible.
 165       *
 166       * @param string $class the name of the class.
 167       * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
 168       */
 169      protected static function psr_classloader($class) {
 170          // Iterate through each PSR-4 namespace prefix.
 171          foreach (self::$psr4namespaces as $prefix => $path) {
 172              $file = self::get_class_file($class, $prefix, $path, array('\\'));
 173              if (!empty($file) && file_exists($file)) {
 174                  return $file;
 175              }
 176          }
 177  
 178          // Iterate through each PSR-0 namespace prefix.
 179          foreach (self::$psr0namespaces as $prefix => $path) {
 180              $file = self::get_class_file($class, $prefix, $path, array('\\', '_'));
 181              if (!empty($file) && file_exists($file)) {
 182                  return $file;
 183              }
 184          }
 185  
 186          return false;
 187      }
 188  
 189      /**
 190       * Return the path to the class based on the given namespace prefix and path it corresponds to.
 191       *
 192       * Will return the path even if the file does not exist. Check the file esists before requiring.
 193       *
 194       * @param string $class the name of the class.
 195       * @param string $prefix The namespace prefix used to identify the base directory of the source files.
 196       * @param string $path The relative path to the base directory of the source files.
 197       * @param string[] $separators The characters that should be used for separating.
 198       * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
 199       */
 200      protected static function get_class_file($class, $prefix, $path, $separators) {
 201          global $CFG;
 202  
 203          // Does the class use the namespace prefix?
 204          $len = strlen($prefix);
 205          if (strncmp($prefix, $class, $len) !== 0) {
 206              // No, move to the next prefix.
 207              return false;
 208          }
 209          $path = $CFG->dirroot . '/' . $path;
 210  
 211          // Get the relative class name.
 212          $relativeclass = substr($class, $len);
 213  
 214          // Replace the namespace prefix with the base directory, replace namespace
 215          // separators with directory separators in the relative class name, append
 216          // with .php.
 217          $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
 218  
 219          return $file;
 220      }
 221  
 222  
 223      /**
 224       * Initialise caches, always call before accessing self:: caches.
 225       */
 226      protected static function init() {
 227          global $CFG;
 228  
 229          // Init only once per request/CLI execution, we ignore changes done afterwards.
 230          if (isset(self::$plugintypes)) {
 231              return;
 232          }
 233  
 234          if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
 235              self::fill_all_caches();
 236              return;
 237          }
 238  
 239          if (!empty($CFG->alternative_component_cache)) {
 240              // Hack for heavily clustered sites that want to manage component cache invalidation manually.
 241              $cachefile = $CFG->alternative_component_cache;
 242  
 243              if (file_exists($cachefile)) {
 244                  if (CACHE_DISABLE_ALL) {
 245                      // Verify the cache state only on upgrade pages.
 246                      $content = self::get_cache_content();
 247                      if (sha1_file($cachefile) !== sha1($content)) {
 248                          die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
 249                      }
 250                      return;
 251                  }
 252                  $cache = array();
 253                  include($cachefile);
 254                  self::$plugintypes      = $cache['plugintypes'];
 255                  self::$plugins          = $cache['plugins'];
 256                  self::$subsystems       = $cache['subsystems'];
 257                  self::$parents          = $cache['parents'];
 258                  self::$subplugins       = $cache['subplugins'];
 259                  self::$classmap         = $cache['classmap'];
 260                  self::$classmaprenames  = $cache['classmaprenames'];
 261                  self::$filemap          = $cache['filemap'];
 262                  return;
 263              }
 264  
 265              if (!is_writable(dirname($cachefile))) {
 266                  die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
 267              }
 268  
 269              // Lets try to create the file, it might be in some writable directory or a local cache dir.
 270  
 271          } else {
 272              // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
 273              //       use $CFG->alternative_component_cache if you do not like it.
 274              $cachefile = "$CFG->cachedir/core_component.php";
 275          }
 276  
 277          if (!CACHE_DISABLE_ALL and !self::is_developer()) {
 278              // 1/ Use the cache only outside of install and upgrade.
 279              // 2/ Let developers add/remove classes in developer mode.
 280              if (is_readable($cachefile)) {
 281                  $cache = false;
 282                  include($cachefile);
 283                  if (!is_array($cache)) {
 284                      // Something is very wrong.
 285                  } else if (!isset($cache['version'])) {
 286                      // Something is very wrong.
 287                  } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
 288                      // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
 289                      error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
 290                  } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
 291                      // $CFG->dirroot was changed.
 292                  } else {
 293                      // The cache looks ok, let's use it.
 294                      self::$plugintypes      = $cache['plugintypes'];
 295                      self::$plugins          = $cache['plugins'];
 296                      self::$subsystems       = $cache['subsystems'];
 297                      self::$parents          = $cache['parents'];
 298                      self::$subplugins       = $cache['subplugins'];
 299                      self::$classmap         = $cache['classmap'];
 300                      self::$classmaprenames  = $cache['classmaprenames'];
 301                      self::$filemap          = $cache['filemap'];
 302                      return;
 303                  }
 304                  // Note: we do not verify $CFG->admin here intentionally,
 305                  //       they must visit admin/index.php after any change.
 306              }
 307          }
 308  
 309          if (!isset(self::$plugintypes)) {
 310              // This needs to be atomic and self-fixing as much as possible.
 311  
 312              $content = self::get_cache_content();
 313              if (file_exists($cachefile)) {
 314                  if (sha1_file($cachefile) === sha1($content)) {
 315                      return;
 316                  }
 317                  // Stale cache detected!
 318                  unlink($cachefile);
 319              }
 320  
 321              // Permissions might not be setup properly in installers.
 322              $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
 323              $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
 324  
 325              clearstatcache();
 326              $cachedir = dirname($cachefile);
 327              if (!is_dir($cachedir)) {
 328                  mkdir($cachedir, $dirpermissions, true);
 329              }
 330  
 331              if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
 332                  fwrite($fp, $content);
 333                  fclose($fp);
 334                  @rename($cachefile.'.tmp', $cachefile);
 335                  @chmod($cachefile, $filepermissions);
 336              }
 337              @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
 338              self::invalidate_opcode_php_cache($cachefile);
 339          }
 340      }
 341  
 342      /**
 343       * Are we in developer debug mode?
 344       *
 345       * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
 346       *       the reason is we need to use this before we setup DB connection or caches for CFG.
 347       *
 348       * @return bool
 349       */
 350      protected static function is_developer() {
 351          global $CFG;
 352  
 353          // Note we can not rely on $CFG->debug here because DB is not initialised yet.
 354          if (isset($CFG->config_php_settings['debug'])) {
 355              $debug = (int)$CFG->config_php_settings['debug'];
 356          } else {
 357              return false;
 358          }
 359  
 360          if ($debug & E_ALL and $debug & E_STRICT) {
 361              return true;
 362          }
 363  
 364          return false;
 365      }
 366  
 367      /**
 368       * Create cache file content.
 369       *
 370       * @private this is intended for $CFG->alternative_component_cache only.
 371       *
 372       * @return string
 373       */
 374      public static function get_cache_content() {
 375          if (!isset(self::$plugintypes)) {
 376              self::fill_all_caches();
 377          }
 378  
 379          $cache = array(
 380              'subsystems'        => self::$subsystems,
 381              'plugintypes'       => self::$plugintypes,
 382              'plugins'           => self::$plugins,
 383              'parents'           => self::$parents,
 384              'subplugins'        => self::$subplugins,
 385              'classmap'          => self::$classmap,
 386              'classmaprenames'   => self::$classmaprenames,
 387              'filemap'           => self::$filemap,
 388              'version'           => self::$version,
 389          );
 390  
 391          return '<?php
 392  $cache = '.var_export($cache, true).';
 393  ';
 394      }
 395  
 396      /**
 397       * Fill all caches.
 398       */
 399      protected static function fill_all_caches() {
 400          self::$subsystems = self::fetch_subsystems();
 401  
 402          list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
 403  
 404          self::$plugins = array();
 405          foreach (self::$plugintypes as $type => $fulldir) {
 406              self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
 407          }
 408  
 409          self::fill_classmap_cache();
 410          self::fill_classmap_renames_cache();
 411          self::fill_filemap_cache();
 412          self::fetch_core_version();
 413      }
 414  
 415      /**
 416       * Get the core version.
 417       *
 418       * In order for this to work properly, opcache should be reset beforehand.
 419       *
 420       * @return float core version.
 421       */
 422      protected static function fetch_core_version() {
 423          global $CFG;
 424          if (self::$version === null) {
 425              $version = null; // Prevent IDE complaints.
 426              require($CFG->dirroot . '/version.php');
 427              self::$version = $version;
 428          }
 429          return self::$version;
 430      }
 431  
 432      /**
 433       * Returns list of core subsystems.
 434       * @return array
 435       */
 436      protected static function fetch_subsystems() {
 437          global $CFG;
 438  
 439          // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
 440          $info = [];
 441          foreach (self::fetch_component_source('subsystems') as $subsystem => $path) {
 442              // Replace admin/ directory with the config setting.
 443              if ($CFG->admin !== 'admin') {
 444                  if ($path === 'admin') {
 445                      $path = $CFG->admin;
 446                  }
 447                  if (strpos($path, 'admin/') === 0) {
 448                      $path = $CFG->admin . substr($path, 5);
 449                  }
 450              }
 451  
 452              $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
 453          }
 454  
 455          return $info;
 456      }
 457  
 458      /**
 459       * Returns list of known plugin types.
 460       * @return array
 461       */
 462      protected static function fetch_plugintypes() {
 463          global $CFG;
 464  
 465          $types = [];
 466          foreach (self::fetch_component_source('plugintypes') as $plugintype => $path) {
 467              // Replace admin/ with the config setting.
 468              if ($CFG->admin !== 'admin' && strpos($path, 'admin/') === 0) {
 469                  $path = $CFG->admin . substr($path, 5);
 470              }
 471              $types[$plugintype] = "{$CFG->dirroot}/{$path}";
 472          }
 473  
 474          $parents = array();
 475          $subplugins = array();
 476  
 477          if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
 478              $types['theme'] = $CFG->themedir;
 479          } else {
 480              $types['theme'] = $CFG->dirroot.'/theme';
 481          }
 482  
 483          foreach (self::$supportsubplugins as $type) {
 484              if ($type === 'local') {
 485                  // Local subplugins must be after local plugins.
 486                  continue;
 487              }
 488              $plugins = self::fetch_plugins($type, $types[$type]);
 489              foreach ($plugins as $plugin => $fulldir) {
 490                  $subtypes = self::fetch_subtypes($fulldir);
 491                  if (!$subtypes) {
 492                      continue;
 493                  }
 494                  $subplugins[$type.'_'.$plugin] = array();
 495                  foreach($subtypes as $subtype => $subdir) {
 496                      if (isset($types[$subtype])) {
 497                          error_log("Invalid subtype '$subtype', duplicate detected.");
 498                          continue;
 499                      }
 500                      $types[$subtype] = $subdir;
 501                      $parents[$subtype] = $type.'_'.$plugin;
 502                      $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
 503                  }
 504              }
 505          }
 506          // Local is always last!
 507          $types['local'] = $CFG->dirroot.'/local';
 508  
 509          if (in_array('local', self::$supportsubplugins)) {
 510              $type = 'local';
 511              $plugins = self::fetch_plugins($type, $types[$type]);
 512              foreach ($plugins as $plugin => $fulldir) {
 513                  $subtypes = self::fetch_subtypes($fulldir);
 514                  if (!$subtypes) {
 515                      continue;
 516                  }
 517                  $subplugins[$type.'_'.$plugin] = array();
 518                  foreach($subtypes as $subtype => $subdir) {
 519                      if (isset($types[$subtype])) {
 520                          error_log("Invalid subtype '$subtype', duplicate detected.");
 521                          continue;
 522                      }
 523                      $types[$subtype] = $subdir;
 524                      $parents[$subtype] = $type.'_'.$plugin;
 525                      $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
 526                  }
 527              }
 528          }
 529  
 530          return array($types, $parents, $subplugins);
 531      }
 532  
 533      /**
 534       * Returns the component source content as loaded from /lib/components.json.
 535       *
 536       * @return array
 537       */
 538      protected static function fetch_component_source(string $key) {
 539          if (null === self::$componentsource) {
 540              self::$componentsource = (array) json_decode(file_get_contents(__DIR__ . '/../components.json'));
 541          }
 542  
 543          return (array) self::$componentsource[$key];
 544      }
 545  
 546      /**
 547       * Returns list of subtypes.
 548       * @param string $ownerdir
 549       * @return array
 550       */
 551      protected static function fetch_subtypes($ownerdir) {
 552          global $CFG;
 553  
 554          $types = array();
 555          $subplugins = array();
 556          if (file_exists("$ownerdir/db/subplugins.json")) {
 557              $subplugins = [];
 558              $subpluginsjson = json_decode(file_get_contents("$ownerdir/db/subplugins.json"));
 559              if (json_last_error() === JSON_ERROR_NONE) {
 560                  if (!empty($subpluginsjson->plugintypes)) {
 561                      $subplugins = (array) $subpluginsjson->plugintypes;
 562                  } else {
 563                      error_log("No plugintypes defined in $ownerdir/db/subplugins.json");
 564                  }
 565              } else {
 566                  $jsonerror = json_last_error_msg();
 567                  error_log("$ownerdir/db/subplugins.json is invalid ($jsonerror)");
 568              }
 569          } else if (file_exists("$ownerdir/db/subplugins.php")) {
 570              error_log('Use of subplugins.php has been deprecated. ' .
 571                  "Please update your '$ownerdir' plugin to provide a subplugins.json file instead.");
 572              include("$ownerdir/db/subplugins.php");
 573          }
 574  
 575          foreach ($subplugins as $subtype => $dir) {
 576              if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
 577                  error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
 578                  continue;
 579              }
 580              if (isset(self::$subsystems[$subtype])) {
 581                  error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
 582                  continue;
 583              }
 584              if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
 585                  $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
 586              }
 587              if (!is_dir("$CFG->dirroot/$dir")) {
 588                  error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
 589                  continue;
 590              }
 591              $types[$subtype] = "$CFG->dirroot/$dir";
 592          }
 593  
 594          return $types;
 595      }
 596  
 597      /**
 598       * Returns list of plugins of given type in given directory.
 599       * @param string $plugintype
 600       * @param string $fulldir
 601       * @return array
 602       */
 603      protected static function fetch_plugins($plugintype, $fulldir) {
 604          global $CFG;
 605  
 606          $fulldirs = (array)$fulldir;
 607          if ($plugintype === 'theme') {
 608              if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
 609                  // Include themes in standard location too.
 610                  array_unshift($fulldirs, $CFG->dirroot.'/theme');
 611              }
 612          }
 613  
 614          $result = array();
 615  
 616          foreach ($fulldirs as $fulldir) {
 617              if (!is_dir($fulldir)) {
 618                  continue;
 619              }
 620              $items = new \DirectoryIterator($fulldir);
 621              foreach ($items as $item) {
 622                  if ($item->isDot() or !$item->isDir()) {
 623                      continue;
 624                  }
 625                  $pluginname = $item->getFilename();
 626                  if ($plugintype === 'auth' and $pluginname === 'db') {
 627                      // Special exception for this wrong plugin name.
 628                  } else if (isset(self::$ignoreddirs[$pluginname])) {
 629                      continue;
 630                  }
 631                  if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
 632                      // Always ignore plugins with problematic names here.
 633                      continue;
 634                  }
 635                  $result[$pluginname] = $fulldir.'/'.$pluginname;
 636                  unset($item);
 637              }
 638              unset($items);
 639          }
 640  
 641          ksort($result);
 642          return $result;
 643      }
 644  
 645      /**
 646       * Find all classes that can be autoloaded including frankenstyle namespaces.
 647       */
 648      protected static function fill_classmap_cache() {
 649          global $CFG;
 650  
 651          self::$classmap = array();
 652  
 653          self::load_classes('core', "$CFG->dirroot/lib/classes");
 654  
 655          foreach (self::$subsystems as $subsystem => $fulldir) {
 656              if (!$fulldir) {
 657                  continue;
 658              }
 659              self::load_classes('core_'.$subsystem, "$fulldir/classes");
 660          }
 661  
 662          foreach (self::$plugins as $plugintype => $plugins) {
 663              foreach ($plugins as $pluginname => $fulldir) {
 664                  self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
 665              }
 666          }
 667          ksort(self::$classmap);
 668      }
 669  
 670      /**
 671       * Fills up the cache defining what plugins have certain files.
 672       *
 673       * @see self::get_plugin_list_with_file
 674       * @return void
 675       */
 676      protected static function fill_filemap_cache() {
 677          global $CFG;
 678  
 679          self::$filemap = array();
 680  
 681          foreach (self::$filestomap as $file) {
 682              if (!isset(self::$filemap[$file])) {
 683                  self::$filemap[$file] = array();
 684              }
 685              foreach (self::$plugins as $plugintype => $plugins) {
 686                  if (!isset(self::$filemap[$file][$plugintype])) {
 687                      self::$filemap[$file][$plugintype] = array();
 688                  }
 689                  foreach ($plugins as $pluginname => $fulldir) {
 690                      if (file_exists("$fulldir/$file")) {
 691                          self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
 692                      }
 693                  }
 694              }
 695          }
 696      }
 697  
 698      /**
 699       * Find classes in directory and recurse to subdirs.
 700       * @param string $component
 701       * @param string $fulldir
 702       * @param string $namespace
 703       */
 704      protected static function load_classes($component, $fulldir, $namespace = '') {
 705          if (!is_dir($fulldir)) {
 706              return;
 707          }
 708  
 709          if (!is_readable($fulldir)) {
 710              // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
 711              // because its pretty likely to lead to a missing class error further down the line.
 712              // But our early setup code can't handle errors this early at the moment.
 713              return;
 714          }
 715  
 716          $items = new \DirectoryIterator($fulldir);
 717          foreach ($items as $item) {
 718              if ($item->isDot()) {
 719                  continue;
 720              }
 721              if ($item->isDir()) {
 722                  $dirname = $item->getFilename();
 723                  self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
 724                  continue;
 725              }
 726  
 727              $filename = $item->getFilename();
 728              $classname = preg_replace('/\.php$/', '', $filename);
 729  
 730              if ($filename === $classname) {
 731                  // Not a php file.
 732                  continue;
 733              }
 734              if ($namespace === '') {
 735                  // Legacy long frankenstyle class name.
 736                  self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
 737              }
 738              // New namespaced classes.
 739              self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
 740          }
 741          unset($item);
 742          unset($items);
 743      }
 744  
 745  
 746      /**
 747       * List all core subsystems and their location
 748       *
 749       * This is a list of components that are part of the core and their
 750       * language strings are defined in /lang/en/<<subsystem>>.php. If a given
 751       * plugin is not listed here and it does not have proper plugintype prefix,
 752       * then it is considered as course activity module.
 753       *
 754       * The location is absolute file path to dir. NULL means there is no special
 755       * directory for this subsystem. If the location is set, the subsystem's
 756       * renderer.php is expected to be there.
 757       *
 758       * @return array of (string)name => (string|null)full dir location
 759       */
 760      public static function get_core_subsystems() {
 761          self::init();
 762          return self::$subsystems;
 763      }
 764  
 765      /**
 766       * Get list of available plugin types together with their location.
 767       *
 768       * @return array as (string)plugintype => (string)fulldir
 769       */
 770      public static function get_plugin_types() {
 771          self::init();
 772          return self::$plugintypes;
 773      }
 774  
 775      /**
 776       * Get list of plugins of given type.
 777       *
 778       * @param string $plugintype
 779       * @return array as (string)pluginname => (string)fulldir
 780       */
 781      public static function get_plugin_list($plugintype) {
 782          self::init();
 783  
 784          if (!isset(self::$plugins[$plugintype])) {
 785              return array();
 786          }
 787          return self::$plugins[$plugintype];
 788      }
 789  
 790      /**
 791       * Get a list of all the plugins of a given type that define a certain class
 792       * in a certain file. The plugin component names and class names are returned.
 793       *
 794       * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
 795       * @param string $class the part of the name of the class after the
 796       *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
 797       *      names like report_courselist_thing. If you are looking for classes with
 798       *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
 799       *      Frankenstyle namespaces are also supported.
 800       * @param string $file the name of file within the plugin that defines the class.
 801       * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
 802       *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
 803       */
 804      public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
 805          global $CFG; // Necessary in case it is referenced by included PHP scripts.
 806  
 807          if ($class) {
 808              $suffix = '_' . $class;
 809          } else {
 810              $suffix = '';
 811          }
 812  
 813          $pluginclasses = array();
 814          $plugins = self::get_plugin_list($plugintype);
 815          foreach ($plugins as $plugin => $fulldir) {
 816              // Try class in frankenstyle namespace.
 817              if ($class) {
 818                  $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
 819                  if (class_exists($classname, true)) {
 820                      $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 821                      continue;
 822                  }
 823              }
 824  
 825              // Try autoloading of class with frankenstyle prefix.
 826              $classname = $plugintype . '_' . $plugin . $suffix;
 827              if (class_exists($classname, true)) {
 828                  $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 829                  continue;
 830              }
 831  
 832              // Fall back to old file location and class name.
 833              if ($file and file_exists("$fulldir/$file")) {
 834                  include_once("$fulldir/$file");
 835                  if (class_exists($classname, false)) {
 836                      $pluginclasses[$plugintype . '_' . $plugin] = $classname;
 837                      continue;
 838                  }
 839              }
 840          }
 841  
 842          return $pluginclasses;
 843      }
 844  
 845      /**
 846       * Get a list of all the plugins of a given type that contain a particular file.
 847       *
 848       * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
 849       * @param string $file the name of file that must be present in the plugin.
 850       *                     (e.g. 'view.php', 'db/install.xml').
 851       * @param bool $include if true (default false), the file will be include_once-ed if found.
 852       * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
 853       *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
 854       */
 855      public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
 856          global $CFG; // Necessary in case it is referenced by included PHP scripts.
 857          $pluginfiles = array();
 858  
 859          if (isset(self::$filemap[$file])) {
 860              // If the file was supposed to be mapped, then it should have been set in the array.
 861              if (isset(self::$filemap[$file][$plugintype])) {
 862                  $pluginfiles = self::$filemap[$file][$plugintype];
 863              }
 864          } else {
 865              // Old-style search for non-cached files.
 866              $plugins = self::get_plugin_list($plugintype);
 867              foreach ($plugins as $plugin => $fulldir) {
 868                  $path = $fulldir . '/' . $file;
 869                  if (file_exists($path)) {
 870                      $pluginfiles[$plugin] = $path;
 871                  }
 872              }
 873          }
 874  
 875          if ($include) {
 876              foreach ($pluginfiles as $path) {
 877                  include_once($path);
 878              }
 879          }
 880  
 881          return $pluginfiles;
 882      }
 883  
 884      /**
 885       * Returns all classes in a component matching the provided namespace.
 886       *
 887       * It checks that the class exists.
 888       *
 889       * e.g. get_component_classes_in_namespace('mod_forum', 'event')
 890       *
 891       * @param string|null $component A valid moodle component (frankenstyle) or null if searching all components
 892       * @param string $namespace Namespace from the component name or empty string if all $component classes.
 893       * @return array The full class name as key and the class path as value, empty array if $component is `null`
 894       * and $namespace is empty.
 895       */
 896      public static function get_component_classes_in_namespace($component = null, $namespace = '') {
 897  
 898          $classes = array();
 899  
 900          // Only look for components if a component name is set or a namespace is set.
 901          if (isset($component) || !empty($namespace)) {
 902  
 903              // If a component parameter value is set we only want to look in that component.
 904              // Otherwise we want to check all components.
 905              $component = (isset($component)) ? self::normalize_componentname($component) : '\w+';
 906              if ($namespace) {
 907  
 908                  // We will add them later.
 909                  $namespace = trim($namespace, '\\');
 910  
 911                  // We need add double backslashes as it is how classes are stored into self::$classmap.
 912                  $namespace = implode('\\\\', explode('\\', $namespace));
 913                  $namespace = $namespace . '\\\\';
 914              }
 915              $regex = '|^' . $component . '\\\\' . $namespace . '|';
 916              $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
 917  
 918              // We want to be sure that they exist.
 919              foreach ($it as $classname => $classpath) {
 920                  if (class_exists($classname)) {
 921                      $classes[$classname] = $classpath;
 922                  }
 923              }
 924          }
 925  
 926          return $classes;
 927      }
 928  
 929      /**
 930       * Returns the exact absolute path to plugin directory.
 931       *
 932       * @param string $plugintype type of plugin
 933       * @param string $pluginname name of the plugin
 934       * @return string full path to plugin directory; null if not found
 935       */
 936      public static function get_plugin_directory($plugintype, $pluginname) {
 937          if (empty($pluginname)) {
 938              // Invalid plugin name, sorry.
 939              return null;
 940          }
 941  
 942          self::init();
 943  
 944          if (!isset(self::$plugins[$plugintype][$pluginname])) {
 945              return null;
 946          }
 947          return self::$plugins[$plugintype][$pluginname];
 948      }
 949  
 950      /**
 951       * Returns the exact absolute path to plugin directory.
 952       *
 953       * @param string $subsystem type of core subsystem
 954       * @return string full path to subsystem directory; null if not found
 955       */
 956      public static function get_subsystem_directory($subsystem) {
 957          self::init();
 958  
 959          if (!isset(self::$subsystems[$subsystem])) {
 960              return null;
 961          }
 962          return self::$subsystems[$subsystem];
 963      }
 964  
 965      /**
 966       * This method validates a plug name. It is much faster than calling clean_param.
 967       *
 968       * @param string $plugintype type of plugin
 969       * @param string $pluginname a string that might be a plugin name.
 970       * @return bool if this string is a valid plugin name.
 971       */
 972      public static function is_valid_plugin_name($plugintype, $pluginname) {
 973          if ($plugintype === 'mod') {
 974              // Modules must not have the same name as core subsystems.
 975              if (!isset(self::$subsystems)) {
 976                  // Watch out, this is called from init!
 977                  self::init();
 978              }
 979              if (isset(self::$subsystems[$pluginname])) {
 980                  return false;
 981              }
 982              // Modules MUST NOT have any underscores,
 983              // component normalisation would break very badly otherwise!
 984              return !is_null($pluginname) && (bool) preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
 985          } else {
 986              return !is_null($pluginname) && (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  }