Merge branch 'MDL-58454-master' of git://github.com/junpataleta/moodle
[moodle.git] / lib / classes / component.php
blob11ce5a6d5520d00f8aad766eb3c81afae6b87ae5
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 - watch out for auth/db exception */
46 protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true);
47 /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
48 protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
50 /** @var array cache of plugin types */
51 protected static $plugintypes = null;
52 /** @var array cache of plugin locations */
53 protected static $plugins = null;
54 /** @var array cache of core subsystems */
55 protected static $subsystems = null;
56 /** @var array subplugin type parents */
57 protected static $parents = null;
58 /** @var array subplugins */
59 protected static $subplugins = null;
60 /** @var array list of all known classes that can be autoloaded */
61 protected static $classmap = null;
62 /** @var array list of all classes that have been renamed to be autoloaded */
63 protected static $classmaprenames = null;
64 /** @var array list of some known files that can be included. */
65 protected static $filemap = null;
66 /** @var int|float core version. */
67 protected static $version = null;
68 /** @var array list of the files to map. */
69 protected static $filestomap = array('lib.php', 'settings.php');
70 /** @var array associative array of PSR-0 namespaces and corresponding paths. */
71 protected static $psr0namespaces = array(
72 'Horde' => 'lib/horde/framework/Horde',
73 'Mustache' => 'lib/mustache/src/Mustache',
75 /** @var array associative array of PRS-4 namespaces and corresponding paths. */
76 protected static $psr4namespaces = array(
77 'MaxMind' => 'lib/maxmind/MaxMind',
78 'GeoIp2' => 'lib/maxmind/GeoIp2',
79 'Sabberworm\\CSS' => 'lib/php-css-parser',
80 'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
81 'Leafo\\ScssPhp' => 'lib/scssphp',
82 'Box\\Spout' => 'lib/spout/src/Spout',
83 'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
84 'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
85 'IMSGlobal\LTI' => 'lib/ltiprovider/src',
86 'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
87 'PHPMailer\\PHPMailer' => 'lib/phpmailer/src',
88 'RedeyeVentures\\GeoPattern' => 'lib/geopattern-php/GeoPattern',
91 /**
92 * Class loader for Frankenstyle named classes in standard locations.
93 * Frankenstyle namespaces are supported.
95 * The expected location for core classes is:
96 * 1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
97 * 2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
98 * 3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
100 * The expected location for plugin classes is:
101 * 1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
102 * 2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
103 * 3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
105 * @param string $classname
107 public static function classloader($classname) {
108 self::init();
110 if (isset(self::$classmap[$classname])) {
111 // Global $CFG is expected in included scripts.
112 global $CFG;
113 // Function include would be faster, but for BC it is better to include only once.
114 include_once(self::$classmap[$classname]);
115 return;
117 if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
118 $newclassname = self::$classmaprenames[$classname];
119 $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
120 debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
121 if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
122 throw new \coding_exception("Cannot alias $classname to $newclassname");
124 class_alias($newclassname, $classname);
125 return;
128 $file = self::psr_classloader($classname);
129 // If the file is found, require it.
130 if (!empty($file)) {
131 require($file);
132 return;
137 * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
138 * demand. Only returns paths to files that exist.
140 * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
141 * compatible.
143 * @param string $class the name of the class.
144 * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
146 protected static function psr_classloader($class) {
147 // Iterate through each PSR-4 namespace prefix.
148 foreach (self::$psr4namespaces as $prefix => $path) {
149 $file = self::get_class_file($class, $prefix, $path, array('\\'));
150 if (!empty($file) && file_exists($file)) {
151 return $file;
155 // Iterate through each PSR-0 namespace prefix.
156 foreach (self::$psr0namespaces as $prefix => $path) {
157 $file = self::get_class_file($class, $prefix, $path, array('\\', '_'));
158 if (!empty($file) && file_exists($file)) {
159 return $file;
163 return false;
167 * Return the path to the class based on the given namespace prefix and path it corresponds to.
169 * Will return the path even if the file does not exist. Check the file esists before requiring.
171 * @param string $class the name of the class.
172 * @param string $prefix The namespace prefix used to identify the base directory of the source files.
173 * @param string $path The relative path to the base directory of the source files.
174 * @param string[] $separators The characters that should be used for separating.
175 * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
177 protected static function get_class_file($class, $prefix, $path, $separators) {
178 global $CFG;
180 // Does the class use the namespace prefix?
181 $len = strlen($prefix);
182 if (strncmp($prefix, $class, $len) !== 0) {
183 // No, move to the next prefix.
184 return false;
186 $path = $CFG->dirroot . '/' . $path;
188 // Get the relative class name.
189 $relativeclass = substr($class, $len);
191 // Replace the namespace prefix with the base directory, replace namespace
192 // separators with directory separators in the relative class name, append
193 // with .php.
194 $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
196 return $file;
201 * Initialise caches, always call before accessing self:: caches.
203 protected static function init() {
204 global $CFG;
206 // Init only once per request/CLI execution, we ignore changes done afterwards.
207 if (isset(self::$plugintypes)) {
208 return;
211 if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
212 self::fill_all_caches();
213 return;
216 if (!empty($CFG->alternative_component_cache)) {
217 // Hack for heavily clustered sites that want to manage component cache invalidation manually.
218 $cachefile = $CFG->alternative_component_cache;
220 if (file_exists($cachefile)) {
221 if (CACHE_DISABLE_ALL) {
222 // Verify the cache state only on upgrade pages.
223 $content = self::get_cache_content();
224 if (sha1_file($cachefile) !== sha1($content)) {
225 die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
227 return;
229 $cache = array();
230 include($cachefile);
231 self::$plugintypes = $cache['plugintypes'];
232 self::$plugins = $cache['plugins'];
233 self::$subsystems = $cache['subsystems'];
234 self::$parents = $cache['parents'];
235 self::$subplugins = $cache['subplugins'];
236 self::$classmap = $cache['classmap'];
237 self::$classmaprenames = $cache['classmaprenames'];
238 self::$filemap = $cache['filemap'];
239 return;
242 if (!is_writable(dirname($cachefile))) {
243 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
246 // Lets try to create the file, it might be in some writable directory or a local cache dir.
248 } else {
249 // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
250 // use $CFG->alternative_component_cache if you do not like it.
251 $cachefile = "$CFG->cachedir/core_component.php";
254 if (!CACHE_DISABLE_ALL and !self::is_developer()) {
255 // 1/ Use the cache only outside of install and upgrade.
256 // 2/ Let developers add/remove classes in developer mode.
257 if (is_readable($cachefile)) {
258 $cache = false;
259 include($cachefile);
260 if (!is_array($cache)) {
261 // Something is very wrong.
262 } else if (!isset($cache['version'])) {
263 // Something is very wrong.
264 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
265 // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
266 error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
267 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
268 // $CFG->dirroot was changed.
269 } else {
270 // The cache looks ok, let's use it.
271 self::$plugintypes = $cache['plugintypes'];
272 self::$plugins = $cache['plugins'];
273 self::$subsystems = $cache['subsystems'];
274 self::$parents = $cache['parents'];
275 self::$subplugins = $cache['subplugins'];
276 self::$classmap = $cache['classmap'];
277 self::$classmaprenames = $cache['classmaprenames'];
278 self::$filemap = $cache['filemap'];
279 return;
281 // Note: we do not verify $CFG->admin here intentionally,
282 // they must visit admin/index.php after any change.
286 if (!isset(self::$plugintypes)) {
287 // This needs to be atomic and self-fixing as much as possible.
289 $content = self::get_cache_content();
290 if (file_exists($cachefile)) {
291 if (sha1_file($cachefile) === sha1($content)) {
292 return;
294 // Stale cache detected!
295 unlink($cachefile);
298 // Permissions might not be setup properly in installers.
299 $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
300 $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
302 clearstatcache();
303 $cachedir = dirname($cachefile);
304 if (!is_dir($cachedir)) {
305 mkdir($cachedir, $dirpermissions, true);
308 if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
309 fwrite($fp, $content);
310 fclose($fp);
311 @rename($cachefile.'.tmp', $cachefile);
312 @chmod($cachefile, $filepermissions);
314 @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
315 self::invalidate_opcode_php_cache($cachefile);
320 * Are we in developer debug mode?
322 * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
323 * the reason is we need to use this before we setup DB connection or caches for CFG.
325 * @return bool
327 protected static function is_developer() {
328 global $CFG;
330 // Note we can not rely on $CFG->debug here because DB is not initialised yet.
331 if (isset($CFG->config_php_settings['debug'])) {
332 $debug = (int)$CFG->config_php_settings['debug'];
333 } else {
334 return false;
337 if ($debug & E_ALL and $debug & E_STRICT) {
338 return true;
341 return false;
345 * Create cache file content.
347 * @private this is intended for $CFG->alternative_component_cache only.
349 * @return string
351 public static function get_cache_content() {
352 if (!isset(self::$plugintypes)) {
353 self::fill_all_caches();
356 $cache = array(
357 'subsystems' => self::$subsystems,
358 'plugintypes' => self::$plugintypes,
359 'plugins' => self::$plugins,
360 'parents' => self::$parents,
361 'subplugins' => self::$subplugins,
362 'classmap' => self::$classmap,
363 'classmaprenames' => self::$classmaprenames,
364 'filemap' => self::$filemap,
365 'version' => self::$version,
368 return '<?php
369 $cache = '.var_export($cache, true).';
374 * Fill all caches.
376 protected static function fill_all_caches() {
377 self::$subsystems = self::fetch_subsystems();
379 list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
381 self::$plugins = array();
382 foreach (self::$plugintypes as $type => $fulldir) {
383 self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
386 self::fill_classmap_cache();
387 self::fill_classmap_renames_cache();
388 self::fill_filemap_cache();
389 self::fetch_core_version();
393 * Get the core version.
395 * In order for this to work properly, opcache should be reset beforehand.
397 * @return float core version.
399 protected static function fetch_core_version() {
400 global $CFG;
401 if (self::$version === null) {
402 $version = null; // Prevent IDE complaints.
403 require($CFG->dirroot . '/version.php');
404 self::$version = $version;
406 return self::$version;
410 * Returns list of core subsystems.
411 * @return array
413 protected static function fetch_subsystems() {
414 global $CFG;
416 // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
418 $info = array(
419 'access' => null,
420 'admin' => $CFG->dirroot.'/'.$CFG->admin,
421 'analytics' => $CFG->dirroot . '/analytics',
422 'antivirus' => $CFG->dirroot . '/lib/antivirus',
423 'auth' => $CFG->dirroot.'/auth',
424 'availability' => $CFG->dirroot . '/availability',
425 'backup' => $CFG->dirroot.'/backup/util/ui',
426 'badges' => $CFG->dirroot.'/badges',
427 'block' => $CFG->dirroot.'/blocks',
428 'blog' => $CFG->dirroot.'/blog',
429 'bulkusers' => null,
430 'cache' => $CFG->dirroot.'/cache',
431 'calendar' => $CFG->dirroot.'/calendar',
432 'cohort' => $CFG->dirroot.'/cohort',
433 'comment' => $CFG->dirroot.'/comment',
434 'competency' => $CFG->dirroot.'/competency',
435 'completion' => $CFG->dirroot.'/completion',
436 'countries' => null,
437 'course' => $CFG->dirroot.'/course',
438 'currencies' => null,
439 'dbtransfer' => null,
440 'debug' => null,
441 'editor' => $CFG->dirroot.'/lib/editor',
442 'edufields' => null,
443 'enrol' => $CFG->dirroot.'/enrol',
444 'error' => null,
445 'favourites' => $CFG->dirroot . '/favourites',
446 'filepicker' => null,
447 'fileconverter' => $CFG->dirroot.'/files/converter',
448 'files' => $CFG->dirroot.'/files',
449 'filters' => $CFG->dirroot.'/filter',
450 //'fonts' => null, // Bogus.
451 'form' => $CFG->dirroot.'/lib/form',
452 'grades' => $CFG->dirroot.'/grade',
453 'grading' => $CFG->dirroot.'/grade/grading',
454 'group' => $CFG->dirroot.'/group',
455 'help' => null,
456 'hub' => null,
457 'imscc' => null,
458 'install' => null,
459 'iso6392' => null,
460 'langconfig' => null,
461 'license' => null,
462 'mathslib' => null,
463 'media' => $CFG->dirroot.'/media',
464 'message' => $CFG->dirroot.'/message',
465 'mimetypes' => null,
466 'mnet' => $CFG->dirroot.'/mnet',
467 //'moodle.org' => null, // Not used any more.
468 'my' => $CFG->dirroot.'/my',
469 'notes' => $CFG->dirroot.'/notes',
470 'pagetype' => null,
471 'pix' => null,
472 'plagiarism' => $CFG->dirroot.'/plagiarism',
473 'plugin' => null,
474 'portfolio' => $CFG->dirroot.'/portfolio',
475 'privacy' => $CFG->dirroot . '/privacy',
476 'question' => $CFG->dirroot.'/question',
477 'rating' => $CFG->dirroot.'/rating',
478 'repository' => $CFG->dirroot.'/repository',
479 'rss' => $CFG->dirroot.'/rss',
480 'role' => $CFG->dirroot.'/'.$CFG->admin.'/roles',
481 'search' => $CFG->dirroot.'/search',
482 'table' => null,
483 'tag' => $CFG->dirroot.'/tag',
484 'timezones' => null,
485 'user' => $CFG->dirroot.'/user',
486 'userkey' => $CFG->dirroot.'/lib/userkey',
487 'webservice' => $CFG->dirroot.'/webservice',
490 return $info;
494 * Returns list of known plugin types.
495 * @return array
497 protected static function fetch_plugintypes() {
498 global $CFG;
500 $types = array(
501 'antivirus' => $CFG->dirroot . '/lib/antivirus',
502 'availability' => $CFG->dirroot . '/availability/condition',
503 'qtype' => $CFG->dirroot.'/question/type',
504 'mod' => $CFG->dirroot.'/mod',
505 'auth' => $CFG->dirroot.'/auth',
506 'calendartype' => $CFG->dirroot.'/calendar/type',
507 'enrol' => $CFG->dirroot.'/enrol',
508 'message' => $CFG->dirroot.'/message/output',
509 'block' => $CFG->dirroot.'/blocks',
510 'media' => $CFG->dirroot.'/media/player',
511 'filter' => $CFG->dirroot.'/filter',
512 'editor' => $CFG->dirroot.'/lib/editor',
513 'format' => $CFG->dirroot.'/course/format',
514 'dataformat' => $CFG->dirroot.'/dataformat',
515 'profilefield' => $CFG->dirroot.'/user/profile/field',
516 'report' => $CFG->dirroot.'/report',
517 'coursereport' => $CFG->dirroot.'/course/report', // Must be after system reports.
518 'gradeexport' => $CFG->dirroot.'/grade/export',
519 'gradeimport' => $CFG->dirroot.'/grade/import',
520 'gradereport' => $CFG->dirroot.'/grade/report',
521 'gradingform' => $CFG->dirroot.'/grade/grading/form',
522 'mlbackend' => $CFG->dirroot.'/lib/mlbackend',
523 'mnetservice' => $CFG->dirroot.'/mnet/service',
524 'webservice' => $CFG->dirroot.'/webservice',
525 'repository' => $CFG->dirroot.'/repository',
526 'portfolio' => $CFG->dirroot.'/portfolio',
527 'search' => $CFG->dirroot.'/search/engine',
528 'qbehaviour' => $CFG->dirroot.'/question/behaviour',
529 'qformat' => $CFG->dirroot.'/question/format',
530 'plagiarism' => $CFG->dirroot.'/plagiarism',
531 'tool' => $CFG->dirroot.'/'.$CFG->admin.'/tool',
532 'cachestore' => $CFG->dirroot.'/cache/stores',
533 'cachelock' => $CFG->dirroot.'/cache/locks',
534 'fileconverter' => $CFG->dirroot.'/files/converter',
536 $parents = array();
537 $subplugins = array();
539 if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
540 $types['theme'] = $CFG->themedir;
541 } else {
542 $types['theme'] = $CFG->dirroot.'/theme';
545 foreach (self::$supportsubplugins as $type) {
546 if ($type === 'local') {
547 // Local subplugins must be after local plugins.
548 continue;
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] = array();
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));
568 // Local is always last!
569 $types['local'] = $CFG->dirroot.'/local';
571 if (in_array('local', self::$supportsubplugins)) {
572 $type = 'local';
573 $plugins = self::fetch_plugins($type, $types[$type]);
574 foreach ($plugins as $plugin => $fulldir) {
575 $subtypes = self::fetch_subtypes($fulldir);
576 if (!$subtypes) {
577 continue;
579 $subplugins[$type.'_'.$plugin] = array();
580 foreach($subtypes as $subtype => $subdir) {
581 if (isset($types[$subtype])) {
582 error_log("Invalid subtype '$subtype', duplicate detected.");
583 continue;
585 $types[$subtype] = $subdir;
586 $parents[$subtype] = $type.'_'.$plugin;
587 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
592 return array($types, $parents, $subplugins);
596 * Returns list of subtypes.
597 * @param string $ownerdir
598 * @return array
600 protected static function fetch_subtypes($ownerdir) {
601 global $CFG;
603 $types = array();
604 if (file_exists("$ownerdir/db/subplugins.php")) {
605 $subplugins = array();
606 include("$ownerdir/db/subplugins.php");
607 foreach ($subplugins as $subtype => $dir) {
608 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
609 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
610 continue;
612 if (isset(self::$subsystems[$subtype])) {
613 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
614 continue;
616 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
617 $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
619 if (!is_dir("$CFG->dirroot/$dir")) {
620 error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
621 continue;
623 $types[$subtype] = "$CFG->dirroot/$dir";
626 return $types;
630 * Returns list of plugins of given type in given directory.
631 * @param string $plugintype
632 * @param string $fulldir
633 * @return array
635 protected static function fetch_plugins($plugintype, $fulldir) {
636 global $CFG;
638 $fulldirs = (array)$fulldir;
639 if ($plugintype === 'theme') {
640 if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
641 // Include themes in standard location too.
642 array_unshift($fulldirs, $CFG->dirroot.'/theme');
646 $result = array();
648 foreach ($fulldirs as $fulldir) {
649 if (!is_dir($fulldir)) {
650 continue;
652 $items = new \DirectoryIterator($fulldir);
653 foreach ($items as $item) {
654 if ($item->isDot() or !$item->isDir()) {
655 continue;
657 $pluginname = $item->getFilename();
658 if ($plugintype === 'auth' and $pluginname === 'db') {
659 // Special exception for this wrong plugin name.
660 } else if (isset(self::$ignoreddirs[$pluginname])) {
661 continue;
663 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
664 // Always ignore plugins with problematic names here.
665 continue;
667 $result[$pluginname] = $fulldir.'/'.$pluginname;
668 unset($item);
670 unset($items);
673 ksort($result);
674 return $result;
678 * Find all classes that can be autoloaded including frankenstyle namespaces.
680 protected static function fill_classmap_cache() {
681 global $CFG;
683 self::$classmap = array();
685 self::load_classes('core', "$CFG->dirroot/lib/classes");
687 foreach (self::$subsystems as $subsystem => $fulldir) {
688 if (!$fulldir) {
689 continue;
691 self::load_classes('core_'.$subsystem, "$fulldir/classes");
694 foreach (self::$plugins as $plugintype => $plugins) {
695 foreach ($plugins as $pluginname => $fulldir) {
696 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
699 ksort(self::$classmap);
703 * Fills up the cache defining what plugins have certain files.
705 * @see self::get_plugin_list_with_file
706 * @return void
708 protected static function fill_filemap_cache() {
709 global $CFG;
711 self::$filemap = array();
713 foreach (self::$filestomap as $file) {
714 if (!isset(self::$filemap[$file])) {
715 self::$filemap[$file] = array();
717 foreach (self::$plugins as $plugintype => $plugins) {
718 if (!isset(self::$filemap[$file][$plugintype])) {
719 self::$filemap[$file][$plugintype] = array();
721 foreach ($plugins as $pluginname => $fulldir) {
722 if (file_exists("$fulldir/$file")) {
723 self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
731 * Find classes in directory and recurse to subdirs.
732 * @param string $component
733 * @param string $fulldir
734 * @param string $namespace
736 protected static function load_classes($component, $fulldir, $namespace = '') {
737 if (!is_dir($fulldir)) {
738 return;
741 if (!is_readable($fulldir)) {
742 // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
743 // because its pretty likely to lead to a missing class error further down the line.
744 // But our early setup code can't handle errors this early at the moment.
745 return;
748 $items = new \DirectoryIterator($fulldir);
749 foreach ($items as $item) {
750 if ($item->isDot()) {
751 continue;
753 if ($item->isDir()) {
754 $dirname = $item->getFilename();
755 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
756 continue;
759 $filename = $item->getFilename();
760 $classname = preg_replace('/\.php$/', '', $filename);
762 if ($filename === $classname) {
763 // Not a php file.
764 continue;
766 if ($namespace === '') {
767 // Legacy long frankenstyle class name.
768 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
770 // New namespaced classes.
771 self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
773 unset($item);
774 unset($items);
779 * List all core subsystems and their location
781 * This is a whitelist of components that are part of the core and their
782 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
783 * plugin is not listed here and it does not have proper plugintype prefix,
784 * then it is considered as course activity module.
786 * The location is absolute file path to dir. NULL means there is no special
787 * directory for this subsystem. If the location is set, the subsystem's
788 * renderer.php is expected to be there.
790 * @return array of (string)name => (string|null)full dir location
792 public static function get_core_subsystems() {
793 self::init();
794 return self::$subsystems;
798 * Get list of available plugin types together with their location.
800 * @return array as (string)plugintype => (string)fulldir
802 public static function get_plugin_types() {
803 self::init();
804 return self::$plugintypes;
808 * Get list of plugins of given type.
810 * @param string $plugintype
811 * @return array as (string)pluginname => (string)fulldir
813 public static function get_plugin_list($plugintype) {
814 self::init();
816 if (!isset(self::$plugins[$plugintype])) {
817 return array();
819 return self::$plugins[$plugintype];
823 * Get a list of all the plugins of a given type that define a certain class
824 * in a certain file. The plugin component names and class names are returned.
826 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
827 * @param string $class the part of the name of the class after the
828 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
829 * names like report_courselist_thing. If you are looking for classes with
830 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
831 * Frankenstyle namespaces are also supported.
832 * @param string $file the name of file within the plugin that defines the class.
833 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
834 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
836 public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
837 global $CFG; // Necessary in case it is referenced by included PHP scripts.
839 if ($class) {
840 $suffix = '_' . $class;
841 } else {
842 $suffix = '';
845 $pluginclasses = array();
846 $plugins = self::get_plugin_list($plugintype);
847 foreach ($plugins as $plugin => $fulldir) {
848 // Try class in frankenstyle namespace.
849 if ($class) {
850 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
851 if (class_exists($classname, true)) {
852 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
853 continue;
857 // Try autoloading of class with frankenstyle prefix.
858 $classname = $plugintype . '_' . $plugin . $suffix;
859 if (class_exists($classname, true)) {
860 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
861 continue;
864 // Fall back to old file location and class name.
865 if ($file and file_exists("$fulldir/$file")) {
866 include_once("$fulldir/$file");
867 if (class_exists($classname, false)) {
868 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
869 continue;
874 return $pluginclasses;
878 * Get a list of all the plugins of a given type that contain a particular file.
880 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
881 * @param string $file the name of file that must be present in the plugin.
882 * (e.g. 'view.php', 'db/install.xml').
883 * @param bool $include if true (default false), the file will be include_once-ed if found.
884 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
885 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
887 public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
888 global $CFG; // Necessary in case it is referenced by included PHP scripts.
889 $pluginfiles = array();
891 if (isset(self::$filemap[$file])) {
892 // If the file was supposed to be mapped, then it should have been set in the array.
893 if (isset(self::$filemap[$file][$plugintype])) {
894 $pluginfiles = self::$filemap[$file][$plugintype];
896 } else {
897 // Old-style search for non-cached files.
898 $plugins = self::get_plugin_list($plugintype);
899 foreach ($plugins as $plugin => $fulldir) {
900 $path = $fulldir . '/' . $file;
901 if (file_exists($path)) {
902 $pluginfiles[$plugin] = $path;
907 if ($include) {
908 foreach ($pluginfiles as $path) {
909 include_once($path);
913 return $pluginfiles;
917 * Returns all classes in a component matching the provided namespace.
919 * It checks that the class exists.
921 * e.g. get_component_classes_in_namespace('mod_forum', 'event')
923 * @param string $component A valid moodle component (frankenstyle)
924 * @param string $namespace Namespace from the component name or empty if all $component namespace classes.
925 * @return array The full class name as key and the class path as value.
927 public static function get_component_classes_in_namespace($component, $namespace = '') {
929 $component = self::normalize_componentname($component);
931 if ($namespace) {
933 // We will add them later.
934 $namespace = trim($namespace, '\\');
936 // We need add double backslashes as it is how classes are stored into self::$classmap.
937 $namespace = implode('\\\\', explode('\\', $namespace));
938 $namespace = $namespace . '\\\\';
941 $regex = '|^' . $component . '\\\\' . $namespace . '|';
942 $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
944 // We want to be sure that they exist.
945 $classes = array();
946 foreach ($it as $classname => $classpath) {
947 if (class_exists($classname)) {
948 $classes[$classname] = $classpath;
952 return $classes;
956 * Returns the exact absolute path to plugin directory.
958 * @param string $plugintype type of plugin
959 * @param string $pluginname name of the plugin
960 * @return string full path to plugin directory; null if not found
962 public static function get_plugin_directory($plugintype, $pluginname) {
963 if (empty($pluginname)) {
964 // Invalid plugin name, sorry.
965 return null;
968 self::init();
970 if (!isset(self::$plugins[$plugintype][$pluginname])) {
971 return null;
973 return self::$plugins[$plugintype][$pluginname];
977 * Returns the exact absolute path to plugin directory.
979 * @param string $subsystem type of core subsystem
980 * @return string full path to subsystem directory; null if not found
982 public static function get_subsystem_directory($subsystem) {
983 self::init();
985 if (!isset(self::$subsystems[$subsystem])) {
986 return null;
988 return self::$subsystems[$subsystem];
992 * This method validates a plug name. It is much faster than calling clean_param.
994 * @param string $plugintype type of plugin
995 * @param string $pluginname a string that might be a plugin name.
996 * @return bool if this string is a valid plugin name.
998 public static function is_valid_plugin_name($plugintype, $pluginname) {
999 if ($plugintype === 'mod') {
1000 // Modules must not have the same name as core subsystems.
1001 if (!isset(self::$subsystems)) {
1002 // Watch out, this is called from init!
1003 self::init();
1005 if (isset(self::$subsystems[$pluginname])) {
1006 return false;
1008 // Modules MUST NOT have any underscores,
1009 // component normalisation would break very badly otherwise!
1010 return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1012 } else {
1013 return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1018 * Normalize the component name.
1020 * Note: this does not verify the validity of the plugin or component.
1022 * @param string $component
1023 * @return string
1025 public static function normalize_componentname($componentname) {
1026 list($plugintype, $pluginname) = self::normalize_component($componentname);
1027 if ($plugintype === 'core' && is_null($pluginname)) {
1028 return $plugintype;
1030 return $plugintype . '_' . $pluginname;
1034 * Normalize the component name using the "frankenstyle" rules.
1036 * Note: this does not verify the validity of plugin or type names.
1038 * @param string $component
1039 * @return array two-items list of [(string)type, (string|null)name]
1041 public static function normalize_component($component) {
1042 if ($component === 'moodle' or $component === 'core' or $component === '') {
1043 return array('core', null);
1046 if (strpos($component, '_') === false) {
1047 self::init();
1048 if (array_key_exists($component, self::$subsystems)) {
1049 $type = 'core';
1050 $plugin = $component;
1051 } else {
1052 // Everything else without underscore is a module.
1053 $type = 'mod';
1054 $plugin = $component;
1057 } else {
1058 list($type, $plugin) = explode('_', $component, 2);
1059 if ($type === 'moodle') {
1060 $type = 'core';
1062 // Any unknown type must be a subplugin.
1065 return array($type, $plugin);
1069 * Return exact absolute path to a plugin directory.
1071 * @param string $component name such as 'moodle', 'mod_forum'
1072 * @return string full path to component directory; NULL if not found
1074 public static function get_component_directory($component) {
1075 global $CFG;
1077 list($type, $plugin) = self::normalize_component($component);
1079 if ($type === 'core') {
1080 if ($plugin === null) {
1081 return $path = $CFG->libdir;
1083 return self::get_subsystem_directory($plugin);
1086 return self::get_plugin_directory($type, $plugin);
1090 * Returns list of plugin types that allow subplugins.
1091 * @return array as (string)plugintype => (string)fulldir
1093 public static function get_plugin_types_with_subplugins() {
1094 self::init();
1096 $return = array();
1097 foreach (self::$supportsubplugins as $type) {
1098 $return[$type] = self::$plugintypes[$type];
1100 return $return;
1104 * Returns parent of this subplugin type.
1106 * @param string $type
1107 * @return string parent component or null
1109 public static function get_subtype_parent($type) {
1110 self::init();
1112 if (isset(self::$parents[$type])) {
1113 return self::$parents[$type];
1116 return null;
1120 * Return all subplugins of this component.
1121 * @param string $component.
1122 * @return array $subtype=>array($component, ..), null if no subtypes defined
1124 public static function get_subplugins($component) {
1125 self::init();
1127 if (isset(self::$subplugins[$component])) {
1128 return self::$subplugins[$component];
1131 return null;
1135 * Returns hash of all versions including core and all plugins.
1137 * This is relatively slow and not fully cached, use with care!
1139 * @return string sha1 hash
1141 public static function get_all_versions_hash() {
1142 global $CFG;
1144 self::init();
1146 $versions = array();
1148 // Main version first.
1149 $versions['core'] = self::fetch_core_version();
1151 // The problem here is tha the component cache might be stable,
1152 // we want this to work also on frontpage without resetting the component cache.
1153 $usecache = false;
1154 if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1155 $usecache = true;
1158 // Now all plugins.
1159 $plugintypes = core_component::get_plugin_types();
1160 foreach ($plugintypes as $type => $typedir) {
1161 if ($usecache) {
1162 $plugs = core_component::get_plugin_list($type);
1163 } else {
1164 $plugs = self::fetch_plugins($type, $typedir);
1166 foreach ($plugs as $plug => $fullplug) {
1167 $plugin = new stdClass();
1168 $plugin->version = null;
1169 $module = $plugin;
1170 include($fullplug.'/version.php');
1171 $versions[$type.'_'.$plug] = $plugin->version;
1175 return sha1(serialize($versions));
1179 * Invalidate opcode cache for given file, this is intended for
1180 * php files that are stored in dataroot.
1182 * Note: we need it here because this class must be self-contained.
1184 * @param string $file
1186 public static function invalidate_opcode_php_cache($file) {
1187 if (function_exists('opcache_invalidate')) {
1188 if (!file_exists($file)) {
1189 return;
1191 opcache_invalidate($file, true);
1196 * Return true if subsystemname is core subsystem.
1198 * @param string $subsystemname name of the subsystem.
1199 * @return bool true if core subsystem.
1201 public static function is_core_subsystem($subsystemname) {
1202 return isset(self::$subsystems[$subsystemname]);
1206 * Records all class renames that have been made to facilitate autoloading.
1208 protected static function fill_classmap_renames_cache() {
1209 global $CFG;
1211 self::$classmaprenames = array();
1213 self::load_renamed_classes("$CFG->dirroot/lib/");
1215 foreach (self::$subsystems as $subsystem => $fulldir) {
1216 self::load_renamed_classes($fulldir);
1219 foreach (self::$plugins as $plugintype => $plugins) {
1220 foreach ($plugins as $pluginname => $fulldir) {
1221 self::load_renamed_classes($fulldir);
1227 * Loads the db/renamedclasses.php file from the given directory.
1229 * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1230 * and the value is the new class name.
1231 * It is only included when we are populating the component cache. After that is not needed.
1233 * @param string $fulldir
1235 protected static function load_renamed_classes($fulldir) {
1236 $file = $fulldir . '/db/renamedclasses.php';
1237 if (is_readable($file)) {
1238 $renamedclasses = null;
1239 require($file);
1240 if (is_array($renamedclasses)) {
1241 foreach ($renamedclasses as $oldclass => $newclass) {
1242 self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1249 * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1251 * E.g.
1253 * 'mod' => [
1254 * 'mod_forum' => FORUM_PLUGIN_PATH,
1255 * ...
1256 * ],
1257 * ...
1258 * 'core' => [
1259 * 'core_comment' => COMMENT_SUBSYSTEM_PATH,
1260 * ...
1264 * @return array an associative array of components and their corresponding paths.
1266 public static function get_component_list() : array {
1267 $components = [];
1268 // Get all plugins.
1269 foreach (self::get_plugin_types() as $plugintype => $typedir) {
1270 $components[$plugintype] = [];
1271 foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1272 $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1275 // Get all subsystems.
1276 foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1277 $components['core']['core_' . $subsystemname] = $subsystempath;
1279 return $components;
1283 * Returns a list of frankenstyle component names.
1285 * E.g.
1287 * 'core_course',
1288 * 'core_message',
1289 * 'mod_assign',
1290 * ...
1292 * @return array the list of frankenstyle component names.
1294 public static function get_component_names() : array {
1295 $componentnames = [];
1296 // Get all plugins.
1297 foreach (self::get_plugin_types() as $plugintype => $typedir) {
1298 foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1299 $componentnames[] = $plugintype . '_' . $pluginname;
1302 // Get all subsystems.
1303 foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1304 $componentnames[] = 'core_' . $subsystemname;
1306 return $componentnames;