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