MDL-71731 phpunit: isolate tests requiring lots of peak mem
[moodle.git] / lib / classes / component.php
blobe3366b6a6e3b36140b16edd2f93e7507a53efe47
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.
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.
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
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.
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,
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',
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',
114 * Class loader for Frankenstyle named classes in standard locations.
115 * Frankenstyle namespaces are supported.
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
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
127 * @param string $classname
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;
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");
146 class_alias($newclassname, $classname);
147 return;
150 $file = self::psr_classloader($classname);
151 // If the file is found, require it.
152 if (!empty($file)) {
153 require($file);
154 return;
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.
162 * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
163 * compatible.
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.
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;
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;
185 return false;
189 * Return the path to the class based on the given namespace prefix and path it corresponds to.
191 * Will return the path even if the file does not exist. Check the file esists before requiring.
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.
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;
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;
223 * Initialise caches, always call before accessing self:: caches.
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;
233 if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
234 self::fill_all_caches();
235 return;
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');
249 return;
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;
264 if (!is_writable(dirname($cachefile))) {
265 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
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";
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;
303 // Note: we do not verify $CFG->admin here intentionally,
304 // they must visit admin/index.php after any change.
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;
316 // Stale cache detected!
317 unlink($cachefile);
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);
330 if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
331 fwrite($fp, $content);
332 fclose($fp);
333 @rename($cachefile.'.tmp', $cachefile);
334 @chmod($cachefile, $filepermissions);
336 @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
337 self::invalidate_opcode_php_cache($cachefile);
342 * Are we in developer debug mode?
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.
347 * @return bool
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;
359 if ($debug & E_ALL and $debug & E_STRICT) {
360 return true;
363 return false;
367 * Create cache file content.
369 * @private this is intended for $CFG->alternative_component_cache only.
371 * @return string
373 public static function get_cache_content() {
374 if (!isset(self::$plugintypes)) {
375 self::fill_all_caches();
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,
390 return '<?php
391 $cache = '.var_export($cache, true).';
396 * Fill all caches.
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);
408 self::fill_classmap_cache();
409 self::fill_classmap_renames_cache();
410 self::fill_filemap_cache();
411 self::fetch_core_version();
415 * Get the core version.
417 * In order for this to work properly, opcache should be reset beforehand.
419 * @return float core version.
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;
428 return self::$version;
432 * Returns list of core subsystems.
433 * @return array
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;
446 if (strpos($path, 'admin/') === 0) {
447 $path = $CFG->admin . substr($path, 0, 5);
451 $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
454 return $info;
458 * Returns list of known plugin types.
459 * @return array
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);
470 $types[$plugintype] = "{$CFG->dirroot}/{$path}";
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';
482 foreach (self::$supportsubplugins as $type) {
483 if ($type === 'local') {
484 // Local subplugins must be after local plugins.
485 continue;
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;
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;
499 $types[$subtype] = $subdir;
500 $parents[$subtype] = $type.'_'.$plugin;
501 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
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;
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;
522 $types[$subtype] = $subdir;
523 $parents[$subtype] = $type.'_'.$plugin;
524 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
529 return array($types, $parents, $subplugins);
533 * Returns the component source content as loaded from /lib/components.json.
535 * @return array
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'));
542 return (array) self::$componentsource[$key];
546 * Returns list of subtypes.
547 * @param string $ownerdir
548 * @return array
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");
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;
568 if (isset(self::$subsystems[$subtype])) {
569 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
570 continue;
572 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
573 $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
575 if (!is_dir("$CFG->dirroot/$dir")) {
576 error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
577 continue;
579 $types[$subtype] = "$CFG->dirroot/$dir";
582 return $types;
586 * Returns list of plugins of given type in given directory.
587 * @param string $plugintype
588 * @param string $fulldir
589 * @return array
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');
602 $result = array();
604 foreach ($fulldirs as $fulldir) {
605 if (!is_dir($fulldir)) {
606 continue;
608 $items = new \DirectoryIterator($fulldir);
609 foreach ($items as $item) {
610 if ($item->isDot() or !$item->isDir()) {
611 continue;
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;
619 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
620 // Always ignore plugins with problematic names here.
621 continue;
623 $result[$pluginname] = $fulldir.'/'.$pluginname;
624 unset($item);
626 unset($items);
629 ksort($result);
630 return $result;
634 * Find all classes that can be autoloaded including frankenstyle namespaces.
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;
647 self::load_classes('core_'.$subsystem, "$fulldir/classes");
650 foreach (self::$plugins as $plugintype => $plugins) {
651 foreach ($plugins as $pluginname => $fulldir) {
652 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
655 ksort(self::$classmap);
659 * Fills up the cache defining what plugins have certain files.
661 * @see self::get_plugin_list_with_file
662 * @return void
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();
673 foreach (self::$plugins as $plugintype => $plugins) {
674 if (!isset(self::$filemap[$file][$plugintype])) {
675 self::$filemap[$file][$plugintype] = array();
677 foreach ($plugins as $pluginname => $fulldir) {
678 if (file_exists("$fulldir/$file")) {
679 self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
687 * Find classes in directory and recurse to subdirs.
688 * @param string $component
689 * @param string $fulldir
690 * @param string $namespace
692 protected static function load_classes($component, $fulldir, $namespace = '') {
693 if (!is_dir($fulldir)) {
694 return;
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;
704 $items = new \DirectoryIterator($fulldir);
705 foreach ($items as $item) {
706 if ($item->isDot()) {
707 continue;
709 if ($item->isDir()) {
710 $dirname = $item->getFilename();
711 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
712 continue;
715 $filename = $item->getFilename();
716 $classname = preg_replace('/\.php$/', '', $filename);
718 if ($filename === $classname) {
719 // Not a php file.
720 continue;
722 if ($namespace === '') {
723 // Legacy long frankenstyle class name.
724 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
726 // New namespaced classes.
727 self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
729 unset($item);
730 unset($items);
735 * List all core subsystems and their location
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.
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.
746 * @return array of (string)name => (string|null)full dir location
748 public static function get_core_subsystems() {
749 self::init();
750 return self::$subsystems;
754 * Get list of available plugin types together with their location.
756 * @return array as (string)plugintype => (string)fulldir
758 public static function get_plugin_types() {
759 self::init();
760 return self::$plugintypes;
764 * Get list of plugins of given type.
766 * @param string $plugintype
767 * @return array as (string)pluginname => (string)fulldir
769 public static function get_plugin_list($plugintype) {
770 self::init();
772 if (!isset(self::$plugins[$plugintype])) {
773 return array();
775 return self::$plugins[$plugintype];
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.
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').
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 = '';
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;
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;
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;
830 return $pluginclasses;
834 * Get a list of all the plugins of a given type that contain a particular file.
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").
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];
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;
863 if ($include) {
864 foreach ($pluginfiles as $path) {
865 include_once($path);
869 return $pluginfiles;
873 * Returns all classes in a component matching the provided namespace.
875 * It checks that the class exists.
877 * e.g. get_component_classes_in_namespace('mod_forum', 'event')
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.
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 . '\\\\';
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;
914 return $classes;
918 * Returns the exact absolute path to plugin directory.
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
924 public static function get_plugin_directory($plugintype, $pluginname) {
925 if (empty($pluginname)) {
926 // Invalid plugin name, sorry.
927 return null;
930 self::init();
932 if (!isset(self::$plugins[$plugintype][$pluginname])) {
933 return null;
935 return self::$plugins[$plugintype][$pluginname];
939 * Returns the exact absolute path to plugin directory.
941 * @param string $subsystem type of core subsystem
942 * @return string full path to subsystem directory; null if not found
944 public static function get_subsystem_directory($subsystem) {
945 self::init();
947 if (!isset(self::$subsystems[$subsystem])) {
948 return null;
950 return self::$subsystems[$subsystem];
954 * This method validates a plug name. It is much faster than calling clean_param.
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.
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();
967 if (isset(self::$subsystems[$pluginname])) {
968 return false;
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);
980 * Normalize the component name.
982 * Note: this does not verify the validity of the plugin or component.
984 * @param string $component
985 * @return string
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;
992 return $plugintype . '_' . $pluginname;
996 * Normalize the component name using the "frankenstyle" rules.
998 * Note: this does not verify the validity of plugin or type names.
1000 * @param string $component
1001 * @return array two-items list of [(string)type, (string|null)name]
1003 public static function normalize_component($component) {
1004 if ($component === 'moodle' or $component === 'core' or $component === '') {
1005 return array('core', null);
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;
1019 } else {
1020 list($type, $plugin) = explode('_', $component, 2);
1021 if ($type === 'moodle') {
1022 $type = 'core';
1024 // Any unknown type must be a subplugin.
1027 return array($type, $plugin);
1031 * Return exact absolute path to a plugin directory.
1033 * @param string $component name such as 'moodle', 'mod_forum'
1034 * @return string full path to component directory; NULL if not found
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;
1045 return self::get_subsystem_directory($plugin);
1048 return self::get_plugin_directory($type, $plugin);
1052 * Returns list of plugin types that allow subplugins.
1053 * @return array as (string)plugintype => (string)fulldir
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];
1062 return $return;
1066 * Returns parent of this subplugin type.
1068 * @param string $type
1069 * @return string parent component or null
1071 public static function get_subtype_parent($type) {
1072 self::init();
1074 if (isset(self::$parents[$type])) {
1075 return self::$parents[$type];
1078 return null;
1082 * Return all subplugins of this component.
1083 * @param string $component.
1084 * @return array $subtype=>array($component, ..), null if no subtypes defined
1086 public static function get_subplugins($component) {
1087 self::init();
1089 if (isset(self::$subplugins[$component])) {
1090 return self::$subplugins[$component];
1093 return null;
1097 * Returns hash of all versions including core and all plugins.
1099 * This is relatively slow and not fully cached, use with care!
1101 * @return string sha1 hash
1103 public static function get_all_versions_hash() {
1104 return sha1(serialize(self::get_all_versions()));
1108 * Returns hash of all versions including core and all plugins.
1110 * This is relatively slow and not fully cached, use with care!
1112 * @return array as (string)plugintype_pluginname => (int)version
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;
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);
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;
1148 return $versions;
1152 * Invalidate opcode cache for given file, this is intended for
1153 * php files that are stored in dataroot.
1155 * Note: we need it here because this class must be self-contained.
1157 * @param string $file
1159 public static function invalidate_opcode_php_cache($file) {
1160 if (function_exists('opcache_invalidate')) {
1161 if (!file_exists($file)) {
1162 return;
1164 opcache_invalidate($file, true);
1169 * Return true if subsystemname is core subsystem.
1171 * @param string $subsystemname name of the subsystem.
1172 * @return bool true if core subsystem.
1174 public static function is_core_subsystem($subsystemname) {
1175 return isset(self::$subsystems[$subsystemname]);
1179 * Records all class renames that have been made to facilitate autoloading.
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);
1192 foreach (self::$plugins as $plugintype => $plugins) {
1193 foreach ($plugins as $pluginname => $fulldir) {
1194 self::load_renamed_classes($fulldir);
1200 * Loads the db/renamedclasses.php file from the given directory.
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.
1206 * @param string $fulldir
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;
1222 * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1224 * E.g.
1226 * 'mod' => [
1227 * 'mod_forum' => FORUM_PLUGIN_PATH,
1228 * ...
1229 * ],
1230 * ...
1231 * 'core' => [
1232 * 'core_comment' => COMMENT_SUBSYSTEM_PATH,
1233 * ...
1237 * @return array an associative array of components and their corresponding paths.
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;
1248 // Get all subsystems.
1249 foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1250 $components['core']['core_' . $subsystemname] = $subsystempath;
1252 return $components;
1256 * Returns a list of frankenstyle component names.
1258 * E.g.
1260 * 'core_course',
1261 * 'core_message',
1262 * 'mod_assign',
1263 * ...
1265 * @return array the list of frankenstyle component names.
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;
1275 // Get all subsystems.
1276 foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1277 $componentnames[] = 'core_' . $subsystemname;
1279 return $componentnames;