Merge branch 'MDL-42957-26' of https://github.com/jamiepratt/moodle into MOODLE_26_STABLE
[moodle.git] / lib / classes / component.php
blob53c024144564cb142c5c3f2d075097b3b64345b1
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.
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.
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
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.
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 null cache of plugin types */
51 protected static $plugintypes = null;
52 /** @var null cache of plugin locations */
53 protected static $plugins = null;
54 /** @var null cache of core subsystems */
55 protected static $subsystems = null;
56 /** @var null subplugin type parents */
57 protected static $parents = null;
58 /** @var null subplugins */
59 protected static $subplugins = null;
60 /** @var null list of all known classes that can be autoloaded */
61 protected static $classmap = null;
62 /** @var null list of some known files that can be included. */
63 protected static $filemap = null;
64 /** @var int|float core version. */
65 protected static $version = null;
66 /** @var array list of the files to map. */
67 protected static $filestomap = array('lib.php', 'settings.php');
69 /**
70 * Class loader for Frankenstyle named classes in standard locations.
71 * Frankenstyle namespaces are supported.
73 * The expected location for core classes is:
74 * 1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
75 * 2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
76 * 3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
78 * The expected location for plugin classes is:
79 * 1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
80 * 2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
81 * 3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
83 * @param string $classname
85 public static function classloader($classname) {
86 self::init();
88 if (isset(self::$classmap[$classname])) {
89 // Global $CFG is expected in included scripts.
90 global $CFG;
91 // Function include would be faster, but for BC it is better to include only once.
92 include_once(self::$classmap[$classname]);
93 return;
97 /**
98 * Initialise caches, always call before accessing self:: caches.
100 protected static function init() {
101 global $CFG;
103 // Init only once per request/CLI execution, we ignore changes done afterwards.
104 if (isset(self::$plugintypes)) {
105 return;
108 if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
109 self::fill_all_caches();
110 return;
113 if (!empty($CFG->alternative_component_cache)) {
114 // Hack for heavily clustered sites that want to manage component cache invalidation manually.
115 $cachefile = $CFG->alternative_component_cache;
117 if (file_exists($cachefile)) {
118 if (CACHE_DISABLE_ALL) {
119 // Verify the cache state only on upgrade pages.
120 $content = self::get_cache_content();
121 if (sha1_file($cachefile) !== sha1($content)) {
122 die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
124 return;
126 $cache = array();
127 include($cachefile);
128 self::$plugintypes = $cache['plugintypes'];
129 self::$plugins = $cache['plugins'];
130 self::$subsystems = $cache['subsystems'];
131 self::$parents = $cache['parents'];
132 self::$subplugins = $cache['subplugins'];
133 self::$classmap = $cache['classmap'];
134 self::$filemap = $cache['filemap'];
135 return;
138 if (!is_writable(dirname($cachefile))) {
139 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
142 // Lets try to create the file, it might be in some writable directory or a local cache dir.
144 } else {
145 // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
146 // use $CFG->alternative_component_cache if you do not like it.
147 $cachefile = "$CFG->cachedir/core_component.php";
150 if (!CACHE_DISABLE_ALL and !self::is_developer()) {
151 // 1/ Use the cache only outside of install and upgrade.
152 // 2/ Let developers add/remove classes in developer mode.
153 if (is_readable($cachefile)) {
154 $cache = false;
155 include($cachefile);
156 if (!is_array($cache)) {
157 // Something is very wrong.
158 } else if (!isset($cache['version'])) {
159 // Something is very wrong.
160 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
161 // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
162 error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
163 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
164 // $CFG->dirroot was changed.
165 } else {
166 // The cache looks ok, let's use it.
167 self::$plugintypes = $cache['plugintypes'];
168 self::$plugins = $cache['plugins'];
169 self::$subsystems = $cache['subsystems'];
170 self::$parents = $cache['parents'];
171 self::$subplugins = $cache['subplugins'];
172 self::$classmap = $cache['classmap'];
173 self::$filemap = $cache['filemap'];
174 return;
176 // Note: we do not verify $CFG->admin here intentionally,
177 // they must visit admin/index.php after any change.
181 if (!isset(self::$plugintypes)) {
182 // This needs to be atomic and self-fixing as much as possible.
184 $content = self::get_cache_content();
185 if (file_exists($cachefile)) {
186 if (sha1_file($cachefile) === sha1($content)) {
187 return;
189 // Stale cache detected!
190 unlink($cachefile);
193 // Permissions might not be setup properly in installers.
194 $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
195 $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
197 clearstatcache();
198 $cachedir = dirname($cachefile);
199 if (!is_dir($cachedir)) {
200 mkdir($cachedir, $dirpermissions, true);
203 if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
204 fwrite($fp, $content);
205 fclose($fp);
206 @rename($cachefile.'.tmp', $cachefile);
207 @chmod($cachefile, $filepermissions);
209 @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
210 self::invalidate_opcode_php_cache($cachefile);
215 * Are we in developer debug mode?
217 * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
218 * the reason is we need to use this before we setup DB connection or caches for CFG.
220 * @return bool
222 protected static function is_developer() {
223 global $CFG;
225 // Note we can not rely on $CFG->debug here because DB is not initialised yet.
226 if (isset($CFG->config_php_settings['debug'])) {
227 $debug = (int)$CFG->config_php_settings['debug'];
228 } else {
229 return false;
232 if ($debug & E_ALL and $debug & E_STRICT) {
233 return true;
236 return false;
240 * Create cache file content.
242 * @private this is intended for $CFG->alternative_component_cache only.
244 * @return string
246 public static function get_cache_content() {
247 if (!isset(self::$plugintypes)) {
248 self::fill_all_caches();
251 $cache = array(
252 'subsystems' => self::$subsystems,
253 'plugintypes' => self::$plugintypes,
254 'plugins' => self::$plugins,
255 'parents' => self::$parents,
256 'subplugins' => self::$subplugins,
257 'classmap' => self::$classmap,
258 'filemap' => self::$filemap,
259 'version' => self::$version,
262 return '<?php
263 $cache = '.var_export($cache, true).';
268 * Fill all caches.
270 protected static function fill_all_caches() {
271 self::$subsystems = self::fetch_subsystems();
273 list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
275 self::$plugins = array();
276 foreach (self::$plugintypes as $type => $fulldir) {
277 self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
280 self::fill_classmap_cache();
281 self::fill_filemap_cache();
282 self::fetch_core_version();
286 * Get the core version.
288 * In order for this to work properly, opcache should be reset beforehand.
290 * @return float core version.
292 protected static function fetch_core_version() {
293 global $CFG;
294 if (self::$version === null) {
295 $version = null; // Prevent IDE complaints.
296 require($CFG->dirroot . '/version.php');
297 self::$version = $version;
299 return self::$version;
303 * Returns list of core subsystems.
304 * @return array
306 protected static function fetch_subsystems() {
307 global $CFG;
309 // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
311 $info = array(
312 'access' => null,
313 'admin' => $CFG->dirroot.'/'.$CFG->admin,
314 'auth' => $CFG->dirroot.'/auth',
315 'backup' => $CFG->dirroot.'/backup/util/ui',
316 'badges' => $CFG->dirroot.'/badges',
317 'block' => $CFG->dirroot.'/blocks',
318 'blog' => $CFG->dirroot.'/blog',
319 'bulkusers' => null,
320 'cache' => $CFG->dirroot.'/cache',
321 'calendar' => $CFG->dirroot.'/calendar',
322 'cohort' => $CFG->dirroot.'/cohort',
323 'condition' => null,
324 'completion' => null,
325 'countries' => null,
326 'course' => $CFG->dirroot.'/course',
327 'currencies' => null,
328 'dbtransfer' => null,
329 'debug' => null,
330 'editor' => $CFG->dirroot.'/lib/editor',
331 'edufields' => null,
332 'enrol' => $CFG->dirroot.'/enrol',
333 'error' => null,
334 'filepicker' => null,
335 'files' => $CFG->dirroot.'/files',
336 'filters' => null,
337 //'fonts' => null, // Bogus.
338 'form' => $CFG->dirroot.'/lib/form',
339 'grades' => $CFG->dirroot.'/grade',
340 'grading' => $CFG->dirroot.'/grade/grading',
341 'group' => $CFG->dirroot.'/group',
342 'help' => null,
343 'hub' => null,
344 'imscc' => null,
345 'install' => null,
346 'iso6392' => null,
347 'langconfig' => null,
348 'license' => null,
349 'mathslib' => null,
350 'media' => null,
351 'message' => $CFG->dirroot.'/message',
352 'mimetypes' => null,
353 'mnet' => $CFG->dirroot.'/mnet',
354 //'moodle.org' => null, // Not used any more.
355 'my' => $CFG->dirroot.'/my',
356 'notes' => $CFG->dirroot.'/notes',
357 'pagetype' => null,
358 'pix' => null,
359 'plagiarism' => $CFG->dirroot.'/plagiarism',
360 'plugin' => null,
361 'portfolio' => $CFG->dirroot.'/portfolio',
362 'publish' => $CFG->dirroot.'/course/publish',
363 'question' => $CFG->dirroot.'/question',
364 'rating' => $CFG->dirroot.'/rating',
365 'register' => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
366 'repository' => $CFG->dirroot.'/repository',
367 'rss' => $CFG->dirroot.'/rss',
368 'role' => $CFG->dirroot.'/'.$CFG->admin.'/roles',
369 'search' => null,
370 'table' => null,
371 'tag' => $CFG->dirroot.'/tag',
372 'timezones' => null,
373 'user' => $CFG->dirroot.'/user',
374 'userkey' => null,
375 'webservice' => $CFG->dirroot.'/webservice',
378 return $info;
382 * Returns list of known plugin types.
383 * @return array
385 protected static function fetch_plugintypes() {
386 global $CFG;
388 $types = array(
389 'qtype' => $CFG->dirroot.'/question/type',
390 'mod' => $CFG->dirroot.'/mod',
391 'auth' => $CFG->dirroot.'/auth',
392 'calendartype' => $CFG->dirroot.'/calendar/type',
393 'enrol' => $CFG->dirroot.'/enrol',
394 'message' => $CFG->dirroot.'/message/output',
395 'block' => $CFG->dirroot.'/blocks',
396 'filter' => $CFG->dirroot.'/filter',
397 'editor' => $CFG->dirroot.'/lib/editor',
398 'format' => $CFG->dirroot.'/course/format',
399 'profilefield' => $CFG->dirroot.'/user/profile/field',
400 'report' => $CFG->dirroot.'/report',
401 'coursereport' => $CFG->dirroot.'/course/report', // Must be after system reports.
402 'gradeexport' => $CFG->dirroot.'/grade/export',
403 'gradeimport' => $CFG->dirroot.'/grade/import',
404 'gradereport' => $CFG->dirroot.'/grade/report',
405 'gradingform' => $CFG->dirroot.'/grade/grading/form',
406 'mnetservice' => $CFG->dirroot.'/mnet/service',
407 'webservice' => $CFG->dirroot.'/webservice',
408 'repository' => $CFG->dirroot.'/repository',
409 'portfolio' => $CFG->dirroot.'/portfolio',
410 'qbehaviour' => $CFG->dirroot.'/question/behaviour',
411 'qformat' => $CFG->dirroot.'/question/format',
412 'plagiarism' => $CFG->dirroot.'/plagiarism',
413 'tool' => $CFG->dirroot.'/'.$CFG->admin.'/tool',
414 'cachestore' => $CFG->dirroot.'/cache/stores',
415 'cachelock' => $CFG->dirroot.'/cache/locks',
417 $parents = array();
418 $subplugins = array();
420 if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
421 $types['theme'] = $CFG->themedir;
422 } else {
423 $types['theme'] = $CFG->dirroot.'/theme';
426 foreach (self::$supportsubplugins as $type) {
427 if ($type === 'local') {
428 // Local subplugins must be after local plugins.
429 continue;
431 $plugins = self::fetch_plugins($type, $types[$type]);
432 foreach ($plugins as $plugin => $fulldir) {
433 $subtypes = self::fetch_subtypes($fulldir);
434 if (!$subtypes) {
435 continue;
437 $subplugins[$type.'_'.$plugin] = array();
438 foreach($subtypes as $subtype => $subdir) {
439 if (isset($types[$subtype])) {
440 error_log("Invalid subtype '$subtype', duplicate detected.");
441 continue;
443 $types[$subtype] = $subdir;
444 $parents[$subtype] = $type.'_'.$plugin;
445 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
449 // Local is always last!
450 $types['local'] = $CFG->dirroot.'/local';
452 if (in_array('local', self::$supportsubplugins)) {
453 $type = 'local';
454 $plugins = self::fetch_plugins($type, $types[$type]);
455 foreach ($plugins as $plugin => $fulldir) {
456 $subtypes = self::fetch_subtypes($fulldir);
457 if (!$subtypes) {
458 continue;
460 $subplugins[$type.'_'.$plugin] = array();
461 foreach($subtypes as $subtype => $subdir) {
462 if (isset($types[$subtype])) {
463 error_log("Invalid subtype '$subtype', duplicate detected.");
464 continue;
466 $types[$subtype] = $subdir;
467 $parents[$subtype] = $type.'_'.$plugin;
468 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
473 return array($types, $parents, $subplugins);
477 * Returns list of subtypes.
478 * @param string $ownerdir
479 * @return array
481 protected static function fetch_subtypes($ownerdir) {
482 global $CFG;
484 $types = array();
485 if (file_exists("$ownerdir/db/subplugins.php")) {
486 $subplugins = array();
487 include("$ownerdir/db/subplugins.php");
488 foreach ($subplugins as $subtype => $dir) {
489 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
490 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
491 continue;
493 if (isset(self::$subsystems[$subtype])) {
494 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
495 continue;
497 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
498 $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
500 if (!is_dir("$CFG->dirroot/$dir")) {
501 error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
502 continue;
504 $types[$subtype] = "$CFG->dirroot/$dir";
507 return $types;
511 * Returns list of plugins of given type in given directory.
512 * @param string $plugintype
513 * @param string $fulldir
514 * @return array
516 protected static function fetch_plugins($plugintype, $fulldir) {
517 global $CFG;
519 $fulldirs = (array)$fulldir;
520 if ($plugintype === 'theme') {
521 if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
522 // Include themes in standard location too.
523 array_unshift($fulldirs, $CFG->dirroot.'/theme');
527 $result = array();
529 foreach ($fulldirs as $fulldir) {
530 if (!is_dir($fulldir)) {
531 continue;
533 $items = new \DirectoryIterator($fulldir);
534 foreach ($items as $item) {
535 if ($item->isDot() or !$item->isDir()) {
536 continue;
538 $pluginname = $item->getFilename();
539 if ($plugintype === 'auth' and $pluginname === 'db') {
540 // Special exception for this wrong plugin name.
541 } else if (isset(self::$ignoreddirs[$pluginname])) {
542 continue;
544 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
545 // Always ignore plugins with problematic names here.
546 continue;
548 $result[$pluginname] = $fulldir.'/'.$pluginname;
549 unset($item);
551 unset($items);
554 ksort($result);
555 return $result;
559 * Find all classes that can be autoloaded including frankenstyle namespaces.
561 protected static function fill_classmap_cache() {
562 global $CFG;
564 self::$classmap = array();
566 self::load_classes('core', "$CFG->dirroot/lib/classes");
568 foreach (self::$subsystems as $subsystem => $fulldir) {
569 self::load_classes('core_'.$subsystem, "$fulldir/classes");
572 foreach (self::$plugins as $plugintype => $plugins) {
573 foreach ($plugins as $pluginname => $fulldir) {
574 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
578 // Note: Add extra deprecated legacy classes here as necessary.
579 self::$classmap['textlib'] = "$CFG->dirroot/lib/classes/text.php";
580 self::$classmap['collatorlib'] = "$CFG->dirroot/lib/classes/collator.php";
585 * Fills up the cache defining what plugins have certain files.
587 * @see self::get_plugin_list_with_file
588 * @return void
590 protected static function fill_filemap_cache() {
591 global $CFG;
593 self::$filemap = array();
595 foreach (self::$filestomap as $file) {
596 if (!isset(self::$filemap[$file])) {
597 self::$filemap[$file] = array();
599 foreach (self::$plugins as $plugintype => $plugins) {
600 if (!isset(self::$filemap[$file][$plugintype])) {
601 self::$filemap[$file][$plugintype] = array();
603 foreach ($plugins as $pluginname => $fulldir) {
604 if (file_exists("$fulldir/$file")) {
605 self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
613 * Find classes in directory and recurse to subdirs.
614 * @param string $component
615 * @param string $fulldir
616 * @param string $namespace
618 protected static function load_classes($component, $fulldir, $namespace = '') {
619 if (!is_dir($fulldir)) {
620 return;
623 $items = new \DirectoryIterator($fulldir);
624 foreach ($items as $item) {
625 if ($item->isDot()) {
626 continue;
628 if ($item->isDir()) {
629 $dirname = $item->getFilename();
630 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
631 continue;
634 $filename = $item->getFilename();
635 $classname = preg_replace('/\.php$/', '', $filename);
637 if ($filename === $classname) {
638 // Not a php file.
639 continue;
641 if ($namespace === '') {
642 // Legacy long frankenstyle class name.
643 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
645 // New namespaced classes.
646 self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
648 unset($item);
649 unset($items);
653 * List all core subsystems and their location
655 * This is a whitelist of components that are part of the core and their
656 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
657 * plugin is not listed here and it does not have proper plugintype prefix,
658 * then it is considered as course activity module.
660 * The location is absolute file path to dir. NULL means there is no special
661 * directory for this subsystem. If the location is set, the subsystem's
662 * renderer.php is expected to be there.
664 * @return array of (string)name => (string|null)full dir location
666 public static function get_core_subsystems() {
667 self::init();
668 return self::$subsystems;
672 * Get list of available plugin types together with their location.
674 * @return array as (string)plugintype => (string)fulldir
676 public static function get_plugin_types() {
677 self::init();
678 return self::$plugintypes;
682 * Get list of plugins of given type.
684 * @param string $plugintype
685 * @return array as (string)pluginname => (string)fulldir
687 public static function get_plugin_list($plugintype) {
688 self::init();
690 if (!isset(self::$plugins[$plugintype])) {
691 return array();
693 return self::$plugins[$plugintype];
697 * Get a list of all the plugins of a given type that define a certain class
698 * in a certain file. The plugin component names and class names are returned.
700 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
701 * @param string $class the part of the name of the class after the
702 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
703 * names like report_courselist_thing. If you are looking for classes with
704 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
705 * Frankenstyle namespaces are also supported.
706 * @param string $file the name of file within the plugin that defines the class.
707 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
708 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
710 public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
711 global $CFG; // Necessary in case it is referenced by included PHP scripts.
713 if ($class) {
714 $suffix = '_' . $class;
715 } else {
716 $suffix = '';
719 $pluginclasses = array();
720 $plugins = self::get_plugin_list($plugintype);
721 foreach ($plugins as $plugin => $fulldir) {
722 // Try class in frankenstyle namespace.
723 if ($class) {
724 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
725 if (class_exists($classname, true)) {
726 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
727 continue;
731 // Try autoloading of class with frankenstyle prefix.
732 $classname = $plugintype . '_' . $plugin . $suffix;
733 if (class_exists($classname, true)) {
734 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
735 continue;
738 // Fall back to old file location and class name.
739 if ($file and file_exists("$fulldir/$file")) {
740 include_once("$fulldir/$file");
741 if (class_exists($classname, false)) {
742 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
743 continue;
748 return $pluginclasses;
752 * Get a list of all the plugins of a given type that contain a particular file.
754 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
755 * @param string $file the name of file that must be present in the plugin.
756 * (e.g. 'view.php', 'db/install.xml').
757 * @param bool $include if true (default false), the file will be include_once-ed if found.
758 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
759 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
761 public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
762 global $CFG; // Necessary in case it is referenced by included PHP scripts.
763 $pluginfiles = array();
765 if (isset(self::$filemap[$file])) {
766 // If the file was supposed to be mapped, then it should have been set in the array.
767 if (isset(self::$filemap[$file][$plugintype])) {
768 $pluginfiles = self::$filemap[$file][$plugintype];
770 } else {
771 // Old-style search for non-cached files.
772 $plugins = self::get_plugin_list($plugintype);
773 foreach ($plugins as $plugin => $fulldir) {
774 $path = $fulldir . '/' . $file;
775 if (file_exists($path)) {
776 $pluginfiles[$plugin] = $path;
781 if ($include) {
782 foreach ($pluginfiles as $path) {
783 include_once($path);
787 return $pluginfiles;
791 * Returns the exact absolute path to plugin directory.
793 * @param string $plugintype type of plugin
794 * @param string $pluginname name of the plugin
795 * @return string full path to plugin directory; null if not found
797 public static function get_plugin_directory($plugintype, $pluginname) {
798 if (empty($pluginname)) {
799 // Invalid plugin name, sorry.
800 return null;
803 self::init();
805 if (!isset(self::$plugins[$plugintype][$pluginname])) {
806 return null;
808 return self::$plugins[$plugintype][$pluginname];
812 * Returns the exact absolute path to plugin directory.
814 * @param string $subsystem type of core subsystem
815 * @return string full path to subsystem directory; null if not found
817 public static function get_subsystem_directory($subsystem) {
818 self::init();
820 if (!isset(self::$subsystems[$subsystem])) {
821 return null;
823 return self::$subsystems[$subsystem];
827 * This method validates a plug name. It is much faster than calling clean_param.
829 * @param string $plugintype type of plugin
830 * @param string $pluginname a string that might be a plugin name.
831 * @return bool if this string is a valid plugin name.
833 public static function is_valid_plugin_name($plugintype, $pluginname) {
834 if ($plugintype === 'mod') {
835 // Modules must not have the same name as core subsystems.
836 if (!isset(self::$subsystems)) {
837 // Watch out, this is called from init!
838 self::init();
840 if (isset(self::$subsystems[$pluginname])) {
841 return false;
843 // Modules MUST NOT have any underscores,
844 // component normalisation would break very badly otherwise!
845 return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
847 } else {
848 return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
853 * Normalize the component name using the "frankenstyle" rules.
855 * Note: this does not verify the validity of plugin or type names.
857 * @param string $component
858 * @return array as (string)$type => (string)$plugin
860 public static function normalize_component($component) {
861 if ($component === 'moodle' or $component === 'core' or $component === '') {
862 return array('core', null);
865 if (strpos($component, '_') === false) {
866 self::init();
867 if (array_key_exists($component, self::$subsystems)) {
868 $type = 'core';
869 $plugin = $component;
870 } else {
871 // Everything else without underscore is a module.
872 $type = 'mod';
873 $plugin = $component;
876 } else {
877 list($type, $plugin) = explode('_', $component, 2);
878 if ($type === 'moodle') {
879 $type = 'core';
881 // Any unknown type must be a subplugin.
884 return array($type, $plugin);
888 * Return exact absolute path to a plugin directory.
890 * @param string $component name such as 'moodle', 'mod_forum'
891 * @return string full path to component directory; NULL if not found
893 public static function get_component_directory($component) {
894 global $CFG;
896 list($type, $plugin) = self::normalize_component($component);
898 if ($type === 'core') {
899 if ($plugin === null) {
900 return $path = $CFG->libdir;
902 return self::get_subsystem_directory($plugin);
905 return self::get_plugin_directory($type, $plugin);
909 * Returns list of plugin types that allow subplugins.
910 * @return array as (string)plugintype => (string)fulldir
912 public static function get_plugin_types_with_subplugins() {
913 self::init();
915 $return = array();
916 foreach (self::$supportsubplugins as $type) {
917 $return[$type] = self::$plugintypes[$type];
919 return $return;
923 * Returns parent of this subplugin type.
925 * @param string $type
926 * @return string parent component or null
928 public static function get_subtype_parent($type) {
929 self::init();
931 if (isset(self::$parents[$type])) {
932 return self::$parents[$type];
935 return null;
939 * Return all subplugins of this component.
940 * @param string $component.
941 * @return array $subtype=>array($component, ..), null if no subtypes defined
943 public static function get_subplugins($component) {
944 self::init();
946 if (isset(self::$subplugins[$component])) {
947 return self::$subplugins[$component];
950 return null;
954 * Returns hash of all versions including core and all plugins.
956 * This is relatively slow and not fully cached, use with care!
958 * @return string sha1 hash
960 public static function get_all_versions_hash() {
961 global $CFG;
963 self::init();
965 $versions = array();
967 // Main version first.
968 $versions['core'] = self::fetch_core_version();
970 // The problem here is tha the component cache might be stable,
971 // we want this to work also on frontpage without resetting the component cache.
972 $usecache = false;
973 if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
974 $usecache = true;
977 // Now all plugins.
978 $plugintypes = core_component::get_plugin_types();
979 foreach ($plugintypes as $type => $typedir) {
980 if ($usecache) {
981 $plugs = core_component::get_plugin_list($type);
982 } else {
983 $plugs = self::fetch_plugins($type, $typedir);
985 foreach ($plugs as $plug => $fullplug) {
986 $plugin = new stdClass();
987 $plugin->version = null;
988 $module = $plugin;
989 @include($fullplug.'/version.php');
990 $versions[$type.'_'.$plug] = $plugin->version;
994 return sha1(serialize($versions));
998 * Invalidate opcode cache for given file, this is intended for
999 * php files that are stored in dataroot.
1001 * Note: we need it here because this class must be self-contained.
1003 * @param string $file
1005 public static function invalidate_opcode_php_cache($file) {
1006 if (function_exists('opcache_invalidate')) {
1007 if (!file_exists($file)) {
1008 return;
1010 opcache_invalidate($file, true);