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