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