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