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