11ce5a6d5520d00f8aad766eb3c81afae6b87ae5
[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             'dbtransfer'  => null,
440             'debug'       => null,
441             'editor'      => $CFG->dirroot.'/lib/editor',
442             'edufields'   => null,
443             'enrol'       => $CFG->dirroot.'/enrol',
444             'error'       => null,
445             'favourites'  => $CFG->dirroot . '/favourites',
446             'filepicker'  => null,
447             'fileconverter' => $CFG->dirroot.'/files/converter',
448             'files'       => $CFG->dirroot.'/files',
449             'filters'     => $CFG->dirroot.'/filter',
450             //'fonts'       => null, // Bogus.
451             'form'        => $CFG->dirroot.'/lib/form',
452             'grades'      => $CFG->dirroot.'/grade',
453             'grading'     => $CFG->dirroot.'/grade/grading',
454             'group'       => $CFG->dirroot.'/group',
455             'help'        => null,
456             'hub'         => null,
457             'imscc'       => null,
458             'install'     => null,
459             'iso6392'     => null,
460             'langconfig'  => null,
461             'license'     => null,
462             'mathslib'    => null,
463             'media'       => $CFG->dirroot.'/media',
464             'message'     => $CFG->dirroot.'/message',
465             'mimetypes'   => null,
466             'mnet'        => $CFG->dirroot.'/mnet',
467             //'moodle.org'  => null, // Not used any more.
468             'my'          => $CFG->dirroot.'/my',
469             'notes'       => $CFG->dirroot.'/notes',
470             'pagetype'    => null,
471             'pix'         => null,
472             'plagiarism'  => $CFG->dirroot.'/plagiarism',
473             'plugin'      => null,
474             'portfolio'   => $CFG->dirroot.'/portfolio',
475             'privacy'     => $CFG->dirroot . '/privacy',
476             'question'    => $CFG->dirroot.'/question',
477             'rating'      => $CFG->dirroot.'/rating',
478             'repository'  => $CFG->dirroot.'/repository',
479             'rss'         => $CFG->dirroot.'/rss',
480             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
481             'search'      => $CFG->dirroot.'/search',
482             'table'       => null,
483             'tag'         => $CFG->dirroot.'/tag',
484             'timezones'   => null,
485             'user'        => $CFG->dirroot.'/user',
486             'userkey'     => $CFG->dirroot.'/lib/userkey',
487             'webservice'  => $CFG->dirroot.'/webservice',
488         );
490         return $info;
491     }
493     /**
494      * Returns list of known plugin types.
495      * @return array
496      */
497     protected static function fetch_plugintypes() {
498         global $CFG;
500         $types = array(
501             'antivirus'     => $CFG->dirroot . '/lib/antivirus',
502             'availability'  => $CFG->dirroot . '/availability/condition',
503             'qtype'         => $CFG->dirroot.'/question/type',
504             'mod'           => $CFG->dirroot.'/mod',
505             'auth'          => $CFG->dirroot.'/auth',
506             'calendartype'  => $CFG->dirroot.'/calendar/type',
507             'enrol'         => $CFG->dirroot.'/enrol',
508             'message'       => $CFG->dirroot.'/message/output',
509             'block'         => $CFG->dirroot.'/blocks',
510             'media'         => $CFG->dirroot.'/media/player',
511             'filter'        => $CFG->dirroot.'/filter',
512             'editor'        => $CFG->dirroot.'/lib/editor',
513             'format'        => $CFG->dirroot.'/course/format',
514             'dataformat'    => $CFG->dirroot.'/dataformat',
515             'profilefield'  => $CFG->dirroot.'/user/profile/field',
516             'report'        => $CFG->dirroot.'/report',
517             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
518             'gradeexport'   => $CFG->dirroot.'/grade/export',
519             'gradeimport'   => $CFG->dirroot.'/grade/import',
520             'gradereport'   => $CFG->dirroot.'/grade/report',
521             'gradingform'   => $CFG->dirroot.'/grade/grading/form',
522             'mlbackend'     => $CFG->dirroot.'/lib/mlbackend',
523             'mnetservice'   => $CFG->dirroot.'/mnet/service',
524             'webservice'    => $CFG->dirroot.'/webservice',
525             'repository'    => $CFG->dirroot.'/repository',
526             'portfolio'     => $CFG->dirroot.'/portfolio',
527             'search'        => $CFG->dirroot.'/search/engine',
528             'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
529             'qformat'       => $CFG->dirroot.'/question/format',
530             'plagiarism'    => $CFG->dirroot.'/plagiarism',
531             'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
532             'cachestore'    => $CFG->dirroot.'/cache/stores',
533             'cachelock'     => $CFG->dirroot.'/cache/locks',
534             'fileconverter' => $CFG->dirroot.'/files/converter',
535         );
536         $parents = array();
537         $subplugins = array();
539         if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
540             $types['theme'] = $CFG->themedir;
541         } else {
542             $types['theme'] = $CFG->dirroot.'/theme';
543         }
545         foreach (self::$supportsubplugins as $type) {
546             if ($type === 'local') {
547                 // Local subplugins must be after local plugins.
548                 continue;
549             }
550             $plugins = self::fetch_plugins($type, $types[$type]);
551             foreach ($plugins as $plugin => $fulldir) {
552                 $subtypes = self::fetch_subtypes($fulldir);
553                 if (!$subtypes) {
554                     continue;
555                 }
556                 $subplugins[$type.'_'.$plugin] = array();
557                 foreach($subtypes as $subtype => $subdir) {
558                     if (isset($types[$subtype])) {
559                         error_log("Invalid subtype '$subtype', duplicate detected.");
560                         continue;
561                     }
562                     $types[$subtype] = $subdir;
563                     $parents[$subtype] = $type.'_'.$plugin;
564                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
565                 }
566             }
567         }
568         // Local is always last!
569         $types['local'] = $CFG->dirroot.'/local';
571         if (in_array('local', self::$supportsubplugins)) {
572             $type = 'local';
573             $plugins = self::fetch_plugins($type, $types[$type]);
574             foreach ($plugins as $plugin => $fulldir) {
575                 $subtypes = self::fetch_subtypes($fulldir);
576                 if (!$subtypes) {
577                     continue;
578                 }
579                 $subplugins[$type.'_'.$plugin] = array();
580                 foreach($subtypes as $subtype => $subdir) {
581                     if (isset($types[$subtype])) {
582                         error_log("Invalid subtype '$subtype', duplicate detected.");
583                         continue;
584                     }
585                     $types[$subtype] = $subdir;
586                     $parents[$subtype] = $type.'_'.$plugin;
587                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
588                 }
589             }
590         }
592         return array($types, $parents, $subplugins);
593     }
595     /**
596      * Returns list of subtypes.
597      * @param string $ownerdir
598      * @return array
599      */
600     protected static function fetch_subtypes($ownerdir) {
601         global $CFG;
603         $types = array();
604         if (file_exists("$ownerdir/db/subplugins.php")) {
605             $subplugins = array();
606             include("$ownerdir/db/subplugins.php");
607             foreach ($subplugins as $subtype => $dir) {
608                 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
609                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
610                     continue;
611                 }
612                 if (isset(self::$subsystems[$subtype])) {
613                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
614                     continue;
615                 }
616                 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
617                     $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
618                 }
619                 if (!is_dir("$CFG->dirroot/$dir")) {
620                     error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
621                     continue;
622                 }
623                 $types[$subtype] = "$CFG->dirroot/$dir";
624             }
625         }
626         return $types;
627     }
629     /**
630      * Returns list of plugins of given type in given directory.
631      * @param string $plugintype
632      * @param string $fulldir
633      * @return array
634      */
635     protected static function fetch_plugins($plugintype, $fulldir) {
636         global $CFG;
638         $fulldirs = (array)$fulldir;
639         if ($plugintype === 'theme') {
640             if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
641                 // Include themes in standard location too.
642                 array_unshift($fulldirs, $CFG->dirroot.'/theme');
643             }
644         }
646         $result = array();
648         foreach ($fulldirs as $fulldir) {
649             if (!is_dir($fulldir)) {
650                 continue;
651             }
652             $items = new \DirectoryIterator($fulldir);
653             foreach ($items as $item) {
654                 if ($item->isDot() or !$item->isDir()) {
655                     continue;
656                 }
657                 $pluginname = $item->getFilename();
658                 if ($plugintype === 'auth' and $pluginname === 'db') {
659                     // Special exception for this wrong plugin name.
660                 } else if (isset(self::$ignoreddirs[$pluginname])) {
661                     continue;
662                 }
663                 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
664                     // Always ignore plugins with problematic names here.
665                     continue;
666                 }
667                 $result[$pluginname] = $fulldir.'/'.$pluginname;
668                 unset($item);
669             }
670             unset($items);
671         }
673         ksort($result);
674         return $result;
675     }
677     /**
678      * Find all classes that can be autoloaded including frankenstyle namespaces.
679      */
680     protected static function fill_classmap_cache() {
681         global $CFG;
683         self::$classmap = array();
685         self::load_classes('core', "$CFG->dirroot/lib/classes");
687         foreach (self::$subsystems as $subsystem => $fulldir) {
688             if (!$fulldir) {
689                 continue;
690             }
691             self::load_classes('core_'.$subsystem, "$fulldir/classes");
692         }
694         foreach (self::$plugins as $plugintype => $plugins) {
695             foreach ($plugins as $pluginname => $fulldir) {
696                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
697             }
698         }
699         ksort(self::$classmap);
700     }
702     /**
703      * Fills up the cache defining what plugins have certain files.
704      *
705      * @see self::get_plugin_list_with_file
706      * @return void
707      */
708     protected static function fill_filemap_cache() {
709         global $CFG;
711         self::$filemap = array();
713         foreach (self::$filestomap as $file) {
714             if (!isset(self::$filemap[$file])) {
715                 self::$filemap[$file] = array();
716             }
717             foreach (self::$plugins as $plugintype => $plugins) {
718                 if (!isset(self::$filemap[$file][$plugintype])) {
719                     self::$filemap[$file][$plugintype] = array();
720                 }
721                 foreach ($plugins as $pluginname => $fulldir) {
722                     if (file_exists("$fulldir/$file")) {
723                         self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
724                     }
725                 }
726             }
727         }
728     }
730     /**
731      * Find classes in directory and recurse to subdirs.
732      * @param string $component
733      * @param string $fulldir
734      * @param string $namespace
735      */
736     protected static function load_classes($component, $fulldir, $namespace = '') {
737         if (!is_dir($fulldir)) {
738             return;
739         }
741         if (!is_readable($fulldir)) {
742             // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
743             // because its pretty likely to lead to a missing class error further down the line.
744             // But our early setup code can't handle errors this early at the moment.
745             return;
746         }
748         $items = new \DirectoryIterator($fulldir);
749         foreach ($items as $item) {
750             if ($item->isDot()) {
751                 continue;
752             }
753             if ($item->isDir()) {
754                 $dirname = $item->getFilename();
755                 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
756                 continue;
757             }
759             $filename = $item->getFilename();
760             $classname = preg_replace('/\.php$/', '', $filename);
762             if ($filename === $classname) {
763                 // Not a php file.
764                 continue;
765             }
766             if ($namespace === '') {
767                 // Legacy long frankenstyle class name.
768                 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
769             }
770             // New namespaced classes.
771             self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
772         }
773         unset($item);
774         unset($items);
775     }
778     /**
779      * List all core subsystems and their location
780      *
781      * This is a whitelist of components that are part of the core and their
782      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
783      * plugin is not listed here and it does not have proper plugintype prefix,
784      * then it is considered as course activity module.
785      *
786      * The location is absolute file path to dir. NULL means there is no special
787      * directory for this subsystem. If the location is set, the subsystem's
788      * renderer.php is expected to be there.
789      *
790      * @return array of (string)name => (string|null)full dir location
791      */
792     public static function get_core_subsystems() {
793         self::init();
794         return self::$subsystems;
795     }
797     /**
798      * Get list of available plugin types together with their location.
799      *
800      * @return array as (string)plugintype => (string)fulldir
801      */
802     public static function get_plugin_types() {
803         self::init();
804         return self::$plugintypes;
805     }
807     /**
808      * Get list of plugins of given type.
809      *
810      * @param string $plugintype
811      * @return array as (string)pluginname => (string)fulldir
812      */
813     public static function get_plugin_list($plugintype) {
814         self::init();
816         if (!isset(self::$plugins[$plugintype])) {
817             return array();
818         }
819         return self::$plugins[$plugintype];
820     }
822     /**
823      * Get a list of all the plugins of a given type that define a certain class
824      * in a certain file. The plugin component names and class names are returned.
825      *
826      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
827      * @param string $class the part of the name of the class after the
828      *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
829      *      names like report_courselist_thing. If you are looking for classes with
830      *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
831      *      Frankenstyle namespaces are also supported.
832      * @param string $file the name of file within the plugin that defines the class.
833      * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
834      *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
835      */
836     public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
837         global $CFG; // Necessary in case it is referenced by included PHP scripts.
839         if ($class) {
840             $suffix = '_' . $class;
841         } else {
842             $suffix = '';
843         }
845         $pluginclasses = array();
846         $plugins = self::get_plugin_list($plugintype);
847         foreach ($plugins as $plugin => $fulldir) {
848             // Try class in frankenstyle namespace.
849             if ($class) {
850                 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
851                 if (class_exists($classname, true)) {
852                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
853                     continue;
854                 }
855             }
857             // Try autoloading of class with frankenstyle prefix.
858             $classname = $plugintype . '_' . $plugin . $suffix;
859             if (class_exists($classname, true)) {
860                 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
861                 continue;
862             }
864             // Fall back to old file location and class name.
865             if ($file and file_exists("$fulldir/$file")) {
866                 include_once("$fulldir/$file");
867                 if (class_exists($classname, false)) {
868                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
869                     continue;
870                 }
871             }
872         }
874         return $pluginclasses;
875     }
877     /**
878      * Get a list of all the plugins of a given type that contain a particular file.
879      *
880      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
881      * @param string $file the name of file that must be present in the plugin.
882      *                     (e.g. 'view.php', 'db/install.xml').
883      * @param bool $include if true (default false), the file will be include_once-ed if found.
884      * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
885      *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
886      */
887     public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
888         global $CFG; // Necessary in case it is referenced by included PHP scripts.
889         $pluginfiles = array();
891         if (isset(self::$filemap[$file])) {
892             // If the file was supposed to be mapped, then it should have been set in the array.
893             if (isset(self::$filemap[$file][$plugintype])) {
894                 $pluginfiles = self::$filemap[$file][$plugintype];
895             }
896         } else {
897             // Old-style search for non-cached files.
898             $plugins = self::get_plugin_list($plugintype);
899             foreach ($plugins as $plugin => $fulldir) {
900                 $path = $fulldir . '/' . $file;
901                 if (file_exists($path)) {
902                     $pluginfiles[$plugin] = $path;
903                 }
904             }
905         }
907         if ($include) {
908             foreach ($pluginfiles as $path) {
909                 include_once($path);
910             }
911         }
913         return $pluginfiles;
914     }
916     /**
917      * Returns all classes in a component matching the provided namespace.
918      *
919      * It checks that the class exists.
920      *
921      * e.g. get_component_classes_in_namespace('mod_forum', 'event')
922      *
923      * @param string $component A valid moodle component (frankenstyle)
924      * @param string $namespace Namespace from the component name or empty if all $component namespace classes.
925      * @return array The full class name as key and the class path as value.
926      */
927     public static function get_component_classes_in_namespace($component, $namespace = '') {
929         $component = self::normalize_componentname($component);
931         if ($namespace) {
933             // We will add them later.
934             $namespace = trim($namespace, '\\');
936             // We need add double backslashes as it is how classes are stored into self::$classmap.
937             $namespace = implode('\\\\', explode('\\', $namespace));
938             $namespace = $namespace . '\\\\';
939         }
941         $regex = '|^' . $component . '\\\\' . $namespace . '|';
942         $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
944         // We want to be sure that they exist.
945         $classes = array();
946         foreach ($it as $classname => $classpath) {
947             if (class_exists($classname)) {
948                 $classes[$classname] = $classpath;
949             }
950         }
952         return $classes;
953     }
955     /**
956      * Returns the exact absolute path to plugin directory.
957      *
958      * @param string $plugintype type of plugin
959      * @param string $pluginname name of the plugin
960      * @return string full path to plugin directory; null if not found
961      */
962     public static function get_plugin_directory($plugintype, $pluginname) {
963         if (empty($pluginname)) {
964             // Invalid plugin name, sorry.
965             return null;
966         }
968         self::init();
970         if (!isset(self::$plugins[$plugintype][$pluginname])) {
971             return null;
972         }
973         return self::$plugins[$plugintype][$pluginname];
974     }
976     /**
977      * Returns the exact absolute path to plugin directory.
978      *
979      * @param string $subsystem type of core subsystem
980      * @return string full path to subsystem directory; null if not found
981      */
982     public static function get_subsystem_directory($subsystem) {
983         self::init();
985         if (!isset(self::$subsystems[$subsystem])) {
986             return null;
987         }
988         return self::$subsystems[$subsystem];
989     }
991     /**
992      * This method validates a plug name. It is much faster than calling clean_param.
993      *
994      * @param string $plugintype type of plugin
995      * @param string $pluginname a string that might be a plugin name.
996      * @return bool if this string is a valid plugin name.
997      */
998     public static function is_valid_plugin_name($plugintype, $pluginname) {
999         if ($plugintype === 'mod') {
1000             // Modules must not have the same name as core subsystems.
1001             if (!isset(self::$subsystems)) {
1002                 // Watch out, this is called from init!
1003                 self::init();
1004             }
1005             if (isset(self::$subsystems[$pluginname])) {
1006                 return false;
1007             }
1008             // Modules MUST NOT have any underscores,
1009             // component normalisation would break very badly otherwise!
1010             return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1012         } else {
1013             return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1014         }
1015     }
1017     /**
1018      * Normalize the component name.
1019      *
1020      * Note: this does not verify the validity of the plugin or component.
1021      *
1022      * @param string $component
1023      * @return string
1024      */
1025     public static function normalize_componentname($componentname) {
1026         list($plugintype, $pluginname) = self::normalize_component($componentname);
1027         if ($plugintype === 'core' && is_null($pluginname)) {
1028             return $plugintype;
1029         }
1030         return $plugintype . '_' . $pluginname;
1031     }
1033     /**
1034      * Normalize the component name using the "frankenstyle" rules.
1035      *
1036      * Note: this does not verify the validity of plugin or type names.
1037      *
1038      * @param string $component
1039      * @return array two-items list of [(string)type, (string|null)name]
1040      */
1041     public static function normalize_component($component) {
1042         if ($component === 'moodle' or $component === 'core' or $component === '') {
1043             return array('core', null);
1044         }
1046         if (strpos($component, '_') === false) {
1047             self::init();
1048             if (array_key_exists($component, self::$subsystems)) {
1049                 $type   = 'core';
1050                 $plugin = $component;
1051             } else {
1052                 // Everything else without underscore is a module.
1053                 $type   = 'mod';
1054                 $plugin = $component;
1055             }
1057         } else {
1058             list($type, $plugin) = explode('_', $component, 2);
1059             if ($type === 'moodle') {
1060                 $type = 'core';
1061             }
1062             // Any unknown type must be a subplugin.
1063         }
1065         return array($type, $plugin);
1066     }
1068     /**
1069      * Return exact absolute path to a plugin directory.
1070      *
1071      * @param string $component name such as 'moodle', 'mod_forum'
1072      * @return string full path to component directory; NULL if not found
1073      */
1074     public static function get_component_directory($component) {
1075         global $CFG;
1077         list($type, $plugin) = self::normalize_component($component);
1079         if ($type === 'core') {
1080             if ($plugin === null) {
1081                 return $path = $CFG->libdir;
1082             }
1083             return self::get_subsystem_directory($plugin);
1084         }
1086         return self::get_plugin_directory($type, $plugin);
1087     }
1089     /**
1090      * Returns list of plugin types that allow subplugins.
1091      * @return array as (string)plugintype => (string)fulldir
1092      */
1093     public static function get_plugin_types_with_subplugins() {
1094         self::init();
1096         $return = array();
1097         foreach (self::$supportsubplugins as $type) {
1098             $return[$type] = self::$plugintypes[$type];
1099         }
1100         return $return;
1101     }
1103     /**
1104      * Returns parent of this subplugin type.
1105      *
1106      * @param string $type
1107      * @return string parent component or null
1108      */
1109     public static function get_subtype_parent($type) {
1110         self::init();
1112         if (isset(self::$parents[$type])) {
1113             return self::$parents[$type];
1114         }
1116         return null;
1117     }
1119     /**
1120      * Return all subplugins of this component.
1121      * @param string $component.
1122      * @return array $subtype=>array($component, ..), null if no subtypes defined
1123      */
1124     public static function get_subplugins($component) {
1125         self::init();
1127         if (isset(self::$subplugins[$component])) {
1128             return self::$subplugins[$component];
1129         }
1131         return null;
1132     }
1134     /**
1135      * Returns hash of all versions including core and all plugins.
1136      *
1137      * This is relatively slow and not fully cached, use with care!
1138      *
1139      * @return string sha1 hash
1140      */
1141     public static function get_all_versions_hash() {
1142         global $CFG;
1144         self::init();
1146         $versions = array();
1148         // Main version first.
1149         $versions['core'] = self::fetch_core_version();
1151         // The problem here is tha the component cache might be stable,
1152         // we want this to work also on frontpage without resetting the component cache.
1153         $usecache = false;
1154         if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1155             $usecache = true;
1156         }
1158         // Now all plugins.
1159         $plugintypes = core_component::get_plugin_types();
1160         foreach ($plugintypes as $type => $typedir) {
1161             if ($usecache) {
1162                 $plugs = core_component::get_plugin_list($type);
1163             } else {
1164                 $plugs = self::fetch_plugins($type, $typedir);
1165             }
1166             foreach ($plugs as $plug => $fullplug) {
1167                 $plugin = new stdClass();
1168                 $plugin->version = null;
1169                 $module = $plugin;
1170                 include($fullplug.'/version.php');
1171                 $versions[$type.'_'.$plug] = $plugin->version;
1172             }
1173         }
1175         return sha1(serialize($versions));
1176     }
1178     /**
1179      * Invalidate opcode cache for given file, this is intended for
1180      * php files that are stored in dataroot.
1181      *
1182      * Note: we need it here because this class must be self-contained.
1183      *
1184      * @param string $file
1185      */
1186     public static function invalidate_opcode_php_cache($file) {
1187         if (function_exists('opcache_invalidate')) {
1188             if (!file_exists($file)) {
1189                 return;
1190             }
1191             opcache_invalidate($file, true);
1192         }
1193     }
1195     /**
1196      * Return true if subsystemname is core subsystem.
1197      *
1198      * @param string $subsystemname name of the subsystem.
1199      * @return bool true if core subsystem.
1200      */
1201     public static function is_core_subsystem($subsystemname) {
1202         return isset(self::$subsystems[$subsystemname]);
1203     }
1205     /**
1206      * Records all class renames that have been made to facilitate autoloading.
1207      */
1208     protected static function fill_classmap_renames_cache() {
1209         global $CFG;
1211         self::$classmaprenames = array();
1213         self::load_renamed_classes("$CFG->dirroot/lib/");
1215         foreach (self::$subsystems as $subsystem => $fulldir) {
1216             self::load_renamed_classes($fulldir);
1217         }
1219         foreach (self::$plugins as $plugintype => $plugins) {
1220             foreach ($plugins as $pluginname => $fulldir) {
1221                 self::load_renamed_classes($fulldir);
1222             }
1223         }
1224     }
1226     /**
1227      * Loads the db/renamedclasses.php file from the given directory.
1228      *
1229      * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1230      * and the value is the new class name.
1231      * It is only included when we are populating the component cache. After that is not needed.
1232      *
1233      * @param string $fulldir
1234      */
1235     protected static function load_renamed_classes($fulldir) {
1236         $file = $fulldir . '/db/renamedclasses.php';
1237         if (is_readable($file)) {
1238             $renamedclasses = null;
1239             require($file);
1240             if (is_array($renamedclasses)) {
1241                 foreach ($renamedclasses as $oldclass => $newclass) {
1242                     self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1243                 }
1244             }
1245         }
1246     }
1248     /**
1249      * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1250      *
1251      * E.g.
1252      *  [
1253      *      'mod' => [
1254      *          'mod_forum' => FORUM_PLUGIN_PATH,
1255      *          ...
1256      *      ],
1257      *      ...
1258      *      'core' => [
1259      *          'core_comment' => COMMENT_SUBSYSTEM_PATH,
1260      *          ...
1261      *      ]
1262      * ]
1263      *
1264      * @return array an associative array of components and their corresponding paths.
1265      */
1266     public static function get_component_list() : array {
1267         $components = [];
1268         // Get all plugins.
1269         foreach (self::get_plugin_types() as $plugintype => $typedir) {
1270             $components[$plugintype] = [];
1271             foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1272                 $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1273             }
1274         }
1275         // Get all subsystems.
1276         foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1277             $components['core']['core_' . $subsystemname] = $subsystempath;
1278         }
1279         return $components;
1280     }
1282     /**
1283      * Returns a list of frankenstyle component names.
1284      *
1285      * E.g.
1286      *  [
1287      *      'core_course',
1288      *      'core_message',
1289      *      'mod_assign',
1290      *      ...
1291      *  ]
1292      * @return array the list of frankenstyle component names.
1293      */
1294     public static function get_component_names() : array {
1295         $componentnames = [];
1296         // Get all plugins.
1297         foreach (self::get_plugin_types() as $plugintype => $typedir) {
1298             foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1299                 $componentnames[] = $plugintype . '_' . $pluginname;
1300             }
1301         }
1302         // Get all subsystems.
1303         foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1304             $componentnames[] = 'core_' . $subsystemname;
1305         }
1306         return $componentnames;
1307     }