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