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