weekly release 3.2dev
[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             'antivirus'   => $CFG->dirroot . '/lib/antivirus',
345             'auth'        => $CFG->dirroot.'/auth',
346             'availability' => $CFG->dirroot . '/availability',
347             'backup'      => $CFG->dirroot.'/backup/util/ui',
348             'badges'      => $CFG->dirroot.'/badges',
349             'block'       => $CFG->dirroot.'/blocks',
350             'blog'        => $CFG->dirroot.'/blog',
351             'bulkusers'   => null,
352             'cache'       => $CFG->dirroot.'/cache',
353             'calendar'    => $CFG->dirroot.'/calendar',
354             'cohort'      => $CFG->dirroot.'/cohort',
355             'comment'     => $CFG->dirroot.'/comment',
356             'competency'  => $CFG->dirroot.'/competency',
357             'completion'  => $CFG->dirroot.'/completion',
358             'countries'   => null,
359             'course'      => $CFG->dirroot.'/course',
360             'currencies'  => null,
361             'dbtransfer'  => null,
362             'debug'       => null,
363             'editor'      => $CFG->dirroot.'/lib/editor',
364             'edufields'   => null,
365             'enrol'       => $CFG->dirroot.'/enrol',
366             'error'       => null,
367             'filepicker'  => null,
368             'files'       => $CFG->dirroot.'/files',
369             'filters'     => null,
370             //'fonts'       => null, // Bogus.
371             'form'        => $CFG->dirroot.'/lib/form',
372             'grades'      => $CFG->dirroot.'/grade',
373             'grading'     => $CFG->dirroot.'/grade/grading',
374             'group'       => $CFG->dirroot.'/group',
375             'help'        => null,
376             'hub'         => null,
377             'imscc'       => null,
378             'install'     => null,
379             'iso6392'     => null,
380             'langconfig'  => null,
381             'license'     => null,
382             'mathslib'    => null,
383             'media'       => null,
384             'message'     => $CFG->dirroot.'/message',
385             'mimetypes'   => null,
386             'mnet'        => $CFG->dirroot.'/mnet',
387             //'moodle.org'  => null, // Not used any more.
388             'my'          => $CFG->dirroot.'/my',
389             'notes'       => $CFG->dirroot.'/notes',
390             'pagetype'    => null,
391             'pix'         => null,
392             'plagiarism'  => $CFG->dirroot.'/plagiarism',
393             'plugin'      => null,
394             'portfolio'   => $CFG->dirroot.'/portfolio',
395             'publish'     => $CFG->dirroot.'/course/publish',
396             'question'    => $CFG->dirroot.'/question',
397             'rating'      => $CFG->dirroot.'/rating',
398             'register'    => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
399             'repository'  => $CFG->dirroot.'/repository',
400             'rss'         => $CFG->dirroot.'/rss',
401             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
402             'search'      => $CFG->dirroot.'/search',
403             'table'       => null,
404             'tag'         => $CFG->dirroot.'/tag',
405             'timezones'   => null,
406             'user'        => $CFG->dirroot.'/user',
407             'userkey'     => null,
408             'webservice'  => $CFG->dirroot.'/webservice',
409         );
411         return $info;
412     }
414     /**
415      * Returns list of known plugin types.
416      * @return array
417      */
418     protected static function fetch_plugintypes() {
419         global $CFG;
421         $types = array(
422             'antivirus'     => $CFG->dirroot . '/lib/antivirus',
423             'availability'  => $CFG->dirroot . '/availability/condition',
424             'qtype'         => $CFG->dirroot.'/question/type',
425             'mod'           => $CFG->dirroot.'/mod',
426             'auth'          => $CFG->dirroot.'/auth',
427             'calendartype'  => $CFG->dirroot.'/calendar/type',
428             'enrol'         => $CFG->dirroot.'/enrol',
429             'message'       => $CFG->dirroot.'/message/output',
430             'block'         => $CFG->dirroot.'/blocks',
431             'filter'        => $CFG->dirroot.'/filter',
432             'editor'        => $CFG->dirroot.'/lib/editor',
433             'format'        => $CFG->dirroot.'/course/format',
434             'dataformat'    => $CFG->dirroot.'/dataformat',
435             'profilefield'  => $CFG->dirroot.'/user/profile/field',
436             'report'        => $CFG->dirroot.'/report',
437             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
438             'gradeexport'   => $CFG->dirroot.'/grade/export',
439             'gradeimport'   => $CFG->dirroot.'/grade/import',
440             'gradereport'   => $CFG->dirroot.'/grade/report',
441             'gradingform'   => $CFG->dirroot.'/grade/grading/form',
442             'mnetservice'   => $CFG->dirroot.'/mnet/service',
443             'webservice'    => $CFG->dirroot.'/webservice',
444             'repository'    => $CFG->dirroot.'/repository',
445             'portfolio'     => $CFG->dirroot.'/portfolio',
446             'search'        => $CFG->dirroot.'/search/engine',
447             'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
448             'qformat'       => $CFG->dirroot.'/question/format',
449             'plagiarism'    => $CFG->dirroot.'/plagiarism',
450             'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
451             'cachestore'    => $CFG->dirroot.'/cache/stores',
452             'cachelock'     => $CFG->dirroot.'/cache/locks',
453         );
454         $parents = array();
455         $subplugins = array();
457         if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
458             $types['theme'] = $CFG->themedir;
459         } else {
460             $types['theme'] = $CFG->dirroot.'/theme';
461         }
463         foreach (self::$supportsubplugins as $type) {
464             if ($type === 'local') {
465                 // Local subplugins must be after local plugins.
466                 continue;
467             }
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         }
486         // Local is always last!
487         $types['local'] = $CFG->dirroot.'/local';
489         if (in_array('local', self::$supportsubplugins)) {
490             $type = 'local';
491             $plugins = self::fetch_plugins($type, $types[$type]);
492             foreach ($plugins as $plugin => $fulldir) {
493                 $subtypes = self::fetch_subtypes($fulldir);
494                 if (!$subtypes) {
495                     continue;
496                 }
497                 $subplugins[$type.'_'.$plugin] = array();
498                 foreach($subtypes as $subtype => $subdir) {
499                     if (isset($types[$subtype])) {
500                         error_log("Invalid subtype '$subtype', duplicate detected.");
501                         continue;
502                     }
503                     $types[$subtype] = $subdir;
504                     $parents[$subtype] = $type.'_'.$plugin;
505                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
506                 }
507             }
508         }
510         return array($types, $parents, $subplugins);
511     }
513     /**
514      * Returns list of subtypes.
515      * @param string $ownerdir
516      * @return array
517      */
518     protected static function fetch_subtypes($ownerdir) {
519         global $CFG;
521         $types = array();
522         if (file_exists("$ownerdir/db/subplugins.php")) {
523             $subplugins = array();
524             include("$ownerdir/db/subplugins.php");
525             foreach ($subplugins as $subtype => $dir) {
526                 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
527                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
528                     continue;
529                 }
530                 if (isset(self::$subsystems[$subtype])) {
531                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
532                     continue;
533                 }
534                 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
535                     $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
536                 }
537                 if (!is_dir("$CFG->dirroot/$dir")) {
538                     error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
539                     continue;
540                 }
541                 $types[$subtype] = "$CFG->dirroot/$dir";
542             }
543         }
544         return $types;
545     }
547     /**
548      * Returns list of plugins of given type in given directory.
549      * @param string $plugintype
550      * @param string $fulldir
551      * @return array
552      */
553     protected static function fetch_plugins($plugintype, $fulldir) {
554         global $CFG;
556         $fulldirs = (array)$fulldir;
557         if ($plugintype === 'theme') {
558             if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
559                 // Include themes in standard location too.
560                 array_unshift($fulldirs, $CFG->dirroot.'/theme');
561             }
562         }
564         $result = array();
566         foreach ($fulldirs as $fulldir) {
567             if (!is_dir($fulldir)) {
568                 continue;
569             }
570             $items = new \DirectoryIterator($fulldir);
571             foreach ($items as $item) {
572                 if ($item->isDot() or !$item->isDir()) {
573                     continue;
574                 }
575                 $pluginname = $item->getFilename();
576                 if ($plugintype === 'auth' and $pluginname === 'db') {
577                     // Special exception for this wrong plugin name.
578                 } else if (isset(self::$ignoreddirs[$pluginname])) {
579                     continue;
580                 }
581                 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
582                     // Always ignore plugins with problematic names here.
583                     continue;
584                 }
585                 $result[$pluginname] = $fulldir.'/'.$pluginname;
586                 unset($item);
587             }
588             unset($items);
589         }
591         ksort($result);
592         return $result;
593     }
595     /**
596      * Find all classes that can be autoloaded including frankenstyle namespaces.
597      */
598     protected static function fill_classmap_cache() {
599         global $CFG;
601         self::$classmap = array();
603         self::load_classes('core', "$CFG->dirroot/lib/classes");
605         foreach (self::$subsystems as $subsystem => $fulldir) {
606             if (!$fulldir) {
607                 continue;
608             }
609             self::load_classes('core_'.$subsystem, "$fulldir/classes");
610         }
612         foreach (self::$plugins as $plugintype => $plugins) {
613             foreach ($plugins as $pluginname => $fulldir) {
614                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
615             }
616         }
617         ksort(self::$classmap);
618     }
620     /**
621      * Fills up the cache defining what plugins have certain files.
622      *
623      * @see self::get_plugin_list_with_file
624      * @return void
625      */
626     protected static function fill_filemap_cache() {
627         global $CFG;
629         self::$filemap = array();
631         foreach (self::$filestomap as $file) {
632             if (!isset(self::$filemap[$file])) {
633                 self::$filemap[$file] = array();
634             }
635             foreach (self::$plugins as $plugintype => $plugins) {
636                 if (!isset(self::$filemap[$file][$plugintype])) {
637                     self::$filemap[$file][$plugintype] = array();
638                 }
639                 foreach ($plugins as $pluginname => $fulldir) {
640                     if (file_exists("$fulldir/$file")) {
641                         self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
642                     }
643                 }
644             }
645         }
646     }
648     /**
649      * Find classes in directory and recurse to subdirs.
650      * @param string $component
651      * @param string $fulldir
652      * @param string $namespace
653      */
654     protected static function load_classes($component, $fulldir, $namespace = '') {
655         if (!is_dir($fulldir)) {
656             return;
657         }
659         if (!is_readable($fulldir)) {
660             // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
661             // because its pretty likely to lead to a missing class error further down the line.
662             // But our early setup code can't handle errors this early at the moment.
663             return;
664         }
666         $items = new \DirectoryIterator($fulldir);
667         foreach ($items as $item) {
668             if ($item->isDot()) {
669                 continue;
670             }
671             if ($item->isDir()) {
672                 $dirname = $item->getFilename();
673                 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
674                 continue;
675             }
677             $filename = $item->getFilename();
678             $classname = preg_replace('/\.php$/', '', $filename);
680             if ($filename === $classname) {
681                 // Not a php file.
682                 continue;
683             }
684             if ($namespace === '') {
685                 // Legacy long frankenstyle class name.
686                 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
687             }
688             // New namespaced classes.
689             self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
690         }
691         unset($item);
692         unset($items);
693     }
695     /**
696      * Fill caches for classes following the PSR-0 standard for the
697      * specified Vendors.
698      *
699      * PSR Autoloading is detailed at http://www.php-fig.org/psr/psr-0/.
700      */
701     protected static function fill_psr_cache() {
702         global $CFG;
704         $psrsystems = array(
705             'Horde' => 'horde/framework',
706         );
707         self::$psrclassmap = array();
709         foreach ($psrsystems as $system => $fulldir) {
710             if (!$fulldir) {
711                 continue;
712             }
713             self::load_psr_classes($CFG->libdir . DIRECTORY_SEPARATOR . $fulldir);
714         }
715     }
717     /**
718      * Find all PSR-0 style classes in within the base directory.
719      *
720      * @param string $basedir The base directory that the PSR-type library can be found in.
721      * @param string $subdir The directory within the basedir to search for classes within.
722      */
723     protected static function load_psr_classes($basedir, $subdir = null) {
724         if ($subdir) {
725             $fulldir = realpath($basedir . DIRECTORY_SEPARATOR . $subdir);
726             $classnameprefix = preg_replace('#' . preg_quote(DIRECTORY_SEPARATOR) . '#', '_', $subdir);
727         } else {
728             $fulldir = $basedir;
729         }
730         if (!$fulldir || !is_dir($fulldir)) {
731             return;
732         }
734         $items = new \DirectoryIterator($fulldir);
735         foreach ($items as $item) {
736             if ($item->isDot()) {
737                 continue;
738             }
739             if ($item->isDir()) {
740                 $dirname = $item->getFilename();
741                 $newsubdir = $dirname;
742                 if ($subdir) {
743                     $newsubdir = implode(DIRECTORY_SEPARATOR, array($subdir, $dirname));
744                 }
745                 self::load_psr_classes($basedir, $newsubdir);
746                 continue;
747             }
749             $filename = $item->getFilename();
750             $classname = preg_replace('/\.php$/', '', $filename);
752             if ($filename === $classname) {
753                 // Not a php file.
754                 continue;
755             }
757             if ($classnameprefix) {
758                 $classname = $classnameprefix . '_' . $classname;
759             }
761             self::$psrclassmap[$classname] = $fulldir . DIRECTORY_SEPARATOR . $filename;
762         }
763         unset($item);
764         unset($items);
765     }
767     /**
768      * List all core subsystems and their location
769      *
770      * This is a whitelist of components that are part of the core and their
771      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
772      * plugin is not listed here and it does not have proper plugintype prefix,
773      * then it is considered as course activity module.
774      *
775      * The location is absolute file path to dir. NULL means there is no special
776      * directory for this subsystem. If the location is set, the subsystem's
777      * renderer.php is expected to be there.
778      *
779      * @return array of (string)name => (string|null)full dir location
780      */
781     public static function get_core_subsystems() {
782         self::init();
783         return self::$subsystems;
784     }
786     /**
787      * Get list of available plugin types together with their location.
788      *
789      * @return array as (string)plugintype => (string)fulldir
790      */
791     public static function get_plugin_types() {
792         self::init();
793         return self::$plugintypes;
794     }
796     /**
797      * Get list of plugins of given type.
798      *
799      * @param string $plugintype
800      * @return array as (string)pluginname => (string)fulldir
801      */
802     public static function get_plugin_list($plugintype) {
803         self::init();
805         if (!isset(self::$plugins[$plugintype])) {
806             return array();
807         }
808         return self::$plugins[$plugintype];
809     }
811     /**
812      * Get a list of all the plugins of a given type that define a certain class
813      * in a certain file. The plugin component names and class names are returned.
814      *
815      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
816      * @param string $class the part of the name of the class after the
817      *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
818      *      names like report_courselist_thing. If you are looking for classes with
819      *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
820      *      Frankenstyle namespaces are also supported.
821      * @param string $file the name of file within the plugin that defines the class.
822      * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
823      *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
824      */
825     public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
826         global $CFG; // Necessary in case it is referenced by included PHP scripts.
828         if ($class) {
829             $suffix = '_' . $class;
830         } else {
831             $suffix = '';
832         }
834         $pluginclasses = array();
835         $plugins = self::get_plugin_list($plugintype);
836         foreach ($plugins as $plugin => $fulldir) {
837             // Try class in frankenstyle namespace.
838             if ($class) {
839                 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
840                 if (class_exists($classname, true)) {
841                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
842                     continue;
843                 }
844             }
846             // Try autoloading of class with frankenstyle prefix.
847             $classname = $plugintype . '_' . $plugin . $suffix;
848             if (class_exists($classname, true)) {
849                 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
850                 continue;
851             }
853             // Fall back to old file location and class name.
854             if ($file and file_exists("$fulldir/$file")) {
855                 include_once("$fulldir/$file");
856                 if (class_exists($classname, false)) {
857                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
858                     continue;
859                 }
860             }
861         }
863         return $pluginclasses;
864     }
866     /**
867      * Get a list of all the plugins of a given type that contain a particular file.
868      *
869      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
870      * @param string $file the name of file that must be present in the plugin.
871      *                     (e.g. 'view.php', 'db/install.xml').
872      * @param bool $include if true (default false), the file will be include_once-ed if found.
873      * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
874      *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
875      */
876     public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
877         global $CFG; // Necessary in case it is referenced by included PHP scripts.
878         $pluginfiles = array();
880         if (isset(self::$filemap[$file])) {
881             // If the file was supposed to be mapped, then it should have been set in the array.
882             if (isset(self::$filemap[$file][$plugintype])) {
883                 $pluginfiles = self::$filemap[$file][$plugintype];
884             }
885         } else {
886             // Old-style search for non-cached files.
887             $plugins = self::get_plugin_list($plugintype);
888             foreach ($plugins as $plugin => $fulldir) {
889                 $path = $fulldir . '/' . $file;
890                 if (file_exists($path)) {
891                     $pluginfiles[$plugin] = $path;
892                 }
893             }
894         }
896         if ($include) {
897             foreach ($pluginfiles as $path) {
898                 include_once($path);
899             }
900         }
902         return $pluginfiles;
903     }
905     /**
906      * Returns all classes in a component matching the provided namespace.
907      *
908      * It checks that the class exists.
909      *
910      * e.g. get_component_classes_in_namespace('mod_forum', 'event')
911      *
912      * @param string $component A valid moodle component (frankenstyle)
913      * @param string $namespace Namespace from the component name.
914      * @return array The full class name as key and the class path as value.
915      */
916     public static function get_component_classes_in_namespace($component, $namespace = '') {
918         // We will add them later.
919         $namespace = ltrim($namespace, '\\');
921         // We need add double backslashes as it is how classes are stored into self::$classmap.
922         $namespace = implode('\\\\', explode('\\', $namespace));
924         $regex = '/^' . $component . '\\\\' . $namespace . '/';
925         $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
927         // We want to be sure that they exist.
928         $classes = array();
929         foreach ($it as $classname => $classpath) {
930             if (class_exists($classname)) {
931                 $classes[$classname] = $classpath;
932             }
933         }
935         return $classes;
936     }
938     /**
939      * Returns the exact absolute path to plugin directory.
940      *
941      * @param string $plugintype type of plugin
942      * @param string $pluginname name of the plugin
943      * @return string full path to plugin directory; null if not found
944      */
945     public static function get_plugin_directory($plugintype, $pluginname) {
946         if (empty($pluginname)) {
947             // Invalid plugin name, sorry.
948             return null;
949         }
951         self::init();
953         if (!isset(self::$plugins[$plugintype][$pluginname])) {
954             return null;
955         }
956         return self::$plugins[$plugintype][$pluginname];
957     }
959     /**
960      * Returns the exact absolute path to plugin directory.
961      *
962      * @param string $subsystem type of core subsystem
963      * @return string full path to subsystem directory; null if not found
964      */
965     public static function get_subsystem_directory($subsystem) {
966         self::init();
968         if (!isset(self::$subsystems[$subsystem])) {
969             return null;
970         }
971         return self::$subsystems[$subsystem];
972     }
974     /**
975      * This method validates a plug name. It is much faster than calling clean_param.
976      *
977      * @param string $plugintype type of plugin
978      * @param string $pluginname a string that might be a plugin name.
979      * @return bool if this string is a valid plugin name.
980      */
981     public static function is_valid_plugin_name($plugintype, $pluginname) {
982         if ($plugintype === 'mod') {
983             // Modules must not have the same name as core subsystems.
984             if (!isset(self::$subsystems)) {
985                 // Watch out, this is called from init!
986                 self::init();
987             }
988             if (isset(self::$subsystems[$pluginname])) {
989                 return false;
990             }
991             // Modules MUST NOT have any underscores,
992             // component normalisation would break very badly otherwise!
993             return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
995         } else {
996             return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
997         }
998     }
1000     /**
1001      * Normalize the component name.
1002      *
1003      * Note: this does not verify the validity of the plugin or component.
1004      *
1005      * @param string $component
1006      * @return string
1007      */
1008     public static function normalize_componentname($componentname) {
1009         list($plugintype, $pluginname) = self::normalize_component($componentname);
1010         if ($plugintype === 'core' && is_null($pluginname)) {
1011             return $plugintype;
1012         }
1013         return $plugintype . '_' . $pluginname;
1014     }
1016     /**
1017      * Normalize the component name using the "frankenstyle" rules.
1018      *
1019      * Note: this does not verify the validity of plugin or type names.
1020      *
1021      * @param string $component
1022      * @return array as (string)$type => (string)$plugin
1023      */
1024     public static function normalize_component($component) {
1025         if ($component === 'moodle' or $component === 'core' or $component === '') {
1026             return array('core', null);
1027         }
1029         if (strpos($component, '_') === false) {
1030             self::init();
1031             if (array_key_exists($component, self::$subsystems)) {
1032                 $type   = 'core';
1033                 $plugin = $component;
1034             } else {
1035                 // Everything else without underscore is a module.
1036                 $type   = 'mod';
1037                 $plugin = $component;
1038             }
1040         } else {
1041             list($type, $plugin) = explode('_', $component, 2);
1042             if ($type === 'moodle') {
1043                 $type = 'core';
1044             }
1045             // Any unknown type must be a subplugin.
1046         }
1048         return array($type, $plugin);
1049     }
1051     /**
1052      * Return exact absolute path to a plugin directory.
1053      *
1054      * @param string $component name such as 'moodle', 'mod_forum'
1055      * @return string full path to component directory; NULL if not found
1056      */
1057     public static function get_component_directory($component) {
1058         global $CFG;
1060         list($type, $plugin) = self::normalize_component($component);
1062         if ($type === 'core') {
1063             if ($plugin === null) {
1064                 return $path = $CFG->libdir;
1065             }
1066             return self::get_subsystem_directory($plugin);
1067         }
1069         return self::get_plugin_directory($type, $plugin);
1070     }
1072     /**
1073      * Returns list of plugin types that allow subplugins.
1074      * @return array as (string)plugintype => (string)fulldir
1075      */
1076     public static function get_plugin_types_with_subplugins() {
1077         self::init();
1079         $return = array();
1080         foreach (self::$supportsubplugins as $type) {
1081             $return[$type] = self::$plugintypes[$type];
1082         }
1083         return $return;
1084     }
1086     /**
1087      * Returns parent of this subplugin type.
1088      *
1089      * @param string $type
1090      * @return string parent component or null
1091      */
1092     public static function get_subtype_parent($type) {
1093         self::init();
1095         if (isset(self::$parents[$type])) {
1096             return self::$parents[$type];
1097         }
1099         return null;
1100     }
1102     /**
1103      * Return all subplugins of this component.
1104      * @param string $component.
1105      * @return array $subtype=>array($component, ..), null if no subtypes defined
1106      */
1107     public static function get_subplugins($component) {
1108         self::init();
1110         if (isset(self::$subplugins[$component])) {
1111             return self::$subplugins[$component];
1112         }
1114         return null;
1115     }
1117     /**
1118      * Returns hash of all versions including core and all plugins.
1119      *
1120      * This is relatively slow and not fully cached, use with care!
1121      *
1122      * @return string sha1 hash
1123      */
1124     public static function get_all_versions_hash() {
1125         global $CFG;
1127         self::init();
1129         $versions = array();
1131         // Main version first.
1132         $versions['core'] = self::fetch_core_version();
1134         // The problem here is tha the component cache might be stable,
1135         // we want this to work also on frontpage without resetting the component cache.
1136         $usecache = false;
1137         if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1138             $usecache = true;
1139         }
1141         // Now all plugins.
1142         $plugintypes = core_component::get_plugin_types();
1143         foreach ($plugintypes as $type => $typedir) {
1144             if ($usecache) {
1145                 $plugs = core_component::get_plugin_list($type);
1146             } else {
1147                 $plugs = self::fetch_plugins($type, $typedir);
1148             }
1149             foreach ($plugs as $plug => $fullplug) {
1150                 $plugin = new stdClass();
1151                 $plugin->version = null;
1152                 $module = $plugin;
1153                 include($fullplug.'/version.php');
1154                 $versions[$type.'_'.$plug] = $plugin->version;
1155             }
1156         }
1158         return sha1(serialize($versions));
1159     }
1161     /**
1162      * Invalidate opcode cache for given file, this is intended for
1163      * php files that are stored in dataroot.
1164      *
1165      * Note: we need it here because this class must be self-contained.
1166      *
1167      * @param string $file
1168      */
1169     public static function invalidate_opcode_php_cache($file) {
1170         if (function_exists('opcache_invalidate')) {
1171             if (!file_exists($file)) {
1172                 return;
1173             }
1174             opcache_invalidate($file, true);
1175         }
1176     }
1178     /**
1179      * Return true if subsystemname is core subsystem.
1180      *
1181      * @param string $subsystemname name of the subsystem.
1182      * @return bool true if core subsystem.
1183      */
1184     public static function is_core_subsystem($subsystemname) {
1185         return isset(self::$subsystems[$subsystemname]);
1186     }
1188     /**
1189      * Records all class renames that have been made to facilitate autoloading.
1190      */
1191     protected static function fill_classmap_renames_cache() {
1192         global $CFG;
1194         self::$classmaprenames = array();
1196         self::load_renamed_classes("$CFG->dirroot/lib/");
1198         foreach (self::$subsystems as $subsystem => $fulldir) {
1199             self::load_renamed_classes($fulldir);
1200         }
1202         foreach (self::$plugins as $plugintype => $plugins) {
1203             foreach ($plugins as $pluginname => $fulldir) {
1204                 self::load_renamed_classes($fulldir);
1205             }
1206         }
1207     }
1209     /**
1210      * Loads the db/renamedclasses.php file from the given directory.
1211      *
1212      * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1213      * and the value is the new class name.
1214      * It is only included when we are populating the component cache. After that is not needed.
1215      *
1216      * @param string $fulldir
1217      */
1218     protected static function load_renamed_classes($fulldir) {
1219         $file = $fulldir . '/db/renamedclasses.php';
1220         if (is_readable($file)) {
1221             $renamedclasses = null;
1222             require($file);
1223             if (is_array($renamedclasses)) {
1224                 foreach ($renamedclasses as $oldclass => $newclass) {
1225                     self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1226                 }
1227             }
1228         }
1229     }