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