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