MDL-39088 fix enrol migration redirect url
[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
25/**
26 * Collection of components related methods.
27 */
28class core_component {
29 /** @var array list of ignored directories - watch out for auth/db exception */
30 protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true);
31 /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
32 protected static $supportsubplugins = array('mod', 'editor');
33
34 /** @var null cache of plugin types */
35 protected static $plugintypes = null;
36 /** @var null cache of plugin locations */
37 protected static $plugins = null;
38 /** @var null cache of core subsystems */
39 protected static $subsystems = null;
40 /** @var null list of all known classes that can be autoloaded */
41 protected static $classmap = null;
42
43 /**
44 * Class loader for Frankenstyle named classes in standard locations.
45 * Frankenstyle namespaces are supported.
46 *
47 * The expected location for core classes is:
48 * 1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
49 * 2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
50 * 3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
51 *
52 * The expected location for plugin classes is:
53 * 1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
54 * 2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
55 * 3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
56 *
57 * @param string $classname
58 */
59 public static function classloader($classname) {
60 self::init();
61
62 if (isset(self::$classmap[$classname])) {
63 // Global $CFG is expected in included scripts.
64 global $CFG;
65 // Function include would be faster, but for BC it is better to include only once.
66 include_once(self::$classmap[$classname]);
67 return;
68 }
69 }
70
71 /**
72 * Initialise caches, always call before accessing self:: caches.
73 */
74 protected static function init() {
75 global $CFG;
76
77 // Init only once per request/CLI execution, we ignore changes done afterwards.
78 if (isset(self::$plugintypes)) {
79 return;
80 }
81
1d4ae19d
DM
82 if (PHPUNIT_TEST or !empty($CFG->early_install_lang) or
83 (defined('BEHAT_UTIL') and BEHAT_UTIL) or
84 (defined('BEHAT_TEST') and BEHAT_TEST)) {
9e19a0f0
PS
85 // 1/ Do not bother storing the file for unit tests,
86 // we need fresh copy for each execution and
87 // later we keep it in memory.
88 // 2/ We can not write to dataroot in installer yet.
89 self::fill_all_caches();
90 return;
91 }
92
93 // Note: cachedir MUST be shared by all servers in a cluster, sorry guys...
94 // MUC should use classloading, we can not depend on it here.
95 $cachefile = "$CFG->cachedir/core_component.php";
96
97 if (!CACHE_DISABLE_ALL and !self::is_developer()) {
98 // 1/ Use the cache only outside of install and upgrade.
99 // 2/ Let developers add/remove classes in developer mode.
100 if (is_readable($cachefile)) {
101 $cache = false;
102 include($cachefile);
103 if (!is_array($cache)) {
104 // Something is very wrong.
105 } else if (!isset($cache['plugintypes']) or !isset($cache['plugins']) or !isset($cache['subsystems']) or !isset($cache['classmap'])) {
106 // Something is very wrong.
107 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
108 // Dirroot was changed.
109 } else {
110 // The cache looks ok, let's use it.
111 self::$plugintypes = $cache['plugintypes'];
112 self::$plugins = $cache['plugins'];
113 self::$subsystems = $cache['subsystems'];
114 self::$classmap = $cache['classmap'];
115 return;
116 }
117 }
118 }
119
120 $cachedir = dirname($cachefile);
121 if (!is_dir($cachedir)) {
122 mkdir($cachedir, $CFG->directorypermissions, true);
123 }
124
125 if (!isset(self::$plugintypes)) {
126 self::fill_all_caches();
127
128 // This needs to be atomic and self-fixing as much as possible.
129
130 $content = self::get_cache_content();
131 if (file_exists($cachefile)) {
132 if (sha1_file($cachefile) === sha1($content)) {
133 return;
134 }
135 unlink($cachefile);
136 }
137
138 if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
139 fwrite($fp, $content);
140 fclose($fp);
141 @rename($cachefile.'.tmp', $cachefile);
142 @chmod($cachefile, $CFG->filepermissions);
143 }
144 @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
145 }
146 }
147
148 /**
149 * Are we in developer debug mode?
150 *
151 * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
152 * the reason is we need to use this before we setup DB connection or caches for CFG.
153 *
154 * @return bool
155 */
156 protected static function is_developer() {
157 global $CFG;
158
159 if (!isset($CFG->config_php_settings['debug'])) {
160 return false;
161 }
162
163 $debug = (int)$CFG->config_php_settings['debug'];
164 if ($debug & E_ALL and $debug & E_STRICT) {
165 return true;
166 }
167
168 return false;
169 }
170
171 /**
172 * Create cache file content.
173 *
174 * @return string
175 */
176 protected static function get_cache_content() {
177 $cache = array(
1652aa9c
PS
178 'subsystems' => self::$subsystems,
179 'plugintypes' => self::$plugintypes,
180 'plugins' => self::$plugins,
181 'classmap' => self::$classmap,
9e19a0f0
PS
182 );
183
184 return '<?php
185$cache = '.var_export($cache, true).';
186';
187 }
188
189 /**
190 * Fill all caches.
191 */
192 protected static function fill_all_caches() {
193 self::$subsystems = self::fetch_subsystems();
194
195 self::$plugintypes = self::fetch_plugintypes();
196
197 self::$plugins = array();
198 foreach (self::$plugintypes as $type => $fulldir) {
199 self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
200 }
201
202 self::fill_classmap_cache();
203 }
204
205 /**
206 * Returns list of core subsystems.
207 * @return array
208 */
209 protected static function fetch_subsystems() {
210 global $CFG;
211
212 // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
213
214 $info = array(
215 'access' => null,
216 'admin' => $CFG->dirroot.'/'.$CFG->admin,
217 'auth' => $CFG->dirroot.'/auth',
218 'backup' => $CFG->dirroot.'/backup/util/ui',
219 'badges' => $CFG->dirroot.'/badges',
220 'block' => $CFG->dirroot.'/blocks',
221 'blog' => $CFG->dirroot.'/blog',
222 'bulkusers' => null,
223 'cache' => $CFG->dirroot.'/cache',
224 'calendar' => $CFG->dirroot.'/calendar',
225 'cohort' => $CFG->dirroot.'/cohort',
226 'condition' => null,
227 'completion' => null,
228 'countries' => null,
229 'course' => $CFG->dirroot.'/course',
230 'currencies' => null,
231 'dbtransfer' => null,
232 'debug' => null,
233 'dock' => null,
234 'editor' => $CFG->dirroot.'/lib/editor',
235 'edufields' => null,
236 'enrol' => $CFG->dirroot.'/enrol',
237 'error' => null,
238 'filepicker' => null,
239 'files' => $CFG->dirroot.'/files',
240 'filters' => null,
241 //'fonts' => null, // Bogus.
242 'form' => $CFG->dirroot.'/lib/form',
243 'grades' => $CFG->dirroot.'/grade',
244 'grading' => $CFG->dirroot.'/grade/grading',
245 'group' => $CFG->dirroot.'/group',
246 'help' => null,
247 'hub' => null,
248 'imscc' => null,
249 'install' => null,
250 'iso6392' => null,
251 'langconfig' => null,
252 'license' => null,
253 'mathslib' => null,
254 'media' => null,
255 'message' => $CFG->dirroot.'/message',
256 'mimetypes' => null,
257 'mnet' => $CFG->dirroot.'/mnet',
258 //'moodle.org' => null, // Not used any more.
259 'my' => $CFG->dirroot.'/my',
260 'notes' => $CFG->dirroot.'/notes',
261 'pagetype' => null,
262 'pix' => null,
263 'plagiarism' => $CFG->dirroot.'/plagiarism',
264 'plugin' => null,
265 'portfolio' => $CFG->dirroot.'/portfolio',
266 'publish' => $CFG->dirroot.'/course/publish',
267 'question' => $CFG->dirroot.'/question',
268 'rating' => $CFG->dirroot.'/rating',
269 'register' => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
270 'repository' => $CFG->dirroot.'/repository',
271 'rss' => $CFG->dirroot.'/rss',
272 'role' => $CFG->dirroot.'/'.$CFG->admin.'/roles',
273 'search' => null,
274 'table' => null,
275 'tag' => $CFG->dirroot.'/tag',
276 'timezones' => null,
277 'user' => $CFG->dirroot.'/user',
278 'userkey' => null,
279 'webservice' => $CFG->dirroot.'/webservice',
280 );
281
282 return $info;
283 }
284
285 /**
286 * Returns list of known plugin types.
287 * @return array
288 */
289 protected static function fetch_plugintypes() {
290 global $CFG;
291
292 $types = array(
293 'qtype' => $CFG->dirroot.'/question/type',
294 'mod' => $CFG->dirroot.'/mod',
295 'auth' => $CFG->dirroot.'/auth',
296 'enrol' => $CFG->dirroot.'/enrol',
297 'message' => $CFG->dirroot.'/message/output',
298 'block' => $CFG->dirroot.'/blocks',
299 'filter' => $CFG->dirroot.'/filter',
300 'editor' => $CFG->dirroot.'/lib/editor',
301 'format' => $CFG->dirroot.'/course/format',
302 'profilefield' => $CFG->dirroot.'/user/profile/field',
303 'report' => $CFG->dirroot.'/report',
304 'coursereport' => $CFG->dirroot.'/course/report', // Must be after system reports.
305 'gradeexport' => $CFG->dirroot.'/grade/export',
306 'gradeimport' => $CFG->dirroot.'/grade/import',
307 'gradereport' => $CFG->dirroot.'/grade/report',
308 'gradingform' => $CFG->dirroot.'/grade/grading/form',
309 'mnetservice' => $CFG->dirroot.'/mnet/service',
310 'webservice' => $CFG->dirroot.'/webservice',
311 'repository' => $CFG->dirroot.'/repository',
312 'portfolio' => $CFG->dirroot.'/portfolio',
313 'qbehaviour' => $CFG->dirroot.'/question/behaviour',
314 'qformat' => $CFG->dirroot.'/question/format',
315 'plagiarism' => $CFG->dirroot.'/plagiarism',
316 'tool' => $CFG->dirroot.'/'.$CFG->admin.'/tool',
317 'cachestore' => $CFG->dirroot.'/cache/stores',
318 'cachelock' => $CFG->dirroot.'/cache/locks',
319
320 );
321
322 if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
323 $types['theme'] = $CFG->themedir;
324 } else {
325 $types['theme'] = $CFG->dirroot.'/theme';
326 }
327
328 foreach (self::$supportsubplugins as $type) {
329 $subpluginowners = self::fetch_plugins($type, $types[$type]);
330 foreach ($subpluginowners as $ownerdir) {
331 if (file_exists("$ownerdir/db/subplugins.php")) {
332 $subplugins = array();
333 include("$ownerdir/db/subplugins.php");
334 foreach ($subplugins as $subtype => $dir) {
335 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
336 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
337 continue;
338 }
339 if (isset(self::$subsystems[$subtype])) {
340 error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
341 continue;
342 }
343 $types[$subtype] = $CFG->dirroot.'/'.$dir;
344 }
345 }
346 }
347 }
348
349 // Local is always last!
350 $types['local'] = $CFG->dirroot.'/local';
351
352 return $types;
353 }
354
355 /**
356 * Returns list of plugins of given type in given directory.
357 * @param string $plugintype
358 * @param string $fulldir
359 * @return array
360 */
361 protected static function fetch_plugins($plugintype, $fulldir) {
362 global $CFG;
363
364 $fulldirs = (array)$fulldir;
365 if ($plugintype === 'theme') {
366 if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
367 // Include themes in standard location too.
368 array_unshift($fulldirs, $CFG->dirroot.'/theme');
369 }
370 }
371
372 $result = array();
373
374 foreach ($fulldirs as $fulldir) {
375 if (!is_dir($fulldir)) {
376 continue;
377 }
378 $items = new \DirectoryIterator($fulldir);
379 foreach ($items as $item) {
380 if ($item->isDot() or !$item->isDir()) {
381 continue;
382 }
383 $pluginname = $item->getFilename();
384 if ($plugintype === 'auth' and $pluginname === 'db') {
385 // Special exception for this wrong plugin name.
386 } else if (isset(self::$ignoreddirs[$pluginname])) {
387 continue;
388 }
389 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
390 // Always ignore plugins with problematic names here.
391 continue;
392 }
393 $result[$pluginname] = $fulldir.'/'.$pluginname;
394 unset($item);
395 }
396 unset($items);
397 }
398
399 ksort($result);
400 return $result;
401 }
402
403 /**
404 * Find all classes that can be autoloaded including frankenstyle namespaces.
405 */
406 protected static function fill_classmap_cache() {
407 global $CFG;
408
409 self::$classmap = array();
410
411 self::load_classes('core', "$CFG->dirroot/lib/classes");
412
413 foreach (self::$subsystems as $subsystem => $fulldir) {
414 self::load_classes('core_'.$subsystem, "$fulldir/classes");
415 }
416
417 foreach (self::$plugins as $plugintype => $plugins) {
418 foreach ($plugins as $pluginname => $fulldir) {
419 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
420 }
421 }
422
423 // Note: Add a few extra legacy classes here if necessary.
424 //self::$classmap['textlib'] = "$CFG->dirroot/lib/textlib.class.php";
425 //self::$classmap['collatorlib'] = "$CFG->dirroot/lib/textlib.class.php";
426 }
427
428 /**
429 * Find classes in directory and recurse to subdirs.
430 * @param string $component
431 * @param string $fulldir
432 * @param string $namespace
433 */
434 protected static function load_classes($component, $fulldir, $namespace = '') {
435 if (!is_dir($fulldir)) {
436 return;
437 }
438
439 $items = new \DirectoryIterator($fulldir);
440 foreach ($items as $item) {
441 if ($item->isDot()) {
442 continue;
443 }
444 if ($item->isDir()) {
445 $dirname = $item->getFilename();
446 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
447 continue;
448 }
449
450 $filename = $item->getFilename();
451 $classname = preg_replace('/\.php$/', '', $filename);
452
453 if ($filename === $classname) {
454 // Not a php file.
455 continue;
456 }
457 if ($namespace === '') {
458 // Legacy long frankenstyle class name.
459 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
460 }
461 // New namespaced classes.
462 self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
463 }
464 unset($item);
465 unset($items);
466 }
467
468 /**
469 * List all core subsystems and their location
470 *
471 * This is a whitelist of components that are part of the core and their
472 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
473 * plugin is not listed here and it does not have proper plugintype prefix,
474 * then it is considered as course activity module.
475 *
476 * The location is absolute file path to dir. NULL means there is no special
477 * directory for this subsystem. If the location is set, the subsystem's
478 * renderer.php is expected to be there.
479 *
480 * @return array of (string)name => (string|null)full dir location
481 */
482 public static function get_core_subsystems() {
483 self::init();
484 return self::$subsystems;
485 }
486
487 /**
488 * Get list of available plugin types together with their location.
489 *
490 * @return array as (string)plugintype => (string)fulldir
491 */
492 public static function get_plugin_types() {
493 self::init();
494 return self::$plugintypes;
495 }
496
497 /**
498 * Get list of plugins of given type.
499 *
500 * @param string $plugintype
501 * @return array as (string)pluginname => (string)fulldir
502 */
503 public static function get_plugin_list($plugintype) {
504 self::init();
505
506 if (!isset(self::$plugins[$plugintype])) {
507 return array();
508 }
509 return self::$plugins[$plugintype];
510 }
511
512 /**
513 * Get a list of all the plugins of a given type that define a certain class
514 * in a certain file. The plugin component names and class names are returned.
515 *
516 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
517 * @param string $class the part of the name of the class after the
518 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
519 * names like report_courselist_thing. If you are looking for classes with
520 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
521 * Frankenstyle namespaces are also supported.
522 * @param string $file the name of file within the plugin that defines the class.
523 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
524 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
525 */
526 public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
527 global $CFG; // Necessary in case it is referenced by included PHP scripts.
528
529 if ($class) {
530 $suffix = '_' . $class;
531 } else {
532 $suffix = '';
533 }
534
535 $pluginclasses = array();
536 $plugins = self::get_plugin_list($plugintype);
537 foreach ($plugins as $plugin => $fulldir) {
538 // Try class in frankenstyle namespace.
539 if ($class) {
540 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
541 if (class_exists($classname, true)) {
542 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
543 continue;
544 }
545 }
546
547 // Try autoloading of class with frankenstyle prefix.
548 $classname = $plugintype . '_' . $plugin . $suffix;
549 if (class_exists($classname, true)) {
550 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
551 continue;
552 }
553
554 // Fall back to old file location and class name.
555 if ($file and file_exists("$fulldir/$file")) {
556 include_once("$fulldir/$file");
557 if (class_exists($classname, false)) {
558 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
559 continue;
560 }
561 }
562 }
563
564 return $pluginclasses;
565 }
566
567 /**
568 * Returns the exact absolute path to plugin directory.
569 *
570 * @param string $plugintype type of plugin
571 * @param string $pluginname name of the plugin
572 * @return string full path to plugin directory; null if not found
573 */
574 public static function get_plugin_directory($plugintype, $pluginname) {
575 if (empty($pluginname)) {
576 // Invalid plugin name, sorry.
577 return null;
578 }
579
580 self::init();
581
582 if (!isset(self::$plugins[$plugintype][$pluginname])) {
583 return null;
584 }
585 return self::$plugins[$plugintype][$pluginname];
586 }
587
588 /**
589 * Returns the exact absolute path to plugin directory.
590 *
591 * @param string $subsystem type of core subsystem
592 * @return string full path to subsystem directory; null if not found
593 */
594 public static function get_subsystem_directory($subsystem) {
595 self::init();
596
597 if (!isset(self::$subsystems[$subsystem])) {
598 return null;
599 }
600 return self::$subsystems[$subsystem];
601 }
602
603 /**
604 * This method validates a plug name. It is much faster than calling clean_param.
605 *
606 * @param string $plugintype type of plugin
607 * @param string $pluginname a string that might be a plugin name.
608 * @return bool if this string is a valid plugin name.
609 */
610 public static function is_valid_plugin_name($plugintype, $pluginname) {
611 if ($plugintype === 'mod') {
612 // Modules must not have the same name as core subsystems.
613 if (!isset(self::$subsystems)) {
614 // Watch out, this is called from init!
615 self::init();
616 }
617 if (isset(self::$subsystems[$pluginname])) {
618 return false;
619 }
620 // Modules MUST NOT have any underscores,
621 // component normalisation would break very badly otherwise!
622 return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
623
624 } else {
625 return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]$/', $pluginname);
626 }
627 }
628
629 /**
630 * Normalize the component name using the "frankenstyle" rules.
631 *
632 * Note: this does not verify the validity of plugin or type names.
633 *
634 * @param string $component
635 * @return array as (string)$type => (string)$plugin
636 */
637 public static function normalize_component($component) {
638 if ($component === 'moodle' or $component === 'core' or $component === '') {
639 return array('core', null);
640 }
641
642 if (strpos($component, '_') === false) {
643 self::init();
644 if (array_key_exists($component, self::$subsystems)) {
645 $type = 'core';
646 $plugin = $component;
647 } else {
648 // Everything else without underscore is a module.
649 $type = 'mod';
650 $plugin = $component;
651 }
652
653 } else {
654 list($type, $plugin) = explode('_', $component, 2);
655 if ($type === 'moodle') {
656 $type = 'core';
657 }
658 // Any unknown type must be a subplugin.
659 }
660
661 return array($type, $plugin);
662 }
663
664 /**
665 * Return exact absolute path to a plugin directory.
666 *
667 * @param string $component name such as 'moodle', 'mod_forum'
668 * @return string full path to component directory; NULL if not found
669 */
670 public static function get_component_directory($component) {
671 global $CFG;
672
673 list($type, $plugin) = self::normalize_component($component);
674
675 if ($type === 'core') {
676 if ($plugin === null) {
677 return $path = $CFG->libdir;
678 }
679 return self::get_subsystem_directory($plugin);
680 }
681
682 return self::get_plugin_directory($type, $plugin);
683 }
684}