MDL-62138 core: remove core_register and core_publish
[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 {
45 /** @var array list of ignored directories - watch out for auth/db exception */
9ba6076c 46 protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true);
9e19a0f0 47 /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
ac2b2713 48 protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
9e19a0f0 49
37e60007 50 /** @var array cache of plugin types */
9e19a0f0 51 protected static $plugintypes = null;
37e60007 52 /** @var array cache of plugin locations */
9e19a0f0 53 protected static $plugins = null;
37e60007 54 /** @var array cache of core subsystems */
9e19a0f0 55 protected static $subsystems = null;
37e60007 56 /** @var array subplugin type parents */
e87214bd 57 protected static $parents = null;
37e60007 58 /** @var array subplugins */
e87214bd 59 protected static $subplugins = null;
37e60007 60 /** @var array list of all known classes that can be autoloaded */
9e19a0f0 61 protected static $classmap = null;
37e60007
SH
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. */
d26ec8a5 65 protected static $filemap = null;
3274c5db
FM
66 /** @var int|float core version. */
67 protected static $version = null;
d26ec8a5
FM
68 /** @var array list of the files to map. */
69 protected static $filestomap = array('lib.php', 'settings.php');
64f631c1
JO
70 /** @var array associative array of PSR-0 namespaces and corresponding paths. */
71 protected static $psr0namespaces = array(
a57e3d3b
DP
72 'Horde' => 'lib/horde/framework/Horde',
73 'Mustache' => 'lib/mustache/src/Mustache',
64f631c1
JO
74 );
75 /** @var array associative array of PRS-4 namespaces and corresponding paths. */
76 protected static $psr4namespaces = array(
eacc36a2 77 'MaxMind' => 'lib/maxmind/MaxMind',
948e6e3f 78 'GeoIp2' => 'lib/maxmind/GeoIp2',
5d9f0bb3
DW
79 'Sabberworm\\CSS' => 'lib/php-css-parser',
80 'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
dd9ee3d8 81 'Leafo\\ScssPhp' => 'lib/scssphp',
4cb24aa9
DP
82 'Box\\Spout' => 'lib/spout/src/Spout',
83 'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
84 'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
a1f3821e 85 'IMSGlobal\LTI' => 'lib/ltiprovider/src',
413f19bc 86 'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
56e62bbb 87 'PHPMailer\\PHPMailer' => 'lib/phpmailer/src',
91c07d06 88 );
9e19a0f0
PS
89
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();
108
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 }
37e60007
SH
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);
303936aa
TL
120 if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
121 throw new \coding_exception("Cannot alias $classname to $newclassname");
122 }
37e60007
SH
123 class_alias($newclassname, $classname);
124 return;
125 }
4ba38f2a 126
64f631c1
JO
127 $file = self::psr_classloader($classname);
128 // If the file is found, require it.
129 if (!empty($file)) {
130 require($file);
4ba38f2a
AN
131 return;
132 }
9e19a0f0
PS
133 }
134
91c07d06 135 /**
64f631c1
JO
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.
91c07d06
DP
138 *
139 * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
140 * compatible.
141 *
64f631c1
JO
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.
91c07d06
DP
144 */
145 protected static function psr_classloader($class) {
64f631c1
JO
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;
91c07d06 151 }
64f631c1 152 }
91c07d06 153
64f631c1
JO
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 }
91c07d06 161
64f631c1
JO
162 return false;
163 }
91c07d06 164
64f631c1
JO
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;
91c07d06 178
64f631c1
JO
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;
91c07d06 184 }
64f631c1 185 $path = $CFG->dirroot . '/' . $path;
91c07d06 186
64f631c1
JO
187 // Get the relative class name.
188 $relativeclass = substr($class, $len);
189
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';
194
195 return $file;
91c07d06
DP
196 }
197
198
9e19a0f0
PS
199 /**
200 * Initialise caches, always call before accessing self:: caches.
201 */
202 protected static function init() {
203 global $CFG;
204
205 // Init only once per request/CLI execution, we ignore changes done afterwards.
206 if (isset(self::$plugintypes)) {
207 return;
208 }
209
d7245e34 210 if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
9e19a0f0
PS
211 self::fill_all_caches();
212 return;
213 }
214
d7245e34
PS
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;
218
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);
37e60007
SH
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'];
d7245e34
PS
238 return;
239 }
240
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 }
244
245 // Lets try to create the file, it might be in some writable directory or a local cache dir.
246
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 }
9e19a0f0
PS
252
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.
3274c5db 261 } else if (!isset($cache['version'])) {
9e19a0f0 262 // Something is very wrong.
3274c5db
FM
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());
9e19a0f0 266 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
d7245e34 267 // $CFG->dirroot was changed.
9e19a0f0
PS
268 } else {
269 // The cache looks ok, let's use it.
37e60007
SH
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'];
9e19a0f0
PS
278 return;
279 }
d7245e34
PS
280 // Note: we do not verify $CFG->admin here intentionally,
281 // they must visit admin/index.php after any change.
9e19a0f0
PS
282 }
283 }
284
9e19a0f0 285 if (!isset(self::$plugintypes)) {
9e19a0f0
PS
286 // This needs to be atomic and self-fixing as much as possible.
287
288 $content = self::get_cache_content();
289 if (file_exists($cachefile)) {
290 if (sha1_file($cachefile) === sha1($content)) {
291 return;
292 }
d7245e34 293 // Stale cache detected!
9e19a0f0
PS
294 unlink($cachefile);
295 }
296
766e04f3
PS
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;
300
301 clearstatcache();
d7245e34
PS
302 $cachedir = dirname($cachefile);
303 if (!is_dir($cachedir)) {
766e04f3 304 mkdir($cachedir, $dirpermissions, true);
d7245e34
PS
305 }
306
9e19a0f0
PS
307 if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
308 fwrite($fp, $content);
309 fclose($fp);
310 @rename($cachefile.'.tmp', $cachefile);
766e04f3 311 @chmod($cachefile, $filepermissions);
9e19a0f0
PS
312 }
313 @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
c05a5099 314 self::invalidate_opcode_php_cache($cachefile);
9e19a0f0
PS
315 }
316 }
317
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;
328
96f81ea3 329 // Note we can not rely on $CFG->debug here because DB is not initialised yet.
d7245e34 330 if (isset($CFG->config_php_settings['debug'])) {
d7245e34 331 $debug = (int)$CFG->config_php_settings['debug'];
d7245e34 332 } else {
9e19a0f0
PS
333 return false;
334 }
335
9e19a0f0
PS
336 if ($debug & E_ALL and $debug & E_STRICT) {
337 return true;
338 }
339
340 return false;
341 }
342
343 /**
344 * Create cache file content.
345 *
d7245e34
PS
346 * @private this is intended for $CFG->alternative_component_cache only.
347 *
9e19a0f0
PS
348 * @return string
349 */
d7245e34
PS
350 public static function get_cache_content() {
351 if (!isset(self::$plugintypes)) {
352 self::fill_all_caches();
353 }
354
9e19a0f0 355 $cache = array(
37e60007
SH
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,
9e19a0f0
PS
365 );
366
367 return '<?php
368$cache = '.var_export($cache, true).';
369';
370 }
371
372 /**
373 * Fill all caches.
374 */
375 protected static function fill_all_caches() {
376 self::$subsystems = self::fetch_subsystems();
377
e87214bd 378 list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
9e19a0f0
PS
379
380 self::$plugins = array();
381 foreach (self::$plugintypes as $type => $fulldir) {
382 self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
383 }
384
385 self::fill_classmap_cache();
37e60007 386 self::fill_classmap_renames_cache();
d26ec8a5 387 self::fill_filemap_cache();
3274c5db
FM
388 self::fetch_core_version();
389 }
390
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) {
81881cb9 401 $version = null; // Prevent IDE complaints.
3274c5db
FM
402 require($CFG->dirroot . '/version.php');
403 self::$version = $version;
404 }
405 return self::$version;
9e19a0f0
PS
406 }
407
408 /**
409 * Returns list of core subsystems.
410 * @return array
411 */
412 protected static function fetch_subsystems() {
413 global $CFG;
414
415 // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
416
417 $info = array(
418 'access' => null,
419 'admin' => $CFG->dirroot.'/'.$CFG->admin,
40fcb365 420 'analytics' => $CFG->dirroot . '/analytics',
146eeb76 421 'antivirus' => $CFG->dirroot . '/lib/antivirus',
9e19a0f0 422 'auth' => $CFG->dirroot.'/auth',
d3db4b03 423 'availability' => $CFG->dirroot . '/availability',
9e19a0f0
PS
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',
be985416 432 'comment' => $CFG->dirroot.'/comment',
67bc0eaf 433 'competency' => $CFG->dirroot.'/competency',
9802bd61 434 'completion' => $CFG->dirroot.'/completion',
9e19a0f0
PS
435 'countries' => null,
436 'course' => $CFG->dirroot.'/course',
437 'currencies' => null,
438 'dbtransfer' => null,
439 'debug' => null,
9e19a0f0
PS
440 'editor' => $CFG->dirroot.'/lib/editor',
441 'edufields' => null,
442 'enrol' => $CFG->dirroot.'/enrol',
443 'error' => null,
444 'filepicker' => null,
34df779a 445 'fileconverter' => $CFG->dirroot.'/files/converter',
9e19a0f0 446 'files' => $CFG->dirroot.'/files',
dc78f150 447 'filters' => $CFG->dirroot.'/filter',
9e19a0f0
PS
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,
fab11235 461 'media' => $CFG->dirroot.'/media',
9e19a0f0
PS
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',
dcc16e15 473 'privacy' => $CFG->dirroot . '/privacy',
9e19a0f0
PS
474 'question' => $CFG->dirroot.'/question',
475 'rating' => $CFG->dirroot.'/rating',
9e19a0f0
PS
476 'repository' => $CFG->dirroot.'/repository',
477 'rss' => $CFG->dirroot.'/rss',
478 'role' => $CFG->dirroot.'/'.$CFG->admin.'/roles',
f900b2b6 479 'search' => $CFG->dirroot.'/search',
9e19a0f0
PS
480 'table' => null,
481 'tag' => $CFG->dirroot.'/tag',
482 'timezones' => null,
483 'user' => $CFG->dirroot.'/user',
484 'userkey' => null,
485 'webservice' => $CFG->dirroot.'/webservice',
486 );
487
488 return $info;
489 }
490
491 /**
492 * Returns list of known plugin types.
493 * @return array
494 */
495 protected static function fetch_plugintypes() {
496 global $CFG;
497
498 $types = array(
146eeb76 499 'antivirus' => $CFG->dirroot . '/lib/antivirus',
d3db4b03 500 'availability' => $CFG->dirroot . '/availability/condition',
9e19a0f0
PS
501 'qtype' => $CFG->dirroot.'/question/type',
502 'mod' => $CFG->dirroot.'/mod',
503 'auth' => $CFG->dirroot.'/auth',
2f00e1b2 504 'calendartype' => $CFG->dirroot.'/calendar/type',
9e19a0f0
PS
505 'enrol' => $CFG->dirroot.'/enrol',
506 'message' => $CFG->dirroot.'/message/output',
507 'block' => $CFG->dirroot.'/blocks',
fab11235 508 'media' => $CFG->dirroot.'/media/player',
9e19a0f0
PS
509 'filter' => $CFG->dirroot.'/filter',
510 'editor' => $CFG->dirroot.'/lib/editor',
511 'format' => $CFG->dirroot.'/course/format',
bff1edbe 512 'dataformat' => $CFG->dirroot.'/dataformat',
9e19a0f0
PS
513 'profilefield' => $CFG->dirroot.'/user/profile/field',
514 'report' => $CFG->dirroot.'/report',
515 'coursereport' => $CFG->dirroot.'/course/report', // Must be after system reports.
516 'gradeexport' => $CFG->dirroot.'/grade/export',
517 'gradeimport' => $CFG->dirroot.'/grade/import',
518 'gradereport' => $CFG->dirroot.'/grade/report',
519 'gradingform' => $CFG->dirroot.'/grade/grading/form',
40fcb365 520 'mlbackend' => $CFG->dirroot.'/lib/mlbackend',
9e19a0f0
PS
521 'mnetservice' => $CFG->dirroot.'/mnet/service',
522 'webservice' => $CFG->dirroot.'/webservice',
523 'repository' => $CFG->dirroot.'/repository',
524 'portfolio' => $CFG->dirroot.'/portfolio',
f900b2b6 525 'search' => $CFG->dirroot.'/search/engine',
9e19a0f0
PS
526 'qbehaviour' => $CFG->dirroot.'/question/behaviour',
527 'qformat' => $CFG->dirroot.'/question/format',
528 'plagiarism' => $CFG->dirroot.'/plagiarism',
529 'tool' => $CFG->dirroot.'/'.$CFG->admin.'/tool',
530 'cachestore' => $CFG->dirroot.'/cache/stores',
531 'cachelock' => $CFG->dirroot.'/cache/locks',
34df779a 532 'fileconverter' => $CFG->dirroot.'/files/converter',
9e19a0f0 533 );
e87214bd
PS
534 $parents = array();
535 $subplugins = array();
9e19a0f0
PS
536
537 if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
538 $types['theme'] = $CFG->themedir;
539 } else {
540 $types['theme'] = $CFG->dirroot.'/theme';
541 }
542
543 foreach (self::$supportsubplugins as $type) {
3601c5f0
PS
544 if ($type === 'local') {
545 // Local subplugins must be after local plugins.
546 continue;
547 }
e87214bd
PS
548 $plugins = self::fetch_plugins($type, $types[$type]);
549 foreach ($plugins as $plugin => $fulldir) {
550 $subtypes = self::fetch_subtypes($fulldir);
551 if (!$subtypes) {
3601c5f0 552 continue;
9e19a0f0 553 }
e87214bd
PS
554 $subplugins[$type.'_'.$plugin] = array();
555 foreach($subtypes as $subtype => $subdir) {
556 if (isset($types[$subtype])) {
557 error_log("Invalid subtype '$subtype', duplicate detected.");
558 continue;
559 }
560 $types[$subtype] = $subdir;
561 $parents[$subtype] = $type.'_'.$plugin;
562 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
563 }
9e19a0f0
PS
564 }
565 }
9e19a0f0
PS
566 // Local is always last!
567 $types['local'] = $CFG->dirroot.'/local';
568
3601c5f0 569 if (in_array('local', self::$supportsubplugins)) {
e87214bd
PS
570 $type = 'local';
571 $plugins = self::fetch_plugins($type, $types[$type]);
572 foreach ($plugins as $plugin => $fulldir) {
573 $subtypes = self::fetch_subtypes($fulldir);
574 if (!$subtypes) {
3601c5f0
PS
575 continue;
576 }
e87214bd
PS
577 $subplugins[$type.'_'.$plugin] = array();
578 foreach($subtypes as $subtype => $subdir) {
579 if (isset($types[$subtype])) {
580 error_log("Invalid subtype '$subtype', duplicate detected.");
581 continue;
582 }
583 $types[$subtype] = $subdir;
584 $parents[$subtype] = $type.'_'.$plugin;
585 $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
586 }
3601c5f0
PS
587 }
588 }
589
e87214bd 590 return array($types, $parents, $subplugins);
3601c5f0
PS
591 }
592
593 /**
e87214bd
PS
594 * Returns list of subtypes.
595 * @param string $ownerdir
3601c5f0
PS
596 * @return array
597 */
e87214bd 598 protected static function fetch_subtypes($ownerdir) {
3601c5f0
PS
599 global $CFG;
600
601 $types = array();
e87214bd
PS
602 if (file_exists("$ownerdir/db/subplugins.php")) {
603 $subplugins = array();
604 include("$ownerdir/db/subplugins.php");
605 foreach ($subplugins as $subtype => $dir) {
606 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
607 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
608 continue;
609 }
610 if (isset(self::$subsystems[$subtype])) {
611 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
612 continue;
613 }
614 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
615 $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
616 }
617 if (!is_dir("$CFG->dirroot/$dir")) {
618 error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
619 continue;
3601c5f0 620 }
e87214bd 621 $types[$subtype] = "$CFG->dirroot/$dir";
3601c5f0
PS
622 }
623 }
9e19a0f0
PS
624 return $types;
625 }
626
627 /**
628 * Returns list of plugins of given type in given directory.
629 * @param string $plugintype
630 * @param string $fulldir
631 * @return array
632 */
633 protected static function fetch_plugins($plugintype, $fulldir) {
634 global $CFG;
635
636 $fulldirs = (array)$fulldir;
637 if ($plugintype === 'theme') {
638 if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
639 // Include themes in standard location too.
640 array_unshift($fulldirs, $CFG->dirroot.'/theme');
641 }
642 }
643
644 $result = array();
645
646 foreach ($fulldirs as $fulldir) {
647 if (!is_dir($fulldir)) {
648 continue;
649 }
650 $items = new \DirectoryIterator($fulldir);
651 foreach ($items as $item) {
652 if ($item->isDot() or !$item->isDir()) {
653 continue;
654 }
655 $pluginname = $item->getFilename();
656 if ($plugintype === 'auth' and $pluginname === 'db') {
657 // Special exception for this wrong plugin name.
658 } else if (isset(self::$ignoreddirs[$pluginname])) {
659 continue;
660 }
661 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
662 // Always ignore plugins with problematic names here.
663 continue;
664 }
665 $result[$pluginname] = $fulldir.'/'.$pluginname;
666 unset($item);
667 }
668 unset($items);
669 }
670
671 ksort($result);
672 return $result;
673 }
674
675 /**
676 * Find all classes that can be autoloaded including frankenstyle namespaces.
677 */
678 protected static function fill_classmap_cache() {
679 global $CFG;
680
681 self::$classmap = array();
682
683 self::load_classes('core', "$CFG->dirroot/lib/classes");
684
685 foreach (self::$subsystems as $subsystem => $fulldir) {
6ef8d163
PS
686 if (!$fulldir) {
687 continue;
688 }
9e19a0f0
PS
689 self::load_classes('core_'.$subsystem, "$fulldir/classes");
690 }
691
692 foreach (self::$plugins as $plugintype => $plugins) {
693 foreach ($plugins as $pluginname => $fulldir) {
694 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
695 }
696 }
be6f3c6e 697 ksort(self::$classmap);
9e19a0f0
PS
698 }
699
d26ec8a5
FM
700 /**
701 * Fills up the cache defining what plugins have certain files.
702 *
703 * @see self::get_plugin_list_with_file
704 * @return void
705 */
706 protected static function fill_filemap_cache() {
707 global $CFG;
708
709 self::$filemap = array();
710
711 foreach (self::$filestomap as $file) {
712 if (!isset(self::$filemap[$file])) {
713 self::$filemap[$file] = array();
714 }
715 foreach (self::$plugins as $plugintype => $plugins) {
716 if (!isset(self::$filemap[$file][$plugintype])) {
717 self::$filemap[$file][$plugintype] = array();
718 }
719 foreach ($plugins as $pluginname => $fulldir) {
720 if (file_exists("$fulldir/$file")) {
721 self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
722 }
723 }
724 }
725 }
726 }
727
9e19a0f0
PS
728 /**
729 * Find classes in directory and recurse to subdirs.
730 * @param string $component
731 * @param string $fulldir
732 * @param string $namespace
733 */
734 protected static function load_classes($component, $fulldir, $namespace = '') {
735 if (!is_dir($fulldir)) {
736 return;
737 }
738
0496ac63
DP
739 if (!is_readable($fulldir)) {
740 // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
741 // because its pretty likely to lead to a missing class error further down the line.
742 // But our early setup code can't handle errors this early at the moment.
743 return;
744 }
745
9e19a0f0
PS
746 $items = new \DirectoryIterator($fulldir);
747 foreach ($items as $item) {
748 if ($item->isDot()) {
749 continue;
750 }
751 if ($item->isDir()) {
752 $dirname = $item->getFilename();
753 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
754 continue;
755 }
756
757 $filename = $item->getFilename();
758 $classname = preg_replace('/\.php$/', '', $filename);
759
760 if ($filename === $classname) {
761 // Not a php file.
762 continue;
763 }
764 if ($namespace === '') {
765 // Legacy long frankenstyle class name.
766 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
767 }
768 // New namespaced classes.
769 self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
770 }
771 unset($item);
772 unset($items);
773 }
774
4ba38f2a 775
9e19a0f0
PS
776 /**
777 * List all core subsystems and their location
778 *
779 * This is a whitelist of components that are part of the core and their
780 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
781 * plugin is not listed here and it does not have proper plugintype prefix,
782 * then it is considered as course activity module.
783 *
784 * The location is absolute file path to dir. NULL means there is no special
785 * directory for this subsystem. If the location is set, the subsystem's
786 * renderer.php is expected to be there.
787 *
788 * @return array of (string)name => (string|null)full dir location
789 */
790 public static function get_core_subsystems() {
791 self::init();
792 return self::$subsystems;
793 }
794
795 /**
796 * Get list of available plugin types together with their location.
797 *
798 * @return array as (string)plugintype => (string)fulldir
799 */
800 public static function get_plugin_types() {
801 self::init();
802 return self::$plugintypes;
803 }
804
805 /**
806 * Get list of plugins of given type.
807 *
808 * @param string $plugintype
809 * @return array as (string)pluginname => (string)fulldir
810 */
811 public static function get_plugin_list($plugintype) {
812 self::init();
813
814 if (!isset(self::$plugins[$plugintype])) {
815 return array();
816 }
817 return self::$plugins[$plugintype];
818 }
819
820 /**
821 * Get a list of all the plugins of a given type that define a certain class
822 * in a certain file. The plugin component names and class names are returned.
823 *
824 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
825 * @param string $class the part of the name of the class after the
826 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
827 * names like report_courselist_thing. If you are looking for classes with
828 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
829 * Frankenstyle namespaces are also supported.
830 * @param string $file the name of file within the plugin that defines the class.
831 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
832 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
833 */
834 public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
835 global $CFG; // Necessary in case it is referenced by included PHP scripts.
836
837 if ($class) {
838 $suffix = '_' . $class;
839 } else {
840 $suffix = '';
841 }
842
843 $pluginclasses = array();
844 $plugins = self::get_plugin_list($plugintype);
845 foreach ($plugins as $plugin => $fulldir) {
846 // Try class in frankenstyle namespace.
847 if ($class) {
848 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
849 if (class_exists($classname, true)) {
850 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
851 continue;
852 }
853 }
854
855 // Try autoloading of class with frankenstyle prefix.
856 $classname = $plugintype . '_' . $plugin . $suffix;
857 if (class_exists($classname, true)) {
858 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
859 continue;
860 }
861
862 // Fall back to old file location and class name.
863 if ($file and file_exists("$fulldir/$file")) {
864 include_once("$fulldir/$file");
865 if (class_exists($classname, false)) {
866 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
867 continue;
868 }
869 }
870 }
871
872 return $pluginclasses;
873 }
874
d26ec8a5
FM
875 /**
876 * Get a list of all the plugins of a given type that contain a particular file.
877 *
878 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
879 * @param string $file the name of file that must be present in the plugin.
880 * (e.g. 'view.php', 'db/install.xml').
881 * @param bool $include if true (default false), the file will be include_once-ed if found.
882 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
883 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
884 */
885 public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
886 global $CFG; // Necessary in case it is referenced by included PHP scripts.
887 $pluginfiles = array();
888
889 if (isset(self::$filemap[$file])) {
890 // If the file was supposed to be mapped, then it should have been set in the array.
891 if (isset(self::$filemap[$file][$plugintype])) {
892 $pluginfiles = self::$filemap[$file][$plugintype];
893 }
894 } else {
895 // Old-style search for non-cached files.
896 $plugins = self::get_plugin_list($plugintype);
897 foreach ($plugins as $plugin => $fulldir) {
898 $path = $fulldir . '/' . $file;
899 if (file_exists($path)) {
900 $pluginfiles[$plugin] = $path;
901 }
902 }
903 }
904
905 if ($include) {
906 foreach ($pluginfiles as $path) {
907 include_once($path);
908 }
909 }
910
911 return $pluginfiles;
912 }
913
f900b2b6
DM
914 /**
915 * Returns all classes in a component matching the provided namespace.
916 *
917 * It checks that the class exists.
918 *
919 * e.g. get_component_classes_in_namespace('mod_forum', 'event')
920 *
921 * @param string $component A valid moodle component (frankenstyle)
8bee2d8d 922 * @param string $namespace Namespace from the component name or empty if all $component namespace classes.
f900b2b6
DM
923 * @return array The full class name as key and the class path as value.
924 */
925 public static function get_component_classes_in_namespace($component, $namespace = '') {
926
8bee2d8d 927 $component = self::normalize_componentname($component);
f900b2b6 928
8bee2d8d 929 if ($namespace) {
f900b2b6 930
8bee2d8d
DM
931 // We will add them later.
932 $namespace = trim($namespace, '\\');
933
934 // We need add double backslashes as it is how classes are stored into self::$classmap.
935 $namespace = implode('\\\\', explode('\\', $namespace));
936 $namespace = $namespace . '\\\\';
937 }
938
939 $regex = '|^' . $component . '\\\\' . $namespace . '|';
f900b2b6
DM
940 $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
941
942 // We want to be sure that they exist.
943 $classes = array();
944 foreach ($it as $classname => $classpath) {
945 if (class_exists($classname)) {
946 $classes[$classname] = $classpath;
947 }
948 }
949
950 return $classes;
951 }
952
9e19a0f0
PS
953 /**
954 * Returns the exact absolute path to plugin directory.
955 *
956 * @param string $plugintype type of plugin
957 * @param string $pluginname name of the plugin
958 * @return string full path to plugin directory; null if not found
959 */
960 public static function get_plugin_directory($plugintype, $pluginname) {
961 if (empty($pluginname)) {
962 // Invalid plugin name, sorry.
963 return null;
964 }
965
966 self::init();
967
968 if (!isset(self::$plugins[$plugintype][$pluginname])) {
969 return null;
970 }
971 return self::$plugins[$plugintype][$pluginname];
972 }
973
974 /**
975 * Returns the exact absolute path to plugin directory.
976 *
977 * @param string $subsystem type of core subsystem
978 * @return string full path to subsystem directory; null if not found
979 */
980 public static function get_subsystem_directory($subsystem) {
981 self::init();
982
983 if (!isset(self::$subsystems[$subsystem])) {
984 return null;
985 }
986 return self::$subsystems[$subsystem];
987 }
988
989 /**
990 * This method validates a plug name. It is much faster than calling clean_param.
991 *
992 * @param string $plugintype type of plugin
993 * @param string $pluginname a string that might be a plugin name.
994 * @return bool if this string is a valid plugin name.
995 */
996 public static function is_valid_plugin_name($plugintype, $pluginname) {
997 if ($plugintype === 'mod') {
998 // Modules must not have the same name as core subsystems.
999 if (!isset(self::$subsystems)) {
1000 // Watch out, this is called from init!
1001 self::init();
1002 }
1003 if (isset(self::$subsystems[$pluginname])) {
1004 return false;
1005 }
1006 // Modules MUST NOT have any underscores,
1007 // component normalisation would break very badly otherwise!
1008 return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1009
1010 } else {
a41d1ca0 1011 return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
9e19a0f0
PS
1012 }
1013 }
1014
7ace3287
AN
1015 /**
1016 * Normalize the component name.
1017 *
1018 * Note: this does not verify the validity of the plugin or component.
1019 *
1020 * @param string $component
1021 * @return string
1022 */
1023 public static function normalize_componentname($componentname) {
1024 list($plugintype, $pluginname) = self::normalize_component($componentname);
1025 if ($plugintype === 'core' && is_null($pluginname)) {
1026 return $plugintype;
1027 }
1028 return $plugintype . '_' . $pluginname;
1029 }
1030
9e19a0f0
PS
1031 /**
1032 * Normalize the component name using the "frankenstyle" rules.
1033 *
1034 * Note: this does not verify the validity of plugin or type names.
1035 *
1036 * @param string $component
6b6a2ec4 1037 * @return array two-items list of [(string)type, (string|null)name]
9e19a0f0
PS
1038 */
1039 public static function normalize_component($component) {
1040 if ($component === 'moodle' or $component === 'core' or $component === '') {
1041 return array('core', null);
1042 }
1043
1044 if (strpos($component, '_') === false) {
1045 self::init();
1046 if (array_key_exists($component, self::$subsystems)) {
1047 $type = 'core';
1048 $plugin = $component;
1049 } else {
1050 // Everything else without underscore is a module.
1051 $type = 'mod';
1052 $plugin = $component;
1053 }
1054
1055 } else {
1056 list($type, $plugin) = explode('_', $component, 2);
1057 if ($type === 'moodle') {
1058 $type = 'core';
1059 }
1060 // Any unknown type must be a subplugin.
1061 }
1062
1063 return array($type, $plugin);
1064 }
1065
1066 /**
1067 * Return exact absolute path to a plugin directory.
1068 *
1069 * @param string $component name such as 'moodle', 'mod_forum'
1070 * @return string full path to component directory; NULL if not found
1071 */
1072 public static function get_component_directory($component) {
1073 global $CFG;
1074
1075 list($type, $plugin) = self::normalize_component($component);
1076
1077 if ($type === 'core') {
1078 if ($plugin === null) {
1079 return $path = $CFG->libdir;
1080 }
1081 return self::get_subsystem_directory($plugin);
1082 }
1083
1084 return self::get_plugin_directory($type, $plugin);
1085 }
3601c5f0
PS
1086
1087 /**
1088 * Returns list of plugin types that allow subplugins.
1089 * @return array as (string)plugintype => (string)fulldir
1090 */
1091 public static function get_plugin_types_with_subplugins() {
1092 self::init();
1093
1094 $return = array();
1095 foreach (self::$supportsubplugins as $type) {
1096 $return[$type] = self::$plugintypes[$type];
1097 }
1098 return $return;
1099 }
c05a5099 1100
e87214bd
PS
1101 /**
1102 * Returns parent of this subplugin type.
1103 *
1104 * @param string $type
1105 * @return string parent component or null
1106 */
1107 public static function get_subtype_parent($type) {
1108 self::init();
1109
1110 if (isset(self::$parents[$type])) {
1111 return self::$parents[$type];
1112 }
1113
1114 return null;
1115 }
1116
1117 /**
1118 * Return all subplugins of this component.
1119 * @param string $component.
1120 * @return array $subtype=>array($component, ..), null if no subtypes defined
1121 */
1122 public static function get_subplugins($component) {
1123 self::init();
1124
1125 if (isset(self::$subplugins[$component])) {
1126 return self::$subplugins[$component];
1127 }
1128
1129 return null;
1130 }
1131
c5701ce7
PS
1132 /**
1133 * Returns hash of all versions including core and all plugins.
1134 *
1135 * This is relatively slow and not fully cached, use with care!
1136 *
1137 * @return string sha1 hash
1138 */
1139 public static function get_all_versions_hash() {
1140 global $CFG;
1141
1142 self::init();
1143
1144 $versions = array();
1145
1146 // Main version first.
3274c5db 1147 $versions['core'] = self::fetch_core_version();
c5701ce7
PS
1148
1149 // The problem here is tha the component cache might be stable,
1150 // we want this to work also on frontpage without resetting the component cache.
1151 $usecache = false;
1152 if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1153 $usecache = true;
1154 }
1155
1156 // Now all plugins.
1157 $plugintypes = core_component::get_plugin_types();
1158 foreach ($plugintypes as $type => $typedir) {
1159 if ($usecache) {
1160 $plugs = core_component::get_plugin_list($type);
1161 } else {
1162 $plugs = self::fetch_plugins($type, $typedir);
1163 }
1164 foreach ($plugs as $plug => $fullplug) {
bde002b8
PS
1165 $plugin = new stdClass();
1166 $plugin->version = null;
1167 $module = $plugin;
0b468c59 1168 include($fullplug.'/version.php');
bde002b8 1169 $versions[$type.'_'.$plug] = $plugin->version;
c5701ce7
PS
1170 }
1171 }
1172
1173 return sha1(serialize($versions));
1174 }
1175
c05a5099
PS
1176 /**
1177 * Invalidate opcode cache for given file, this is intended for
1178 * php files that are stored in dataroot.
1179 *
1180 * Note: we need it here because this class must be self-contained.
1181 *
1182 * @param string $file
1183 */
1184 public static function invalidate_opcode_php_cache($file) {
1185 if (function_exists('opcache_invalidate')) {
1186 if (!file_exists($file)) {
1187 return;
1188 }
1189 opcache_invalidate($file, true);
1190 }
1191 }
a55eaf03
RT
1192
1193 /**
1194 * Return true if subsystemname is core subsystem.
1195 *
1196 * @param string $subsystemname name of the subsystem.
1197 * @return bool true if core subsystem.
1198 */
1199 public static function is_core_subsystem($subsystemname) {
1200 return isset(self::$subsystems[$subsystemname]);
1201 }
37e60007
SH
1202
1203 /**
1204 * Records all class renames that have been made to facilitate autoloading.
1205 */
1206 protected static function fill_classmap_renames_cache() {
1207 global $CFG;
1208
1209 self::$classmaprenames = array();
1210
1211 self::load_renamed_classes("$CFG->dirroot/lib/");
1212
1213 foreach (self::$subsystems as $subsystem => $fulldir) {
1214 self::load_renamed_classes($fulldir);
1215 }
1216
1217 foreach (self::$plugins as $plugintype => $plugins) {
1218 foreach ($plugins as $pluginname => $fulldir) {
1219 self::load_renamed_classes($fulldir);
1220 }
1221 }
1222 }
1223
1224 /**
1225 * Loads the db/renamedclasses.php file from the given directory.
1226 *
1227 * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1228 * and the value is the new class name.
1229 * It is only included when we are populating the component cache. After that is not needed.
1230 *
1231 * @param string $fulldir
1232 */
1233 protected static function load_renamed_classes($fulldir) {
1234 $file = $fulldir . '/db/renamedclasses.php';
1235 if (is_readable($file)) {
1236 $renamedclasses = null;
1237 require($file);
1238 if (is_array($renamedclasses)) {
1239 foreach ($renamedclasses as $oldclass => $newclass) {
1240 self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1241 }
1242 }
1243 }
1244 }
5749f8a4
JD
1245
1246 /**
1247 * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1248 *
1249 * E.g.
1250 * [
1251 * 'mod' => [
1252 * 'mod_forum' => FORUM_PLUGIN_PATH,
1253 * ...
1254 * ],
1255 * ...
1256 * 'core' => [
1257 * 'core_comment' => COMMENT_SUBSYSTEM_PATH,
1258 * ...
1259 * ]
1260 * ]
1261 *
1262 * @return array an associative array of components and their corresponding paths.
1263 */
1264 public static function get_component_list() : array {
1265 $components = [];
1266 // Get all plugins.
1267 foreach (self::get_plugin_types() as $plugintype => $typedir) {
1268 $components[$plugintype] = [];
1269 foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1270 $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1271 }
1272 }
1273 // Get all subsystems.
1274 foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1275 $components['core']['core_' . $subsystemname] = $subsystempath;
1276 }
1277 return $components;
1278 }
9e19a0f0 1279}