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