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