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