MDL-51360 core_grades: Improve documentation of grade_get_grades().
[moodle.git] / lib / classes / component.php
blob258d7eb25a299329a7e3028ef291034ed922ae6a
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 // Constants used in version.php files, these must exist when core_component executes.
27 // We make use of error_log as debugging is not always available.
28 // phpcs:disable moodle.PHP.ForbiddenFunctions.FoundWithAlternative
29 // We make use of empty if statements to make complex decisions clearer.
30 // phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedIf
32 /** Software maturity level - internals can be tested using white box techniques. */
33 define('MATURITY_ALPHA', 50);
34 /** Software maturity level - feature complete, ready for preview and testing. */
35 define('MATURITY_BETA', 100);
36 /** Software maturity level - tested, will be released unless there are fatal bugs. */
37 define('MATURITY_RC', 150);
38 /** Software maturity level - ready for production deployment. */
39 define('MATURITY_STABLE', 200);
40 /** Any version - special value that can be used in $plugin->dependencies in version.php files. */
41 define('ANY_VERSION', 'any');
43 /**
44 * Collection of components related methods.
46 class core_component {
47 /** @var array list of ignored directories in plugin type roots - watch out for auth/db exception */
48 protected static $ignoreddirs = [
49 'CVS' => true,
50 '_vti_cnf' => true,
51 'amd' => true,
52 'classes' => true,
53 'db' => true,
54 'fonts' => true,
55 'lang' => true,
56 'pix' => true,
57 'simpletest' => true,
58 'templates' => true,
59 'tests' => true,
60 'yui' => true,
62 /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
63 protected static $supportsubplugins = ['mod', 'editor', 'tool', 'local'];
65 /** @var object JSON source of the component data */
66 protected static $componentsource = null;
67 /** @var array cache of plugin types */
68 protected static $plugintypes = null;
69 /** @var array cache of plugin locations */
70 protected static $plugins = null;
71 /** @var array cache of core subsystems */
72 protected static $subsystems = null;
73 /** @var array subplugin type parents */
74 protected static $parents = null;
75 /** @var array subplugins */
76 protected static $subplugins = null;
77 /** @var array cache of core APIs */
78 protected static $apis = null;
79 /** @var array list of all known classes that can be autoloaded */
80 protected static $classmap = null;
81 /** @var array list of all classes that have been renamed to be autoloaded */
82 protected static $classmaprenames = null;
83 /** @var array list of some known files that can be included. */
84 protected static $filemap = null;
85 /** @var int|float core version. */
86 protected static $version = null;
87 /** @var array list of the files to map. */
88 protected static $filestomap = ['lib.php', 'settings.php'];
89 /** @var array associative array of PSR-0 namespaces and corresponding paths. */
90 protected static $psr0namespaces = [
91 'Mustache' => 'lib/mustache/src/Mustache',
92 'CFPropertyList' => 'lib/plist/classes/CFPropertyList',
94 /** @var array<string|array<string>> associative array of PRS-4 namespaces and corresponding paths. */
95 protected static $psr4namespaces = [
96 'MaxMind' => 'lib/maxmind/MaxMind',
97 'GeoIp2' => 'lib/maxmind/GeoIp2',
98 'Sabberworm\\CSS' => 'lib/php-css-parser',
99 'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
100 'ScssPhp\\ScssPhp' => 'lib/scssphp',
101 'OpenSpout' => 'lib/openspout/src',
102 'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
103 'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
104 'IMSGlobal\LTI' => 'lib/ltiprovider/src',
105 'Packback\\Lti1p3' => 'lib/lti1p3/src',
106 'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
107 'PHPMailer\\PHPMailer' => 'lib/phpmailer/src',
108 'RedeyeVentures\\GeoPattern' => 'lib/geopattern-php/GeoPattern',
109 'Firebase\\JWT' => 'lib/php-jwt/src',
110 'ZipStream' => 'lib/zipstream/src/',
111 'MyCLabs\\Enum' => 'lib/php-enum/src',
112 'PhpXmlRpc' => 'lib/phpxmlrpc',
113 'Psr\\Http\\Client' => 'lib/psr/http-client/src',
114 'Psr\\Http\\Message' => [
115 'lib/psr/http-message/src',
116 'lib/psr/http-factory/src',
118 'Psr\\EventDispatcher' => 'lib/psr/event-dispatcher/src',
119 'Psr\\Clock' => 'lib/psr/clock/src',
120 'Psr\\Container' => 'lib/psr/container/src',
121 'GuzzleHttp\\Psr7' => 'lib/guzzlehttp/psr7/src',
122 'GuzzleHttp\\Promise' => 'lib/guzzlehttp/promises/src',
123 'GuzzleHttp' => 'lib/guzzlehttp/guzzle/src',
124 'Kevinrob\\GuzzleCache' => 'lib/guzzlehttp/kevinrob/guzzlecache/src',
125 'Aws' => 'lib/aws-sdk/src',
126 'JmesPath' => 'lib/jmespath/src',
127 'Laravel\\SerializableClosure' => 'lib/laravel/serializable-closure/src',
128 'DI' => 'lib/php-di/php-di/src',
129 'Invoker' => 'lib/php-di/invoker/src',
133 * Class loader for Frankenstyle named classes in standard locations.
134 * Frankenstyle namespaces are supported.
136 * The expected location for core classes is:
137 * 1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
138 * 2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
139 * 3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
141 * The expected location for plugin classes is:
142 * 1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
143 * 2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
144 * 3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
146 * @param string $classname
148 public static function classloader($classname) {
149 self::init();
151 if (isset(self::$classmap[$classname])) {
152 // Global $CFG is expected in included scripts.
153 global $CFG;
154 // Function include would be faster, but for BC it is better to include only once.
155 include_once(self::$classmap[$classname]);
156 return;
158 if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
159 $newclassname = self::$classmaprenames[$classname];
160 $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
161 debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
162 if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
163 throw new \coding_exception("Cannot alias $classname to $newclassname");
165 class_alias($newclassname, $classname);
166 return;
169 $file = self::psr_classloader($classname);
170 // If the file is found, require it.
171 if (!empty($file)) {
172 require($file);
173 return;
178 * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
179 * demand. Only returns paths to files that exist.
181 * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
182 * compatible.
184 * @param string $class the name of the class.
185 * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
187 protected static function psr_classloader($class) {
188 // Iterate through each PSR-4 namespace prefix.
189 foreach (self::$psr4namespaces as $prefix => $paths) {
190 if (!is_array($paths)) {
191 $paths = [$paths];
193 foreach ($paths as $path) {
194 $file = self::get_class_file($class, $prefix, $path, ['\\']);
195 if (!empty($file) && file_exists($file)) {
196 return $file;
201 // Iterate through each PSR-0 namespace prefix.
202 foreach (self::$psr0namespaces as $prefix => $path) {
203 $file = self::get_class_file($class, $prefix, $path, ['\\', '_']);
204 if (!empty($file) && file_exists($file)) {
205 return $file;
209 return false;
213 * Return the path to the class based on the given namespace prefix and path it corresponds to.
215 * Will return the path even if the file does not exist. Check the file esists before requiring.
217 * @param string $class the name of the class.
218 * @param string $prefix The namespace prefix used to identify the base directory of the source files.
219 * @param string $path The relative path to the base directory of the source files.
220 * @param string[] $separators The characters that should be used for separating.
221 * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
223 protected static function get_class_file($class, $prefix, $path, $separators) {
224 global $CFG;
226 // Does the class use the namespace prefix?
227 $len = strlen($prefix);
228 if (strncmp($prefix, $class, $len) !== 0) {
229 // No, move to the next prefix.
230 return false;
232 $path = $CFG->dirroot . '/' . $path;
234 // Get the relative class name.
235 $relativeclass = substr($class, $len);
237 // Replace the namespace prefix with the base directory, replace namespace
238 // separators with directory separators in the relative class name, append
239 // with .php.
240 $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
242 return $file;
247 * Initialise caches, always call before accessing self:: caches.
249 protected static function init() {
250 global $CFG;
252 // Init only once per request/CLI execution, we ignore changes done afterwards.
253 if (isset(self::$plugintypes)) {
254 return;
257 if (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE) {
258 self::fill_all_caches();
259 return;
262 if (!empty($CFG->alternative_component_cache)) {
263 // Hack for heavily clustered sites that want to manage component cache invalidation manually.
264 $cachefile = $CFG->alternative_component_cache;
266 if (file_exists($cachefile)) {
267 if (CACHE_DISABLE_ALL) {
268 // Verify the cache state only on upgrade pages.
269 $content = self::get_cache_content();
270 if (sha1_file($cachefile) !== sha1($content)) {
271 die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
273 return;
275 $cache = [];
276 include($cachefile);
277 self::$plugintypes = $cache['plugintypes'];
278 self::$plugins = $cache['plugins'];
279 self::$subsystems = $cache['subsystems'];
280 self::$parents = $cache['parents'];
281 self::$subplugins = $cache['subplugins'];
282 self::$apis = $cache['apis'];
283 self::$classmap = $cache['classmap'];
284 self::$classmaprenames = $cache['classmaprenames'];
285 self::$filemap = $cache['filemap'];
286 return;
289 if (!is_writable(dirname($cachefile))) {
290 die(
291 'Can not create alternative component cache file defined in ' .
292 '$CFG->alternative_component_cache, can not continue'
296 // Lets try to create the file, it might be in some writable directory or a local cache dir.
297 } else {
298 // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
299 // use $CFG->alternative_component_cache if you do not like it.
300 $cachefile = "$CFG->cachedir/core_component.php";
303 if (!CACHE_DISABLE_ALL && !self::is_developer()) {
304 // 1/ Use the cache only outside of install and upgrade.
305 // 2/ Let developers add/remove classes in developer mode.
306 if (is_readable($cachefile)) {
307 $cache = false;
308 include($cachefile);
309 if (!is_array($cache)) {
310 // Something is very wrong.
311 } else if (!isset($cache['version'])) {
312 // Something is very wrong.
313 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
314 // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
315 error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
316 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
317 // phpcs:ignore moodle.Commenting.InlineComment.NotCapital
318 // $CFG->dirroot was changed.
319 } else {
320 // The cache looks ok, let's use it.
321 self::$plugintypes = $cache['plugintypes'];
322 self::$plugins = $cache['plugins'];
323 self::$subsystems = $cache['subsystems'];
324 self::$parents = $cache['parents'];
325 self::$subplugins = $cache['subplugins'];
326 self::$apis = $cache['apis'];
327 self::$classmap = $cache['classmap'];
328 self::$classmaprenames = $cache['classmaprenames'];
329 self::$filemap = $cache['filemap'];
330 return;
332 // Note: we do not verify $CFG->admin here intentionally,
333 // they must visit admin/index.php after any change.
337 if (!isset(self::$plugintypes)) {
338 // This needs to be atomic and self-fixing as much as possible.
340 $content = self::get_cache_content();
341 if (file_exists($cachefile)) {
342 if (sha1_file($cachefile) === sha1($content)) {
343 return;
345 // Stale cache detected!
346 unlink($cachefile);
349 // Permissions might not be setup properly in installers.
350 $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
351 $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
353 clearstatcache();
354 $cachedir = dirname($cachefile);
355 if (!is_dir($cachedir)) {
356 mkdir($cachedir, $dirpermissions, true);
359 if ($fp = @fopen($cachefile . '.tmp', 'xb')) {
360 fwrite($fp, $content);
361 fclose($fp);
362 @rename($cachefile . '.tmp', $cachefile);
363 @chmod($cachefile, $filepermissions);
365 @unlink($cachefile . '.tmp'); // Just in case anything fails (race condition).
366 self::invalidate_opcode_php_cache($cachefile);
371 * Are we in developer debug mode?
373 * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
374 * the reason is we need to use this before we setup DB connection or caches for CFG.
376 * @return bool
378 protected static function is_developer() {
379 global $CFG;
381 // Note we can not rely on $CFG->debug here because DB is not initialised yet.
382 if (isset($CFG->config_php_settings['debug'])) {
383 $debug = (int)$CFG->config_php_settings['debug'];
384 } else {
385 return false;
388 if ($debug & E_ALL && $debug & E_STRICT) {
389 return true;
392 return false;
396 * Create cache file content.
398 * @private this is intended for $CFG->alternative_component_cache only.
400 * @return string
402 public static function get_cache_content() {
403 if (!isset(self::$plugintypes)) {
404 self::fill_all_caches();
407 $cache = [
408 'subsystems' => self::$subsystems,
409 'plugintypes' => self::$plugintypes,
410 'plugins' => self::$plugins,
411 'parents' => self::$parents,
412 'subplugins' => self::$subplugins,
413 'apis' => self::$apis,
414 'classmap' => self::$classmap,
415 'classmaprenames' => self::$classmaprenames,
416 'filemap' => self::$filemap,
417 'version' => self::$version,
420 return '<?php
421 $cache = ' . var_export($cache, true) . ';
426 * Fill all caches.
428 protected static function fill_all_caches() {
429 self::$subsystems = self::fetch_subsystems();
431 [self::$plugintypes, self::$parents, self::$subplugins] = self::fetch_plugintypes();
433 self::$plugins = [];
434 foreach (self::$plugintypes as $type => $fulldir) {
435 self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
438 self::$apis = self::fetch_apis();
440 self::fill_classmap_cache();
441 self::fill_classmap_renames_cache();
442 self::fill_filemap_cache();
443 self::fetch_core_version();
447 * Get the core version.
449 * In order for this to work properly, opcache should be reset beforehand.
451 * @return float core version.
453 protected static function fetch_core_version() {
454 global $CFG;
455 if (self::$version === null) {
456 $version = null; // Prevent IDE complaints.
457 require($CFG->dirroot . '/version.php');
458 self::$version = $version;
460 return self::$version;
464 * Returns list of core subsystems.
465 * @return array
467 protected static function fetch_subsystems() {
468 global $CFG;
470 // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
471 $info = [];
472 foreach (self::fetch_component_source('subsystems') as $subsystem => $path) {
473 // Replace admin/ directory with the config setting.
474 if ($CFG->admin !== 'admin') {
475 if ($path === 'admin') {
476 $path = $CFG->admin;
478 if (strpos($path, 'admin/') === 0) {
479 $path = $CFG->admin . substr($path, 5);
483 $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
486 return $info;
490 * Returns list of core APIs.
491 * @return stdClass[]
493 protected static function fetch_apis() {
494 return (array) json_decode(file_get_contents(__DIR__ . '/../apis.json'));
498 * Returns list of known plugin types.
499 * @return array
501 protected static function fetch_plugintypes() {
502 global $CFG;
504 $types = [];
505 foreach (self::fetch_component_source('plugintypes') as $plugintype => $path) {
506 // Replace admin/ with the config setting.
507 if ($CFG->admin !== 'admin' && strpos($path, 'admin/') === 0) {
508 $path = $CFG->admin . substr($path, 5);
510 $types[$plugintype] = "{$CFG->dirroot}/{$path}";
513 $parents = [];
514 $subplugins = [];
516 if (!empty($CFG->themedir) && is_dir($CFG->themedir)) {
517 $types['theme'] = $CFG->themedir;
518 } else {
519 $types['theme'] = $CFG->dirroot . '/theme';
522 foreach (self::$supportsubplugins as $type) {
523 if ($type === 'local') {
524 // Local subplugins must be after local plugins.
525 continue;
527 $plugins = self::fetch_plugins($type, $types[$type]);
528 foreach ($plugins as $plugin => $fulldir) {
529 $subtypes = self::fetch_subtypes($fulldir);
530 if (!$subtypes) {
531 continue;
533 $subplugins[$type . '_' . $plugin] = [];
534 foreach ($subtypes as $subtype => $subdir) {
535 if (isset($types[$subtype])) {
536 error_log("Invalid subtype '$subtype', duplicate detected.");
537 continue;
539 $types[$subtype] = $subdir;
540 $parents[$subtype] = $type . '_' . $plugin;
541 $subplugins[$type . '_' . $plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
545 // Local is always last!
546 $types['local'] = $CFG->dirroot . '/local';
548 if (in_array('local', self::$supportsubplugins)) {
549 $type = 'local';
550 $plugins = self::fetch_plugins($type, $types[$type]);
551 foreach ($plugins as $plugin => $fulldir) {
552 $subtypes = self::fetch_subtypes($fulldir);
553 if (!$subtypes) {
554 continue;
556 $subplugins[$type . '_' . $plugin] = [];
557 foreach ($subtypes as $subtype => $subdir) {
558 if (isset($types[$subtype])) {
559 error_log("Invalid subtype '$subtype', duplicate detected.");
560 continue;
562 $types[$subtype] = $subdir;
563 $parents[$subtype] = $type . '_' . $plugin;
564 $subplugins[$type . '_' . $plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
569 return [$types, $parents, $subplugins];
573 * Returns the component source content as loaded from /lib/components.json.
575 * @return array
577 protected static function fetch_component_source(string $key) {
578 if (null === self::$componentsource) {
579 self::$componentsource = (array) json_decode(file_get_contents(__DIR__ . '/../components.json'));
582 return (array) self::$componentsource[$key];
586 * Returns list of subtypes.
587 * @param string $ownerdir
588 * @return array
590 protected static function fetch_subtypes($ownerdir) {
591 global $CFG;
593 $types = [];
594 $subplugins = [];
595 if (file_exists("$ownerdir/db/subplugins.json")) {
596 $subplugins = [];
597 $subpluginsjson = json_decode(file_get_contents("$ownerdir/db/subplugins.json"));
598 if (json_last_error() === JSON_ERROR_NONE) {
599 if (!empty($subpluginsjson->plugintypes)) {
600 $subplugins = (array) $subpluginsjson->plugintypes;
601 } else {
602 error_log("No plugintypes defined in $ownerdir/db/subplugins.json");
604 } else {
605 $jsonerror = json_last_error_msg();
606 error_log("$ownerdir/db/subplugins.json is invalid ($jsonerror)");
608 } else if (file_exists("$ownerdir/db/subplugins.php")) {
609 error_log('Use of subplugins.php has been deprecated. ' .
610 "Please update your '$ownerdir' plugin to provide a subplugins.json file instead.");
611 include("$ownerdir/db/subplugins.php");
614 foreach ($subplugins as $subtype => $dir) {
615 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
616 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
617 continue;
619 if (isset(self::$subsystems[$subtype])) {
620 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
621 continue;
623 if ($CFG->admin !== 'admin' && strpos($dir, 'admin/') === 0) {
624 $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
626 if (!is_dir("$CFG->dirroot/$dir")) {
627 error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
628 continue;
630 $types[$subtype] = "$CFG->dirroot/$dir";
633 return $types;
637 * Returns list of plugins of given type in given directory.
638 * @param string $plugintype
639 * @param string $fulldir
640 * @return array
642 protected static function fetch_plugins($plugintype, $fulldir) {
643 global $CFG;
645 $fulldirs = (array)$fulldir;
646 if ($plugintype === 'theme') {
647 if (realpath($fulldir) !== realpath($CFG->dirroot . '/theme')) {
648 // Include themes in standard location too.
649 array_unshift($fulldirs, $CFG->dirroot . '/theme');
653 $result = [];
655 foreach ($fulldirs as $fulldir) {
656 if (!is_dir($fulldir)) {
657 continue;
659 $items = new \DirectoryIterator($fulldir);
660 foreach ($items as $item) {
661 if ($item->isDot() || !$item->isDir()) {
662 continue;
664 $pluginname = $item->getFilename();
665 if ($plugintype === 'auth' && $pluginname === 'db') {
666 // Special exception for this wrong plugin name.
667 } else if (isset(self::$ignoreddirs[$pluginname])) {
668 continue;
670 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
671 // Always ignore plugins with problematic names here.
672 continue;
674 $result[$pluginname] = $fulldir . '/' . $pluginname;
675 unset($item);
677 unset($items);
680 ksort($result);
681 return $result;
685 * Find all classes that can be autoloaded including frankenstyle namespaces.
687 protected static function fill_classmap_cache() {
688 global $CFG;
690 self::$classmap = [];
692 self::load_classes('core', "$CFG->dirroot/lib/classes");
694 foreach (self::$subsystems as $subsystem => $fulldir) {
695 if (!$fulldir) {
696 continue;
698 self::load_classes('core_' . $subsystem, "$fulldir/classes");
701 foreach (self::$plugins as $plugintype => $plugins) {
702 foreach ($plugins as $pluginname => $fulldir) {
703 self::load_classes($plugintype . '_' . $pluginname, "$fulldir/classes");
706 ksort(self::$classmap);
710 * Fills up the cache defining what plugins have certain files.
712 * @see self::get_plugin_list_with_file
713 * @return void
715 protected static function fill_filemap_cache() {
716 global $CFG;
718 self::$filemap = [];
720 foreach (self::$filestomap as $file) {
721 if (!isset(self::$filemap[$file])) {
722 self::$filemap[$file] = [];
724 foreach (self::$plugins as $plugintype => $plugins) {
725 if (!isset(self::$filemap[$file][$plugintype])) {
726 self::$filemap[$file][$plugintype] = [];
728 foreach ($plugins as $pluginname => $fulldir) {
729 if (file_exists("$fulldir/$file")) {
730 self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
738 * Find classes in directory and recurse to subdirs.
739 * @param string $component
740 * @param string $fulldir
741 * @param string $namespace
743 protected static function load_classes($component, $fulldir, $namespace = '') {
744 if (!is_dir($fulldir)) {
745 return;
748 if (!is_readable($fulldir)) {
749 // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
750 // because its pretty likely to lead to a missing class error further down the line.
751 // But our early setup code can't handle errors this early at the moment.
752 return;
755 $items = new \DirectoryIterator($fulldir);
756 foreach ($items as $item) {
757 if ($item->isDot()) {
758 continue;
760 if ($item->isDir()) {
761 $dirname = $item->getFilename();
762 self::load_classes($component, "$fulldir/$dirname", $namespace . '\\' . $dirname);
763 continue;
766 $filename = $item->getFilename();
767 $classname = preg_replace('/\.php$/', '', $filename);
769 if ($filename === $classname) {
770 // Not a php file.
771 continue;
773 if ($namespace === '') {
774 // Legacy long frankenstyle class name.
775 self::$classmap[$component . '_' . $classname] = "$fulldir/$filename";
777 // New namespaced classes.
778 self::$classmap[$component . $namespace . '\\' . $classname] = "$fulldir/$filename";
780 unset($item);
781 unset($items);
786 * List all core subsystems and their location
788 * This is a list of components that are part of the core and their
789 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
790 * plugin is not listed here and it does not have proper plugintype prefix,
791 * then it is considered as course activity module.
793 * The location is absolute file path to dir. NULL means there is no special
794 * directory for this subsystem. If the location is set, the subsystem's
795 * renderer.php is expected to be there.
797 * @return array of (string)name => (string|null)full dir location
799 public static function get_core_subsystems() {
800 self::init();
801 return self::$subsystems;
805 * List all core APIs and their attributes.
807 * This is a list of all the existing / allowed APIs in moodle, each one with the
808 * following attributes:
809 * - component: the component, usually a subsystem or core, the API belongs to.
810 * - allowedlevel2: if the API is allowed as level2 namespace or no.
811 * - allowedspread: if the API can spread out from its component or no.
813 * @return stdClass[] array of APIs (as keys) with their attributes as object instances.
815 public static function get_core_apis() {
816 self::init();
817 return self::$apis;
821 * Get list of available plugin types together with their location.
823 * @return array as (string)plugintype => (string)fulldir
825 public static function get_plugin_types() {
826 self::init();
827 return self::$plugintypes;
831 * Get list of plugins of given type.
833 * @param string $plugintype
834 * @return array as (string)pluginname => (string)fulldir
836 public static function get_plugin_list($plugintype) {
837 self::init();
839 if (!isset(self::$plugins[$plugintype])) {
840 return [];
842 return self::$plugins[$plugintype];
846 * Get a list of all the plugins of a given type that define a certain class
847 * in a certain file. The plugin component names and class names are returned.
849 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
850 * @param string $class the part of the name of the class after the
851 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
852 * names like report_courselist_thing. If you are looking for classes with
853 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
854 * Frankenstyle namespaces are also supported.
855 * @param string $file the name of file within the plugin that defines the class.
856 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
857 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
859 public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
860 global $CFG; // Necessary in case it is referenced by included PHP scripts.
862 if ($class) {
863 $suffix = '_' . $class;
864 } else {
865 $suffix = '';
868 $pluginclasses = [];
869 $plugins = self::get_plugin_list($plugintype);
870 foreach ($plugins as $plugin => $fulldir) {
871 // Try class in frankenstyle namespace.
872 if ($class) {
873 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
874 if (class_exists($classname, true)) {
875 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
876 continue;
880 // Try autoloading of class with frankenstyle prefix.
881 $classname = $plugintype . '_' . $plugin . $suffix;
882 if (class_exists($classname, true)) {
883 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
884 continue;
887 // Fall back to old file location and class name.
888 if ($file && file_exists("$fulldir/$file")) {
889 include_once("$fulldir/$file");
890 if (class_exists($classname, false)) {
891 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
892 continue;
897 return $pluginclasses;
901 * Get a list of all the plugins of a given type that contain a particular file.
903 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
904 * @param string $file the name of file that must be present in the plugin.
905 * (e.g. 'view.php', 'db/install.xml').
906 * @param bool $include if true (default false), the file will be include_once-ed if found.
907 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
908 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
910 public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
911 global $CFG; // Necessary in case it is referenced by included PHP scripts.
912 $pluginfiles = [];
914 if (isset(self::$filemap[$file])) {
915 // If the file was supposed to be mapped, then it should have been set in the array.
916 if (isset(self::$filemap[$file][$plugintype])) {
917 $pluginfiles = self::$filemap[$file][$plugintype];
919 } else {
920 // Old-style search for non-cached files.
921 $plugins = self::get_plugin_list($plugintype);
922 foreach ($plugins as $plugin => $fulldir) {
923 $path = $fulldir . '/' . $file;
924 if (file_exists($path)) {
925 $pluginfiles[$plugin] = $path;
930 if ($include) {
931 foreach ($pluginfiles as $path) {
932 include_once($path);
936 return $pluginfiles;
940 * Returns all classes in a component matching the provided namespace.
942 * It checks that the class exists.
944 * e.g. get_component_classes_in_namespace('mod_forum', 'event')
946 * @param string|null $component A valid moodle component (frankenstyle) or null if searching all components
947 * @param string $namespace Namespace from the component name or empty string if all $component classes.
948 * @return array The full class name as key and the class path as value, empty array if $component is `null`
949 * and $namespace is empty.
951 public static function get_component_classes_in_namespace($component = null, $namespace = '') {
953 $classes = [];
955 // Only look for components if a component name is set or a namespace is set.
956 if (isset($component) || !empty($namespace)) {
957 // If a component parameter value is set we only want to look in that component.
958 // Otherwise we want to check all components.
959 $component = (isset($component)) ? self::normalize_componentname($component) : '\w+';
960 if ($namespace) {
961 // We will add them later.
962 $namespace = trim($namespace, '\\');
964 // We need add double backslashes as it is how classes are stored into self::$classmap.
965 $namespace = implode('\\\\', explode('\\', $namespace));
966 $namespace = $namespace . '\\\\';
968 $regex = '|^' . $component . '\\\\' . $namespace . '|';
969 $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
971 // We want to be sure that they exist.
972 foreach ($it as $classname => $classpath) {
973 if (class_exists($classname)) {
974 $classes[$classname] = $classpath;
979 return $classes;
983 * Returns the exact absolute path to plugin directory.
985 * @param string $plugintype type of plugin
986 * @param string $pluginname name of the plugin
987 * @return string full path to plugin directory; null if not found
989 public static function get_plugin_directory($plugintype, $pluginname) {
990 if (empty($pluginname)) {
991 // Invalid plugin name, sorry.
992 return null;
995 self::init();
997 if (!isset(self::$plugins[$plugintype][$pluginname])) {
998 return null;
1000 return self::$plugins[$plugintype][$pluginname];
1004 * Returns the exact absolute path to plugin directory.
1006 * @param string $subsystem type of core subsystem
1007 * @return string full path to subsystem directory; null if not found
1009 public static function get_subsystem_directory($subsystem) {
1010 self::init();
1012 if (!isset(self::$subsystems[$subsystem])) {
1013 return null;
1015 return self::$subsystems[$subsystem];
1019 * This method validates a plug name. It is much faster than calling clean_param.
1021 * @param string $plugintype type of plugin
1022 * @param string $pluginname a string that might be a plugin name.
1023 * @return bool if this string is a valid plugin name.
1025 public static function is_valid_plugin_name($plugintype, $pluginname) {
1026 if ($plugintype === 'mod') {
1027 // Modules must not have the same name as core subsystems.
1028 if (!isset(self::$subsystems)) {
1029 // Watch out, this is called from init!
1030 self::init();
1032 if (isset(self::$subsystems[$pluginname])) {
1033 return false;
1035 // Modules MUST NOT have any underscores,
1036 // component normalisation would break very badly otherwise!
1037 return !is_null($pluginname) && (bool) preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1038 } else {
1039 return !is_null($pluginname) && (bool) preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1044 * Normalize the component name.
1046 * Note: this does not verify the validity of the plugin or component.
1048 * @param string $component
1049 * @return string
1051 public static function normalize_componentname($componentname) {
1052 [$plugintype, $pluginname] = self::normalize_component($componentname);
1053 if ($plugintype === 'core' && is_null($pluginname)) {
1054 return $plugintype;
1056 return $plugintype . '_' . $pluginname;
1060 * Normalize the component name using the "frankenstyle" rules.
1062 * Note: this does not verify the validity of plugin or type names.
1064 * @param string $component
1065 * @return array two-items list of [(string)type, (string|null)name]
1067 public static function normalize_component($component) {
1068 if ($component === 'moodle' || $component === 'core' || $component === '') {
1069 return ['core', null];
1072 if (strpos($component, '_') === false) {
1073 self::init();
1074 if (array_key_exists($component, self::$subsystems)) {
1075 $type = 'core';
1076 $plugin = $component;
1077 } else {
1078 // Everything else without underscore is a module.
1079 $type = 'mod';
1080 $plugin = $component;
1082 } else {
1083 [$type, $plugin] = explode('_', $component, 2);
1084 if ($type === 'moodle') {
1085 $type = 'core';
1087 // Any unknown type must be a subplugin.
1090 return [$type, $plugin];
1094 * Fetch the component name from a Moodle PSR-like namespace.
1096 * Note: Classnames in the flat underscore_class_name_format are not supported.
1098 * @param string $classname
1099 * @return null|string The component name, or null if a matching component was not found
1101 public static function get_component_from_classname(string $classname): ?string {
1102 $components = static::get_component_names(true);
1104 $classname = ltrim($classname, '\\');
1106 // Prefer PSR-4 classnames.
1107 $parts = explode('\\', $classname);
1108 if ($parts) {
1109 $component = array_shift($parts);
1110 if (array_search($component, $components) !== false) {
1111 return $component;
1115 // Note: Frankenstyle classnames are not supported as they lead to false positives, for example:
1116 // \core_typo\example => \core instead of \core_typo because it does not exist
1117 // Please *do not* add support for Frankenstyle classnames. They will break other things.
1119 return null;
1123 * Return exact absolute path to a plugin directory.
1125 * @param string $component name such as 'moodle', 'mod_forum'
1126 * @return string full path to component directory; NULL if not found
1128 public static function get_component_directory($component) {
1129 global $CFG;
1131 [$type, $plugin] = self::normalize_component($component);
1133 if ($type === 'core') {
1134 if ($plugin === null) {
1135 return $path = $CFG->libdir;
1137 return self::get_subsystem_directory($plugin);
1140 return self::get_plugin_directory($type, $plugin);
1144 * Returns list of plugin types that allow subplugins.
1145 * @return array as (string)plugintype => (string)fulldir
1147 public static function get_plugin_types_with_subplugins() {
1148 self::init();
1150 $return = [];
1151 foreach (self::$supportsubplugins as $type) {
1152 $return[$type] = self::$plugintypes[$type];
1154 return $return;
1158 * Returns parent of this subplugin type.
1160 * @param string $type
1161 * @return string parent component or null
1163 public static function get_subtype_parent($type) {
1164 self::init();
1166 if (isset(self::$parents[$type])) {
1167 return self::$parents[$type];
1170 return null;
1174 * Return all subplugins of this component.
1175 * @param string $component.
1176 * @return array $subtype=>array($component, ..), null if no subtypes defined
1178 public static function get_subplugins($component) {
1179 self::init();
1181 if (isset(self::$subplugins[$component])) {
1182 return self::$subplugins[$component];
1185 return null;
1189 * Returns hash of all versions including core and all plugins.
1191 * This is relatively slow and not fully cached, use with care!
1193 * @return string sha1 hash
1195 public static function get_all_versions_hash() {
1196 return sha1(serialize(self::get_all_versions()));
1200 * Returns hash of all versions including core and all plugins.
1202 * This is relatively slow and not fully cached, use with care!
1204 * @return array as (string)plugintype_pluginname => (int)version
1206 public static function get_all_versions(): array {
1207 global $CFG;
1209 self::init();
1211 $versions = [];
1213 // Main version first.
1214 $versions['core'] = self::fetch_core_version();
1216 // The problem here is tha the component cache might be stable,
1217 // we want this to work also on frontpage without resetting the component cache.
1218 $usecache = false;
1219 if (CACHE_DISABLE_ALL || (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE)) {
1220 $usecache = true;
1223 // Now all plugins.
1224 $plugintypes = self::get_plugin_types();
1225 foreach ($plugintypes as $type => $typedir) {
1226 if ($usecache) {
1227 $plugs = self::get_plugin_list($type);
1228 } else {
1229 $plugs = self::fetch_plugins($type, $typedir);
1231 foreach ($plugs as $plug => $fullplug) {
1232 $plugin = new stdClass();
1233 $plugin->version = null;
1234 $module = $plugin;
1235 include($fullplug . '/version.php');
1236 $versions[$type . '_' . $plug] = $plugin->version;
1240 return $versions;
1244 * Returns hash of all core + plugin /db/ directories.
1246 * This is relatively slow and not fully cached, use with care!
1248 * @param array|null $components optional component directory => hash array to use. Only used in PHPUnit.
1249 * @return string sha1 hash.
1251 public static function get_all_component_hash(?array $components = null): string {
1252 $tohash = $components ?? self::get_all_directory_hashes();
1253 return sha1(serialize($tohash));
1257 * Get the hashes of all core + plugin /db/ directories.
1259 * @param array|null $directories optional component directory array to hash. Only used in PHPUnit.
1260 * @return array of directory => hash.
1262 public static function get_all_directory_hashes(?array $directories = null): array {
1263 global $CFG;
1265 self::init();
1267 // The problem here is that the component cache might be stale,
1268 // we want this to work also on frontpage without resetting the component cache.
1269 $usecache = false;
1270 if (CACHE_DISABLE_ALL || (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE)) {
1271 $usecache = true;
1274 if (empty($directories)) {
1275 $directories = [
1276 $CFG->libdir . '/db',
1278 // For all components, get the directory of the /db directory.
1279 $plugintypes = self::get_plugin_types();
1280 foreach ($plugintypes as $type => $typedir) {
1281 if ($usecache) {
1282 $plugs = self::get_plugin_list($type);
1283 } else {
1284 $plugs = self::fetch_plugins($type, $typedir);
1286 foreach ($plugs as $plug) {
1287 $directories[] = $plug . '/db';
1292 // Create a mapping of directories to their hash.
1293 $hashes = [];
1294 foreach ($directories as $directory) {
1295 if (!is_dir($directory)) {
1296 // Just hash an empty string as the non-existing representation.
1297 $hashes[$directory] = sha1('');
1298 continue;
1301 $scan = scandir($directory);
1302 if ($scan) {
1303 sort($scan);
1305 $scanhashes = [];
1306 foreach ($scan as $file) {
1307 $file = $directory . '/' . $file;
1308 // Moodle ignores directories.
1309 if (!is_dir($file)) {
1310 $scanhashes[] = hash_file('sha1', $file);
1313 // Finally we can serialize and hash the whole dir.
1314 $hashes[$directory] = sha1(serialize($scanhashes));
1317 return $hashes;
1321 * Invalidate opcode cache for given file, this is intended for
1322 * php files that are stored in dataroot.
1324 * Note: we need it here because this class must be self-contained.
1326 * @param string $file
1328 public static function invalidate_opcode_php_cache($file) {
1329 if (function_exists('opcache_invalidate')) {
1330 if (!file_exists($file)) {
1331 return;
1333 opcache_invalidate($file, true);
1338 * Return true if subsystemname is core subsystem.
1340 * @param string $subsystemname name of the subsystem.
1341 * @return bool true if core subsystem.
1343 public static function is_core_subsystem($subsystemname) {
1344 return isset(self::$subsystems[$subsystemname]);
1348 * Return true if apiname is a core API.
1350 * @param string $apiname name of the API.
1351 * @return bool true if core API.
1353 public static function is_core_api($apiname) {
1354 return isset(self::$apis[$apiname]);
1358 * Records all class renames that have been made to facilitate autoloading.
1360 protected static function fill_classmap_renames_cache() {
1361 global $CFG;
1363 self::$classmaprenames = [];
1365 self::load_renamed_classes("$CFG->dirroot/lib/");
1367 foreach (self::$subsystems as $subsystem => $fulldir) {
1368 self::load_renamed_classes($fulldir);
1371 foreach (self::$plugins as $plugintype => $plugins) {
1372 foreach ($plugins as $pluginname => $fulldir) {
1373 self::load_renamed_classes($fulldir);
1379 * Loads the db/renamedclasses.php file from the given directory.
1381 * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1382 * and the value is the new class name.
1383 * It is only included when we are populating the component cache. After that is not needed.
1385 * @param string|null $fulldir The directory to the renamed classes.
1387 protected static function load_renamed_classes(?string $fulldir) {
1388 if (is_null($fulldir)) {
1389 return;
1392 $file = $fulldir . '/db/renamedclasses.php';
1393 if (is_readable($file)) {
1394 $renamedclasses = null;
1395 require($file);
1396 if (is_array($renamedclasses)) {
1397 foreach ($renamedclasses as $oldclass => $newclass) {
1398 self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1405 * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1407 * E.g.
1409 * 'mod' => [
1410 * 'mod_forum' => FORUM_PLUGIN_PATH,
1411 * ...
1412 * ],
1413 * ...
1414 * 'core' => [
1415 * 'core_comment' => COMMENT_SUBSYSTEM_PATH,
1416 * ...
1420 * @return array an associative array of components and their corresponding paths.
1422 public static function get_component_list(): array {
1423 $components = [];
1424 // Get all plugins.
1425 foreach (self::get_plugin_types() as $plugintype => $typedir) {
1426 $components[$plugintype] = [];
1427 foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1428 $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1431 // Get all subsystems.
1432 foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1433 $components['core']['core_' . $subsystemname] = $subsystempath;
1435 return $components;
1439 * Returns a list of frankenstyle component names, including all plugins, subplugins, and subsystems.
1441 * Note: By default the 'core' subsystem is not included.
1443 * @param bool $includecore Whether to include the 'core' subsystem
1444 * @return string[] the list of frankenstyle component names.
1446 public static function get_component_names(
1447 bool $includecore = false,
1448 ): array {
1449 $componentnames = [];
1450 // Get all plugins.
1451 foreach (self::get_plugin_types() as $plugintype => $typedir) {
1452 foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1453 $componentnames[] = $plugintype . '_' . $pluginname;
1456 // Get all subsystems.
1457 foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1458 $componentnames[] = 'core_' . $subsystemname;
1461 if ($includecore) {
1462 $componentnames[] = 'core';
1465 return $componentnames;
1469 * Returns the list of available API names.
1471 * @return string[] the list of available API names.
1473 public static function get_core_api_names(): array {
1474 return array_keys(self::get_core_apis());
1478 * Checks for the presence of monologo icons within a plugin.
1480 * Only checks monologo icons in PNG and SVG formats as they are
1481 * formats that can have transparent background.
1483 * @param string $plugintype The plugin type.
1484 * @param string $pluginname The plugin name.
1485 * @return bool True if the plugin has a monologo icon
1487 public static function has_monologo_icon(string $plugintype, string $pluginname): bool {
1488 $plugindir = self::get_plugin_directory($plugintype, $pluginname);
1489 if ($plugindir === null) {
1490 return false;
1492 return file_exists("$plugindir/pix/monologo.svg") || file_exists("$plugindir/pix/monologo.png");