MDL-57898 core_customfield: Custom fields API
[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 associative array of PSR-0 namespaces and corresponding paths. */
71     protected static $psr0namespaces = array(
72         'Horde' => 'lib/horde/framework/Horde',
73         'Mustache' => 'lib/mustache/src/Mustache',
74     );
75     /** @var array associative array of PRS-4 namespaces and corresponding paths. */
76     protected static $psr4namespaces = array(
77         'MaxMind' => 'lib/maxmind/MaxMind',
78         'GeoIp2' => 'lib/maxmind/GeoIp2',
79         'Sabberworm\\CSS' => 'lib/php-css-parser',
80         'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
81         'Leafo\\ScssPhp' => 'lib/scssphp',
82         'Box\\Spout' => 'lib/spout/src/Spout',
83         'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
84         'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
85         'IMSGlobal\LTI' => 'lib/ltiprovider/src',
86         'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
87         'PHPMailer\\PHPMailer' => 'lib/phpmailer/src',
88         'RedeyeVentures\\GeoPattern' => 'lib/geopattern-php/GeoPattern',
89     );
91     /**
92      * Class loader for Frankenstyle named classes in standard locations.
93      * Frankenstyle namespaces are supported.
94      *
95      * The expected location for core classes is:
96      *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
97      *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
98      *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
99      *
100      * The expected location for plugin classes is:
101      *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
102      *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
103      *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
104      *
105      * @param string $classname
106      */
107     public static function classloader($classname) {
108         self::init();
110         if (isset(self::$classmap[$classname])) {
111             // Global $CFG is expected in included scripts.
112             global $CFG;
113             // Function include would be faster, but for BC it is better to include only once.
114             include_once(self::$classmap[$classname]);
115             return;
116         }
117         if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
118             $newclassname = self::$classmaprenames[$classname];
119             $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
120             debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
121             if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
122                 throw new \coding_exception("Cannot alias $classname to $newclassname");
123             }
124             class_alias($newclassname, $classname);
125             return;
126         }
128         $file = self::psr_classloader($classname);
129         // If the file is found, require it.
130         if (!empty($file)) {
131             require($file);
132             return;
133         }
134     }
136     /**
137      * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
138      * demand. Only returns paths to files that exist.
139      *
140      * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
141      * compatible.
142      *
143      * @param string $class the name of the class.
144      * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
145      */
146     protected static function psr_classloader($class) {
147         // Iterate through each PSR-4 namespace prefix.
148         foreach (self::$psr4namespaces as $prefix => $path) {
149             $file = self::get_class_file($class, $prefix, $path, array('\\'));
150             if (!empty($file) && file_exists($file)) {
151                 return $file;
152             }
153         }
155         // Iterate through each PSR-0 namespace prefix.
156         foreach (self::$psr0namespaces as $prefix => $path) {
157             $file = self::get_class_file($class, $prefix, $path, array('\\', '_'));
158             if (!empty($file) && file_exists($file)) {
159                 return $file;
160             }
161         }
163         return false;
164     }
166     /**
167      * Return the path to the class based on the given namespace prefix and path it corresponds to.
168      *
169      * Will return the path even if the file does not exist. Check the file esists before requiring.
170      *
171      * @param string $class the name of the class.
172      * @param string $prefix The namespace prefix used to identify the base directory of the source files.
173      * @param string $path The relative path to the base directory of the source files.
174      * @param string[] $separators The characters that should be used for separating.
175      * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
176      */
177     protected static function get_class_file($class, $prefix, $path, $separators) {
178         global $CFG;
180         // Does the class use the namespace prefix?
181         $len = strlen($prefix);
182         if (strncmp($prefix, $class, $len) !== 0) {
183             // No, move to the next prefix.
184             return false;
185         }
186         $path = $CFG->dirroot . '/' . $path;
188         // Get the relative class name.
189         $relativeclass = substr($class, $len);
191         // Replace the namespace prefix with the base directory, replace namespace
192         // separators with directory separators in the relative class name, append
193         // with .php.
194         $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
196         return $file;
197     }
200     /**
201      * Initialise caches, always call before accessing self:: caches.
202      */
203     protected static function init() {
204         global $CFG;
206         // Init only once per request/CLI execution, we ignore changes done afterwards.
207         if (isset(self::$plugintypes)) {
208             return;
209         }
211         if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
212             self::fill_all_caches();
213             return;
214         }
216         if (!empty($CFG->alternative_component_cache)) {
217             // Hack for heavily clustered sites that want to manage component cache invalidation manually.
218             $cachefile = $CFG->alternative_component_cache;
220             if (file_exists($cachefile)) {
221                 if (CACHE_DISABLE_ALL) {
222                     // Verify the cache state only on upgrade pages.
223                     $content = self::get_cache_content();
224                     if (sha1_file($cachefile) !== sha1($content)) {
225                         die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
226                     }
227                     return;
228                 }
229                 $cache = array();
230                 include($cachefile);
231                 self::$plugintypes      = $cache['plugintypes'];
232                 self::$plugins          = $cache['plugins'];
233                 self::$subsystems       = $cache['subsystems'];
234                 self::$parents          = $cache['parents'];
235                 self::$subplugins       = $cache['subplugins'];
236                 self::$classmap         = $cache['classmap'];
237                 self::$classmaprenames  = $cache['classmaprenames'];
238                 self::$filemap          = $cache['filemap'];
239                 return;
240             }
242             if (!is_writable(dirname($cachefile))) {
243                 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
244             }
246             // Lets try to create the file, it might be in some writable directory or a local cache dir.
248         } else {
249             // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
250             //       use $CFG->alternative_component_cache if you do not like it.
251             $cachefile = "$CFG->cachedir/core_component.php";
252         }
254         if (!CACHE_DISABLE_ALL and !self::is_developer()) {
255             // 1/ Use the cache only outside of install and upgrade.
256             // 2/ Let developers add/remove classes in developer mode.
257             if (is_readable($cachefile)) {
258                 $cache = false;
259                 include($cachefile);
260                 if (!is_array($cache)) {
261                     // Something is very wrong.
262                 } else if (!isset($cache['version'])) {
263                     // Something is very wrong.
264                 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
265                     // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
266                     error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
267                 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
268                     // $CFG->dirroot was changed.
269                 } else {
270                     // The cache looks ok, let's use it.
271                     self::$plugintypes      = $cache['plugintypes'];
272                     self::$plugins          = $cache['plugins'];
273                     self::$subsystems       = $cache['subsystems'];
274                     self::$parents          = $cache['parents'];
275                     self::$subplugins       = $cache['subplugins'];
276                     self::$classmap         = $cache['classmap'];
277                     self::$classmaprenames  = $cache['classmaprenames'];
278                     self::$filemap          = $cache['filemap'];
279                     return;
280                 }
281                 // Note: we do not verify $CFG->admin here intentionally,
282                 //       they must visit admin/index.php after any change.
283             }
284         }
286         if (!isset(self::$plugintypes)) {
287             // This needs to be atomic and self-fixing as much as possible.
289             $content = self::get_cache_content();
290             if (file_exists($cachefile)) {
291                 if (sha1_file($cachefile) === sha1($content)) {
292                     return;
293                 }
294                 // Stale cache detected!
295                 unlink($cachefile);
296             }
298             // Permissions might not be setup properly in installers.
299             $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
300             $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
302             clearstatcache();
303             $cachedir = dirname($cachefile);
304             if (!is_dir($cachedir)) {
305                 mkdir($cachedir, $dirpermissions, true);
306             }
308             if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
309                 fwrite($fp, $content);
310                 fclose($fp);
311                 @rename($cachefile.'.tmp', $cachefile);
312                 @chmod($cachefile, $filepermissions);
313             }
314             @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
315             self::invalidate_opcode_php_cache($cachefile);
316         }
317     }
319     /**
320      * Are we in developer debug mode?
321      *
322      * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
323      *       the reason is we need to use this before we setup DB connection or caches for CFG.
324      *
325      * @return bool
326      */
327     protected static function is_developer() {
328         global $CFG;
330         // Note we can not rely on $CFG->debug here because DB is not initialised yet.
331         if (isset($CFG->config_php_settings['debug'])) {
332             $debug = (int)$CFG->config_php_settings['debug'];
333         } else {
334             return false;
335         }
337         if ($debug & E_ALL and $debug & E_STRICT) {
338             return true;
339         }
341         return false;
342     }
344     /**
345      * Create cache file content.
346      *
347      * @private this is intended for $CFG->alternative_component_cache only.
348      *
349      * @return string
350      */
351     public static function get_cache_content() {
352         if (!isset(self::$plugintypes)) {
353             self::fill_all_caches();
354         }
356         $cache = array(
357             'subsystems'        => self::$subsystems,
358             'plugintypes'       => self::$plugintypes,
359             'plugins'           => self::$plugins,
360             'parents'           => self::$parents,
361             'subplugins'        => self::$subplugins,
362             'classmap'          => self::$classmap,
363             'classmaprenames'   => self::$classmaprenames,
364             'filemap'           => self::$filemap,
365             'version'           => self::$version,
366         );
368         return '<?php
369 $cache = '.var_export($cache, true).';
370 ';
371     }
373     /**
374      * Fill all caches.
375      */
376     protected static function fill_all_caches() {
377         self::$subsystems = self::fetch_subsystems();
379         list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
381         self::$plugins = array();
382         foreach (self::$plugintypes as $type => $fulldir) {
383             self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
384         }
386         self::fill_classmap_cache();
387         self::fill_classmap_renames_cache();
388         self::fill_filemap_cache();
389         self::fetch_core_version();
390     }
392     /**
393      * Get the core version.
394      *
395      * In order for this to work properly, opcache should be reset beforehand.
396      *
397      * @return float core version.
398      */
399     protected static function fetch_core_version() {
400         global $CFG;
401         if (self::$version === null) {
402             $version = null; // Prevent IDE complaints.
403             require($CFG->dirroot . '/version.php');
404             self::$version = $version;
405         }
406         return self::$version;
407     }
409     /**
410      * Returns list of core subsystems.
411      * @return array
412      */
413     protected static function fetch_subsystems() {
414         global $CFG;
416         // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
418         $info = array(
419             'access'      => null,
420             'admin'       => $CFG->dirroot.'/'.$CFG->admin,
421             'analytics'   => $CFG->dirroot . '/analytics',
422             'antivirus'   => $CFG->dirroot . '/lib/antivirus',
423             'auth'        => $CFG->dirroot.'/auth',
424             'availability' => $CFG->dirroot . '/availability',
425             'backup'      => $CFG->dirroot.'/backup/util/ui',
426             'badges'      => $CFG->dirroot.'/badges',
427             'block'       => $CFG->dirroot.'/blocks',
428             'blog'        => $CFG->dirroot.'/blog',
429             'bulkusers'   => null,
430             'cache'       => $CFG->dirroot.'/cache',
431             'calendar'    => $CFG->dirroot.'/calendar',
432             'cohort'      => $CFG->dirroot.'/cohort',
433             'comment'     => $CFG->dirroot.'/comment',
434             'competency'  => $CFG->dirroot.'/competency',
435             'completion'  => $CFG->dirroot.'/completion',
436             'countries'   => null,
437             'course'      => $CFG->dirroot.'/course',
438             'currencies'  => null,
439             'customfield' => $CFG->dirroot.'/customfield',
440             'dbtransfer'  => null,
441             'debug'       => null,
442             'editor'      => $CFG->dirroot.'/lib/editor',
443             'edufields'   => null,
444             'enrol'       => $CFG->dirroot.'/enrol',
445             'error'       => null,
446             'favourites'  => $CFG->dirroot . '/favourites',
447             'filepicker'  => null,
448             'fileconverter' => $CFG->dirroot.'/files/converter',
449             'files'       => $CFG->dirroot.'/files',
450             'filters'     => $CFG->dirroot.'/filter',
451             //'fonts'       => null, // Bogus.
452             'form'        => $CFG->dirroot.'/lib/form',
453             'grades'      => $CFG->dirroot.'/grade',
454             'grading'     => $CFG->dirroot.'/grade/grading',
455             'group'       => $CFG->dirroot.'/group',
456             'help'        => null,
457             'hub'         => null,
458             'imscc'       => null,
459             'install'     => null,
460             'iso6392'     => null,
461             'langconfig'  => null,
462             'license'     => null,
463             'mathslib'    => null,
464             'media'       => $CFG->dirroot.'/media',
465             'message'     => $CFG->dirroot.'/message',
466             'mimetypes'   => null,
467             'mnet'        => $CFG->dirroot.'/mnet',
468             //'moodle.org'  => null, // Not used any more.
469             'my'          => $CFG->dirroot.'/my',
470             'notes'       => $CFG->dirroot.'/notes',
471             'pagetype'    => null,
472             'pix'         => null,
473             'plagiarism'  => $CFG->dirroot.'/plagiarism',
474             'plugin'      => null,
475             'portfolio'   => $CFG->dirroot.'/portfolio',
476             'privacy'     => $CFG->dirroot . '/privacy',
477             'question'    => $CFG->dirroot.'/question',
478             'rating'      => $CFG->dirroot.'/rating',
479             'repository'  => $CFG->dirroot.'/repository',
480             'rss'         => $CFG->dirroot.'/rss',
481             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
482             'search'      => $CFG->dirroot.'/search',
483             'table'       => null,
484             'tag'         => $CFG->dirroot.'/tag',
485             'timezones'   => null,
486             'user'        => $CFG->dirroot.'/user',
487             'userkey'     => $CFG->dirroot.'/lib/userkey',
488             'webservice'  => $CFG->dirroot.'/webservice',
489         );
491         return $info;
492     }
494     /**
495      * Returns list of known plugin types.
496      * @return array
497      */
498     protected static function fetch_plugintypes() {
499         global $CFG;
501         $types = array(
502             'antivirus'     => $CFG->dirroot . '/lib/antivirus',
503             'availability'  => $CFG->dirroot . '/availability/condition',
504             'qtype'         => $CFG->dirroot.'/question/type',
505             'mod'           => $CFG->dirroot.'/mod',
506             'auth'          => $CFG->dirroot.'/auth',
507             'calendartype'  => $CFG->dirroot.'/calendar/type',
508             'customfield'   => $CFG->dirroot.'/customfield/field',
509             'enrol'         => $CFG->dirroot.'/enrol',
510             'message'       => $CFG->dirroot.'/message/output',
511             'block'         => $CFG->dirroot.'/blocks',
512             'media'         => $CFG->dirroot.'/media/player',
513             'filter'        => $CFG->dirroot.'/filter',
514             'editor'        => $CFG->dirroot.'/lib/editor',
515             'format'        => $CFG->dirroot.'/course/format',
516             'dataformat'    => $CFG->dirroot.'/dataformat',
517             'profilefield'  => $CFG->dirroot.'/user/profile/field',
518             'report'        => $CFG->dirroot.'/report',
519             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
520             'gradeexport'   => $CFG->dirroot.'/grade/export',
521             'gradeimport'   => $CFG->dirroot.'/grade/import',
522             'gradereport'   => $CFG->dirroot.'/grade/report',
523             'gradingform'   => $CFG->dirroot.'/grade/grading/form',
524             'mlbackend'     => $CFG->dirroot.'/lib/mlbackend',
525             'mnetservice'   => $CFG->dirroot.'/mnet/service',
526             'webservice'    => $CFG->dirroot.'/webservice',
527             'repository'    => $CFG->dirroot.'/repository',
528             'portfolio'     => $CFG->dirroot.'/portfolio',
529             'search'        => $CFG->dirroot.'/search/engine',
530             'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
531             'qformat'       => $CFG->dirroot.'/question/format',
532             'plagiarism'    => $CFG->dirroot.'/plagiarism',
533             'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
534             'cachestore'    => $CFG->dirroot.'/cache/stores',
535             'cachelock'     => $CFG->dirroot.'/cache/locks',
536             'fileconverter' => $CFG->dirroot.'/files/converter',
537         );
538         $parents = array();
539         $subplugins = array();
541         if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
542             $types['theme'] = $CFG->themedir;
543         } else {
544             $types['theme'] = $CFG->dirroot.'/theme';
545         }
547         foreach (self::$supportsubplugins as $type) {
548             if ($type === 'local') {
549                 // Local subplugins must be after local plugins.
550                 continue;
551             }
552             $plugins = self::fetch_plugins($type, $types[$type]);
553             foreach ($plugins as $plugin => $fulldir) {
554                 $subtypes = self::fetch_subtypes($fulldir);
555                 if (!$subtypes) {
556                     continue;
557                 }
558                 $subplugins[$type.'_'.$plugin] = array();
559                 foreach($subtypes as $subtype => $subdir) {
560                     if (isset($types[$subtype])) {
561                         error_log("Invalid subtype '$subtype', duplicate detected.");
562                         continue;
563                     }
564                     $types[$subtype] = $subdir;
565                     $parents[$subtype] = $type.'_'.$plugin;
566                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
567                 }
568             }
569         }
570         // Local is always last!
571         $types['local'] = $CFG->dirroot.'/local';
573         if (in_array('local', self::$supportsubplugins)) {
574             $type = 'local';
575             $plugins = self::fetch_plugins($type, $types[$type]);
576             foreach ($plugins as $plugin => $fulldir) {
577                 $subtypes = self::fetch_subtypes($fulldir);
578                 if (!$subtypes) {
579                     continue;
580                 }
581                 $subplugins[$type.'_'.$plugin] = array();
582                 foreach($subtypes as $subtype => $subdir) {
583                     if (isset($types[$subtype])) {
584                         error_log("Invalid subtype '$subtype', duplicate detected.");
585                         continue;
586                     }
587                     $types[$subtype] = $subdir;
588                     $parents[$subtype] = $type.'_'.$plugin;
589                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
590                 }
591             }
592         }
594         return array($types, $parents, $subplugins);
595     }
597     /**
598      * Returns list of subtypes.
599      * @param string $ownerdir
600      * @return array
601      */
602     protected static function fetch_subtypes($ownerdir) {
603         global $CFG;
605         $types = array();
606         if (file_exists("$ownerdir/db/subplugins.php")) {
607             $subplugins = array();
608             include("$ownerdir/db/subplugins.php");
609             foreach ($subplugins as $subtype => $dir) {
610                 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
611                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
612                     continue;
613                 }
614                 if (isset(self::$subsystems[$subtype])) {
615                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
616                     continue;
617                 }
618                 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
619                     $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
620                 }
621                 if (!is_dir("$CFG->dirroot/$dir")) {
622                     error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
623                     continue;
624                 }
625                 $types[$subtype] = "$CFG->dirroot/$dir";
626             }
627         }
628         return $types;
629     }
631     /**
632      * Returns list of plugins of given type in given directory.
633      * @param string $plugintype
634      * @param string $fulldir
635      * @return array
636      */
637     protected static function fetch_plugins($plugintype, $fulldir) {
638         global $CFG;
640         $fulldirs = (array)$fulldir;
641         if ($plugintype === 'theme') {
642             if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
643                 // Include themes in standard location too.
644                 array_unshift($fulldirs, $CFG->dirroot.'/theme');
645             }
646         }
648         $result = array();
650         foreach ($fulldirs as $fulldir) {
651             if (!is_dir($fulldir)) {
652                 continue;
653             }
654             $items = new \DirectoryIterator($fulldir);
655             foreach ($items as $item) {
656                 if ($item->isDot() or !$item->isDir()) {
657                     continue;
658                 }
659                 $pluginname = $item->getFilename();
660                 if ($plugintype === 'auth' and $pluginname === 'db') {
661                     // Special exception for this wrong plugin name.
662                 } else if (isset(self::$ignoreddirs[$pluginname])) {
663                     continue;
664                 }
665                 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
666                     // Always ignore plugins with problematic names here.
667                     continue;
668                 }
669                 $result[$pluginname] = $fulldir.'/'.$pluginname;
670                 unset($item);
671             }
672             unset($items);
673         }
675         ksort($result);
676         return $result;
677     }
679     /**
680      * Find all classes that can be autoloaded including frankenstyle namespaces.
681      */
682     protected static function fill_classmap_cache() {
683         global $CFG;
685         self::$classmap = array();
687         self::load_classes('core', "$CFG->dirroot/lib/classes");
689         foreach (self::$subsystems as $subsystem => $fulldir) {
690             if (!$fulldir) {
691                 continue;
692             }
693             self::load_classes('core_'.$subsystem, "$fulldir/classes");
694         }
696         foreach (self::$plugins as $plugintype => $plugins) {
697             foreach ($plugins as $pluginname => $fulldir) {
698                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
699             }
700         }
701         ksort(self::$classmap);
702     }
704     /**
705      * Fills up the cache defining what plugins have certain files.
706      *
707      * @see self::get_plugin_list_with_file
708      * @return void
709      */
710     protected static function fill_filemap_cache() {
711         global $CFG;
713         self::$filemap = array();
715         foreach (self::$filestomap as $file) {
716             if (!isset(self::$filemap[$file])) {
717                 self::$filemap[$file] = array();
718             }
719             foreach (self::$plugins as $plugintype => $plugins) {
720                 if (!isset(self::$filemap[$file][$plugintype])) {
721                     self::$filemap[$file][$plugintype] = array();
722                 }
723                 foreach ($plugins as $pluginname => $fulldir) {
724                     if (file_exists("$fulldir/$file")) {
725                         self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
726                     }
727                 }
728             }
729         }
730     }
732     /**
733      * Find classes in directory and recurse to subdirs.
734      * @param string $component
735      * @param string $fulldir
736      * @param string $namespace
737      */
738     protected static function load_classes($component, $fulldir, $namespace = '') {
739         if (!is_dir($fulldir)) {
740             return;
741         }
743         if (!is_readable($fulldir)) {
744             // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
745             // because its pretty likely to lead to a missing class error further down the line.
746             // But our early setup code can't handle errors this early at the moment.
747             return;
748         }
750         $items = new \DirectoryIterator($fulldir);
751         foreach ($items as $item) {
752             if ($item->isDot()) {
753                 continue;
754             }
755             if ($item->isDir()) {
756                 $dirname = $item->getFilename();
757                 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
758                 continue;
759             }
761             $filename = $item->getFilename();
762             $classname = preg_replace('/\.php$/', '', $filename);
764             if ($filename === $classname) {
765                 // Not a php file.
766                 continue;
767             }
768             if ($namespace === '') {
769                 // Legacy long frankenstyle class name.
770                 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
771             }
772             // New namespaced classes.
773             self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
774         }
775         unset($item);
776         unset($items);
777     }
780     /**
781      * List all core subsystems and their location
782      *
783      * This is a whitelist of components that are part of the core and their
784      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
785      * plugin is not listed here and it does not have proper plugintype prefix,
786      * then it is considered as course activity module.
787      *
788      * The location is absolute file path to dir. NULL means there is no special
789      * directory for this subsystem. If the location is set, the subsystem's
790      * renderer.php is expected to be there.
791      *
792      * @return array of (string)name => (string|null)full dir location
793      */
794     public static function get_core_subsystems() {
795         self::init();
796         return self::$subsystems;
797     }
799     /**
800      * Get list of available plugin types together with their location.
801      *
802      * @return array as (string)plugintype => (string)fulldir
803      */
804     public static function get_plugin_types() {
805         self::init();
806         return self::$plugintypes;
807     }
809     /**
810      * Get list of plugins of given type.
811      *
812      * @param string $plugintype
813      * @return array as (string)pluginname => (string)fulldir
814      */
815     public static function get_plugin_list($plugintype) {
816         self::init();
818         if (!isset(self::$plugins[$plugintype])) {
819             return array();
820         }
821         return self::$plugins[$plugintype];
822     }
824     /**
825      * Get a list of all the plugins of a given type that define a certain class
826      * in a certain file. The plugin component names and class names are returned.
827      *
828      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
829      * @param string $class the part of the name of the class after the
830      *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
831      *      names like report_courselist_thing. If you are looking for classes with
832      *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
833      *      Frankenstyle namespaces are also supported.
834      * @param string $file the name of file within the plugin that defines the class.
835      * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
836      *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
837      */
838     public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
839         global $CFG; // Necessary in case it is referenced by included PHP scripts.
841         if ($class) {
842             $suffix = '_' . $class;
843         } else {
844             $suffix = '';
845         }
847         $pluginclasses = array();
848         $plugins = self::get_plugin_list($plugintype);
849         foreach ($plugins as $plugin => $fulldir) {
850             // Try class in frankenstyle namespace.
851             if ($class) {
852                 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
853                 if (class_exists($classname, true)) {
854                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
855                     continue;
856                 }
857             }
859             // Try autoloading of class with frankenstyle prefix.
860             $classname = $plugintype . '_' . $plugin . $suffix;
861             if (class_exists($classname, true)) {
862                 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
863                 continue;
864             }
866             // Fall back to old file location and class name.
867             if ($file and file_exists("$fulldir/$file")) {
868                 include_once("$fulldir/$file");
869                 if (class_exists($classname, false)) {
870                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
871                     continue;
872                 }
873             }
874         }
876         return $pluginclasses;
877     }
879     /**
880      * Get a list of all the plugins of a given type that contain a particular file.
881      *
882      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
883      * @param string $file the name of file that must be present in the plugin.
884      *                     (e.g. 'view.php', 'db/install.xml').
885      * @param bool $include if true (default false), the file will be include_once-ed if found.
886      * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
887      *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
888      */
889     public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
890         global $CFG; // Necessary in case it is referenced by included PHP scripts.
891         $pluginfiles = array();
893         if (isset(self::$filemap[$file])) {
894             // If the file was supposed to be mapped, then it should have been set in the array.
895             if (isset(self::$filemap[$file][$plugintype])) {
896                 $pluginfiles = self::$filemap[$file][$plugintype];
897             }
898         } else {
899             // Old-style search for non-cached files.
900             $plugins = self::get_plugin_list($plugintype);
901             foreach ($plugins as $plugin => $fulldir) {
902                 $path = $fulldir . '/' . $file;
903                 if (file_exists($path)) {
904                     $pluginfiles[$plugin] = $path;
905                 }
906             }
907         }
909         if ($include) {
910             foreach ($pluginfiles as $path) {
911                 include_once($path);
912             }
913         }
915         return $pluginfiles;
916     }
918     /**
919      * Returns all classes in a component matching the provided namespace.
920      *
921      * It checks that the class exists.
922      *
923      * e.g. get_component_classes_in_namespace('mod_forum', 'event')
924      *
925      * @param string $component A valid moodle component (frankenstyle)
926      * @param string $namespace Namespace from the component name or empty if all $component namespace classes.
927      * @return array The full class name as key and the class path as value.
928      */
929     public static function get_component_classes_in_namespace($component, $namespace = '') {
931         $component = self::normalize_componentname($component);
933         if ($namespace) {
935             // We will add them later.
936             $namespace = trim($namespace, '\\');
938             // We need add double backslashes as it is how classes are stored into self::$classmap.
939             $namespace = implode('\\\\', explode('\\', $namespace));
940             $namespace = $namespace . '\\\\';
941         }
943         $regex = '|^' . $component . '\\\\' . $namespace . '|';
944         $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
946         // We want to be sure that they exist.
947         $classes = array();
948         foreach ($it as $classname => $classpath) {
949             if (class_exists($classname)) {
950                 $classes[$classname] = $classpath;
951             }
952         }
954         return $classes;
955     }
957     /**
958      * Returns the exact absolute path to plugin directory.
959      *
960      * @param string $plugintype type of plugin
961      * @param string $pluginname name of the plugin
962      * @return string full path to plugin directory; null if not found
963      */
964     public static function get_plugin_directory($plugintype, $pluginname) {
965         if (empty($pluginname)) {
966             // Invalid plugin name, sorry.
967             return null;
968         }
970         self::init();
972         if (!isset(self::$plugins[$plugintype][$pluginname])) {
973             return null;
974         }
975         return self::$plugins[$plugintype][$pluginname];
976     }
978     /**
979      * Returns the exact absolute path to plugin directory.
980      *
981      * @param string $subsystem type of core subsystem
982      * @return string full path to subsystem directory; null if not found
983      */
984     public static function get_subsystem_directory($subsystem) {
985         self::init();
987         if (!isset(self::$subsystems[$subsystem])) {
988             return null;
989         }
990         return self::$subsystems[$subsystem];
991     }
993     /**
994      * This method validates a plug name. It is much faster than calling clean_param.
995      *
996      * @param string $plugintype type of plugin
997      * @param string $pluginname a string that might be a plugin name.
998      * @return bool if this string is a valid plugin name.
999      */
1000     public static function is_valid_plugin_name($plugintype, $pluginname) {
1001         if ($plugintype === 'mod') {
1002             // Modules must not have the same name as core subsystems.
1003             if (!isset(self::$subsystems)) {
1004                 // Watch out, this is called from init!
1005                 self::init();
1006             }
1007             if (isset(self::$subsystems[$pluginname])) {
1008                 return false;
1009             }
1010             // Modules MUST NOT have any underscores,
1011             // component normalisation would break very badly otherwise!
1012             return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1014         } else {
1015             return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1016         }
1017     }
1019     /**
1020      * Normalize the component name.
1021      *
1022      * Note: this does not verify the validity of the plugin or component.
1023      *
1024      * @param string $component
1025      * @return string
1026      */
1027     public static function normalize_componentname($componentname) {
1028         list($plugintype, $pluginname) = self::normalize_component($componentname);
1029         if ($plugintype === 'core' && is_null($pluginname)) {
1030             return $plugintype;
1031         }
1032         return $plugintype . '_' . $pluginname;
1033     }
1035     /**
1036      * Normalize the component name using the "frankenstyle" rules.
1037      *
1038      * Note: this does not verify the validity of plugin or type names.
1039      *
1040      * @param string $component
1041      * @return array two-items list of [(string)type, (string|null)name]
1042      */
1043     public static function normalize_component($component) {
1044         if ($component === 'moodle' or $component === 'core' or $component === '') {
1045             return array('core', null);
1046         }
1048         if (strpos($component, '_') === false) {
1049             self::init();
1050             if (array_key_exists($component, self::$subsystems)) {
1051                 $type   = 'core';
1052                 $plugin = $component;
1053             } else {
1054                 // Everything else without underscore is a module.
1055                 $type   = 'mod';
1056                 $plugin = $component;
1057             }
1059         } else {
1060             list($type, $plugin) = explode('_', $component, 2);
1061             if ($type === 'moodle') {
1062                 $type = 'core';
1063             }
1064             // Any unknown type must be a subplugin.
1065         }
1067         return array($type, $plugin);
1068     }
1070     /**
1071      * Return exact absolute path to a plugin directory.
1072      *
1073      * @param string $component name such as 'moodle', 'mod_forum'
1074      * @return string full path to component directory; NULL if not found
1075      */
1076     public static function get_component_directory($component) {
1077         global $CFG;
1079         list($type, $plugin) = self::normalize_component($component);
1081         if ($type === 'core') {
1082             if ($plugin === null) {
1083                 return $path = $CFG->libdir;
1084             }
1085             return self::get_subsystem_directory($plugin);
1086         }
1088         return self::get_plugin_directory($type, $plugin);
1089     }
1091     /**
1092      * Returns list of plugin types that allow subplugins.
1093      * @return array as (string)plugintype => (string)fulldir
1094      */
1095     public static function get_plugin_types_with_subplugins() {
1096         self::init();
1098         $return = array();
1099         foreach (self::$supportsubplugins as $type) {
1100             $return[$type] = self::$plugintypes[$type];
1101         }
1102         return $return;
1103     }
1105     /**
1106      * Returns parent of this subplugin type.
1107      *
1108      * @param string $type
1109      * @return string parent component or null
1110      */
1111     public static function get_subtype_parent($type) {
1112         self::init();
1114         if (isset(self::$parents[$type])) {
1115             return self::$parents[$type];
1116         }
1118         return null;
1119     }
1121     /**
1122      * Return all subplugins of this component.
1123      * @param string $component.
1124      * @return array $subtype=>array($component, ..), null if no subtypes defined
1125      */
1126     public static function get_subplugins($component) {
1127         self::init();
1129         if (isset(self::$subplugins[$component])) {
1130             return self::$subplugins[$component];
1131         }
1133         return null;
1134     }
1136     /**
1137      * Returns hash of all versions including core and all plugins.
1138      *
1139      * This is relatively slow and not fully cached, use with care!
1140      *
1141      * @return string sha1 hash
1142      */
1143     public static function get_all_versions_hash() {
1144         global $CFG;
1146         self::init();
1148         $versions = array();
1150         // Main version first.
1151         $versions['core'] = self::fetch_core_version();
1153         // The problem here is tha the component cache might be stable,
1154         // we want this to work also on frontpage without resetting the component cache.
1155         $usecache = false;
1156         if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1157             $usecache = true;
1158         }
1160         // Now all plugins.
1161         $plugintypes = core_component::get_plugin_types();
1162         foreach ($plugintypes as $type => $typedir) {
1163             if ($usecache) {
1164                 $plugs = core_component::get_plugin_list($type);
1165             } else {
1166                 $plugs = self::fetch_plugins($type, $typedir);
1167             }
1168             foreach ($plugs as $plug => $fullplug) {
1169                 $plugin = new stdClass();
1170                 $plugin->version = null;
1171                 $module = $plugin;
1172                 include($fullplug.'/version.php');
1173                 $versions[$type.'_'.$plug] = $plugin->version;
1174             }
1175         }
1177         return sha1(serialize($versions));
1178     }
1180     /**
1181      * Invalidate opcode cache for given file, this is intended for
1182      * php files that are stored in dataroot.
1183      *
1184      * Note: we need it here because this class must be self-contained.
1185      *
1186      * @param string $file
1187      */
1188     public static function invalidate_opcode_php_cache($file) {
1189         if (function_exists('opcache_invalidate')) {
1190             if (!file_exists($file)) {
1191                 return;
1192             }
1193             opcache_invalidate($file, true);
1194         }
1195     }
1197     /**
1198      * Return true if subsystemname is core subsystem.
1199      *
1200      * @param string $subsystemname name of the subsystem.
1201      * @return bool true if core subsystem.
1202      */
1203     public static function is_core_subsystem($subsystemname) {
1204         return isset(self::$subsystems[$subsystemname]);
1205     }
1207     /**
1208      * Records all class renames that have been made to facilitate autoloading.
1209      */
1210     protected static function fill_classmap_renames_cache() {
1211         global $CFG;
1213         self::$classmaprenames = array();
1215         self::load_renamed_classes("$CFG->dirroot/lib/");
1217         foreach (self::$subsystems as $subsystem => $fulldir) {
1218             self::load_renamed_classes($fulldir);
1219         }
1221         foreach (self::$plugins as $plugintype => $plugins) {
1222             foreach ($plugins as $pluginname => $fulldir) {
1223                 self::load_renamed_classes($fulldir);
1224             }
1225         }
1226     }
1228     /**
1229      * Loads the db/renamedclasses.php file from the given directory.
1230      *
1231      * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1232      * and the value is the new class name.
1233      * It is only included when we are populating the component cache. After that is not needed.
1234      *
1235      * @param string $fulldir
1236      */
1237     protected static function load_renamed_classes($fulldir) {
1238         $file = $fulldir . '/db/renamedclasses.php';
1239         if (is_readable($file)) {
1240             $renamedclasses = null;
1241             require($file);
1242             if (is_array($renamedclasses)) {
1243                 foreach ($renamedclasses as $oldclass => $newclass) {
1244                     self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1245                 }
1246             }
1247         }
1248     }
1250     /**
1251      * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1252      *
1253      * E.g.
1254      *  [
1255      *      'mod' => [
1256      *          'mod_forum' => FORUM_PLUGIN_PATH,
1257      *          ...
1258      *      ],
1259      *      ...
1260      *      'core' => [
1261      *          'core_comment' => COMMENT_SUBSYSTEM_PATH,
1262      *          ...
1263      *      ]
1264      * ]
1265      *
1266      * @return array an associative array of components and their corresponding paths.
1267      */
1268     public static function get_component_list() : array {
1269         $components = [];
1270         // Get all plugins.
1271         foreach (self::get_plugin_types() as $plugintype => $typedir) {
1272             $components[$plugintype] = [];
1273             foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1274                 $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1275             }
1276         }
1277         // Get all subsystems.
1278         foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1279             $components['core']['core_' . $subsystemname] = $subsystempath;
1280         }
1281         return $components;
1282     }
1284     /**
1285      * Returns a list of frankenstyle component names.
1286      *
1287      * E.g.
1288      *  [
1289      *      'core_course',
1290      *      'core_message',
1291      *      'mod_assign',
1292      *      ...
1293      *  ]
1294      * @return array the list of frankenstyle component names.
1295      */
1296     public static function get_component_names() : array {
1297         $componentnames = [];
1298         // Get all plugins.
1299         foreach (self::get_plugin_types() as $plugintype => $typedir) {
1300             foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1301                 $componentnames[] = $plugintype . '_' . $pluginname;
1302             }
1303         }
1304         // Get all subsystems.
1305         foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1306             $componentnames[] = 'core_' . $subsystemname;
1307         }
1308         return $componentnames;
1309     }