Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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