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