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