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