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