2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
18 * Components (core subsystems + plugins) related code.
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');
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',
89 * Class loader for Frankenstyle named classes in standard locations.
90 * Frankenstyle namespaces are supported.
92 * The expected location for core classes is:
93 * 1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
94 * 2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
95 * 3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
97 * The expected location for plugin classes is:
98 * 1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
99 * 2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
100 * 3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
102 * @param string $classname
104 public static function classloader($classname) {
107 if (isset(self
::$classmap[$classname])) {
108 // Global $CFG is expected in included scripts.
110 // Function include would be faster, but for BC it is better to include only once.
111 include_once(self
::$classmap[$classname]);
114 if (isset(self
::$classmaprenames[$classname]) && isset(self
::$classmap[self
::$classmaprenames[$classname]])) {
115 $newclassname = self
::$classmaprenames[$classname];
116 $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
117 debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER
);
118 if (PHP_VERSION_ID
>= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
119 throw new \
coding_exception("Cannot alias $classname to $newclassname");
121 class_alias($newclassname, $classname);
125 $file = self
::psr_classloader($classname);
126 // If the file is found, require it.
134 * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
135 * demand. Only returns paths to files that exist.
137 * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
140 * @param string $class the name of the class.
141 * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
143 protected static function psr_classloader($class) {
144 // Iterate through each PSR-4 namespace prefix.
145 foreach (self
::$psr4namespaces as $prefix => $path) {
146 $file = self
::get_class_file($class, $prefix, $path, array('\\'));
147 if (!empty($file) && file_exists($file)) {
152 // Iterate through each PSR-0 namespace prefix.
153 foreach (self
::$psr0namespaces as $prefix => $path) {
154 $file = self
::get_class_file($class, $prefix, $path, array('\\', '_'));
155 if (!empty($file) && file_exists($file)) {
164 * Return the path to the class based on the given namespace prefix and path it corresponds to.
166 * Will return the path even if the file does not exist. Check the file esists before requiring.
168 * @param string $class the name of the class.
169 * @param string $prefix The namespace prefix used to identify the base directory of the source files.
170 * @param string $path The relative path to the base directory of the source files.
171 * @param string[] $separators The characters that should be used for separating.
172 * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
174 protected static function get_class_file($class, $prefix, $path, $separators) {
177 // Does the class use the namespace prefix?
178 $len = strlen($prefix);
179 if (strncmp($prefix, $class, $len) !== 0) {
180 // No, move to the next prefix.
183 $path = $CFG->dirroot
. '/' . $path;
185 // Get the relative class name.
186 $relativeclass = substr($class, $len);
188 // Replace the namespace prefix with the base directory, replace namespace
189 // separators with directory separators in the relative class name, append
191 $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
198 * Initialise caches, always call before accessing self:: caches.
200 protected static function init() {
203 // Init only once per request/CLI execution, we ignore changes done afterwards.
204 if (isset(self
::$plugintypes)) {
208 if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE
) {
209 self
::fill_all_caches();
213 if (!empty($CFG->alternative_component_cache
)) {
214 // Hack for heavily clustered sites that want to manage component cache invalidation manually.
215 $cachefile = $CFG->alternative_component_cache
;
217 if (file_exists($cachefile)) {
218 if (CACHE_DISABLE_ALL
) {
219 // Verify the cache state only on upgrade pages.
220 $content = self
::get_cache_content();
221 if (sha1_file($cachefile) !== sha1($content)) {
222 die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
228 self
::$plugintypes = $cache['plugintypes'];
229 self
::$plugins = $cache['plugins'];
230 self
::$subsystems = $cache['subsystems'];
231 self
::$parents = $cache['parents'];
232 self
::$subplugins = $cache['subplugins'];
233 self
::$classmap = $cache['classmap'];
234 self
::$classmaprenames = $cache['classmaprenames'];
235 self
::$filemap = $cache['filemap'];
239 if (!is_writable(dirname($cachefile))) {
240 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
243 // Lets try to create the file, it might be in some writable directory or a local cache dir.
246 // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
247 // use $CFG->alternative_component_cache if you do not like it.
248 $cachefile = "$CFG->cachedir/core_component.php";
251 if (!CACHE_DISABLE_ALL
and !self
::is_developer()) {
252 // 1/ Use the cache only outside of install and upgrade.
253 // 2/ Let developers add/remove classes in developer mode.
254 if (is_readable($cachefile)) {
257 if (!is_array($cache)) {
258 // Something is very wrong.
259 } else if (!isset($cache['version'])) {
260 // Something is very wrong.
261 } else if ((float) $cache['version'] !== (float) self
::fetch_core_version()) {
262 // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
263 error_log('Resetting core_component cache after core upgrade to version ' . self
::fetch_core_version());
264 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
265 // $CFG->dirroot was changed.
267 // The cache looks ok, let's use it.
268 self
::$plugintypes = $cache['plugintypes'];
269 self
::$plugins = $cache['plugins'];
270 self
::$subsystems = $cache['subsystems'];
271 self
::$parents = $cache['parents'];
272 self
::$subplugins = $cache['subplugins'];
273 self
::$classmap = $cache['classmap'];
274 self
::$classmaprenames = $cache['classmaprenames'];
275 self
::$filemap = $cache['filemap'];
278 // Note: we do not verify $CFG->admin here intentionally,
279 // they must visit admin/index.php after any change.
283 if (!isset(self
::$plugintypes)) {
284 // This needs to be atomic and self-fixing as much as possible.
286 $content = self
::get_cache_content();
287 if (file_exists($cachefile)) {
288 if (sha1_file($cachefile) === sha1($content)) {
291 // Stale cache detected!
295 // Permissions might not be setup properly in installers.
296 $dirpermissions = !isset($CFG->directorypermissions
) ?
02777 : $CFG->directorypermissions
;
297 $filepermissions = !isset($CFG->filepermissions
) ?
($dirpermissions & 0666) : $CFG->filepermissions
;
300 $cachedir = dirname($cachefile);
301 if (!is_dir($cachedir)) {
302 mkdir($cachedir, $dirpermissions, true);
305 if ($fp = @fopen
($cachefile.'.tmp', 'xb')) {
306 fwrite($fp, $content);
308 @rename
($cachefile.'.tmp', $cachefile);
309 @chmod
($cachefile, $filepermissions);
311 @unlink
($cachefile.'.tmp'); // Just in case anything fails (race condition).
312 self
::invalidate_opcode_php_cache($cachefile);
317 * Are we in developer debug mode?
319 * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
320 * the reason is we need to use this before we setup DB connection or caches for CFG.
324 protected static function is_developer() {
327 // Note we can not rely on $CFG->debug here because DB is not initialised yet.
328 if (isset($CFG->config_php_settings
['debug'])) {
329 $debug = (int)$CFG->config_php_settings
['debug'];
334 if ($debug & E_ALL
and $debug & E_STRICT
) {
342 * Create cache file content.
344 * @private this is intended for $CFG->alternative_component_cache only.
348 public static function get_cache_content() {
349 if (!isset(self
::$plugintypes)) {
350 self
::fill_all_caches();
354 'subsystems' => self
::$subsystems,
355 'plugintypes' => self
::$plugintypes,
356 'plugins' => self
::$plugins,
357 'parents' => self
::$parents,
358 'subplugins' => self
::$subplugins,
359 'classmap' => self
::$classmap,
360 'classmaprenames' => self
::$classmaprenames,
361 'filemap' => self
::$filemap,
362 'version' => self
::$version,
366 $cache = '.var_export($cache, true).';
373 protected static function fill_all_caches() {
374 self
::$subsystems = self
::fetch_subsystems();
376 list(self
::$plugintypes, self
::$parents, self
::$subplugins) = self
::fetch_plugintypes();
378 self
::$plugins = array();
379 foreach (self
::$plugintypes as $type => $fulldir) {
380 self
::$plugins[$type] = self
::fetch_plugins($type, $fulldir);
383 self
::fill_classmap_cache();
384 self
::fill_classmap_renames_cache();
385 self
::fill_filemap_cache();
386 self
::fetch_core_version();
390 * Get the core version.
392 * In order for this to work properly, opcache should be reset beforehand.
394 * @return float core version.
396 protected static function fetch_core_version() {
398 if (self
::$version === null) {
399 $version = null; // Prevent IDE complaints.
400 require($CFG->dirroot
. '/version.php');
401 self
::$version = $version;
403 return self
::$version;
407 * Returns list of core subsystems.
410 protected static function fetch_subsystems() {
413 // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
417 'admin' => $CFG->dirroot
.'/'.$CFG->admin
,
418 'analytics' => $CFG->dirroot
. '/analytics',
419 'antivirus' => $CFG->dirroot
. '/lib/antivirus',
420 'auth' => $CFG->dirroot
.'/auth',
421 'availability' => $CFG->dirroot
. '/availability',
422 'backup' => $CFG->dirroot
.'/backup/util/ui',
423 'badges' => $CFG->dirroot
.'/badges',
424 'block' => $CFG->dirroot
.'/blocks',
425 'blog' => $CFG->dirroot
.'/blog',
427 'cache' => $CFG->dirroot
.'/cache',
428 'calendar' => $CFG->dirroot
.'/calendar',
429 'cohort' => $CFG->dirroot
.'/cohort',
430 'comment' => $CFG->dirroot
.'/comment',
431 'competency' => $CFG->dirroot
.'/competency',
432 'completion' => $CFG->dirroot
.'/completion',
434 'course' => $CFG->dirroot
.'/course',
435 'currencies' => null,
436 'dbtransfer' => null,
438 'editor' => $CFG->dirroot
.'/lib/editor',
440 'enrol' => $CFG->dirroot
.'/enrol',
442 'filepicker' => null,
443 'fileconverter' => $CFG->dirroot
.'/files/converter',
444 'files' => $CFG->dirroot
.'/files',
446 //'fonts' => null, // Bogus.
447 'form' => $CFG->dirroot
.'/lib/form',
448 'grades' => $CFG->dirroot
.'/grade',
449 'grading' => $CFG->dirroot
.'/grade/grading',
450 'group' => $CFG->dirroot
.'/group',
456 'langconfig' => null,
459 'media' => $CFG->dirroot
.'/media',
460 'message' => $CFG->dirroot
.'/message',
462 'mnet' => $CFG->dirroot
.'/mnet',
463 //'moodle.org' => null, // Not used any more.
464 'my' => $CFG->dirroot
.'/my',
465 'notes' => $CFG->dirroot
.'/notes',
468 'plagiarism' => $CFG->dirroot
.'/plagiarism',
470 'portfolio' => $CFG->dirroot
.'/portfolio',
471 'publish' => $CFG->dirroot
.'/course/publish',
472 'question' => $CFG->dirroot
.'/question',
473 'rating' => $CFG->dirroot
.'/rating',
474 'register' => $CFG->dirroot
.'/'.$CFG->admin
.'/registration', // Broken badly if $CFG->admin changed.
475 'repository' => $CFG->dirroot
.'/repository',
476 'rss' => $CFG->dirroot
.'/rss',
477 'role' => $CFG->dirroot
.'/'.$CFG->admin
.'/roles',
478 'search' => $CFG->dirroot
.'/search',
480 'tag' => $CFG->dirroot
.'/tag',
482 'user' => $CFG->dirroot
.'/user',
484 'webservice' => $CFG->dirroot
.'/webservice',
491 * Returns list of known plugin types.
494 protected static function fetch_plugintypes() {
498 'antivirus' => $CFG->dirroot
. '/lib/antivirus',
499 'availability' => $CFG->dirroot
. '/availability/condition',
500 'qtype' => $CFG->dirroot
.'/question/type',
501 'mod' => $CFG->dirroot
.'/mod',
502 'auth' => $CFG->dirroot
.'/auth',
503 'calendartype' => $CFG->dirroot
.'/calendar/type',
504 'enrol' => $CFG->dirroot
.'/enrol',
505 'message' => $CFG->dirroot
.'/message/output',
506 'block' => $CFG->dirroot
.'/blocks',
507 'media' => $CFG->dirroot
.'/media/player',
508 'filter' => $CFG->dirroot
.'/filter',
509 'editor' => $CFG->dirroot
.'/lib/editor',
510 'format' => $CFG->dirroot
.'/course/format',
511 'dataformat' => $CFG->dirroot
.'/dataformat',
512 'profilefield' => $CFG->dirroot
.'/user/profile/field',
513 'report' => $CFG->dirroot
.'/report',
514 'coursereport' => $CFG->dirroot
.'/course/report', // Must be after system reports.
515 'gradeexport' => $CFG->dirroot
.'/grade/export',
516 'gradeimport' => $CFG->dirroot
.'/grade/import',
517 'gradereport' => $CFG->dirroot
.'/grade/report',
518 'gradingform' => $CFG->dirroot
.'/grade/grading/form',
519 'mlbackend' => $CFG->dirroot
.'/lib/mlbackend',
520 'mnetservice' => $CFG->dirroot
.'/mnet/service',
521 'webservice' => $CFG->dirroot
.'/webservice',
522 'repository' => $CFG->dirroot
.'/repository',
523 'portfolio' => $CFG->dirroot
.'/portfolio',
524 'search' => $CFG->dirroot
.'/search/engine',
525 'qbehaviour' => $CFG->dirroot
.'/question/behaviour',
526 'qformat' => $CFG->dirroot
.'/question/format',
527 'plagiarism' => $CFG->dirroot
.'/plagiarism',
528 'tool' => $CFG->dirroot
.'/'.$CFG->admin
.'/tool',
529 'cachestore' => $CFG->dirroot
.'/cache/stores',
530 'cachelock' => $CFG->dirroot
.'/cache/locks',
531 'fileconverter' => $CFG->dirroot
.'/files/converter',
534 $subplugins = array();
536 if (!empty($CFG->themedir
) and is_dir($CFG->themedir
) ) {
537 $types['theme'] = $CFG->themedir
;
539 $types['theme'] = $CFG->dirroot
.'/theme';
542 foreach (self
::$supportsubplugins as $type) {
543 if ($type === 'local') {
544 // Local subplugins must be after local plugins.
547 $plugins = self
::fetch_plugins($type, $types[$type]);
548 foreach ($plugins as $plugin => $fulldir) {
549 $subtypes = self
::fetch_subtypes($fulldir);
553 $subplugins[$type.'_'.$plugin] = array();
554 foreach($subtypes as $subtype => $subdir) {
555 if (isset($types[$subtype])) {
556 error_log("Invalid subtype '$subtype', duplicate detected.");
559 $types[$subtype] = $subdir;
560 $parents[$subtype] = $type.'_'.$plugin;
561 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self
::fetch_plugins($subtype, $subdir));
565 // Local is always last!
566 $types['local'] = $CFG->dirroot
.'/local';
568 if (in_array('local', self
::$supportsubplugins)) {
570 $plugins = self
::fetch_plugins($type, $types[$type]);
571 foreach ($plugins as $plugin => $fulldir) {
572 $subtypes = self
::fetch_subtypes($fulldir);
576 $subplugins[$type.'_'.$plugin] = array();
577 foreach($subtypes as $subtype => $subdir) {
578 if (isset($types[$subtype])) {
579 error_log("Invalid subtype '$subtype', duplicate detected.");
582 $types[$subtype] = $subdir;
583 $parents[$subtype] = $type.'_'.$plugin;
584 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self
::fetch_plugins($subtype, $subdir));
589 return array($types, $parents, $subplugins);
593 * Returns list of subtypes.
594 * @param string $ownerdir
597 protected static function fetch_subtypes($ownerdir) {
601 if (file_exists("$ownerdir/db/subplugins.php")) {
602 $subplugins = array();
603 include("$ownerdir/db/subplugins.php");
604 foreach ($subplugins as $subtype => $dir) {
605 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
606 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
609 if (isset(self
::$subsystems[$subtype])) {
610 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
613 if ($CFG->admin
!== 'admin' and strpos($dir, 'admin/') === 0) {
614 $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
616 if (!is_dir("$CFG->dirroot/$dir")) {
617 error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
620 $types[$subtype] = "$CFG->dirroot/$dir";
627 * Returns list of plugins of given type in given directory.
628 * @param string $plugintype
629 * @param string $fulldir
632 protected static function fetch_plugins($plugintype, $fulldir) {
635 $fulldirs = (array)$fulldir;
636 if ($plugintype === 'theme') {
637 if (realpath($fulldir) !== realpath($CFG->dirroot
.'/theme')) {
638 // Include themes in standard location too.
639 array_unshift($fulldirs, $CFG->dirroot
.'/theme');
645 foreach ($fulldirs as $fulldir) {
646 if (!is_dir($fulldir)) {
649 $items = new \
DirectoryIterator($fulldir);
650 foreach ($items as $item) {
651 if ($item->isDot() or !$item->isDir()) {
654 $pluginname = $item->getFilename();
655 if ($plugintype === 'auth' and $pluginname === 'db') {
656 // Special exception for this wrong plugin name.
657 } else if (isset(self
::$ignoreddirs[$pluginname])) {
660 if (!self
::is_valid_plugin_name($plugintype, $pluginname)) {
661 // Always ignore plugins with problematic names here.
664 $result[$pluginname] = $fulldir.'/'.$pluginname;
675 * Find all classes that can be autoloaded including frankenstyle namespaces.
677 protected static function fill_classmap_cache() {
680 self
::$classmap = array();
682 self
::load_classes('core', "$CFG->dirroot/lib/classes");
684 foreach (self
::$subsystems as $subsystem => $fulldir) {
688 self
::load_classes('core_'.$subsystem, "$fulldir/classes");
691 foreach (self
::$plugins as $plugintype => $plugins) {
692 foreach ($plugins as $pluginname => $fulldir) {
693 self
::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
696 ksort(self
::$classmap);
700 * Fills up the cache defining what plugins have certain files.
702 * @see self::get_plugin_list_with_file
705 protected static function fill_filemap_cache() {
708 self
::$filemap = array();
710 foreach (self
::$filestomap as $file) {
711 if (!isset(self
::$filemap[$file])) {
712 self
::$filemap[$file] = array();
714 foreach (self
::$plugins as $plugintype => $plugins) {
715 if (!isset(self
::$filemap[$file][$plugintype])) {
716 self
::$filemap[$file][$plugintype] = array();
718 foreach ($plugins as $pluginname => $fulldir) {
719 if (file_exists("$fulldir/$file")) {
720 self
::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
728 * Find classes in directory and recurse to subdirs.
729 * @param string $component
730 * @param string $fulldir
731 * @param string $namespace
733 protected static function load_classes($component, $fulldir, $namespace = '') {
734 if (!is_dir($fulldir)) {
738 if (!is_readable($fulldir)) {
739 // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
740 // because its pretty likely to lead to a missing class error further down the line.
741 // But our early setup code can't handle errors this early at the moment.
745 $items = new \
DirectoryIterator($fulldir);
746 foreach ($items as $item) {
747 if ($item->isDot()) {
750 if ($item->isDir()) {
751 $dirname = $item->getFilename();
752 self
::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
756 $filename = $item->getFilename();
757 $classname = preg_replace('/\.php$/', '', $filename);
759 if ($filename === $classname) {
763 if ($namespace === '') {
764 // Legacy long frankenstyle class name.
765 self
::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
767 // New namespaced classes.
768 self
::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
776 * List all core subsystems and their location
778 * This is a whitelist of components that are part of the core and their
779 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
780 * plugin is not listed here and it does not have proper plugintype prefix,
781 * then it is considered as course activity module.
783 * The location is absolute file path to dir. NULL means there is no special
784 * directory for this subsystem. If the location is set, the subsystem's
785 * renderer.php is expected to be there.
787 * @return array of (string)name => (string|null)full dir location
789 public static function get_core_subsystems() {
791 return self
::$subsystems;
795 * Get list of available plugin types together with their location.
797 * @return array as (string)plugintype => (string)fulldir
799 public static function get_plugin_types() {
801 return self
::$plugintypes;
805 * Get list of plugins of given type.
807 * @param string $plugintype
808 * @return array as (string)pluginname => (string)fulldir
810 public static function get_plugin_list($plugintype) {
813 if (!isset(self
::$plugins[$plugintype])) {
816 return self
::$plugins[$plugintype];
820 * Get a list of all the plugins of a given type that define a certain class
821 * in a certain file. The plugin component names and class names are returned.
823 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
824 * @param string $class the part of the name of the class after the
825 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
826 * names like report_courselist_thing. If you are looking for classes with
827 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
828 * Frankenstyle namespaces are also supported.
829 * @param string $file the name of file within the plugin that defines the class.
830 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
831 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
833 public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
834 global $CFG; // Necessary in case it is referenced by included PHP scripts.
837 $suffix = '_' . $class;
842 $pluginclasses = array();
843 $plugins = self
::get_plugin_list($plugintype);
844 foreach ($plugins as $plugin => $fulldir) {
845 // Try class in frankenstyle namespace.
847 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
848 if (class_exists($classname, true)) {
849 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
854 // Try autoloading of class with frankenstyle prefix.
855 $classname = $plugintype . '_' . $plugin . $suffix;
856 if (class_exists($classname, true)) {
857 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
861 // Fall back to old file location and class name.
862 if ($file and file_exists("$fulldir/$file")) {
863 include_once("$fulldir/$file");
864 if (class_exists($classname, false)) {
865 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
871 return $pluginclasses;
875 * Get a list of all the plugins of a given type that contain a particular file.
877 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
878 * @param string $file the name of file that must be present in the plugin.
879 * (e.g. 'view.php', 'db/install.xml').
880 * @param bool $include if true (default false), the file will be include_once-ed if found.
881 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
882 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
884 public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
885 global $CFG; // Necessary in case it is referenced by included PHP scripts.
886 $pluginfiles = array();
888 if (isset(self
::$filemap[$file])) {
889 // If the file was supposed to be mapped, then it should have been set in the array.
890 if (isset(self
::$filemap[$file][$plugintype])) {
891 $pluginfiles = self
::$filemap[$file][$plugintype];
894 // Old-style search for non-cached files.
895 $plugins = self
::get_plugin_list($plugintype);
896 foreach ($plugins as $plugin => $fulldir) {
897 $path = $fulldir . '/' . $file;
898 if (file_exists($path)) {
899 $pluginfiles[$plugin] = $path;
905 foreach ($pluginfiles as $path) {
914 * Returns all classes in a component matching the provided namespace.
916 * It checks that the class exists.
918 * e.g. get_component_classes_in_namespace('mod_forum', 'event')
920 * @param string $component A valid moodle component (frankenstyle)
921 * @param string $namespace Namespace from the component name or empty if all $component namespace classes.
922 * @return array The full class name as key and the class path as value.
924 public static function get_component_classes_in_namespace($component, $namespace = '') {
926 $component = self
::normalize_componentname($component);
930 // We will add them later.
931 $namespace = trim($namespace, '\\');
933 // We need add double backslashes as it is how classes are stored into self::$classmap.
934 $namespace = implode('\\\\', explode('\\', $namespace));
935 $namespace = $namespace . '\\\\';
938 $regex = '|^' . $component . '\\\\' . $namespace . '|';
939 $it = new RegexIterator(new ArrayIterator(self
::$classmap), $regex, RegexIterator
::GET_MATCH
, RegexIterator
::USE_KEY
);
941 // We want to be sure that they exist.
943 foreach ($it as $classname => $classpath) {
944 if (class_exists($classname)) {
945 $classes[$classname] = $classpath;
953 * Returns the exact absolute path to plugin directory.
955 * @param string $plugintype type of plugin
956 * @param string $pluginname name of the plugin
957 * @return string full path to plugin directory; null if not found
959 public static function get_plugin_directory($plugintype, $pluginname) {
960 if (empty($pluginname)) {
961 // Invalid plugin name, sorry.
967 if (!isset(self
::$plugins[$plugintype][$pluginname])) {
970 return self
::$plugins[$plugintype][$pluginname];
974 * Returns the exact absolute path to plugin directory.
976 * @param string $subsystem type of core subsystem
977 * @return string full path to subsystem directory; null if not found
979 public static function get_subsystem_directory($subsystem) {
982 if (!isset(self
::$subsystems[$subsystem])) {
985 return self
::$subsystems[$subsystem];
989 * This method validates a plug name. It is much faster than calling clean_param.
991 * @param string $plugintype type of plugin
992 * @param string $pluginname a string that might be a plugin name.
993 * @return bool if this string is a valid plugin name.
995 public static function is_valid_plugin_name($plugintype, $pluginname) {
996 if ($plugintype === 'mod') {
997 // Modules must not have the same name as core subsystems.
998 if (!isset(self
::$subsystems)) {
999 // Watch out, this is called from init!
1002 if (isset(self
::$subsystems[$pluginname])) {
1005 // Modules MUST NOT have any underscores,
1006 // component normalisation would break very badly otherwise!
1007 return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1010 return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1015 * Normalize the component name.
1017 * Note: this does not verify the validity of the plugin or component.
1019 * @param string $component
1022 public static function normalize_componentname($componentname) {
1023 list($plugintype, $pluginname) = self
::normalize_component($componentname);
1024 if ($plugintype === 'core' && is_null($pluginname)) {
1027 return $plugintype . '_' . $pluginname;
1031 * Normalize the component name using the "frankenstyle" rules.
1033 * Note: this does not verify the validity of plugin or type names.
1035 * @param string $component
1036 * @return array two-items list of [(string)type, (string|null)name]
1038 public static function normalize_component($component) {
1039 if ($component === 'moodle' or $component === 'core' or $component === '') {
1040 return array('core', null);
1043 if (strpos($component, '_') === false) {
1045 if (array_key_exists($component, self
::$subsystems)) {
1047 $plugin = $component;
1049 // Everything else without underscore is a module.
1051 $plugin = $component;
1055 list($type, $plugin) = explode('_', $component, 2);
1056 if ($type === 'moodle') {
1059 // Any unknown type must be a subplugin.
1062 return array($type, $plugin);
1066 * Return exact absolute path to a plugin directory.
1068 * @param string $component name such as 'moodle', 'mod_forum'
1069 * @return string full path to component directory; NULL if not found
1071 public static function get_component_directory($component) {
1074 list($type, $plugin) = self
::normalize_component($component);
1076 if ($type === 'core') {
1077 if ($plugin === null) {
1078 return $path = $CFG->libdir
;
1080 return self
::get_subsystem_directory($plugin);
1083 return self
::get_plugin_directory($type, $plugin);
1087 * Returns list of plugin types that allow subplugins.
1088 * @return array as (string)plugintype => (string)fulldir
1090 public static function get_plugin_types_with_subplugins() {
1094 foreach (self
::$supportsubplugins as $type) {
1095 $return[$type] = self
::$plugintypes[$type];
1101 * Returns parent of this subplugin type.
1103 * @param string $type
1104 * @return string parent component or null
1106 public static function get_subtype_parent($type) {
1109 if (isset(self
::$parents[$type])) {
1110 return self
::$parents[$type];
1117 * Return all subplugins of this component.
1118 * @param string $component.
1119 * @return array $subtype=>array($component, ..), null if no subtypes defined
1121 public static function get_subplugins($component) {
1124 if (isset(self
::$subplugins[$component])) {
1125 return self
::$subplugins[$component];
1132 * Returns hash of all versions including core and all plugins.
1134 * This is relatively slow and not fully cached, use with care!
1136 * @return string sha1 hash
1138 public static function get_all_versions_hash() {
1143 $versions = array();
1145 // Main version first.
1146 $versions['core'] = self
::fetch_core_version();
1148 // The problem here is tha the component cache might be stable,
1149 // we want this to work also on frontpage without resetting the component cache.
1151 if (CACHE_DISABLE_ALL
or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE
)) {
1156 $plugintypes = core_component
::get_plugin_types();
1157 foreach ($plugintypes as $type => $typedir) {
1159 $plugs = core_component
::get_plugin_list($type);
1161 $plugs = self
::fetch_plugins($type, $typedir);
1163 foreach ($plugs as $plug => $fullplug) {
1164 $plugin = new stdClass();
1165 $plugin->version
= null;
1167 include($fullplug.'/version.php');
1168 $versions[$type.'_'.$plug] = $plugin->version
;
1172 return sha1(serialize($versions));
1176 * Invalidate opcode cache for given file, this is intended for
1177 * php files that are stored in dataroot.
1179 * Note: we need it here because this class must be self-contained.
1181 * @param string $file
1183 public static function invalidate_opcode_php_cache($file) {
1184 if (function_exists('opcache_invalidate')) {
1185 if (!file_exists($file)) {
1188 opcache_invalidate($file, true);
1193 * Return true if subsystemname is core subsystem.
1195 * @param string $subsystemname name of the subsystem.
1196 * @return bool true if core subsystem.
1198 public static function is_core_subsystem($subsystemname) {
1199 return isset(self
::$subsystems[$subsystemname]);
1203 * Records all class renames that have been made to facilitate autoloading.
1205 protected static function fill_classmap_renames_cache() {
1208 self
::$classmaprenames = array();
1210 self
::load_renamed_classes("$CFG->dirroot/lib/");
1212 foreach (self
::$subsystems as $subsystem => $fulldir) {
1213 self
::load_renamed_classes($fulldir);
1216 foreach (self
::$plugins as $plugintype => $plugins) {
1217 foreach ($plugins as $pluginname => $fulldir) {
1218 self
::load_renamed_classes($fulldir);
1224 * Loads the db/renamedclasses.php file from the given directory.
1226 * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1227 * and the value is the new class name.
1228 * It is only included when we are populating the component cache. After that is not needed.
1230 * @param string $fulldir
1232 protected static function load_renamed_classes($fulldir) {
1233 $file = $fulldir . '/db/renamedclasses.php';
1234 if (is_readable($file)) {
1235 $renamedclasses = null;
1237 if (is_array($renamedclasses)) {
1238 foreach ($renamedclasses as $oldclass => $newclass) {
1239 self
::$classmaprenames[(string)$oldclass] = (string)$newclass;