MDL-54592 cachestore_mongodb: MongoDB cache store use new driver.
[moodle.git] / lib / classes / component.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Components (core subsystems + plugins) related code.
19  *
20  * @package    core
21  * @copyright  2013 Petr Skoda {@link http://skodak.org}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 // Constants used in version.php files, these must exist when core_component executes.
29 /** Software maturity level - internals can be tested using white box techniques. */
30 define('MATURITY_ALPHA',    50);
31 /** Software maturity level - feature complete, ready for preview and testing. */
32 define('MATURITY_BETA',     100);
33 /** Software maturity level - tested, will be released unless there are fatal bugs. */
34 define('MATURITY_RC',       150);
35 /** Software maturity level - ready for production deployment. */
36 define('MATURITY_STABLE',   200);
37 /** Any version - special value that can be used in $plugin->dependencies in version.php files. */
38 define('ANY_VERSION', 'any');
41 /**
42  * Collection of components related methods.
43  */
44 class core_component {
45     /** @var array list of ignored directories - 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',
74     );
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',
89         'MongoDB' => 'cache/stores/mongodb/MongoDB',
90     );
92     /**
93      * Class loader for Frankenstyle named classes in standard locations.
94      * Frankenstyle namespaces are supported.
95      *
96      * The expected location for core classes is:
97      *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
98      *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
99      *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
100      *
101      * The expected location for plugin classes is:
102      *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
103      *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
104      *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
105      *
106      * @param string $classname
107      */
108     public static function classloader($classname) {
109         self::init();
111         if (isset(self::$classmap[$classname])) {
112             // Global $CFG is expected in included scripts.
113             global $CFG;
114             // Function include would be faster, but for BC it is better to include only once.
115             include_once(self::$classmap[$classname]);
116             return;
117         }
118         if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
119             $newclassname = self::$classmaprenames[$classname];
120             $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
121             debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
122             if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
123                 throw new \coding_exception("Cannot alias $classname to $newclassname");
124             }
125             class_alias($newclassname, $classname);
126             return;
127         }
129         $file = self::psr_classloader($classname);
130         // If the file is found, require it.
131         if (!empty($file)) {
132             require($file);
133             return;
134         }
135     }
137     /**
138      * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
139      * demand. Only returns paths to files that exist.
140      *
141      * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
142      * compatible.
143      *
144      * @param string $class the name of the class.
145      * @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      */
147     protected static function psr_classloader($class) {
148         // Iterate through each PSR-4 namespace prefix.
149         foreach (self::$psr4namespaces as $prefix => $path) {
150             $file = self::get_class_file($class, $prefix, $path, array('\\'));
151             if (!empty($file) && file_exists($file)) {
152                 return $file;
153             }
154         }
156         // Iterate through each PSR-0 namespace prefix.
157         foreach (self::$psr0namespaces as $prefix => $path) {
158             $file = self::get_class_file($class, $prefix, $path, array('\\', '_'));
159             if (!empty($file) && file_exists($file)) {
160                 return $file;
161             }
162         }
164         return false;
165     }
167     /**
168      * Return the path to the class based on the given namespace prefix and path it corresponds to.
169      *
170      * Will return the path even if the file does not exist. Check the file esists before requiring.
171      *
172      * @param string $class the name of the class.
173      * @param string $prefix The namespace prefix used to identify the base directory of the source files.
174      * @param string $path The relative path to the base directory of the source files.
175      * @param string[] $separators The characters that should be used for separating.
176      * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
177      */
178     protected static function get_class_file($class, $prefix, $path, $separators) {
179         global $CFG;
181         // Does the class use the namespace prefix?
182         $len = strlen($prefix);
183         if (strncmp($prefix, $class, $len) !== 0) {
184             // No, move to the next prefix.
185             return false;
186         }
187         $path = $CFG->dirroot . '/' . $path;
189         // Get the relative class name.
190         $relativeclass = substr($class, $len);
192         // Replace the namespace prefix with the base directory, replace namespace
193         // separators with directory separators in the relative class name, append
194         // with .php.
195         $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
197         return $file;
198     }
201     /**
202      * Initialise caches, always call before accessing self:: caches.
203      */
204     protected static function init() {
205         global $CFG;
207         // Init only once per request/CLI execution, we ignore changes done afterwards.
208         if (isset(self::$plugintypes)) {
209             return;
210         }
212         if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
213             self::fill_all_caches();
214             return;
215         }
217         if (!empty($CFG->alternative_component_cache)) {
218             // Hack for heavily clustered sites that want to manage component cache invalidation manually.
219             $cachefile = $CFG->alternative_component_cache;
221             if (file_exists($cachefile)) {
222                 if (CACHE_DISABLE_ALL) {
223                     // Verify the cache state only on upgrade pages.
224                     $content = self::get_cache_content();
225                     if (sha1_file($cachefile) !== sha1($content)) {
226                         die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
227                     }
228                     return;
229                 }
230                 $cache = array();
231                 include($cachefile);
232                 self::$plugintypes      = $cache['plugintypes'];
233                 self::$plugins          = $cache['plugins'];
234                 self::$subsystems       = $cache['subsystems'];
235                 self::$parents          = $cache['parents'];
236                 self::$subplugins       = $cache['subplugins'];
237                 self::$classmap         = $cache['classmap'];
238                 self::$classmaprenames  = $cache['classmaprenames'];
239                 self::$filemap          = $cache['filemap'];
240                 return;
241             }
243             if (!is_writable(dirname($cachefile))) {
244                 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
245             }
247             // Lets try to create the file, it might be in some writable directory or a local cache dir.
249         } else {
250             // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
251             //       use $CFG->alternative_component_cache if you do not like it.
252             $cachefile = "$CFG->cachedir/core_component.php";
253         }
255         if (!CACHE_DISABLE_ALL and !self::is_developer()) {
256             // 1/ Use the cache only outside of install and upgrade.
257             // 2/ Let developers add/remove classes in developer mode.
258             if (is_readable($cachefile)) {
259                 $cache = false;
260                 include($cachefile);
261                 if (!is_array($cache)) {
262                     // Something is very wrong.
263                 } else if (!isset($cache['version'])) {
264                     // Something is very wrong.
265                 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
266                     // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
267                     error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
268                 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
269                     // $CFG->dirroot was changed.
270                 } else {
271                     // The cache looks ok, let's use it.
272                     self::$plugintypes      = $cache['plugintypes'];
273                     self::$plugins          = $cache['plugins'];
274                     self::$subsystems       = $cache['subsystems'];
275                     self::$parents          = $cache['parents'];
276                     self::$subplugins       = $cache['subplugins'];
277                     self::$classmap         = $cache['classmap'];
278                     self::$classmaprenames  = $cache['classmaprenames'];
279                     self::$filemap          = $cache['filemap'];
280                     return;
281                 }
282                 // Note: we do not verify $CFG->admin here intentionally,
283                 //       they must visit admin/index.php after any change.
284             }
285         }
287         if (!isset(self::$plugintypes)) {
288             // This needs to be atomic and self-fixing as much as possible.
290             $content = self::get_cache_content();
291             if (file_exists($cachefile)) {
292                 if (sha1_file($cachefile) === sha1($content)) {
293                     return;
294                 }
295                 // Stale cache detected!
296                 unlink($cachefile);
297             }
299             // Permissions might not be setup properly in installers.
300             $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
301             $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
303             clearstatcache();
304             $cachedir = dirname($cachefile);
305             if (!is_dir($cachedir)) {
306                 mkdir($cachedir, $dirpermissions, true);
307             }
309             if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
310                 fwrite($fp, $content);
311                 fclose($fp);
312                 @rename($cachefile.'.tmp', $cachefile);
313                 @chmod($cachefile, $filepermissions);
314             }
315             @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
316             self::invalidate_opcode_php_cache($cachefile);
317         }
318     }
320     /**
321      * Are we in developer debug mode?
322      *
323      * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
324      *       the reason is we need to use this before we setup DB connection or caches for CFG.
325      *
326      * @return bool
327      */
328     protected static function is_developer() {
329         global $CFG;
331         // Note we can not rely on $CFG->debug here because DB is not initialised yet.
332         if (isset($CFG->config_php_settings['debug'])) {
333             $debug = (int)$CFG->config_php_settings['debug'];
334         } else {
335             return false;
336         }
338         if ($debug & E_ALL and $debug & E_STRICT) {
339             return true;
340         }
342         return false;
343     }
345     /**
346      * Create cache file content.
347      *
348      * @private this is intended for $CFG->alternative_component_cache only.
349      *
350      * @return string
351      */
352     public static function get_cache_content() {
353         if (!isset(self::$plugintypes)) {
354             self::fill_all_caches();
355         }
357         $cache = array(
358             'subsystems'        => self::$subsystems,
359             'plugintypes'       => self::$plugintypes,
360             'plugins'           => self::$plugins,
361             'parents'           => self::$parents,
362             'subplugins'        => self::$subplugins,
363             'classmap'          => self::$classmap,
364             'classmaprenames'   => self::$classmaprenames,
365             'filemap'           => self::$filemap,
366             'version'           => self::$version,
367         );
369         return '<?php
370 $cache = '.var_export($cache, true).';
371 ';
372     }
374     /**
375      * Fill all caches.
376      */
377     protected static function fill_all_caches() {
378         self::$subsystems = self::fetch_subsystems();
380         list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
382         self::$plugins = array();
383         foreach (self::$plugintypes as $type => $fulldir) {
384             self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
385         }
387         self::fill_classmap_cache();
388         self::fill_classmap_renames_cache();
389         self::fill_filemap_cache();
390         self::fetch_core_version();
391     }
393     /**
394      * Get the core version.
395      *
396      * In order for this to work properly, opcache should be reset beforehand.
397      *
398      * @return float core version.
399      */
400     protected static function fetch_core_version() {
401         global $CFG;
402         if (self::$version === null) {
403             $version = null; // Prevent IDE complaints.
404             require($CFG->dirroot . '/version.php');
405             self::$version = $version;
406         }
407         return self::$version;
408     }
410     /**
411      * Returns list of core subsystems.
412      * @return array
413      */
414     protected static function fetch_subsystems() {
415         global $CFG;
417         // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
419         $info = array(
420             'access'      => null,
421             'admin'       => $CFG->dirroot.'/'.$CFG->admin,
422             'analytics'   => $CFG->dirroot . '/analytics',
423             'antivirus'   => $CFG->dirroot . '/lib/antivirus',
424             'auth'        => $CFG->dirroot.'/auth',
425             'availability' => $CFG->dirroot . '/availability',
426             'backup'      => $CFG->dirroot.'/backup/util/ui',
427             'badges'      => $CFG->dirroot.'/badges',
428             'block'       => $CFG->dirroot.'/blocks',
429             'blog'        => $CFG->dirroot.'/blog',
430             'bulkusers'   => null,
431             'cache'       => $CFG->dirroot.'/cache',
432             'calendar'    => $CFG->dirroot.'/calendar',
433             'cohort'      => $CFG->dirroot.'/cohort',
434             'comment'     => $CFG->dirroot.'/comment',
435             'competency'  => $CFG->dirroot.'/competency',
436             'completion'  => $CFG->dirroot.'/completion',
437             'countries'   => null,
438             'course'      => $CFG->dirroot.'/course',
439             'currencies'  => null,
440             'customfield' => $CFG->dirroot.'/customfield',
441             'dbtransfer'  => null,
442             'debug'       => null,
443             'editor'      => $CFG->dirroot.'/lib/editor',
444             'edufields'   => null,
445             'enrol'       => $CFG->dirroot.'/enrol',
446             'error'       => null,
447             'favourites'  => $CFG->dirroot . '/favourites',
448             'filepicker'  => null,
449             'fileconverter' => $CFG->dirroot.'/files/converter',
450             'files'       => $CFG->dirroot.'/files',
451             'filters'     => $CFG->dirroot.'/filter',
452             //'fonts'       => null, // Bogus.
453             'form'        => $CFG->dirroot.'/lib/form',
454             'grades'      => $CFG->dirroot.'/grade',
455             'grading'     => $CFG->dirroot.'/grade/grading',
456             'group'       => $CFG->dirroot.'/group',
457             'help'        => null,
458             'hub'         => null,
459             'imscc'       => null,
460             'install'     => null,
461             'iso6392'     => null,
462             'langconfig'  => null,
463             'license'     => null,
464             'mathslib'    => null,
465             'media'       => $CFG->dirroot.'/media',
466             'message'     => $CFG->dirroot.'/message',
467             'mimetypes'   => null,
468             'mnet'        => $CFG->dirroot.'/mnet',
469             //'moodle.org'  => null, // Not used any more.
470             'my'          => $CFG->dirroot.'/my',
471             'notes'       => $CFG->dirroot.'/notes',
472             'pagetype'    => null,
473             'pix'         => null,
474             'plagiarism'  => $CFG->dirroot.'/plagiarism',
475             'plugin'      => null,
476             'portfolio'   => $CFG->dirroot.'/portfolio',
477             'privacy'     => $CFG->dirroot . '/privacy',
478             'question'    => $CFG->dirroot.'/question',
479             'rating'      => $CFG->dirroot.'/rating',
480             'repository'  => $CFG->dirroot.'/repository',
481             'rss'         => $CFG->dirroot.'/rss',
482             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
483             'search'      => $CFG->dirroot.'/search',
484             'table'       => null,
485             'tag'         => $CFG->dirroot.'/tag',
486             'timezones'   => null,
487             'user'        => $CFG->dirroot.'/user',
488             'userkey'     => $CFG->dirroot.'/lib/userkey',
489             'webservice'  => $CFG->dirroot.'/webservice',
490         );
492         return $info;
493     }
495     /**
496      * Returns list of known plugin types.
497      * @return array
498      */
499     protected static function fetch_plugintypes() {
500         global $CFG;
502         $types = array(
503             'antivirus'     => $CFG->dirroot . '/lib/antivirus',
504             'availability'  => $CFG->dirroot . '/availability/condition',
505             'qtype'         => $CFG->dirroot.'/question/type',
506             'mod'           => $CFG->dirroot.'/mod',
507             'auth'          => $CFG->dirroot.'/auth',
508             'calendartype'  => $CFG->dirroot.'/calendar/type',
509             'customfield'   => $CFG->dirroot.'/customfield/field',
510             'enrol'         => $CFG->dirroot.'/enrol',
511             'message'       => $CFG->dirroot.'/message/output',
512             'block'         => $CFG->dirroot.'/blocks',
513             'media'         => $CFG->dirroot.'/media/player',
514             'filter'        => $CFG->dirroot.'/filter',
515             'editor'        => $CFG->dirroot.'/lib/editor',
516             'format'        => $CFG->dirroot.'/course/format',
517             'dataformat'    => $CFG->dirroot.'/dataformat',
518             'profilefield'  => $CFG->dirroot.'/user/profile/field',
519             'report'        => $CFG->dirroot.'/report',
520             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
521             'gradeexport'   => $CFG->dirroot.'/grade/export',
522             'gradeimport'   => $CFG->dirroot.'/grade/import',
523             'gradereport'   => $CFG->dirroot.'/grade/report',
524             'gradingform'   => $CFG->dirroot.'/grade/grading/form',
525             'mlbackend'     => $CFG->dirroot.'/lib/mlbackend',
526             'mnetservice'   => $CFG->dirroot.'/mnet/service',
527             'webservice'    => $CFG->dirroot.'/webservice',
528             'repository'    => $CFG->dirroot.'/repository',
529             'portfolio'     => $CFG->dirroot.'/portfolio',
530             'search'        => $CFG->dirroot.'/search/engine',
531             'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
532             'qformat'       => $CFG->dirroot.'/question/format',
533             'plagiarism'    => $CFG->dirroot.'/plagiarism',
534             'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
535             'cachestore'    => $CFG->dirroot.'/cache/stores',
536             'cachelock'     => $CFG->dirroot.'/cache/locks',
537             'fileconverter' => $CFG->dirroot.'/files/converter',
538         );
539         $parents = array();
540         $subplugins = array();
542         if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
543             $types['theme'] = $CFG->themedir;
544         } else {
545             $types['theme'] = $CFG->dirroot.'/theme';
546         }
548         foreach (self::$supportsubplugins as $type) {
549             if ($type === 'local') {
550                 // Local subplugins must be after local plugins.
551                 continue;
552             }
553             $plugins = self::fetch_plugins($type, $types[$type]);
554             foreach ($plugins as $plugin => $fulldir) {
555                 $subtypes = self::fetch_subtypes($fulldir);
556                 if (!$subtypes) {
557                     continue;
558                 }
559                 $subplugins[$type.'_'.$plugin] = array();
560                 foreach($subtypes as $subtype => $subdir) {
561                     if (isset($types[$subtype])) {
562                         error_log("Invalid subtype '$subtype', duplicate detected.");
563                         continue;
564                     }
565                     $types[$subtype] = $subdir;
566                     $parents[$subtype] = $type.'_'.$plugin;
567                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
568                 }
569             }
570         }
571         // Local is always last!
572         $types['local'] = $CFG->dirroot.'/local';
574         if (in_array('local', self::$supportsubplugins)) {
575             $type = 'local';
576             $plugins = self::fetch_plugins($type, $types[$type]);
577             foreach ($plugins as $plugin => $fulldir) {
578                 $subtypes = self::fetch_subtypes($fulldir);
579                 if (!$subtypes) {
580                     continue;
581                 }
582                 $subplugins[$type.'_'.$plugin] = array();
583                 foreach($subtypes as $subtype => $subdir) {
584                     if (isset($types[$subtype])) {
585                         error_log("Invalid subtype '$subtype', duplicate detected.");
586                         continue;
587                     }
588                     $types[$subtype] = $subdir;
589                     $parents[$subtype] = $type.'_'.$plugin;
590                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
591                 }
592             }
593         }
595         return array($types, $parents, $subplugins);
596     }
598     /**
599      * Returns list of subtypes.
600      * @param string $ownerdir
601      * @return array
602      */
603     protected static function fetch_subtypes($ownerdir) {
604         global $CFG;
606         $types = array();
607         if (file_exists("$ownerdir/db/subplugins.php")) {
608             $subplugins = array();
609             include("$ownerdir/db/subplugins.php");
610             foreach ($subplugins as $subtype => $dir) {
611                 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
612                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
613                     continue;
614                 }
615                 if (isset(self::$subsystems[$subtype])) {
616                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
617                     continue;
618                 }
619                 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
620                     $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
621                 }
622                 if (!is_dir("$CFG->dirroot/$dir")) {
623                     error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
624                     continue;
625                 }
626                 $types[$subtype] = "$CFG->dirroot/$dir";
627             }
628         }
629         return $types;
630     }
632     /**
633      * Returns list of plugins of given type in given directory.
634      * @param string $plugintype
635      * @param string $fulldir
636      * @return array
637      */
638     protected static function fetch_plugins($plugintype, $fulldir) {
639         global $CFG;
641         $fulldirs = (array)$fulldir;
642         if ($plugintype === 'theme') {
643             if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
644                 // Include themes in standard location too.
645                 array_unshift($fulldirs, $CFG->dirroot.'/theme');
646             }
647         }
649         $result = array();
651         foreach ($fulldirs as $fulldir) {
652             if (!is_dir($fulldir)) {
653                 continue;
654             }
655             $items = new \DirectoryIterator($fulldir);
656             foreach ($items as $item) {
657                 if ($item->isDot() or !$item->isDir()) {
658                     continue;
659                 }
660                 $pluginname = $item->getFilename();
661                 if ($plugintype === 'auth' and $pluginname === 'db') {
662                     // Special exception for this wrong plugin name.
663                 } else if (isset(self::$ignoreddirs[$pluginname])) {
664                     continue;
665                 }
666                 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
667                     // Always ignore plugins with problematic names here.
668                     continue;
669                 }
670                 $result[$pluginname] = $fulldir.'/'.$pluginname;
671                 unset($item);
672             }
673             unset($items);
674         }
676         ksort($result);
677         return $result;
678     }
680     /**
681      * Find all classes that can be autoloaded including frankenstyle namespaces.
682      */
683     protected static function fill_classmap_cache() {
684         global $CFG;
686         self::$classmap = array();
688         self::load_classes('core', "$CFG->dirroot/lib/classes");
690         foreach (self::$subsystems as $subsystem => $fulldir) {
691             if (!$fulldir) {
692                 continue;
693             }
694             self::load_classes('core_'.$subsystem, "$fulldir/classes");
695         }
697         foreach (self::$plugins as $plugintype => $plugins) {
698             foreach ($plugins as $pluginname => $fulldir) {
699                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
700             }
701         }
702         ksort(self::$classmap);
703     }
705     /**
706      * Fills up the cache defining what plugins have certain files.
707      *
708      * @see self::get_plugin_list_with_file
709      * @return void
710      */
711     protected static function fill_filemap_cache() {
712         global $CFG;
714         self::$filemap = array();
716         foreach (self::$filestomap as $file) {
717             if (!isset(self::$filemap[$file])) {
718                 self::$filemap[$file] = array();
719             }
720             foreach (self::$plugins as $plugintype => $plugins) {
721                 if (!isset(self::$filemap[$file][$plugintype])) {
722                     self::$filemap[$file][$plugintype] = array();
723                 }
724                 foreach ($plugins as $pluginname => $fulldir) {
725                     if (file_exists("$fulldir/$file")) {
726                         self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
727                     }
728                 }
729             }
730         }
731     }
733     /**
734      * Find classes in directory and recurse to subdirs.
735      * @param string $component
736      * @param string $fulldir
737      * @param string $namespace
738      */
739     protected static function load_classes($component, $fulldir, $namespace = '') {
740         if (!is_dir($fulldir)) {
741             return;
742         }
744         if (!is_readable($fulldir)) {
745             // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
746             // because its pretty likely to lead to a missing class error further down the line.
747             // But our early setup code can't handle errors this early at the moment.
748             return;
749         }
751         $items = new \DirectoryIterator($fulldir);
752         foreach ($items as $item) {
753             if ($item->isDot()) {
754                 continue;
755             }
756             if ($item->isDir()) {
757                 $dirname = $item->getFilename();
758                 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
759                 continue;
760             }
762             $filename = $item->getFilename();
763             $classname = preg_replace('/\.php$/', '', $filename);
765             if ($filename === $classname) {
766                 // Not a php file.
767                 continue;
768             }
769             if ($namespace === '') {
770                 // Legacy long frankenstyle class name.
771                 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
772             }
773             // New namespaced classes.
774             self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
775         }
776         unset($item);
777         unset($items);
778     }
781     /**
782      * List all core subsystems and their location
783      *
784      * This is a whitelist of components that are part of the core and their
785      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
786      * plugin is not listed here and it does not have proper plugintype prefix,
787      * then it is considered as course activity module.
788      *
789      * The location is absolute file path to dir. NULL means there is no special
790      * directory for this subsystem. If the location is set, the subsystem's
791      * renderer.php is expected to be there.
792      *
793      * @return array of (string)name => (string|null)full dir location
794      */
795     public static function get_core_subsystems() {
796         self::init();
797         return self::$subsystems;
798     }
800     /**
801      * Get list of available plugin types together with their location.
802      *
803      * @return array as (string)plugintype => (string)fulldir
804      */
805     public static function get_plugin_types() {
806         self::init();
807         return self::$plugintypes;
808     }
810     /**
811      * Get list of plugins of given type.
812      *
813      * @param string $plugintype
814      * @return array as (string)pluginname => (string)fulldir
815      */
816     public static function get_plugin_list($plugintype) {
817         self::init();
819         if (!isset(self::$plugins[$plugintype])) {
820             return array();
821         }
822         return self::$plugins[$plugintype];
823     }
825     /**
826      * Get a list of all the plugins of a given type that define a certain class
827      * in a certain file. The plugin component names and class names are returned.
828      *
829      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
830      * @param string $class the part of the name of the class after the
831      *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
832      *      names like report_courselist_thing. If you are looking for classes with
833      *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
834      *      Frankenstyle namespaces are also supported.
835      * @param string $file the name of file within the plugin that defines the class.
836      * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
837      *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
838      */
839     public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
840         global $CFG; // Necessary in case it is referenced by included PHP scripts.
842         if ($class) {
843             $suffix = '_' . $class;
844         } else {
845             $suffix = '';
846         }
848         $pluginclasses = array();
849         $plugins = self::get_plugin_list($plugintype);
850         foreach ($plugins as $plugin => $fulldir) {
851             // Try class in frankenstyle namespace.
852             if ($class) {
853                 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
854                 if (class_exists($classname, true)) {
855                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
856                     continue;
857                 }
858             }
860             // Try autoloading of class with frankenstyle prefix.
861             $classname = $plugintype . '_' . $plugin . $suffix;
862             if (class_exists($classname, true)) {
863                 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
864                 continue;
865             }
867             // Fall back to old file location and class name.
868             if ($file and file_exists("$fulldir/$file")) {
869                 include_once("$fulldir/$file");
870                 if (class_exists($classname, false)) {
871                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
872                     continue;
873                 }
874             }
875         }
877         return $pluginclasses;
878     }
880     /**
881      * Get a list of all the plugins of a given type that contain a particular file.
882      *
883      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
884      * @param string $file the name of file that must be present in the plugin.
885      *                     (e.g. 'view.php', 'db/install.xml').
886      * @param bool $include if true (default false), the file will be include_once-ed if found.
887      * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
888      *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
889      */
890     public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
891         global $CFG; // Necessary in case it is referenced by included PHP scripts.
892         $pluginfiles = array();
894         if (isset(self::$filemap[$file])) {
895             // If the file was supposed to be mapped, then it should have been set in the array.
896             if (isset(self::$filemap[$file][$plugintype])) {
897                 $pluginfiles = self::$filemap[$file][$plugintype];
898             }
899         } else {
900             // Old-style search for non-cached files.
901             $plugins = self::get_plugin_list($plugintype);
902             foreach ($plugins as $plugin => $fulldir) {
903                 $path = $fulldir . '/' . $file;
904                 if (file_exists($path)) {
905                     $pluginfiles[$plugin] = $path;
906                 }
907             }
908         }
910         if ($include) {
911             foreach ($pluginfiles as $path) {
912                 include_once($path);
913             }
914         }
916         return $pluginfiles;
917     }
919     /**
920      * Returns all classes in a component matching the provided namespace.
921      *
922      * It checks that the class exists.
923      *
924      * e.g. get_component_classes_in_namespace('mod_forum', 'event')
925      *
926      * @param string $component A valid moodle component (frankenstyle)
927      * @param string $namespace Namespace from the component name or empty if all $component namespace classes.
928      * @return array The full class name as key and the class path as value.
929      */
930     public static function get_component_classes_in_namespace($component, $namespace = '') {
932         $component = self::normalize_componentname($component);
934         if ($namespace) {
936             // We will add them later.
937             $namespace = trim($namespace, '\\');
939             // We need add double backslashes as it is how classes are stored into self::$classmap.
940             $namespace = implode('\\\\', explode('\\', $namespace));
941             $namespace = $namespace . '\\\\';
942         }
944         $regex = '|^' . $component . '\\\\' . $namespace . '|';
945         $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
947         // We want to be sure that they exist.
948         $classes = array();
949         foreach ($it as $classname => $classpath) {
950             if (class_exists($classname)) {
951                 $classes[$classname] = $classpath;
952             }
953         }
955         return $classes;
956     }
958     /**
959      * Returns the exact absolute path to plugin directory.
960      *
961      * @param string $plugintype type of plugin
962      * @param string $pluginname name of the plugin
963      * @return string full path to plugin directory; null if not found
964      */
965     public static function get_plugin_directory($plugintype, $pluginname) {
966         if (empty($pluginname)) {
967             // Invalid plugin name, sorry.
968             return null;
969         }
971         self::init();
973         if (!isset(self::$plugins[$plugintype][$pluginname])) {
974             return null;
975         }
976         return self::$plugins[$plugintype][$pluginname];
977     }
979     /**
980      * Returns the exact absolute path to plugin directory.
981      *
982      * @param string $subsystem type of core subsystem
983      * @return string full path to subsystem directory; null if not found
984      */
985     public static function get_subsystem_directory($subsystem) {
986         self::init();
988         if (!isset(self::$subsystems[$subsystem])) {
989             return null;
990         }
991         return self::$subsystems[$subsystem];
992     }
994     /**
995      * This method validates a plug name. It is much faster than calling clean_param.
996      *
997      * @param string $plugintype type of plugin
998      * @param string $pluginname a string that might be a plugin name.
999      * @return bool if this string is a valid plugin name.
1000      */
1001     public static function is_valid_plugin_name($plugintype, $pluginname) {
1002         if ($plugintype === 'mod') {
1003             // Modules must not have the same name as core subsystems.
1004             if (!isset(self::$subsystems)) {
1005                 // Watch out, this is called from init!
1006                 self::init();
1007             }
1008             if (isset(self::$subsystems[$pluginname])) {
1009                 return false;
1010             }
1011             // Modules MUST NOT have any underscores,
1012             // component normalisation would break very badly otherwise!
1013             return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1015         } else {
1016             return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1017         }
1018     }
1020     /**
1021      * Normalize the component name.
1022      *
1023      * Note: this does not verify the validity of the plugin or component.
1024      *
1025      * @param string $component
1026      * @return string
1027      */
1028     public static function normalize_componentname($componentname) {
1029         list($plugintype, $pluginname) = self::normalize_component($componentname);
1030         if ($plugintype === 'core' && is_null($pluginname)) {
1031             return $plugintype;
1032         }
1033         return $plugintype . '_' . $pluginname;
1034     }
1036     /**
1037      * Normalize the component name using the "frankenstyle" rules.
1038      *
1039      * Note: this does not verify the validity of plugin or type names.
1040      *
1041      * @param string $component
1042      * @return array two-items list of [(string)type, (string|null)name]
1043      */
1044     public static function normalize_component($component) {
1045         if ($component === 'moodle' or $component === 'core' or $component === '') {
1046             return array('core', null);
1047         }
1049         if (strpos($component, '_') === false) {
1050             self::init();
1051             if (array_key_exists($component, self::$subsystems)) {
1052                 $type   = 'core';
1053                 $plugin = $component;
1054             } else {
1055                 // Everything else without underscore is a module.
1056                 $type   = 'mod';
1057                 $plugin = $component;
1058             }
1060         } else {
1061             list($type, $plugin) = explode('_', $component, 2);
1062             if ($type === 'moodle') {
1063                 $type = 'core';
1064             }
1065             // Any unknown type must be a subplugin.
1066         }
1068         return array($type, $plugin);
1069     }
1071     /**
1072      * Return exact absolute path to a plugin directory.
1073      *
1074      * @param string $component name such as 'moodle', 'mod_forum'
1075      * @return string full path to component directory; NULL if not found
1076      */
1077     public static function get_component_directory($component) {
1078         global $CFG;
1080         list($type, $plugin) = self::normalize_component($component);
1082         if ($type === 'core') {
1083             if ($plugin === null) {
1084                 return $path = $CFG->libdir;
1085             }
1086             return self::get_subsystem_directory($plugin);
1087         }
1089         return self::get_plugin_directory($type, $plugin);
1090     }
1092     /**
1093      * Returns list of plugin types that allow subplugins.
1094      * @return array as (string)plugintype => (string)fulldir
1095      */
1096     public static function get_plugin_types_with_subplugins() {
1097         self::init();
1099         $return = array();
1100         foreach (self::$supportsubplugins as $type) {
1101             $return[$type] = self::$plugintypes[$type];
1102         }
1103         return $return;
1104     }
1106     /**
1107      * Returns parent of this subplugin type.
1108      *
1109      * @param string $type
1110      * @return string parent component or null
1111      */
1112     public static function get_subtype_parent($type) {
1113         self::init();
1115         if (isset(self::$parents[$type])) {
1116             return self::$parents[$type];
1117         }
1119         return null;
1120     }
1122     /**
1123      * Return all subplugins of this component.
1124      * @param string $component.
1125      * @return array $subtype=>array($component, ..), null if no subtypes defined
1126      */
1127     public static function get_subplugins($component) {
1128         self::init();
1130         if (isset(self::$subplugins[$component])) {
1131             return self::$subplugins[$component];
1132         }
1134         return null;
1135     }
1137     /**
1138      * Returns hash of all versions including core and all plugins.
1139      *
1140      * This is relatively slow and not fully cached, use with care!
1141      *
1142      * @return string sha1 hash
1143      */
1144     public static function get_all_versions_hash() {
1145         return sha1(serialize(self::get_all_versions()));
1146     }
1148     /**
1149      * Returns hash of all versions including core and all plugins.
1150      *
1151      * This is relatively slow and not fully cached, use with care!
1152      *
1153      * @return array as (string)plugintype_pluginname => (int)version
1154      */
1155     public static function get_all_versions() : array {
1156         global $CFG;
1158         self::init();
1160         $versions = array();
1162         // Main version first.
1163         $versions['core'] = self::fetch_core_version();
1165         // The problem here is tha the component cache might be stable,
1166         // we want this to work also on frontpage without resetting the component cache.
1167         $usecache = false;
1168         if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1169             $usecache = true;
1170         }
1172         // Now all plugins.
1173         $plugintypes = core_component::get_plugin_types();
1174         foreach ($plugintypes as $type => $typedir) {
1175             if ($usecache) {
1176                 $plugs = core_component::get_plugin_list($type);
1177             } else {
1178                 $plugs = self::fetch_plugins($type, $typedir);
1179             }
1180             foreach ($plugs as $plug => $fullplug) {
1181                 $plugin = new stdClass();
1182                 $plugin->version = null;
1183                 $module = $plugin;
1184                 include($fullplug.'/version.php');
1185                 $versions[$type.'_'.$plug] = $plugin->version;
1186             }
1187         }
1189         return $versions;
1190     }
1192     /**
1193      * Invalidate opcode cache for given file, this is intended for
1194      * php files that are stored in dataroot.
1195      *
1196      * Note: we need it here because this class must be self-contained.
1197      *
1198      * @param string $file
1199      */
1200     public static function invalidate_opcode_php_cache($file) {
1201         if (function_exists('opcache_invalidate')) {
1202             if (!file_exists($file)) {
1203                 return;
1204             }
1205             opcache_invalidate($file, true);
1206         }
1207     }
1209     /**
1210      * Return true if subsystemname is core subsystem.
1211      *
1212      * @param string $subsystemname name of the subsystem.
1213      * @return bool true if core subsystem.
1214      */
1215     public static function is_core_subsystem($subsystemname) {
1216         return isset(self::$subsystems[$subsystemname]);
1217     }
1219     /**
1220      * Records all class renames that have been made to facilitate autoloading.
1221      */
1222     protected static function fill_classmap_renames_cache() {
1223         global $CFG;
1225         self::$classmaprenames = array();
1227         self::load_renamed_classes("$CFG->dirroot/lib/");
1229         foreach (self::$subsystems as $subsystem => $fulldir) {
1230             self::load_renamed_classes($fulldir);
1231         }
1233         foreach (self::$plugins as $plugintype => $plugins) {
1234             foreach ($plugins as $pluginname => $fulldir) {
1235                 self::load_renamed_classes($fulldir);
1236             }
1237         }
1238     }
1240     /**
1241      * Loads the db/renamedclasses.php file from the given directory.
1242      *
1243      * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1244      * and the value is the new class name.
1245      * It is only included when we are populating the component cache. After that is not needed.
1246      *
1247      * @param string $fulldir
1248      */
1249     protected static function load_renamed_classes($fulldir) {
1250         $file = $fulldir . '/db/renamedclasses.php';
1251         if (is_readable($file)) {
1252             $renamedclasses = null;
1253             require($file);
1254             if (is_array($renamedclasses)) {
1255                 foreach ($renamedclasses as $oldclass => $newclass) {
1256                     self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1257                 }
1258             }
1259         }
1260     }
1262     /**
1263      * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1264      *
1265      * E.g.
1266      *  [
1267      *      'mod' => [
1268      *          'mod_forum' => FORUM_PLUGIN_PATH,
1269      *          ...
1270      *      ],
1271      *      ...
1272      *      'core' => [
1273      *          'core_comment' => COMMENT_SUBSYSTEM_PATH,
1274      *          ...
1275      *      ]
1276      * ]
1277      *
1278      * @return array an associative array of components and their corresponding paths.
1279      */
1280     public static function get_component_list() : array {
1281         $components = [];
1282         // Get all plugins.
1283         foreach (self::get_plugin_types() as $plugintype => $typedir) {
1284             $components[$plugintype] = [];
1285             foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1286                 $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1287             }
1288         }
1289         // Get all subsystems.
1290         foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1291             $components['core']['core_' . $subsystemname] = $subsystempath;
1292         }
1293         return $components;
1294     }
1296     /**
1297      * Returns a list of frankenstyle component names.
1298      *
1299      * E.g.
1300      *  [
1301      *      'core_course',
1302      *      'core_message',
1303      *      'mod_assign',
1304      *      ...
1305      *  ]
1306      * @return array the list of frankenstyle component names.
1307      */
1308     public static function get_component_names() : array {
1309         $componentnames = [];
1310         // Get all plugins.
1311         foreach (self::get_plugin_types() as $plugintype => $typedir) {
1312             foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1313                 $componentnames[] = $plugintype . '_' . $pluginname;
1314             }
1315         }
1316         // Get all subsystems.
1317         foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1318             $componentnames[] = 'core_' . $subsystemname;
1319         }
1320         return $componentnames;
1321     }