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