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