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