Merge branch 'MDL-18375_master' of git://github.com/markn86/moodle
[moodle.git] / lib / classes / component.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Components (core subsystems + plugins) related code.
19  *
20  * @package    core
21  * @copyright  2013 Petr Skoda {@link http://skodak.org}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 /**
28  * Collection of components related methods.
29  */
30 class core_component {
31     /** @var array list of ignored directories - watch out for auth/db exception */
32     protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true);
33     /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
34     protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
36     /** @var null cache of plugin types */
37     protected static $plugintypes = null;
38     /** @var null cache of plugin locations */
39     protected static $plugins = null;
40     /** @var null cache of core subsystems */
41     protected static $subsystems = null;
42     /** @var null list of all known classes that can be autoloaded */
43     protected static $classmap = null;
44     /** @var null list of some known files that can be included. */
45     protected static $filemap = null;
46     /** @var int|float core version. */
47     protected static $version = null;
48     /** @var array list of the files to map. */
49     protected static $filestomap = array('lib.php', 'settings.php');
51     /**
52      * Class loader for Frankenstyle named classes in standard locations.
53      * Frankenstyle namespaces are supported.
54      *
55      * The expected location for core classes is:
56      *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
57      *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
58      *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
59      *
60      * The expected location for plugin classes is:
61      *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
62      *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
63      *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
64      *
65      * @param string $classname
66      */
67     public static function classloader($classname) {
68         self::init();
70         if (isset(self::$classmap[$classname])) {
71             // Global $CFG is expected in included scripts.
72             global $CFG;
73             // Function include would be faster, but for BC it is better to include only once.
74             include_once(self::$classmap[$classname]);
75             return;
76         }
77     }
79     /**
80      * Initialise caches, always call before accessing self:: caches.
81      */
82     protected static function init() {
83         global $CFG;
85         // Init only once per request/CLI execution, we ignore changes done afterwards.
86         if (isset(self::$plugintypes)) {
87             return;
88         }
90         if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
91             self::fill_all_caches();
92             return;
93         }
95         if (!empty($CFG->alternative_component_cache)) {
96             // Hack for heavily clustered sites that want to manage component cache invalidation manually.
97             $cachefile = $CFG->alternative_component_cache;
99             if (file_exists($cachefile)) {
100                 if (CACHE_DISABLE_ALL) {
101                     // Verify the cache state only on upgrade pages.
102                     $content = self::get_cache_content();
103                     if (sha1_file($cachefile) !== sha1($content)) {
104                         die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
105                     }
106                     return;
107                 }
108                 $cache = array();
109                 include($cachefile);
110                 self::$plugintypes = $cache['plugintypes'];
111                 self::$plugins     = $cache['plugins'];
112                 self::$subsystems  = $cache['subsystems'];
113                 self::$classmap    = $cache['classmap'];
114                 self::$filemap     = $cache['filemap'];
115                 return;
116             }
118             if (!is_writable(dirname($cachefile))) {
119                 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
120             }
122             // Lets try to create the file, it might be in some writable directory or a local cache dir.
124         } else {
125             // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
126             //       use $CFG->alternative_component_cache if you do not like it.
127             $cachefile = "$CFG->cachedir/core_component.php";
128         }
130         if (!CACHE_DISABLE_ALL and !self::is_developer()) {
131             // 1/ Use the cache only outside of install and upgrade.
132             // 2/ Let developers add/remove classes in developer mode.
133             if (is_readable($cachefile)) {
134                 $cache = false;
135                 include($cachefile);
136                 if (!is_array($cache)) {
137                     // Something is very wrong.
138                 } else if (!isset($cache['version'])) {
139                     // Something is very wrong.
140                 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
141                     // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
142                     error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
143                 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
144                     // $CFG->dirroot was changed.
145                 } else {
146                     // The cache looks ok, let's use it.
147                     self::$plugintypes = $cache['plugintypes'];
148                     self::$plugins     = $cache['plugins'];
149                     self::$subsystems  = $cache['subsystems'];
150                     self::$classmap    = $cache['classmap'];
151                     self::$filemap     = $cache['filemap'];
152                     return;
153                 }
154                 // Note: we do not verify $CFG->admin here intentionally,
155                 //       they must visit admin/index.php after any change.
156             }
157         }
159         if (!isset(self::$plugintypes)) {
160             // This needs to be atomic and self-fixing as much as possible.
162             $content = self::get_cache_content();
163             if (file_exists($cachefile)) {
164                 if (sha1_file($cachefile) === sha1($content)) {
165                     return;
166                 }
167                 // Stale cache detected!
168                 unlink($cachefile);
169             }
171             // Permissions might not be setup properly in installers.
172             $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
173             $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
175             clearstatcache();
176             $cachedir = dirname($cachefile);
177             if (!is_dir($cachedir)) {
178                 mkdir($cachedir, $dirpermissions, true);
179             }
181             if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
182                 fwrite($fp, $content);
183                 fclose($fp);
184                 @rename($cachefile.'.tmp', $cachefile);
185                 @chmod($cachefile, $filepermissions);
186             }
187             @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
188             self::invalidate_opcode_php_cache($cachefile);
189         }
190     }
192     /**
193      * Are we in developer debug mode?
194      *
195      * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
196      *       the reason is we need to use this before we setup DB connection or caches for CFG.
197      *
198      * @return bool
199      */
200     protected static function is_developer() {
201         global $CFG;
203         // Note we can not rely on $CFG->debug here because DB is not initialised yet.
204         if (isset($CFG->config_php_settings['debug'])) {
205             $debug = (int)$CFG->config_php_settings['debug'];
206         } else {
207             return false;
208         }
210         if ($debug & E_ALL and $debug & E_STRICT) {
211             return true;
212         }
214         return false;
215     }
217     /**
218      * Create cache file content.
219      *
220      * @private this is intended for $CFG->alternative_component_cache only.
221      *
222      * @return string
223      */
224     public static function get_cache_content() {
225         if (!isset(self::$plugintypes)) {
226             self::fill_all_caches();
227         }
229         $cache = array(
230             'subsystems'  => self::$subsystems,
231             'plugintypes' => self::$plugintypes,
232             'plugins'     => self::$plugins,
233             'classmap'    => self::$classmap,
234             'filemap'     => self::$filemap,
235             'version'     => self::$version,
236         );
238         return '<?php
239 $cache = '.var_export($cache, true).';
240 ';
241     }
243     /**
244      * Fill all caches.
245      */
246     protected static function fill_all_caches() {
247         self::$subsystems = self::fetch_subsystems();
249         self::$plugintypes = self::fetch_plugintypes();
251         self::$plugins = array();
252         foreach (self::$plugintypes as $type => $fulldir) {
253             self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
254         }
256         self::fill_classmap_cache();
257         self::fill_filemap_cache();
258         self::fetch_core_version();
259     }
261     /**
262      * Get the core version.
263      *
264      * In order for this to work properly, opcache should be reset beforehand.
265      *
266      * @return float core version.
267      */
268     protected static function fetch_core_version() {
269         global $CFG;
270         if (self::$version === null) {
271             require($CFG->dirroot . '/version.php');
272             self::$version = $version;
273         }
274         return self::$version;
275     }
277     /**
278      * Returns list of core subsystems.
279      * @return array
280      */
281     protected static function fetch_subsystems() {
282         global $CFG;
284         // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
286         $info = array(
287             'access'      => null,
288             'admin'       => $CFG->dirroot.'/'.$CFG->admin,
289             'auth'        => $CFG->dirroot.'/auth',
290             'backup'      => $CFG->dirroot.'/backup/util/ui',
291             'badges'      => $CFG->dirroot.'/badges',
292             'block'       => $CFG->dirroot.'/blocks',
293             'blog'        => $CFG->dirroot.'/blog',
294             'bulkusers'   => null,
295             'cache'       => $CFG->dirroot.'/cache',
296             'calendar'    => $CFG->dirroot.'/calendar',
297             'cohort'      => $CFG->dirroot.'/cohort',
298             'condition'   => null,
299             'completion'  => null,
300             'countries'   => null,
301             'course'      => $CFG->dirroot.'/course',
302             'currencies'  => null,
303             'dbtransfer'  => null,
304             'debug'       => null,
305             'editor'      => $CFG->dirroot.'/lib/editor',
306             'edufields'   => null,
307             'enrol'       => $CFG->dirroot.'/enrol',
308             'error'       => null,
309             'filepicker'  => null,
310             'files'       => $CFG->dirroot.'/files',
311             'filters'     => null,
312             //'fonts'       => null, // Bogus.
313             'form'        => $CFG->dirroot.'/lib/form',
314             'grades'      => $CFG->dirroot.'/grade',
315             'grading'     => $CFG->dirroot.'/grade/grading',
316             'group'       => $CFG->dirroot.'/group',
317             'help'        => null,
318             'hub'         => null,
319             'imscc'       => null,
320             'install'     => null,
321             'iso6392'     => null,
322             'langconfig'  => null,
323             'license'     => null,
324             'mathslib'    => null,
325             'media'       => null,
326             'message'     => $CFG->dirroot.'/message',
327             'mimetypes'   => null,
328             'mnet'        => $CFG->dirroot.'/mnet',
329             //'moodle.org'  => null, // Not used any more.
330             'my'          => $CFG->dirroot.'/my',
331             'notes'       => $CFG->dirroot.'/notes',
332             'pagetype'    => null,
333             'pix'         => null,
334             'plagiarism'  => $CFG->dirroot.'/plagiarism',
335             'plugin'      => null,
336             'portfolio'   => $CFG->dirroot.'/portfolio',
337             'publish'     => $CFG->dirroot.'/course/publish',
338             'question'    => $CFG->dirroot.'/question',
339             'rating'      => $CFG->dirroot.'/rating',
340             'register'    => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
341             'repository'  => $CFG->dirroot.'/repository',
342             'rss'         => $CFG->dirroot.'/rss',
343             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
344             'search'      => null,
345             'table'       => null,
346             'tag'         => $CFG->dirroot.'/tag',
347             'timezones'   => null,
348             'user'        => $CFG->dirroot.'/user',
349             'userkey'     => null,
350             'webservice'  => $CFG->dirroot.'/webservice',
351         );
353         return $info;
354     }
356     /**
357      * Returns list of known plugin types.
358      * @return array
359      */
360     protected static function fetch_plugintypes() {
361         global $CFG;
363         $types = array(
364             'qtype'         => $CFG->dirroot.'/question/type',
365             'mod'           => $CFG->dirroot.'/mod',
366             'auth'          => $CFG->dirroot.'/auth',
367             'calendartype'  => $CFG->dirroot.'/calendar/type',
368             'enrol'         => $CFG->dirroot.'/enrol',
369             'message'       => $CFG->dirroot.'/message/output',
370             'block'         => $CFG->dirroot.'/blocks',
371             'filter'        => $CFG->dirroot.'/filter',
372             'editor'        => $CFG->dirroot.'/lib/editor',
373             'format'        => $CFG->dirroot.'/course/format',
374             'profilefield'  => $CFG->dirroot.'/user/profile/field',
375             'report'        => $CFG->dirroot.'/report',
376             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
377             'gradeexport'   => $CFG->dirroot.'/grade/export',
378             'gradeimport'   => $CFG->dirroot.'/grade/import',
379             'gradereport'   => $CFG->dirroot.'/grade/report',
380             'gradingform'   => $CFG->dirroot.'/grade/grading/form',
381             'mnetservice'   => $CFG->dirroot.'/mnet/service',
382             'webservice'    => $CFG->dirroot.'/webservice',
383             'repository'    => $CFG->dirroot.'/repository',
384             'portfolio'     => $CFG->dirroot.'/portfolio',
385             'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
386             'qformat'       => $CFG->dirroot.'/question/format',
387             'plagiarism'    => $CFG->dirroot.'/plagiarism',
388             'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
389             'cachestore'    => $CFG->dirroot.'/cache/stores',
390             'cachelock'     => $CFG->dirroot.'/cache/locks',
392         );
394         if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
395             $types['theme'] = $CFG->themedir;
396         } else {
397             $types['theme'] = $CFG->dirroot.'/theme';
398         }
400         foreach (self::$supportsubplugins as $type) {
401             if ($type === 'local') {
402                 // Local subplugins must be after local plugins.
403                 continue;
404             }
405             $subplugins = self::fetch_subplugins($type, $types[$type]);
406             foreach($subplugins as $subtype => $subplugin) {
407                 if (isset($types[$subtype])) {
408                     error_log("Invalid subtype '$subtype', duplicate detected.");
409                     continue;
410                 }
411                 $types[$subtype] = $subplugin;
412             }
413         }
415         // Local is always last!
416         $types['local'] = $CFG->dirroot.'/local';
418         if (in_array('local', self::$supportsubplugins)) {
419             $subplugins = self::fetch_subplugins('local', $types['local']);
420             foreach($subplugins as $subtype => $subplugin) {
421                 if (isset($types[$subtype])) {
422                     error_log("Invalid subtype '$subtype', duplicate detected.");
423                     continue;
424                 }
425                 $types[$subtype] = $subplugin;
426             }
427         }
429         return $types;
430     }
432     /**
433      * Returns list of subtypes defined in given plugin type.
434      * @param string $type
435      * @param string $fulldir
436      * @return array
437      */
438     protected static function fetch_subplugins($type, $fulldir) {
439         global $CFG;
441         $types = array();
442         $subpluginowners = self::fetch_plugins($type, $fulldir);
443         foreach ($subpluginowners as $ownerdir) {
444             if (file_exists("$ownerdir/db/subplugins.php")) {
445                 $subplugins = array();
446                 include("$ownerdir/db/subplugins.php");
447                 foreach ($subplugins as $subtype => $dir) {
448                     if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
449                         error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
450                         continue;
451                     }
452                     if (isset(self::$subsystems[$subtype])) {
453                         error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
454                         continue;
455                     }
456                     if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
457                         $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
458                     }
459                     if (!is_dir("$CFG->dirroot/$dir")) {
460                         error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
461                         continue;
462                     }
463                     $types[$subtype] = "$CFG->dirroot/$dir";
464                 }
465             }
466         }
467         return $types;
468     }
470     /**
471      * Returns list of plugins of given type in given directory.
472      * @param string $plugintype
473      * @param string $fulldir
474      * @return array
475      */
476     protected static function fetch_plugins($plugintype, $fulldir) {
477         global $CFG;
479         $fulldirs = (array)$fulldir;
480         if ($plugintype === 'theme') {
481             if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
482                 // Include themes in standard location too.
483                 array_unshift($fulldirs, $CFG->dirroot.'/theme');
484             }
485         }
487         $result = array();
489         foreach ($fulldirs as $fulldir) {
490             if (!is_dir($fulldir)) {
491                 continue;
492             }
493             $items = new \DirectoryIterator($fulldir);
494             foreach ($items as $item) {
495                 if ($item->isDot() or !$item->isDir()) {
496                     continue;
497                 }
498                 $pluginname = $item->getFilename();
499                 if ($plugintype === 'auth' and $pluginname === 'db') {
500                     // Special exception for this wrong plugin name.
501                 } else if (isset(self::$ignoreddirs[$pluginname])) {
502                     continue;
503                 }
504                 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
505                     // Always ignore plugins with problematic names here.
506                     continue;
507                 }
508                 $result[$pluginname] = $fulldir.'/'.$pluginname;
509                 unset($item);
510             }
511             unset($items);
512         }
514         ksort($result);
515         return $result;
516     }
518     /**
519      * Find all classes that can be autoloaded including frankenstyle namespaces.
520      */
521     protected static function fill_classmap_cache() {
522         global $CFG;
524         self::$classmap = array();
526         self::load_classes('core', "$CFG->dirroot/lib/classes");
528         foreach (self::$subsystems as $subsystem => $fulldir) {
529             self::load_classes('core_'.$subsystem, "$fulldir/classes");
530         }
532         foreach (self::$plugins as $plugintype => $plugins) {
533             foreach ($plugins as $pluginname => $fulldir) {
534                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
535             }
536         }
538         // Note: Add extra deprecated legacy classes here as necessary.
539         self::$classmap['textlib'] = "$CFG->dirroot/lib/classes/text.php";
540         self::$classmap['collatorlib'] = "$CFG->dirroot/lib/classes/collator.php";
541     }
544     /**
545      * Fills up the cache defining what plugins have certain files.
546      *
547      * @see self::get_plugin_list_with_file
548      * @return void
549      */
550     protected static function fill_filemap_cache() {
551         global $CFG;
553         self::$filemap = array();
555         foreach (self::$filestomap as $file) {
556             if (!isset(self::$filemap[$file])) {
557                 self::$filemap[$file] = array();
558             }
559             foreach (self::$plugins as $plugintype => $plugins) {
560                 if (!isset(self::$filemap[$file][$plugintype])) {
561                     self::$filemap[$file][$plugintype] = array();
562                 }
563                 foreach ($plugins as $pluginname => $fulldir) {
564                     if (file_exists("$fulldir/$file")) {
565                         self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
566                     }
567                 }
568             }
569         }
570     }
572     /**
573      * Find classes in directory and recurse to subdirs.
574      * @param string $component
575      * @param string $fulldir
576      * @param string $namespace
577      */
578     protected static function load_classes($component, $fulldir, $namespace = '') {
579         if (!is_dir($fulldir)) {
580             return;
581         }
583         $items = new \DirectoryIterator($fulldir);
584         foreach ($items as $item) {
585             if ($item->isDot()) {
586                 continue;
587             }
588             if ($item->isDir()) {
589                 $dirname = $item->getFilename();
590                 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
591                 continue;
592             }
594             $filename = $item->getFilename();
595             $classname = preg_replace('/\.php$/', '', $filename);
597             if ($filename === $classname) {
598                 // Not a php file.
599                 continue;
600             }
601             if ($namespace === '') {
602                 // Legacy long frankenstyle class name.
603                 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
604             }
605             // New namespaced classes.
606             self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
607         }
608         unset($item);
609         unset($items);
610     }
612     /**
613      * List all core subsystems and their location
614      *
615      * This is a whitelist of components that are part of the core and their
616      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
617      * plugin is not listed here and it does not have proper plugintype prefix,
618      * then it is considered as course activity module.
619      *
620      * The location is absolute file path to dir. NULL means there is no special
621      * directory for this subsystem. If the location is set, the subsystem's
622      * renderer.php is expected to be there.
623      *
624      * @return array of (string)name => (string|null)full dir location
625      */
626     public static function get_core_subsystems() {
627         self::init();
628         return self::$subsystems;
629     }
631     /**
632      * Get list of available plugin types together with their location.
633      *
634      * @return array as (string)plugintype => (string)fulldir
635      */
636     public static function get_plugin_types() {
637         self::init();
638         return self::$plugintypes;
639     }
641     /**
642      * Get list of plugins of given type.
643      *
644      * @param string $plugintype
645      * @return array as (string)pluginname => (string)fulldir
646      */
647     public static function get_plugin_list($plugintype) {
648         self::init();
650         if (!isset(self::$plugins[$plugintype])) {
651             return array();
652         }
653         return self::$plugins[$plugintype];
654     }
656     /**
657      * Get a list of all the plugins of a given type that define a certain class
658      * in a certain file. The plugin component names and class names are returned.
659      *
660      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
661      * @param string $class the part of the name of the class after the
662      *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
663      *      names like report_courselist_thing. If you are looking for classes with
664      *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
665      *      Frankenstyle namespaces are also supported.
666      * @param string $file the name of file within the plugin that defines the class.
667      * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
668      *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
669      */
670     public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
671         global $CFG; // Necessary in case it is referenced by included PHP scripts.
673         if ($class) {
674             $suffix = '_' . $class;
675         } else {
676             $suffix = '';
677         }
679         $pluginclasses = array();
680         $plugins = self::get_plugin_list($plugintype);
681         foreach ($plugins as $plugin => $fulldir) {
682             // Try class in frankenstyle namespace.
683             if ($class) {
684                 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
685                 if (class_exists($classname, true)) {
686                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
687                     continue;
688                 }
689             }
691             // Try autoloading of class with frankenstyle prefix.
692             $classname = $plugintype . '_' . $plugin . $suffix;
693             if (class_exists($classname, true)) {
694                 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
695                 continue;
696             }
698             // Fall back to old file location and class name.
699             if ($file and file_exists("$fulldir/$file")) {
700                 include_once("$fulldir/$file");
701                 if (class_exists($classname, false)) {
702                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
703                     continue;
704                 }
705             }
706         }
708         return $pluginclasses;
709     }
711     /**
712      * Get a list of all the plugins of a given type that contain a particular file.
713      *
714      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
715      * @param string $file the name of file that must be present in the plugin.
716      *                     (e.g. 'view.php', 'db/install.xml').
717      * @param bool $include if true (default false), the file will be include_once-ed if found.
718      * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
719      *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
720      */
721     public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
722         global $CFG; // Necessary in case it is referenced by included PHP scripts.
723         $pluginfiles = array();
725         if (isset(self::$filemap[$file])) {
726             // If the file was supposed to be mapped, then it should have been set in the array.
727             if (isset(self::$filemap[$file][$plugintype])) {
728                 $pluginfiles = self::$filemap[$file][$plugintype];
729             }
730         } else {
731             // Old-style search for non-cached files.
732             $plugins = self::get_plugin_list($plugintype);
733             foreach ($plugins as $plugin => $fulldir) {
734                 $path = $fulldir . '/' . $file;
735                 if (file_exists($path)) {
736                     $pluginfiles[$plugin] = $path;
737                 }
738             }
739         }
741         if ($include) {
742             foreach ($pluginfiles as $path) {
743                 include_once($path);
744             }
745         }
747         return $pluginfiles;
748     }
750     /**
751      * Returns the exact absolute path to plugin directory.
752      *
753      * @param string $plugintype type of plugin
754      * @param string $pluginname name of the plugin
755      * @return string full path to plugin directory; null if not found
756      */
757     public static function get_plugin_directory($plugintype, $pluginname) {
758         if (empty($pluginname)) {
759             // Invalid plugin name, sorry.
760             return null;
761         }
763         self::init();
765         if (!isset(self::$plugins[$plugintype][$pluginname])) {
766             return null;
767         }
768         return self::$plugins[$plugintype][$pluginname];
769     }
771     /**
772      * Returns the exact absolute path to plugin directory.
773      *
774      * @param string $subsystem type of core subsystem
775      * @return string full path to subsystem directory; null if not found
776      */
777     public static function get_subsystem_directory($subsystem) {
778         self::init();
780         if (!isset(self::$subsystems[$subsystem])) {
781             return null;
782         }
783         return self::$subsystems[$subsystem];
784     }
786     /**
787      * This method validates a plug name. It is much faster than calling clean_param.
788      *
789      * @param string $plugintype type of plugin
790      * @param string $pluginname a string that might be a plugin name.
791      * @return bool if this string is a valid plugin name.
792      */
793     public static function is_valid_plugin_name($plugintype, $pluginname) {
794         if ($plugintype === 'mod') {
795             // Modules must not have the same name as core subsystems.
796             if (!isset(self::$subsystems)) {
797                 // Watch out, this is called from init!
798                 self::init();
799             }
800             if (isset(self::$subsystems[$pluginname])) {
801                 return false;
802             }
803             // Modules MUST NOT have any underscores,
804             // component normalisation would break very badly otherwise!
805             return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
807         } else {
808             return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]$/', $pluginname);
809         }
810     }
812     /**
813      * Normalize the component name using the "frankenstyle" rules.
814      *
815      * Note: this does not verify the validity of plugin or type names.
816      *
817      * @param string $component
818      * @return array as (string)$type => (string)$plugin
819      */
820     public static function normalize_component($component) {
821         if ($component === 'moodle' or $component === 'core' or $component === '') {
822             return array('core', null);
823         }
825         if (strpos($component, '_') === false) {
826             self::init();
827             if (array_key_exists($component, self::$subsystems)) {
828                 $type   = 'core';
829                 $plugin = $component;
830             } else {
831                 // Everything else without underscore is a module.
832                 $type   = 'mod';
833                 $plugin = $component;
834             }
836         } else {
837             list($type, $plugin) = explode('_', $component, 2);
838             if ($type === 'moodle') {
839                 $type = 'core';
840             }
841             // Any unknown type must be a subplugin.
842         }
844         return array($type, $plugin);
845     }
847     /**
848      * Return exact absolute path to a plugin directory.
849      *
850      * @param string $component name such as 'moodle', 'mod_forum'
851      * @return string full path to component directory; NULL if not found
852      */
853     public static function get_component_directory($component) {
854         global $CFG;
856         list($type, $plugin) = self::normalize_component($component);
858         if ($type === 'core') {
859             if ($plugin === null) {
860                 return $path = $CFG->libdir;
861             }
862             return self::get_subsystem_directory($plugin);
863         }
865         return self::get_plugin_directory($type, $plugin);
866     }
868     /**
869      * Returns list of plugin types that allow subplugins.
870      * @return array as (string)plugintype => (string)fulldir
871      */
872     public static function get_plugin_types_with_subplugins() {
873         self::init();
875         $return = array();
876         foreach (self::$supportsubplugins as $type) {
877             $return[$type] = self::$plugintypes[$type];
878         }
879         return $return;
880     }
882     /**
883      * Returns hash of all versions including core and all plugins.
884      *
885      * This is relatively slow and not fully cached, use with care!
886      *
887      * @return string sha1 hash
888      */
889     public static function get_all_versions_hash() {
890         global $CFG;
892         self::init();
894         $versions = array();
896         // Main version first.
897         $versions['core'] = self::fetch_core_version();
899         // The problem here is tha the component cache might be stable,
900         // we want this to work also on frontpage without resetting the component cache.
901         $usecache = false;
902         if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
903             $usecache = true;
904         }
906         // Now all plugins.
907         $plugintypes = core_component::get_plugin_types();
908         foreach ($plugintypes as $type => $typedir) {
909             if ($usecache) {
910                 $plugs = core_component::get_plugin_list($type);
911             } else {
912                 $plugs = self::fetch_plugins($type, $typedir);
913             }
914             foreach ($plugs as $plug => $fullplug) {
915                 if ($type === 'mod') {
916                     $module = new stdClass();
917                     $module->version = null;
918                     include($fullplug.'/version.php');
919                     $versions[$plug] = $module->version;
920                 } else {
921                     $plugin = new stdClass();
922                     $plugin->version = null;
923                     @include($fullplug.'/version.php');
924                     $versions[$plug] = $plugin->version;
925                 }
926             }
927         }
929         return sha1(serialize($versions));
930     }
932     /**
933      * Invalidate opcode cache for given file, this is intended for
934      * php files that are stored in dataroot.
935      *
936      * Note: we need it here because this class must be self-contained.
937      *
938      * @param string $file
939      */
940     public static function invalidate_opcode_php_cache($file) {
941         if (function_exists('opcache_invalidate')) {
942             if (!file_exists($file)) {
943                 return;
944             }
945             opcache_invalidate($file, true);
946         }
947     }