MDL-53016 classloader: Load PSR-0 and PSR-4 namespaces dynamically
[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 PRS-4 and PSR-0 namespaces and corresponding paths. */
71     protected static $psrnamespaces = array(
72             'Horde' => 'lib/horde/framework/Horde',
73     );
75     /**
76      * Class loader for Frankenstyle named classes in standard locations.
77      * Frankenstyle namespaces are supported.
78      *
79      * The expected location for core classes is:
80      *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
81      *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
82      *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
83      *
84      * The expected location for plugin classes is:
85      *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
86      *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
87      *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
88      *
89      * @param string $classname
90      */
91     public static function classloader($classname) {
92         self::init();
94         if (isset(self::$classmap[$classname])) {
95             // Global $CFG is expected in included scripts.
96             global $CFG;
97             // Function include would be faster, but for BC it is better to include only once.
98             include_once(self::$classmap[$classname]);
99             return;
100         }
101         if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
102             $newclassname = self::$classmaprenames[$classname];
103             $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
104             debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
105             if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
106                 throw new \coding_exception("Cannot alias $classname to $newclassname");
107             }
108             class_alias($newclassname, $classname);
109             return;
110         }
112         if (self::psr_classloader($classname)) {
113             return;
114         }
115     }
117     /**
118      * Load a class from our defined PSR-0 or PSR-4 standard namespaces on
119      * demand.
120      *
121      * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
122      * compatible.
123      *
124      * @param string $class the name fo the class.
125      * @return bool true if class was loaded.
126      */
127     protected static function psr_classloader($class) {
128         global $CFG;
130         // Iterate through each namespace prefix.
131         foreach (self::$psrnamespaces as $prefix => $path) {
132             // Does the class use the namespace prefix?
133             $len = strlen($prefix);
134             if (strncmp($prefix, $class, $len) !== 0) {
135                 // No, move to the next prefix.
136                 continue;
137             }
139             $path = $CFG->dirroot . DIRECTORY_SEPARATOR . $path;
141             // Get the relative class name.
142             $relativeclass = substr($class, $len);
144             // Replace the namespace prefix with the base directory, replace namespace
145             // separators or _ with directory separators in the relative class name, append
146             // with .php.
147             $file = $path. str_replace(array('\\', '_'), DIRECTORY_SEPARATOR, $relativeclass) . '.php';
149             // If the file exists, require it.
150             if (file_exists($file)) {
151                 require($file);
152                 return true;
153             }
154         }
156         return false;
157     }
160     /**
161      * Initialise caches, always call before accessing self:: caches.
162      */
163     protected static function init() {
164         global $CFG;
166         // Init only once per request/CLI execution, we ignore changes done afterwards.
167         if (isset(self::$plugintypes)) {
168             return;
169         }
171         if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
172             self::fill_all_caches();
173             return;
174         }
176         if (!empty($CFG->alternative_component_cache)) {
177             // Hack for heavily clustered sites that want to manage component cache invalidation manually.
178             $cachefile = $CFG->alternative_component_cache;
180             if (file_exists($cachefile)) {
181                 if (CACHE_DISABLE_ALL) {
182                     // Verify the cache state only on upgrade pages.
183                     $content = self::get_cache_content();
184                     if (sha1_file($cachefile) !== sha1($content)) {
185                         die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
186                     }
187                     return;
188                 }
189                 $cache = array();
190                 include($cachefile);
191                 self::$plugintypes      = $cache['plugintypes'];
192                 self::$plugins          = $cache['plugins'];
193                 self::$subsystems       = $cache['subsystems'];
194                 self::$parents          = $cache['parents'];
195                 self::$subplugins       = $cache['subplugins'];
196                 self::$classmap         = $cache['classmap'];
197                 self::$classmaprenames  = $cache['classmaprenames'];
198                 self::$filemap          = $cache['filemap'];
199                 return;
200             }
202             if (!is_writable(dirname($cachefile))) {
203                 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
204             }
206             // Lets try to create the file, it might be in some writable directory or a local cache dir.
208         } else {
209             // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
210             //       use $CFG->alternative_component_cache if you do not like it.
211             $cachefile = "$CFG->cachedir/core_component.php";
212         }
214         if (!CACHE_DISABLE_ALL and !self::is_developer()) {
215             // 1/ Use the cache only outside of install and upgrade.
216             // 2/ Let developers add/remove classes in developer mode.
217             if (is_readable($cachefile)) {
218                 $cache = false;
219                 include($cachefile);
220                 if (!is_array($cache)) {
221                     // Something is very wrong.
222                 } else if (!isset($cache['version'])) {
223                     // Something is very wrong.
224                 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
225                     // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
226                     error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
227                 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
228                     // $CFG->dirroot was changed.
229                 } else {
230                     // The cache looks ok, let's use it.
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;
240                 }
241                 // Note: we do not verify $CFG->admin here intentionally,
242                 //       they must visit admin/index.php after any change.
243             }
244         }
246         if (!isset(self::$plugintypes)) {
247             // This needs to be atomic and self-fixing as much as possible.
249             $content = self::get_cache_content();
250             if (file_exists($cachefile)) {
251                 if (sha1_file($cachefile) === sha1($content)) {
252                     return;
253                 }
254                 // Stale cache detected!
255                 unlink($cachefile);
256             }
258             // Permissions might not be setup properly in installers.
259             $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
260             $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
262             clearstatcache();
263             $cachedir = dirname($cachefile);
264             if (!is_dir($cachedir)) {
265                 mkdir($cachedir, $dirpermissions, true);
266             }
268             if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
269                 fwrite($fp, $content);
270                 fclose($fp);
271                 @rename($cachefile.'.tmp', $cachefile);
272                 @chmod($cachefile, $filepermissions);
273             }
274             @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
275             self::invalidate_opcode_php_cache($cachefile);
276         }
277     }
279     /**
280      * Are we in developer debug mode?
281      *
282      * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
283      *       the reason is we need to use this before we setup DB connection or caches for CFG.
284      *
285      * @return bool
286      */
287     protected static function is_developer() {
288         global $CFG;
290         // Note we can not rely on $CFG->debug here because DB is not initialised yet.
291         if (isset($CFG->config_php_settings['debug'])) {
292             $debug = (int)$CFG->config_php_settings['debug'];
293         } else {
294             return false;
295         }
297         if ($debug & E_ALL and $debug & E_STRICT) {
298             return true;
299         }
301         return false;
302     }
304     /**
305      * Create cache file content.
306      *
307      * @private this is intended for $CFG->alternative_component_cache only.
308      *
309      * @return string
310      */
311     public static function get_cache_content() {
312         if (!isset(self::$plugintypes)) {
313             self::fill_all_caches();
314         }
316         $cache = array(
317             'subsystems'        => self::$subsystems,
318             'plugintypes'       => self::$plugintypes,
319             'plugins'           => self::$plugins,
320             'parents'           => self::$parents,
321             'subplugins'        => self::$subplugins,
322             'classmap'          => self::$classmap,
323             'classmaprenames'   => self::$classmaprenames,
324             'filemap'           => self::$filemap,
325             'version'           => self::$version,
326         );
328         return '<?php
329 $cache = '.var_export($cache, true).';
330 ';
331     }
333     /**
334      * Fill all caches.
335      */
336     protected static function fill_all_caches() {
337         self::$subsystems = self::fetch_subsystems();
339         list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
341         self::$plugins = array();
342         foreach (self::$plugintypes as $type => $fulldir) {
343             self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
344         }
346         self::fill_classmap_cache();
347         self::fill_classmap_renames_cache();
348         self::fill_filemap_cache();
349         self::fetch_core_version();
350     }
352     /**
353      * Get the core version.
354      *
355      * In order for this to work properly, opcache should be reset beforehand.
356      *
357      * @return float core version.
358      */
359     protected static function fetch_core_version() {
360         global $CFG;
361         if (self::$version === null) {
362             $version = null; // Prevent IDE complaints.
363             require($CFG->dirroot . '/version.php');
364             self::$version = $version;
365         }
366         return self::$version;
367     }
369     /**
370      * Returns list of core subsystems.
371      * @return array
372      */
373     protected static function fetch_subsystems() {
374         global $CFG;
376         // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
378         $info = array(
379             'access'      => null,
380             'admin'       => $CFG->dirroot.'/'.$CFG->admin,
381             'antivirus'   => $CFG->dirroot . '/lib/antivirus',
382             'auth'        => $CFG->dirroot.'/auth',
383             'availability' => $CFG->dirroot . '/availability',
384             'backup'      => $CFG->dirroot.'/backup/util/ui',
385             'badges'      => $CFG->dirroot.'/badges',
386             'block'       => $CFG->dirroot.'/blocks',
387             'blog'        => $CFG->dirroot.'/blog',
388             'bulkusers'   => null,
389             'cache'       => $CFG->dirroot.'/cache',
390             'calendar'    => $CFG->dirroot.'/calendar',
391             'cohort'      => $CFG->dirroot.'/cohort',
392             'comment'     => $CFG->dirroot.'/comment',
393             'competency'  => $CFG->dirroot.'/competency',
394             'completion'  => $CFG->dirroot.'/completion',
395             'countries'   => null,
396             'course'      => $CFG->dirroot.'/course',
397             'currencies'  => null,
398             'dbtransfer'  => null,
399             'debug'       => null,
400             'editor'      => $CFG->dirroot.'/lib/editor',
401             'edufields'   => null,
402             'enrol'       => $CFG->dirroot.'/enrol',
403             'error'       => null,
404             'filepicker'  => null,
405             'files'       => $CFG->dirroot.'/files',
406             'filters'     => null,
407             //'fonts'       => null, // Bogus.
408             'form'        => $CFG->dirroot.'/lib/form',
409             'grades'      => $CFG->dirroot.'/grade',
410             'grading'     => $CFG->dirroot.'/grade/grading',
411             'group'       => $CFG->dirroot.'/group',
412             'help'        => null,
413             'hub'         => null,
414             'imscc'       => null,
415             'install'     => null,
416             'iso6392'     => null,
417             'langconfig'  => null,
418             'license'     => null,
419             'mathslib'    => null,
420             'media'       => null,
421             'message'     => $CFG->dirroot.'/message',
422             'mimetypes'   => null,
423             'mnet'        => $CFG->dirroot.'/mnet',
424             //'moodle.org'  => null, // Not used any more.
425             'my'          => $CFG->dirroot.'/my',
426             'notes'       => $CFG->dirroot.'/notes',
427             'pagetype'    => null,
428             'pix'         => null,
429             'plagiarism'  => $CFG->dirroot.'/plagiarism',
430             'plugin'      => null,
431             'portfolio'   => $CFG->dirroot.'/portfolio',
432             'publish'     => $CFG->dirroot.'/course/publish',
433             'question'    => $CFG->dirroot.'/question',
434             'rating'      => $CFG->dirroot.'/rating',
435             'register'    => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
436             'repository'  => $CFG->dirroot.'/repository',
437             'rss'         => $CFG->dirroot.'/rss',
438             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
439             'search'      => $CFG->dirroot.'/search',
440             'table'       => null,
441             'tag'         => $CFG->dirroot.'/tag',
442             'timezones'   => null,
443             'user'        => $CFG->dirroot.'/user',
444             'userkey'     => null,
445             'webservice'  => $CFG->dirroot.'/webservice',
446         );
448         return $info;
449     }
451     /**
452      * Returns list of known plugin types.
453      * @return array
454      */
455     protected static function fetch_plugintypes() {
456         global $CFG;
458         $types = array(
459             'antivirus'     => $CFG->dirroot . '/lib/antivirus',
460             'availability'  => $CFG->dirroot . '/availability/condition',
461             'qtype'         => $CFG->dirroot.'/question/type',
462             'mod'           => $CFG->dirroot.'/mod',
463             'auth'          => $CFG->dirroot.'/auth',
464             'calendartype'  => $CFG->dirroot.'/calendar/type',
465             'enrol'         => $CFG->dirroot.'/enrol',
466             'message'       => $CFG->dirroot.'/message/output',
467             'block'         => $CFG->dirroot.'/blocks',
468             'filter'        => $CFG->dirroot.'/filter',
469             'editor'        => $CFG->dirroot.'/lib/editor',
470             'format'        => $CFG->dirroot.'/course/format',
471             'dataformat'    => $CFG->dirroot.'/dataformat',
472             'profilefield'  => $CFG->dirroot.'/user/profile/field',
473             'report'        => $CFG->dirroot.'/report',
474             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
475             'gradeexport'   => $CFG->dirroot.'/grade/export',
476             'gradeimport'   => $CFG->dirroot.'/grade/import',
477             'gradereport'   => $CFG->dirroot.'/grade/report',
478             'gradingform'   => $CFG->dirroot.'/grade/grading/form',
479             'mnetservice'   => $CFG->dirroot.'/mnet/service',
480             'webservice'    => $CFG->dirroot.'/webservice',
481             'repository'    => $CFG->dirroot.'/repository',
482             'portfolio'     => $CFG->dirroot.'/portfolio',
483             'search'        => $CFG->dirroot.'/search/engine',
484             'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
485             'qformat'       => $CFG->dirroot.'/question/format',
486             'plagiarism'    => $CFG->dirroot.'/plagiarism',
487             'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
488             'cachestore'    => $CFG->dirroot.'/cache/stores',
489             'cachelock'     => $CFG->dirroot.'/cache/locks',
490         );
491         $parents = array();
492         $subplugins = array();
494         if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
495             $types['theme'] = $CFG->themedir;
496         } else {
497             $types['theme'] = $CFG->dirroot.'/theme';
498         }
500         foreach (self::$supportsubplugins as $type) {
501             if ($type === 'local') {
502                 // Local subplugins must be after local plugins.
503                 continue;
504             }
505             $plugins = self::fetch_plugins($type, $types[$type]);
506             foreach ($plugins as $plugin => $fulldir) {
507                 $subtypes = self::fetch_subtypes($fulldir);
508                 if (!$subtypes) {
509                     continue;
510                 }
511                 $subplugins[$type.'_'.$plugin] = array();
512                 foreach($subtypes as $subtype => $subdir) {
513                     if (isset($types[$subtype])) {
514                         error_log("Invalid subtype '$subtype', duplicate detected.");
515                         continue;
516                     }
517                     $types[$subtype] = $subdir;
518                     $parents[$subtype] = $type.'_'.$plugin;
519                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
520                 }
521             }
522         }
523         // Local is always last!
524         $types['local'] = $CFG->dirroot.'/local';
526         if (in_array('local', self::$supportsubplugins)) {
527             $type = 'local';
528             $plugins = self::fetch_plugins($type, $types[$type]);
529             foreach ($plugins as $plugin => $fulldir) {
530                 $subtypes = self::fetch_subtypes($fulldir);
531                 if (!$subtypes) {
532                     continue;
533                 }
534                 $subplugins[$type.'_'.$plugin] = array();
535                 foreach($subtypes as $subtype => $subdir) {
536                     if (isset($types[$subtype])) {
537                         error_log("Invalid subtype '$subtype', duplicate detected.");
538                         continue;
539                     }
540                     $types[$subtype] = $subdir;
541                     $parents[$subtype] = $type.'_'.$plugin;
542                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
543                 }
544             }
545         }
547         return array($types, $parents, $subplugins);
548     }
550     /**
551      * Returns list of subtypes.
552      * @param string $ownerdir
553      * @return array
554      */
555     protected static function fetch_subtypes($ownerdir) {
556         global $CFG;
558         $types = array();
559         if (file_exists("$ownerdir/db/subplugins.php")) {
560             $subplugins = array();
561             include("$ownerdir/db/subplugins.php");
562             foreach ($subplugins as $subtype => $dir) {
563                 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
564                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
565                     continue;
566                 }
567                 if (isset(self::$subsystems[$subtype])) {
568                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
569                     continue;
570                 }
571                 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
572                     $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
573                 }
574                 if (!is_dir("$CFG->dirroot/$dir")) {
575                     error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
576                     continue;
577                 }
578                 $types[$subtype] = "$CFG->dirroot/$dir";
579             }
580         }
581         return $types;
582     }
584     /**
585      * Returns list of plugins of given type in given directory.
586      * @param string $plugintype
587      * @param string $fulldir
588      * @return array
589      */
590     protected static function fetch_plugins($plugintype, $fulldir) {
591         global $CFG;
593         $fulldirs = (array)$fulldir;
594         if ($plugintype === 'theme') {
595             if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
596                 // Include themes in standard location too.
597                 array_unshift($fulldirs, $CFG->dirroot.'/theme');
598             }
599         }
601         $result = array();
603         foreach ($fulldirs as $fulldir) {
604             if (!is_dir($fulldir)) {
605                 continue;
606             }
607             $items = new \DirectoryIterator($fulldir);
608             foreach ($items as $item) {
609                 if ($item->isDot() or !$item->isDir()) {
610                     continue;
611                 }
612                 $pluginname = $item->getFilename();
613                 if ($plugintype === 'auth' and $pluginname === 'db') {
614                     // Special exception for this wrong plugin name.
615                 } else if (isset(self::$ignoreddirs[$pluginname])) {
616                     continue;
617                 }
618                 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
619                     // Always ignore plugins with problematic names here.
620                     continue;
621                 }
622                 $result[$pluginname] = $fulldir.'/'.$pluginname;
623                 unset($item);
624             }
625             unset($items);
626         }
628         ksort($result);
629         return $result;
630     }
632     /**
633      * Find all classes that can be autoloaded including frankenstyle namespaces.
634      */
635     protected static function fill_classmap_cache() {
636         global $CFG;
638         self::$classmap = array();
640         self::load_classes('core', "$CFG->dirroot/lib/classes");
642         foreach (self::$subsystems as $subsystem => $fulldir) {
643             if (!$fulldir) {
644                 continue;
645             }
646             self::load_classes('core_'.$subsystem, "$fulldir/classes");
647         }
649         foreach (self::$plugins as $plugintype => $plugins) {
650             foreach ($plugins as $pluginname => $fulldir) {
651                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
652             }
653         }
654         ksort(self::$classmap);
655     }
657     /**
658      * Fills up the cache defining what plugins have certain files.
659      *
660      * @see self::get_plugin_list_with_file
661      * @return void
662      */
663     protected static function fill_filemap_cache() {
664         global $CFG;
666         self::$filemap = array();
668         foreach (self::$filestomap as $file) {
669             if (!isset(self::$filemap[$file])) {
670                 self::$filemap[$file] = array();
671             }
672             foreach (self::$plugins as $plugintype => $plugins) {
673                 if (!isset(self::$filemap[$file][$plugintype])) {
674                     self::$filemap[$file][$plugintype] = array();
675                 }
676                 foreach ($plugins as $pluginname => $fulldir) {
677                     if (file_exists("$fulldir/$file")) {
678                         self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
679                     }
680                 }
681             }
682         }
683     }
685     /**
686      * Find classes in directory and recurse to subdirs.
687      * @param string $component
688      * @param string $fulldir
689      * @param string $namespace
690      */
691     protected static function load_classes($component, $fulldir, $namespace = '') {
692         if (!is_dir($fulldir)) {
693             return;
694         }
696         if (!is_readable($fulldir)) {
697             // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
698             // because its pretty likely to lead to a missing class error further down the line.
699             // But our early setup code can't handle errors this early at the moment.
700             return;
701         }
703         $items = new \DirectoryIterator($fulldir);
704         foreach ($items as $item) {
705             if ($item->isDot()) {
706                 continue;
707             }
708             if ($item->isDir()) {
709                 $dirname = $item->getFilename();
710                 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
711                 continue;
712             }
714             $filename = $item->getFilename();
715             $classname = preg_replace('/\.php$/', '', $filename);
717             if ($filename === $classname) {
718                 // Not a php file.
719                 continue;
720             }
721             if ($namespace === '') {
722                 // Legacy long frankenstyle class name.
723                 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
724             }
725             // New namespaced classes.
726             self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
727         }
728         unset($item);
729         unset($items);
730     }
733     /**
734      * List all core subsystems and their location
735      *
736      * This is a whitelist of components that are part of the core and their
737      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
738      * plugin is not listed here and it does not have proper plugintype prefix,
739      * then it is considered as course activity module.
740      *
741      * The location is absolute file path to dir. NULL means there is no special
742      * directory for this subsystem. If the location is set, the subsystem's
743      * renderer.php is expected to be there.
744      *
745      * @return array of (string)name => (string|null)full dir location
746      */
747     public static function get_core_subsystems() {
748         self::init();
749         return self::$subsystems;
750     }
752     /**
753      * Get list of available plugin types together with their location.
754      *
755      * @return array as (string)plugintype => (string)fulldir
756      */
757     public static function get_plugin_types() {
758         self::init();
759         return self::$plugintypes;
760     }
762     /**
763      * Get list of plugins of given type.
764      *
765      * @param string $plugintype
766      * @return array as (string)pluginname => (string)fulldir
767      */
768     public static function get_plugin_list($plugintype) {
769         self::init();
771         if (!isset(self::$plugins[$plugintype])) {
772             return array();
773         }
774         return self::$plugins[$plugintype];
775     }
777     /**
778      * Get a list of all the plugins of a given type that define a certain class
779      * in a certain file. The plugin component names and class names are returned.
780      *
781      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
782      * @param string $class the part of the name of the class after the
783      *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
784      *      names like report_courselist_thing. If you are looking for classes with
785      *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
786      *      Frankenstyle namespaces are also supported.
787      * @param string $file the name of file within the plugin that defines the class.
788      * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
789      *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
790      */
791     public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
792         global $CFG; // Necessary in case it is referenced by included PHP scripts.
794         if ($class) {
795             $suffix = '_' . $class;
796         } else {
797             $suffix = '';
798         }
800         $pluginclasses = array();
801         $plugins = self::get_plugin_list($plugintype);
802         foreach ($plugins as $plugin => $fulldir) {
803             // Try class in frankenstyle namespace.
804             if ($class) {
805                 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
806                 if (class_exists($classname, true)) {
807                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
808                     continue;
809                 }
810             }
812             // Try autoloading of class with frankenstyle prefix.
813             $classname = $plugintype . '_' . $plugin . $suffix;
814             if (class_exists($classname, true)) {
815                 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
816                 continue;
817             }
819             // Fall back to old file location and class name.
820             if ($file and file_exists("$fulldir/$file")) {
821                 include_once("$fulldir/$file");
822                 if (class_exists($classname, false)) {
823                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
824                     continue;
825                 }
826             }
827         }
829         return $pluginclasses;
830     }
832     /**
833      * Get a list of all the plugins of a given type that contain a particular file.
834      *
835      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
836      * @param string $file the name of file that must be present in the plugin.
837      *                     (e.g. 'view.php', 'db/install.xml').
838      * @param bool $include if true (default false), the file will be include_once-ed if found.
839      * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
840      *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
841      */
842     public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
843         global $CFG; // Necessary in case it is referenced by included PHP scripts.
844         $pluginfiles = array();
846         if (isset(self::$filemap[$file])) {
847             // If the file was supposed to be mapped, then it should have been set in the array.
848             if (isset(self::$filemap[$file][$plugintype])) {
849                 $pluginfiles = self::$filemap[$file][$plugintype];
850             }
851         } else {
852             // Old-style search for non-cached files.
853             $plugins = self::get_plugin_list($plugintype);
854             foreach ($plugins as $plugin => $fulldir) {
855                 $path = $fulldir . '/' . $file;
856                 if (file_exists($path)) {
857                     $pluginfiles[$plugin] = $path;
858                 }
859             }
860         }
862         if ($include) {
863             foreach ($pluginfiles as $path) {
864                 include_once($path);
865             }
866         }
868         return $pluginfiles;
869     }
871     /**
872      * Returns all classes in a component matching the provided namespace.
873      *
874      * It checks that the class exists.
875      *
876      * e.g. get_component_classes_in_namespace('mod_forum', 'event')
877      *
878      * @param string $component A valid moodle component (frankenstyle)
879      * @param string $namespace Namespace from the component name.
880      * @return array The full class name as key and the class path as value.
881      */
882     public static function get_component_classes_in_namespace($component, $namespace = '') {
884         // We will add them later.
885         $namespace = ltrim($namespace, '\\');
887         // We need add double backslashes as it is how classes are stored into self::$classmap.
888         $namespace = implode('\\\\', explode('\\', $namespace));
890         $regex = '/^' . $component . '\\\\' . $namespace . '/';
891         $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
893         // We want to be sure that they exist.
894         $classes = array();
895         foreach ($it as $classname => $classpath) {
896             if (class_exists($classname)) {
897                 $classes[$classname] = $classpath;
898             }
899         }
901         return $classes;
902     }
904     /**
905      * Returns the exact absolute path to plugin directory.
906      *
907      * @param string $plugintype type of plugin
908      * @param string $pluginname name of the plugin
909      * @return string full path to plugin directory; null if not found
910      */
911     public static function get_plugin_directory($plugintype, $pluginname) {
912         if (empty($pluginname)) {
913             // Invalid plugin name, sorry.
914             return null;
915         }
917         self::init();
919         if (!isset(self::$plugins[$plugintype][$pluginname])) {
920             return null;
921         }
922         return self::$plugins[$plugintype][$pluginname];
923     }
925     /**
926      * Returns the exact absolute path to plugin directory.
927      *
928      * @param string $subsystem type of core subsystem
929      * @return string full path to subsystem directory; null if not found
930      */
931     public static function get_subsystem_directory($subsystem) {
932         self::init();
934         if (!isset(self::$subsystems[$subsystem])) {
935             return null;
936         }
937         return self::$subsystems[$subsystem];
938     }
940     /**
941      * This method validates a plug name. It is much faster than calling clean_param.
942      *
943      * @param string $plugintype type of plugin
944      * @param string $pluginname a string that might be a plugin name.
945      * @return bool if this string is a valid plugin name.
946      */
947     public static function is_valid_plugin_name($plugintype, $pluginname) {
948         if ($plugintype === 'mod') {
949             // Modules must not have the same name as core subsystems.
950             if (!isset(self::$subsystems)) {
951                 // Watch out, this is called from init!
952                 self::init();
953             }
954             if (isset(self::$subsystems[$pluginname])) {
955                 return false;
956             }
957             // Modules MUST NOT have any underscores,
958             // component normalisation would break very badly otherwise!
959             return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
961         } else {
962             return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
963         }
964     }
966     /**
967      * Normalize the component name.
968      *
969      * Note: this does not verify the validity of the plugin or component.
970      *
971      * @param string $component
972      * @return string
973      */
974     public static function normalize_componentname($componentname) {
975         list($plugintype, $pluginname) = self::normalize_component($componentname);
976         if ($plugintype === 'core' && is_null($pluginname)) {
977             return $plugintype;
978         }
979         return $plugintype . '_' . $pluginname;
980     }
982     /**
983      * Normalize the component name using the "frankenstyle" rules.
984      *
985      * Note: this does not verify the validity of plugin or type names.
986      *
987      * @param string $component
988      * @return array as (string)$type => (string)$plugin
989      */
990     public static function normalize_component($component) {
991         if ($component === 'moodle' or $component === 'core' or $component === '') {
992             return array('core', null);
993         }
995         if (strpos($component, '_') === false) {
996             self::init();
997             if (array_key_exists($component, self::$subsystems)) {
998                 $type   = 'core';
999                 $plugin = $component;
1000             } else {
1001                 // Everything else without underscore is a module.
1002                 $type   = 'mod';
1003                 $plugin = $component;
1004             }
1006         } else {
1007             list($type, $plugin) = explode('_', $component, 2);
1008             if ($type === 'moodle') {
1009                 $type = 'core';
1010             }
1011             // Any unknown type must be a subplugin.
1012         }
1014         return array($type, $plugin);
1015     }
1017     /**
1018      * Return exact absolute path to a plugin directory.
1019      *
1020      * @param string $component name such as 'moodle', 'mod_forum'
1021      * @return string full path to component directory; NULL if not found
1022      */
1023     public static function get_component_directory($component) {
1024         global $CFG;
1026         list($type, $plugin) = self::normalize_component($component);
1028         if ($type === 'core') {
1029             if ($plugin === null) {
1030                 return $path = $CFG->libdir;
1031             }
1032             return self::get_subsystem_directory($plugin);
1033         }
1035         return self::get_plugin_directory($type, $plugin);
1036     }
1038     /**
1039      * Returns list of plugin types that allow subplugins.
1040      * @return array as (string)plugintype => (string)fulldir
1041      */
1042     public static function get_plugin_types_with_subplugins() {
1043         self::init();
1045         $return = array();
1046         foreach (self::$supportsubplugins as $type) {
1047             $return[$type] = self::$plugintypes[$type];
1048         }
1049         return $return;
1050     }
1052     /**
1053      * Returns parent of this subplugin type.
1054      *
1055      * @param string $type
1056      * @return string parent component or null
1057      */
1058     public static function get_subtype_parent($type) {
1059         self::init();
1061         if (isset(self::$parents[$type])) {
1062             return self::$parents[$type];
1063         }
1065         return null;
1066     }
1068     /**
1069      * Return all subplugins of this component.
1070      * @param string $component.
1071      * @return array $subtype=>array($component, ..), null if no subtypes defined
1072      */
1073     public static function get_subplugins($component) {
1074         self::init();
1076         if (isset(self::$subplugins[$component])) {
1077             return self::$subplugins[$component];
1078         }
1080         return null;
1081     }
1083     /**
1084      * Returns hash of all versions including core and all plugins.
1085      *
1086      * This is relatively slow and not fully cached, use with care!
1087      *
1088      * @return string sha1 hash
1089      */
1090     public static function get_all_versions_hash() {
1091         global $CFG;
1093         self::init();
1095         $versions = array();
1097         // Main version first.
1098         $versions['core'] = self::fetch_core_version();
1100         // The problem here is tha the component cache might be stable,
1101         // we want this to work also on frontpage without resetting the component cache.
1102         $usecache = false;
1103         if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1104             $usecache = true;
1105         }
1107         // Now all plugins.
1108         $plugintypes = core_component::get_plugin_types();
1109         foreach ($plugintypes as $type => $typedir) {
1110             if ($usecache) {
1111                 $plugs = core_component::get_plugin_list($type);
1112             } else {
1113                 $plugs = self::fetch_plugins($type, $typedir);
1114             }
1115             foreach ($plugs as $plug => $fullplug) {
1116                 $plugin = new stdClass();
1117                 $plugin->version = null;
1118                 $module = $plugin;
1119                 include($fullplug.'/version.php');
1120                 $versions[$type.'_'.$plug] = $plugin->version;
1121             }
1122         }
1124         return sha1(serialize($versions));
1125     }
1127     /**
1128      * Invalidate opcode cache for given file, this is intended for
1129      * php files that are stored in dataroot.
1130      *
1131      * Note: we need it here because this class must be self-contained.
1132      *
1133      * @param string $file
1134      */
1135     public static function invalidate_opcode_php_cache($file) {
1136         if (function_exists('opcache_invalidate')) {
1137             if (!file_exists($file)) {
1138                 return;
1139             }
1140             opcache_invalidate($file, true);
1141         }
1142     }
1144     /**
1145      * Return true if subsystemname is core subsystem.
1146      *
1147      * @param string $subsystemname name of the subsystem.
1148      * @return bool true if core subsystem.
1149      */
1150     public static function is_core_subsystem($subsystemname) {
1151         return isset(self::$subsystems[$subsystemname]);
1152     }
1154     /**
1155      * Records all class renames that have been made to facilitate autoloading.
1156      */
1157     protected static function fill_classmap_renames_cache() {
1158         global $CFG;
1160         self::$classmaprenames = array();
1162         self::load_renamed_classes("$CFG->dirroot/lib/");
1164         foreach (self::$subsystems as $subsystem => $fulldir) {
1165             self::load_renamed_classes($fulldir);
1166         }
1168         foreach (self::$plugins as $plugintype => $plugins) {
1169             foreach ($plugins as $pluginname => $fulldir) {
1170                 self::load_renamed_classes($fulldir);
1171             }
1172         }
1173     }
1175     /**
1176      * Loads the db/renamedclasses.php file from the given directory.
1177      *
1178      * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1179      * and the value is the new class name.
1180      * It is only included when we are populating the component cache. After that is not needed.
1181      *
1182      * @param string $fulldir
1183      */
1184     protected static function load_renamed_classes($fulldir) {
1185         $file = $fulldir . '/db/renamedclasses.php';
1186         if (is_readable($file)) {
1187             $renamedclasses = null;
1188             require($file);
1189             if (is_array($renamedclasses)) {
1190                 foreach ($renamedclasses as $oldclass => $newclass) {
1191                     self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1192                 }
1193             }
1194         }
1195     }