Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Components (core subsystems + plugins) related code.
19
 *
20
 * @package    core
21
 * @copyright  2013 Petr Skoda {@link http://skodak.org}
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
// Constants used in version.php files, these must exist when core_component executes.
26
 
27
// We make use of error_log as debugging is not always available.
28
// phpcs:disable moodle.PHP.ForbiddenFunctions.FoundWithAlternative
29
// We make use of empty if statements to make complex decisions clearer.
30
// phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedIf
31
 
32
/** Software maturity level - internals can be tested using white box techniques. */
33
define('MATURITY_ALPHA', 50);
34
/** Software maturity level - feature complete, ready for preview and testing. */
35
define('MATURITY_BETA', 100);
36
/** Software maturity level - tested, will be released unless there are fatal bugs. */
37
define('MATURITY_RC', 150);
38
/** Software maturity level - ready for production deployment. */
39
define('MATURITY_STABLE', 200);
40
/** Any version - special value that can be used in $plugin->dependencies in version.php files. */
41
define('ANY_VERSION', 'any');
42
 
43
/**
44
 * Collection of components related methods.
45
 */
46
class core_component {
47
    /** @var array list of ignored directories in plugin type roots - watch out for auth/db exception */
48
    protected static $ignoreddirs = [
49
        'CVS' => true,
50
        '_vti_cnf' => true,
51
        'amd' => true,
52
        'classes' => true,
53
        'db' => true,
54
        'fonts' => true,
55
        'lang' => true,
56
        'pix' => true,
57
        'simpletest' => true,
58
        'templates' => true,
59
        'tests' => true,
60
        'yui' => true,
61
    ];
62
    /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
63
    protected static $supportsubplugins = ['mod', 'editor', 'tool', 'local'];
64
 
65
    /** @var object JSON source of the component data */
66
    protected static $componentsource = null;
67
    /** @var array cache of plugin types */
68
    protected static $plugintypes = null;
69
    /** @var array cache of plugin locations */
70
    protected static $plugins = null;
71
    /** @var array cache of core subsystems */
72
    protected static $subsystems = null;
73
    /** @var array subplugin type parents */
74
    protected static $parents = null;
75
    /** @var array subplugins */
76
    protected static $subplugins = null;
77
    /** @var array cache of core APIs */
78
    protected static $apis = null;
79
    /** @var array list of all known classes that can be autoloaded */
80
    protected static $classmap = null;
81
    /** @var array list of all classes that have been renamed to be autoloaded */
82
    protected static $classmaprenames = null;
83
    /** @var array list of some known files that can be included. */
84
    protected static $filemap = null;
85
    /** @var int|float core version. */
86
    protected static $version = null;
87
    /** @var array list of the files to map. */
88
    protected static $filestomap = ['lib.php', 'settings.php'];
89
    /** @var array associative array of PSR-0 namespaces and corresponding paths. */
90
    protected static $psr0namespaces = [
91
        'Mustache' => 'lib/mustache/src/Mustache',
92
        'CFPropertyList' => 'lib/plist/classes/CFPropertyList',
93
    ];
94
    /** @var array<string|array<string>> associative array of PRS-4 namespaces and corresponding paths. */
95
    protected static $psr4namespaces = [
96
        'MaxMind' => 'lib/maxmind/MaxMind',
97
        'GeoIp2' => 'lib/maxmind/GeoIp2',
98
        'Sabberworm\\CSS' => 'lib/php-css-parser',
99
        'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
100
        'ScssPhp\\ScssPhp' => 'lib/scssphp',
101
        'OpenSpout' => 'lib/openspout/src',
102
        'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
103
        'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
104
        'IMSGlobal\LTI' => 'lib/ltiprovider/src',
105
        'Packback\\Lti1p3' => 'lib/lti1p3/src',
106
        'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
107
        'PHPMailer\\PHPMailer' => 'lib/phpmailer/src',
108
        'RedeyeVentures\\GeoPattern' => 'lib/geopattern-php/GeoPattern',
109
        'Firebase\\JWT' => 'lib/php-jwt/src',
110
        'ZipStream' => 'lib/zipstream/src/',
111
        'MyCLabs\\Enum' => 'lib/php-enum/src',
112
        'PhpXmlRpc' => 'lib/phpxmlrpc',
113
        'Psr\\Http\\Client' => 'lib/psr/http-client/src',
114
        'Psr\\Http\\Message' => [
115
            'lib/psr/http-message/src',
116
            'lib/psr/http-factory/src',
117
        ],
118
        'Psr\\EventDispatcher' => 'lib/psr/event-dispatcher/src',
119
        'Psr\\Clock' => 'lib/psr/clock/src',
120
        'Psr\\Container' => 'lib/psr/container/src',
121
        'GuzzleHttp\\Psr7' => 'lib/guzzlehttp/psr7/src',
122
        'GuzzleHttp\\Promise' => 'lib/guzzlehttp/promises/src',
123
        'GuzzleHttp' => 'lib/guzzlehttp/guzzle/src',
124
        'Kevinrob\\GuzzleCache' => 'lib/guzzlehttp/kevinrob/guzzlecache/src',
125
        'Aws' => 'lib/aws-sdk/src',
126
        'JmesPath' => 'lib/jmespath/src',
127
        'Laravel\\SerializableClosure' => 'lib/laravel/serializable-closure/src',
128
        'DI' => 'lib/php-di/php-di/src',
129
        'Invoker' => 'lib/php-di/invoker/src',
130
    ];
131
 
132
    /**
133
     * Class loader for Frankenstyle named classes in standard locations.
134
     * Frankenstyle namespaces are supported.
135
     *
136
     * The expected location for core classes is:
137
     *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
138
     *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
139
     *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
140
     *
141
     * The expected location for plugin classes is:
142
     *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
143
     *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
144
     *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
145
     *
146
     * @param string $classname
147
     */
148
    public static function classloader($classname) {
149
        self::init();
150
 
151
        if (isset(self::$classmap[$classname])) {
152
            // Global $CFG is expected in included scripts.
153
            global $CFG;
154
            // Function include would be faster, but for BC it is better to include only once.
155
            include_once(self::$classmap[$classname]);
156
            return;
157
        }
158
        if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
159
            $newclassname = self::$classmaprenames[$classname];
160
            $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
161
            debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
162
            if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
163
                throw new \coding_exception("Cannot alias $classname to $newclassname");
164
            }
165
            class_alias($newclassname, $classname);
166
            return;
167
        }
168
 
169
        $file = self::psr_classloader($classname);
170
        // If the file is found, require it.
171
        if (!empty($file)) {
172
            require($file);
173
            return;
174
        }
175
    }
176
 
177
    /**
178
     * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
179
     * demand. Only returns paths to files that exist.
180
     *
181
     * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
182
     * compatible.
183
     *
184
     * @param string $class the name of the class.
185
     * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
186
     */
187
    protected static function psr_classloader($class) {
188
        // Iterate through each PSR-4 namespace prefix.
189
        foreach (self::$psr4namespaces as $prefix => $paths) {
190
            if (!is_array($paths)) {
191
                $paths = [$paths];
192
            }
193
            foreach ($paths as $path) {
194
                $file = self::get_class_file($class, $prefix, $path, ['\\']);
195
                if (!empty($file) && file_exists($file)) {
196
                    return $file;
197
                }
198
            }
199
        }
200
 
201
        // Iterate through each PSR-0 namespace prefix.
202
        foreach (self::$psr0namespaces as $prefix => $path) {
203
            $file = self::get_class_file($class, $prefix, $path, ['\\', '_']);
204
            if (!empty($file) && file_exists($file)) {
205
                return $file;
206
            }
207
        }
208
 
209
        return false;
210
    }
211
 
212
    /**
213
     * Return the path to the class based on the given namespace prefix and path it corresponds to.
214
     *
215
     * Will return the path even if the file does not exist. Check the file esists before requiring.
216
     *
217
     * @param string $class the name of the class.
218
     * @param string $prefix The namespace prefix used to identify the base directory of the source files.
219
     * @param string $path The relative path to the base directory of the source files.
220
     * @param string[] $separators The characters that should be used for separating.
221
     * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
222
     */
223
    protected static function get_class_file($class, $prefix, $path, $separators) {
224
        global $CFG;
225
 
226
        // Does the class use the namespace prefix?
227
        $len = strlen($prefix);
228
        if (strncmp($prefix, $class, $len) !== 0) {
229
            // No, move to the next prefix.
230
            return false;
231
        }
232
        $path = $CFG->dirroot . '/' . $path;
233
 
234
        // Get the relative class name.
235
        $relativeclass = substr($class, $len);
236
 
237
        // Replace the namespace prefix with the base directory, replace namespace
238
        // separators with directory separators in the relative class name, append
239
        // with .php.
240
        $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
241
 
242
        return $file;
243
    }
244
 
245
 
246
    /**
247
     * Initialise caches, always call before accessing self:: caches.
248
     */
249
    protected static function init() {
250
        global $CFG;
251
 
252
        // Init only once per request/CLI execution, we ignore changes done afterwards.
253
        if (isset(self::$plugintypes)) {
254
            return;
255
        }
256
 
257
        if (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE) {
258
            self::fill_all_caches();
259
            return;
260
        }
261
 
262
        if (!empty($CFG->alternative_component_cache)) {
263
            // Hack for heavily clustered sites that want to manage component cache invalidation manually.
264
            $cachefile = $CFG->alternative_component_cache;
265
 
266
            if (file_exists($cachefile)) {
267
                if (CACHE_DISABLE_ALL) {
268
                    // Verify the cache state only on upgrade pages.
269
                    $content = self::get_cache_content();
270
                    if (sha1_file($cachefile) !== sha1($content)) {
271
                        die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
272
                    }
273
                    return;
274
                }
275
                $cache = [];
276
                include($cachefile);
277
                self::$plugintypes      = $cache['plugintypes'];
278
                self::$plugins          = $cache['plugins'];
279
                self::$subsystems       = $cache['subsystems'];
280
                self::$parents          = $cache['parents'];
281
                self::$subplugins       = $cache['subplugins'];
282
                self::$apis             = $cache['apis'];
283
                self::$classmap         = $cache['classmap'];
284
                self::$classmaprenames  = $cache['classmaprenames'];
285
                self::$filemap          = $cache['filemap'];
286
                return;
287
            }
288
 
289
            if (!is_writable(dirname($cachefile))) {
290
                die(
291
                    'Can not create alternative component cache file defined in ' .
292
                    '$CFG->alternative_component_cache, can not continue'
293
                );
294
            }
295
 
296
            // Lets try to create the file, it might be in some writable directory or a local cache dir.
297
        } else {
298
            // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
299
            // use $CFG->alternative_component_cache if you do not like it.
300
            $cachefile = "$CFG->cachedir/core_component.php";
301
        }
302
 
303
        if (!CACHE_DISABLE_ALL && !self::is_developer()) {
304
            // 1/ Use the cache only outside of install and upgrade.
305
            // 2/ Let developers add/remove classes in developer mode.
306
            if (is_readable($cachefile)) {
307
                $cache = false;
308
                include($cachefile);
309
                if (!is_array($cache)) {
310
                    // Something is very wrong.
311
                } else if (!isset($cache['version'])) {
312
                    // Something is very wrong.
313
                } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
314
                    // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
315
                    error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
316
                } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
317
                    // phpcs:ignore moodle.Commenting.InlineComment.NotCapital
318
                    // $CFG->dirroot was changed.
319
                } else {
320
                    // The cache looks ok, let's use it.
321
                    self::$plugintypes      = $cache['plugintypes'];
322
                    self::$plugins          = $cache['plugins'];
323
                    self::$subsystems       = $cache['subsystems'];
324
                    self::$parents          = $cache['parents'];
325
                    self::$subplugins       = $cache['subplugins'];
326
                    self::$apis             = $cache['apis'];
327
                    self::$classmap         = $cache['classmap'];
328
                    self::$classmaprenames  = $cache['classmaprenames'];
329
                    self::$filemap          = $cache['filemap'];
330
                    return;
331
                }
332
                // Note: we do not verify $CFG->admin here intentionally,
333
                // they must visit admin/index.php after any change.
334
            }
335
        }
336
 
337
        if (!isset(self::$plugintypes)) {
338
            // This needs to be atomic and self-fixing as much as possible.
339
 
340
            $content = self::get_cache_content();
341
            if (file_exists($cachefile)) {
342
                if (sha1_file($cachefile) === sha1($content)) {
343
                    return;
344
                }
345
                // Stale cache detected!
346
                unlink($cachefile);
347
            }
348
 
349
            // Permissions might not be setup properly in installers.
350
            $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
351
            $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
352
 
353
            clearstatcache();
354
            $cachedir = dirname($cachefile);
355
            if (!is_dir($cachedir)) {
356
                mkdir($cachedir, $dirpermissions, true);
357
            }
358
 
359
            if ($fp = @fopen($cachefile . '.tmp', 'xb')) {
360
                fwrite($fp, $content);
361
                fclose($fp);
362
                @rename($cachefile . '.tmp', $cachefile);
363
                @chmod($cachefile, $filepermissions);
364
            }
365
            @unlink($cachefile . '.tmp'); // Just in case anything fails (race condition).
366
            self::invalidate_opcode_php_cache($cachefile);
367
        }
368
    }
369
 
370
    /**
371
     * Are we in developer debug mode?
372
     *
373
     * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
374
     *       the reason is we need to use this before we setup DB connection or caches for CFG.
375
     *
376
     * @return bool
377
     */
378
    protected static function is_developer() {
379
        global $CFG;
380
 
381
        // Note we can not rely on $CFG->debug here because DB is not initialised yet.
382
        if (isset($CFG->config_php_settings['debug'])) {
383
            $debug = (int)$CFG->config_php_settings['debug'];
384
        } else {
385
            return false;
386
        }
387
 
388
        if ($debug & E_ALL && $debug & E_STRICT) {
389
            return true;
390
        }
391
 
392
        return false;
393
    }
394
 
395
    /**
396
     * Create cache file content.
397
     *
398
     * @private this is intended for $CFG->alternative_component_cache only.
399
     *
400
     * @return string
401
     */
402
    public static function get_cache_content() {
403
        if (!isset(self::$plugintypes)) {
404
            self::fill_all_caches();
405
        }
406
 
407
        $cache = [
408
            'subsystems'        => self::$subsystems,
409
            'plugintypes'       => self::$plugintypes,
410
            'plugins'           => self::$plugins,
411
            'parents'           => self::$parents,
412
            'subplugins'        => self::$subplugins,
413
            'apis'              => self::$apis,
414
            'classmap'          => self::$classmap,
415
            'classmaprenames'   => self::$classmaprenames,
416
            'filemap'           => self::$filemap,
417
            'version'           => self::$version,
418
        ];
419
 
420
        return '<?php
421
$cache = ' . var_export($cache, true) . ';
422
';
423
    }
424
 
425
    /**
426
     * Fill all caches.
427
     */
428
    protected static function fill_all_caches() {
429
        self::$subsystems = self::fetch_subsystems();
430
 
431
        [self::$plugintypes, self::$parents, self::$subplugins] = self::fetch_plugintypes();
432
 
433
        self::$plugins = [];
434
        foreach (self::$plugintypes as $type => $fulldir) {
435
            self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
436
        }
437
 
438
        self::$apis = self::fetch_apis();
439
 
440
        self::fill_classmap_cache();
441
        self::fill_classmap_renames_cache();
442
        self::fill_filemap_cache();
443
        self::fetch_core_version();
444
    }
445
 
446
    /**
447
     * Get the core version.
448
     *
449
     * In order for this to work properly, opcache should be reset beforehand.
450
     *
451
     * @return float core version.
452
     */
453
    protected static function fetch_core_version() {
454
        global $CFG;
455
        if (self::$version === null) {
456
            $version = null; // Prevent IDE complaints.
457
            require($CFG->dirroot . '/version.php');
458
            self::$version = $version;
459
        }
460
        return self::$version;
461
    }
462
 
463
    /**
464
     * Returns list of core subsystems.
465
     * @return array
466
     */
467
    protected static function fetch_subsystems() {
468
        global $CFG;
469
 
470
        // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
471
        $info = [];
472
        foreach (self::fetch_component_source('subsystems') as $subsystem => $path) {
473
            // Replace admin/ directory with the config setting.
474
            if ($CFG->admin !== 'admin') {
475
                if ($path === 'admin') {
476
                    $path = $CFG->admin;
477
                }
478
                if (strpos($path, 'admin/') === 0) {
479
                    $path = $CFG->admin . substr($path, 5);
480
                }
481
            }
482
 
483
            $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
484
        }
485
 
486
        return $info;
487
    }
488
 
489
    /**
490
     * Returns list of core APIs.
491
     * @return stdClass[]
492
     */
493
    protected static function fetch_apis() {
494
        return (array) json_decode(file_get_contents(__DIR__ . '/../apis.json'));
495
    }
496
 
497
    /**
498
     * Returns list of known plugin types.
499
     * @return array
500
     */
501
    protected static function fetch_plugintypes() {
502
        global $CFG;
503
 
504
        $types = [];
505
        foreach (self::fetch_component_source('plugintypes') as $plugintype => $path) {
506
            // Replace admin/ with the config setting.
507
            if ($CFG->admin !== 'admin' && strpos($path, 'admin/') === 0) {
508
                $path = $CFG->admin . substr($path, 5);
509
            }
510
            $types[$plugintype] = "{$CFG->dirroot}/{$path}";
511
        }
512
 
513
        $parents = [];
514
        $subplugins = [];
515
 
516
        if (!empty($CFG->themedir) && is_dir($CFG->themedir)) {
517
            $types['theme'] = $CFG->themedir;
518
        } else {
519
            $types['theme'] = $CFG->dirroot . '/theme';
520
        }
521
 
522
        foreach (self::$supportsubplugins as $type) {
523
            if ($type === 'local') {
524
                // Local subplugins must be after local plugins.
525
                continue;
526
            }
527
            $plugins = self::fetch_plugins($type, $types[$type]);
528
            foreach ($plugins as $plugin => $fulldir) {
529
                $subtypes = self::fetch_subtypes($fulldir);
530
                if (!$subtypes) {
531
                    continue;
532
                }
533
                $subplugins[$type . '_' . $plugin] = [];
534
                foreach ($subtypes as $subtype => $subdir) {
535
                    if (isset($types[$subtype])) {
536
                        error_log("Invalid subtype '$subtype', duplicate detected.");
537
                        continue;
538
                    }
539
                    $types[$subtype] = $subdir;
540
                    $parents[$subtype] = $type . '_' . $plugin;
541
                    $subplugins[$type . '_' . $plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
542
                }
543
            }
544
        }
545
        // Local is always last!
546
        $types['local'] = $CFG->dirroot . '/local';
547
 
548
        if (in_array('local', self::$supportsubplugins)) {
549
            $type = 'local';
550
            $plugins = self::fetch_plugins($type, $types[$type]);
551
            foreach ($plugins as $plugin => $fulldir) {
552
                $subtypes = self::fetch_subtypes($fulldir);
553
                if (!$subtypes) {
554
                    continue;
555
                }
556
                $subplugins[$type . '_' . $plugin] = [];
557
                foreach ($subtypes as $subtype => $subdir) {
558
                    if (isset($types[$subtype])) {
559
                        error_log("Invalid subtype '$subtype', duplicate detected.");
560
                        continue;
561
                    }
562
                    $types[$subtype] = $subdir;
563
                    $parents[$subtype] = $type . '_' . $plugin;
564
                    $subplugins[$type . '_' . $plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
565
                }
566
            }
567
        }
568
 
569
        return [$types, $parents, $subplugins];
570
    }
571
 
572
    /**
573
     * Returns the component source content as loaded from /lib/components.json.
574
     *
575
     * @return array
576
     */
577
    protected static function fetch_component_source(string $key) {
578
        if (null === self::$componentsource) {
579
            self::$componentsource = (array) json_decode(file_get_contents(__DIR__ . '/../components.json'));
580
        }
581
 
582
        return (array) self::$componentsource[$key];
583
    }
584
 
585
    /**
586
     * Returns list of subtypes.
587
     * @param string $ownerdir
588
     * @return array
589
     */
590
    protected static function fetch_subtypes($ownerdir) {
591
        global $CFG;
592
 
593
        $types = [];
594
        $subplugins = [];
595
        if (file_exists("$ownerdir/db/subplugins.json")) {
596
            $subplugins = [];
597
            $subpluginsjson = json_decode(file_get_contents("$ownerdir/db/subplugins.json"));
598
            if (json_last_error() === JSON_ERROR_NONE) {
599
                if (!empty($subpluginsjson->plugintypes)) {
600
                    $subplugins = (array) $subpluginsjson->plugintypes;
601
                } else {
602
                    error_log("No plugintypes defined in $ownerdir/db/subplugins.json");
603
                }
604
            } else {
605
                $jsonerror = json_last_error_msg();
606
                error_log("$ownerdir/db/subplugins.json is invalid ($jsonerror)");
607
            }
608
        } else if (file_exists("$ownerdir/db/subplugins.php")) {
609
            error_log('Use of subplugins.php has been deprecated. ' .
610
                "Please update your '$ownerdir' plugin to provide a subplugins.json file instead.");
611
            include("$ownerdir/db/subplugins.php");
612
        }
613
 
614
        foreach ($subplugins as $subtype => $dir) {
615
            if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
616
                error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
617
                continue;
618
            }
619
            if (isset(self::$subsystems[$subtype])) {
620
                error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
621
                continue;
622
            }
623
            if ($CFG->admin !== 'admin' && strpos($dir, 'admin/') === 0) {
624
                $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
625
            }
626
            if (!is_dir("$CFG->dirroot/$dir")) {
627
                error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
628
                continue;
629
            }
630
            $types[$subtype] = "$CFG->dirroot/$dir";
631
        }
632
 
633
        return $types;
634
    }
635
 
636
    /**
637
     * Returns list of plugins of given type in given directory.
638
     * @param string $plugintype
639
     * @param string $fulldir
640
     * @return array
641
     */
642
    protected static function fetch_plugins($plugintype, $fulldir) {
643
        global $CFG;
644
 
645
        $fulldirs = (array)$fulldir;
646
        if ($plugintype === 'theme') {
647
            if (realpath($fulldir) !== realpath($CFG->dirroot . '/theme')) {
648
                // Include themes in standard location too.
649
                array_unshift($fulldirs, $CFG->dirroot . '/theme');
650
            }
651
        }
652
 
653
        $result = [];
654
 
655
        foreach ($fulldirs as $fulldir) {
656
            if (!is_dir($fulldir)) {
657
                continue;
658
            }
659
            $items = new \DirectoryIterator($fulldir);
660
            foreach ($items as $item) {
661
                if ($item->isDot() || !$item->isDir()) {
662
                    continue;
663
                }
664
                $pluginname = $item->getFilename();
665
                if ($plugintype === 'auth' && $pluginname === 'db') {
666
                    // Special exception for this wrong plugin name.
667
                } else if (isset(self::$ignoreddirs[$pluginname])) {
668
                    continue;
669
                }
670
                if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
671
                    // Always ignore plugins with problematic names here.
672
                    continue;
673
                }
674
                $result[$pluginname] = $fulldir . '/' . $pluginname;
675
                unset($item);
676
            }
677
            unset($items);
678
        }
679
 
680
        ksort($result);
681
        return $result;
682
    }
683
 
684
    /**
685
     * Find all classes that can be autoloaded including frankenstyle namespaces.
686
     */
687
    protected static function fill_classmap_cache() {
688
        global $CFG;
689
 
690
        self::$classmap = [];
691
 
692
        self::load_classes('core', "$CFG->dirroot/lib/classes");
693
 
694
        foreach (self::$subsystems as $subsystem => $fulldir) {
695
            if (!$fulldir) {
696
                continue;
697
            }
698
            self::load_classes('core_' . $subsystem, "$fulldir/classes");
699
        }
700
 
701
        foreach (self::$plugins as $plugintype => $plugins) {
702
            foreach ($plugins as $pluginname => $fulldir) {
703
                self::load_classes($plugintype . '_' . $pluginname, "$fulldir/classes");
704
            }
705
        }
706
        ksort(self::$classmap);
707
    }
708
 
709
    /**
710
     * Fills up the cache defining what plugins have certain files.
711
     *
712
     * @see self::get_plugin_list_with_file
713
     * @return void
714
     */
715
    protected static function fill_filemap_cache() {
716
        global $CFG;
717
 
718
        self::$filemap = [];
719
 
720
        foreach (self::$filestomap as $file) {
721
            if (!isset(self::$filemap[$file])) {
722
                self::$filemap[$file] = [];
723
            }
724
            foreach (self::$plugins as $plugintype => $plugins) {
725
                if (!isset(self::$filemap[$file][$plugintype])) {
726
                    self::$filemap[$file][$plugintype] = [];
727
                }
728
                foreach ($plugins as $pluginname => $fulldir) {
729
                    if (file_exists("$fulldir/$file")) {
730
                        self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
731
                    }
732
                }
733
            }
734
        }
735
    }
736
 
737
    /**
738
     * Find classes in directory and recurse to subdirs.
739
     * @param string $component
740
     * @param string $fulldir
741
     * @param string $namespace
742
     */
743
    protected static function load_classes($component, $fulldir, $namespace = '') {
744
        if (!is_dir($fulldir)) {
745
            return;
746
        }
747
 
748
        if (!is_readable($fulldir)) {
749
            // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
750
            // because its pretty likely to lead to a missing class error further down the line.
751
            // But our early setup code can't handle errors this early at the moment.
752
            return;
753
        }
754
 
755
        $items = new \DirectoryIterator($fulldir);
756
        foreach ($items as $item) {
757
            if ($item->isDot()) {
758
                continue;
759
            }
760
            if ($item->isDir()) {
761
                $dirname = $item->getFilename();
762
                self::load_classes($component, "$fulldir/$dirname", $namespace . '\\' . $dirname);
763
                continue;
764
            }
765
 
766
            $filename = $item->getFilename();
767
            $classname = preg_replace('/\.php$/', '', $filename);
768
 
769
            if ($filename === $classname) {
770
                // Not a php file.
771
                continue;
772
            }
773
            if ($namespace === '') {
774
                // Legacy long frankenstyle class name.
775
                self::$classmap[$component . '_' . $classname] = "$fulldir/$filename";
776
            }
777
            // New namespaced classes.
778
            self::$classmap[$component . $namespace . '\\' . $classname] = "$fulldir/$filename";
779
        }
780
        unset($item);
781
        unset($items);
782
    }
783
 
784
 
785
    /**
786
     * List all core subsystems and their location
787
     *
788
     * This is a list of components that are part of the core and their
789
     * language strings are defined in /lang/en/<<subsystem>>.php. If a given
790
     * plugin is not listed here and it does not have proper plugintype prefix,
791
     * then it is considered as course activity module.
792
     *
793
     * The location is absolute file path to dir. NULL means there is no special
794
     * directory for this subsystem. If the location is set, the subsystem's
795
     * renderer.php is expected to be there.
796
     *
797
     * @return array of (string)name => (string|null)full dir location
798
     */
799
    public static function get_core_subsystems() {
800
        self::init();
801
        return self::$subsystems;
802
    }
803
 
804
    /**
805
     * List all core APIs and their attributes.
806
     *
807
     * This is a list of all the existing / allowed APIs in moodle, each one with the
808
     * following attributes:
809
     *   - component: the component, usually a subsystem or core, the API belongs to.
810
     *   - allowedlevel2: if the API is allowed as level2 namespace or no.
811
     *   - allowedspread: if the API can spread out from its component or no.
812
     *
813
     * @return stdClass[] array of APIs (as keys) with their attributes as object instances.
814
     */
815
    public static function get_core_apis() {
816
        self::init();
817
        return self::$apis;
818
    }
819
 
820
    /**
821
     * Get list of available plugin types together with their location.
822
     *
823
     * @return array as (string)plugintype => (string)fulldir
824
     */
825
    public static function get_plugin_types() {
826
        self::init();
827
        return self::$plugintypes;
828
    }
829
 
830
    /**
831
     * Get list of plugins of given type.
832
     *
833
     * @param string $plugintype
834
     * @return array as (string)pluginname => (string)fulldir
835
     */
836
    public static function get_plugin_list($plugintype) {
837
        self::init();
838
 
839
        if (!isset(self::$plugins[$plugintype])) {
840
            return [];
841
        }
842
        return self::$plugins[$plugintype];
843
    }
844
 
845
    /**
846
     * Get a list of all the plugins of a given type that define a certain class
847
     * in a certain file. The plugin component names and class names are returned.
848
     *
849
     * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
850
     * @param string $class the part of the name of the class after the
851
     *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
852
     *      names like report_courselist_thing. If you are looking for classes with
853
     *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
854
     *      Frankenstyle namespaces are also supported.
855
     * @param string $file the name of file within the plugin that defines the class.
856
     * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
857
     *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
858
     */
859
    public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
860
        global $CFG; // Necessary in case it is referenced by included PHP scripts.
861
 
862
        if ($class) {
863
            $suffix = '_' . $class;
864
        } else {
865
            $suffix = '';
866
        }
867
 
868
        $pluginclasses = [];
869
        $plugins = self::get_plugin_list($plugintype);
870
        foreach ($plugins as $plugin => $fulldir) {
871
            // Try class in frankenstyle namespace.
872
            if ($class) {
873
                $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
874
                if (class_exists($classname, true)) {
875
                    $pluginclasses[$plugintype . '_' . $plugin] = $classname;
876
                    continue;
877
                }
878
            }
879
 
880
            // Try autoloading of class with frankenstyle prefix.
881
            $classname = $plugintype . '_' . $plugin . $suffix;
882
            if (class_exists($classname, true)) {
883
                $pluginclasses[$plugintype . '_' . $plugin] = $classname;
884
                continue;
885
            }
886
 
887
            // Fall back to old file location and class name.
888
            if ($file && file_exists("$fulldir/$file")) {
889
                include_once("$fulldir/$file");
890
                if (class_exists($classname, false)) {
891
                    $pluginclasses[$plugintype . '_' . $plugin] = $classname;
892
                    continue;
893
                }
894
            }
895
        }
896
 
897
        return $pluginclasses;
898
    }
899
 
900
    /**
901
     * Get a list of all the plugins of a given type that contain a particular file.
902
     *
903
     * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
904
     * @param string $file the name of file that must be present in the plugin.
905
     *                     (e.g. 'view.php', 'db/install.xml').
906
     * @param bool $include if true (default false), the file will be include_once-ed if found.
907
     * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
908
     *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
909
     */
910
    public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
911
        global $CFG; // Necessary in case it is referenced by included PHP scripts.
912
        $pluginfiles = [];
913
 
914
        if (isset(self::$filemap[$file])) {
915
            // If the file was supposed to be mapped, then it should have been set in the array.
916
            if (isset(self::$filemap[$file][$plugintype])) {
917
                $pluginfiles = self::$filemap[$file][$plugintype];
918
            }
919
        } else {
920
            // Old-style search for non-cached files.
921
            $plugins = self::get_plugin_list($plugintype);
922
            foreach ($plugins as $plugin => $fulldir) {
923
                $path = $fulldir . '/' . $file;
924
                if (file_exists($path)) {
925
                    $pluginfiles[$plugin] = $path;
926
                }
927
            }
928
        }
929
 
930
        if ($include) {
931
            foreach ($pluginfiles as $path) {
932
                include_once($path);
933
            }
934
        }
935
 
936
        return $pluginfiles;
937
    }
938
 
939
    /**
940
     * Returns all classes in a component matching the provided namespace.
941
     *
942
     * It checks that the class exists.
943
     *
944
     * e.g. get_component_classes_in_namespace('mod_forum', 'event')
945
     *
946
     * @param string|null $component A valid moodle component (frankenstyle) or null if searching all components
947
     * @param string $namespace Namespace from the component name or empty string if all $component classes.
948
     * @return array The full class name as key and the class path as value, empty array if $component is `null`
949
     * and $namespace is empty.
950
     */
951
    public static function get_component_classes_in_namespace($component = null, $namespace = '') {
952
 
953
        $classes = [];
954
 
955
        // Only look for components if a component name is set or a namespace is set.
956
        if (isset($component) || !empty($namespace)) {
957
            // If a component parameter value is set we only want to look in that component.
958
            // Otherwise we want to check all components.
959
            $component = (isset($component)) ? self::normalize_componentname($component) : '\w+';
960
            if ($namespace) {
961
                // We will add them later.
962
                $namespace = trim($namespace, '\\');
963
 
964
                // We need add double backslashes as it is how classes are stored into self::$classmap.
965
                $namespace = implode('\\\\', explode('\\', $namespace));
966
                $namespace = $namespace . '\\\\';
967
            }
968
            $regex = '|^' . $component . '\\\\' . $namespace . '|';
969
            $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
970
 
971
            // We want to be sure that they exist.
972
            foreach ($it as $classname => $classpath) {
973
                if (class_exists($classname)) {
974
                    $classes[$classname] = $classpath;
975
                }
976
            }
977
        }
978
 
979
        return $classes;
980
    }
981
 
982
    /**
983
     * Returns the exact absolute path to plugin directory.
984
     *
985
     * @param string $plugintype type of plugin
986
     * @param string $pluginname name of the plugin
987
     * @return string full path to plugin directory; null if not found
988
     */
989
    public static function get_plugin_directory($plugintype, $pluginname) {
990
        if (empty($pluginname)) {
991
            // Invalid plugin name, sorry.
992
            return null;
993
        }
994
 
995
        self::init();
996
 
997
        if (!isset(self::$plugins[$plugintype][$pluginname])) {
998
            return null;
999
        }
1000
        return self::$plugins[$plugintype][$pluginname];
1001
    }
1002
 
1003
    /**
1004
     * Returns the exact absolute path to plugin directory.
1005
     *
1006
     * @param string $subsystem type of core subsystem
1007
     * @return string full path to subsystem directory; null if not found
1008
     */
1009
    public static function get_subsystem_directory($subsystem) {
1010
        self::init();
1011
 
1012
        if (!isset(self::$subsystems[$subsystem])) {
1013
            return null;
1014
        }
1015
        return self::$subsystems[$subsystem];
1016
    }
1017
 
1018
    /**
1019
     * This method validates a plug name. It is much faster than calling clean_param.
1020
     *
1021
     * @param string $plugintype type of plugin
1022
     * @param string $pluginname a string that might be a plugin name.
1023
     * @return bool if this string is a valid plugin name.
1024
     */
1025
    public static function is_valid_plugin_name($plugintype, $pluginname) {
1026
        if ($plugintype === 'mod') {
1027
            // Modules must not have the same name as core subsystems.
1028
            if (!isset(self::$subsystems)) {
1029
                // Watch out, this is called from init!
1030
                self::init();
1031
            }
1032
            if (isset(self::$subsystems[$pluginname])) {
1033
                return false;
1034
            }
1035
            // Modules MUST NOT have any underscores,
1036
            // component normalisation would break very badly otherwise!
1037
            return !is_null($pluginname) && (bool) preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1038
        } else {
1039
            return !is_null($pluginname) && (bool) preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1040
        }
1041
    }
1042
 
1043
    /**
1044
     * Normalize the component name.
1045
     *
1046
     * Note: this does not verify the validity of the plugin or component.
1047
     *
1048
     * @param string $component
1049
     * @return string
1050
     */
1051
    public static function normalize_componentname($componentname) {
1052
        [$plugintype, $pluginname] = self::normalize_component($componentname);
1053
        if ($plugintype === 'core' && is_null($pluginname)) {
1054
            return $plugintype;
1055
        }
1056
        return $plugintype . '_' . $pluginname;
1057
    }
1058
 
1059
    /**
1060
     * Normalize the component name using the "frankenstyle" rules.
1061
     *
1062
     * Note: this does not verify the validity of plugin or type names.
1063
     *
1064
     * @param string $component
1065
     * @return array two-items list of [(string)type, (string|null)name]
1066
     */
1067
    public static function normalize_component($component) {
1068
        if ($component === 'moodle' || $component === 'core' || $component === '') {
1069
            return ['core', null];
1070
        }
1071
 
1072
        if (strpos($component, '_') === false) {
1073
            self::init();
1074
            if (array_key_exists($component, self::$subsystems)) {
1075
                $type   = 'core';
1076
                $plugin = $component;
1077
            } else {
1078
                // Everything else without underscore is a module.
1079
                $type   = 'mod';
1080
                $plugin = $component;
1081
            }
1082
        } else {
1083
            [$type, $plugin] = explode('_', $component, 2);
1084
            if ($type === 'moodle') {
1085
                $type = 'core';
1086
            }
1087
            // Any unknown type must be a subplugin.
1088
        }
1089
 
1090
        return [$type, $plugin];
1091
    }
1092
 
1093
    /**
1094
     * Fetch the component name from a Moodle PSR-like namespace.
1095
     *
1096
     * Note: Classnames in the flat underscore_class_name_format are not supported.
1097
     *
1098
     * @param string $classname
1099
     * @return null|string The component name, or null if a matching component was not found
1100
     */
1101
    public static function get_component_from_classname(string $classname): ?string {
1102
        $components = static::get_component_names(true);
1103
 
1104
        $classname = ltrim($classname, '\\');
1105
 
1106
        // Prefer PSR-4 classnames.
1107
        $parts = explode('\\', $classname);
1108
        if ($parts) {
1109
            $component = array_shift($parts);
1110
            if (array_search($component, $components) !== false) {
1111
                return $component;
1112
            }
1113
        }
1114
 
1115
        // Note: Frankenstyle classnames are not supported as they lead to false positives, for example:
1116
        // \core_typo\example => \core instead of \core_typo because it does not exist
1117
        // Please *do not* add support for Frankenstyle classnames. They will break other things.
1118
 
1119
        return null;
1120
    }
1121
 
1122
    /**
1123
     * Return exact absolute path to a plugin directory.
1124
     *
1125
     * @param string $component name such as 'moodle', 'mod_forum'
1126
     * @return string full path to component directory; NULL if not found
1127
     */
1128
    public static function get_component_directory($component) {
1129
        global $CFG;
1130
 
1131
        [$type, $plugin] = self::normalize_component($component);
1132
 
1133
        if ($type === 'core') {
1134
            if ($plugin === null) {
1135
                return $path = $CFG->libdir;
1136
            }
1137
            return self::get_subsystem_directory($plugin);
1138
        }
1139
 
1140
        return self::get_plugin_directory($type, $plugin);
1141
    }
1142
 
1143
    /**
1144
     * Returns list of plugin types that allow subplugins.
1145
     * @return array as (string)plugintype => (string)fulldir
1146
     */
1147
    public static function get_plugin_types_with_subplugins() {
1148
        self::init();
1149
 
1150
        $return = [];
1151
        foreach (self::$supportsubplugins as $type) {
1152
            $return[$type] = self::$plugintypes[$type];
1153
        }
1154
        return $return;
1155
    }
1156
 
1157
    /**
1158
     * Returns parent of this subplugin type.
1159
     *
1160
     * @param string $type
1161
     * @return string parent component or null
1162
     */
1163
    public static function get_subtype_parent($type) {
1164
        self::init();
1165
 
1166
        if (isset(self::$parents[$type])) {
1167
            return self::$parents[$type];
1168
        }
1169
 
1170
        return null;
1171
    }
1172
 
1173
    /**
1174
     * Return all subplugins of this component.
1175
     * @param string $component.
1176
     * @return array $subtype=>array($component, ..), null if no subtypes defined
1177
     */
1178
    public static function get_subplugins($component) {
1179
        self::init();
1180
 
1181
        if (isset(self::$subplugins[$component])) {
1182
            return self::$subplugins[$component];
1183
        }
1184
 
1185
        return null;
1186
    }
1187
 
1188
    /**
1189
     * Returns hash of all versions including core and all plugins.
1190
     *
1191
     * This is relatively slow and not fully cached, use with care!
1192
     *
1193
     * @return string sha1 hash
1194
     */
1195
    public static function get_all_versions_hash() {
1196
        return sha1(serialize(self::get_all_versions()));
1197
    }
1198
 
1199
    /**
1200
     * Returns hash of all versions including core and all plugins.
1201
     *
1202
     * This is relatively slow and not fully cached, use with care!
1203
     *
1204
     * @return array as (string)plugintype_pluginname => (int)version
1205
     */
1206
    public static function get_all_versions(): array {
1207
        global $CFG;
1208
 
1209
        self::init();
1210
 
1211
        $versions = [];
1212
 
1213
        // Main version first.
1214
        $versions['core'] = self::fetch_core_version();
1215
 
1216
        // The problem here is tha the component cache might be stable,
1217
        // we want this to work also on frontpage without resetting the component cache.
1218
        $usecache = false;
1219
        if (CACHE_DISABLE_ALL || (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE)) {
1220
            $usecache = true;
1221
        }
1222
 
1223
        // Now all plugins.
1224
        $plugintypes = self::get_plugin_types();
1225
        foreach ($plugintypes as $type => $typedir) {
1226
            if ($usecache) {
1227
                $plugs = self::get_plugin_list($type);
1228
            } else {
1229
                $plugs = self::fetch_plugins($type, $typedir);
1230
            }
1231
            foreach ($plugs as $plug => $fullplug) {
1232
                $plugin = new stdClass();
1233
                $plugin->version = null;
1234
                $module = $plugin;
1235
                include($fullplug . '/version.php');
1236
                $versions[$type . '_' . $plug] = $plugin->version;
1237
            }
1238
        }
1239
 
1240
        return $versions;
1241
    }
1242
 
1243
    /**
1244
     * Returns hash of all core + plugin /db/ directories.
1245
     *
1246
     * This is relatively slow and not fully cached, use with care!
1247
     *
1248
     * @param array|null $components optional component directory => hash array to use. Only used in PHPUnit.
1249
     * @return string sha1 hash.
1250
     */
1251
    public static function get_all_component_hash(?array $components = null): string {
1252
        $tohash = $components ?? self::get_all_directory_hashes();
1253
        return sha1(serialize($tohash));
1254
    }
1255
 
1256
    /**
1257
     * Get the hashes of all core + plugin /db/ directories.
1258
     *
1259
     * @param array|null $directories optional component directory array to hash. Only used in PHPUnit.
1260
     * @return array of directory => hash.
1261
     */
1262
    public static function get_all_directory_hashes(?array $directories = null): array {
1263
        global $CFG;
1264
 
1265
        self::init();
1266
 
1267
        // The problem here is that the component cache might be stale,
1268
        // we want this to work also on frontpage without resetting the component cache.
1269
        $usecache = false;
1270
        if (CACHE_DISABLE_ALL || (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE)) {
1271
            $usecache = true;
1272
        }
1273
 
1274
        if (empty($directories)) {
1275
            $directories = [
1276
                $CFG->libdir . '/db',
1277
            ];
1278
            // For all components, get the directory of the /db directory.
1279
            $plugintypes = self::get_plugin_types();
1280
            foreach ($plugintypes as $type => $typedir) {
1281
                if ($usecache) {
1282
                    $plugs = self::get_plugin_list($type);
1283
                } else {
1284
                    $plugs = self::fetch_plugins($type, $typedir);
1285
                }
1286
                foreach ($plugs as $plug) {
1287
                    $directories[] = $plug . '/db';
1288
                }
1289
            }
1290
        }
1291
 
1292
        // Create a mapping of directories to their hash.
1293
        $hashes = [];
1294
        foreach ($directories as $directory) {
1295
            if (!is_dir($directory)) {
1296
                // Just hash an empty string as the non-existing representation.
1297
                $hashes[$directory] = sha1('');
1298
                continue;
1299
            }
1300
 
1301
            $scan = scandir($directory);
1302
            if ($scan) {
1303
                sort($scan);
1304
            }
1305
            $scanhashes = [];
1306
            foreach ($scan as $file) {
1307
                $file = $directory . '/' . $file;
1308
                // Moodle ignores directories.
1309
                if (!is_dir($file)) {
1310
                    $scanhashes[] = hash_file('sha1', $file);
1311
                }
1312
            }
1313
            // Finally we can serialize and hash the whole dir.
1314
            $hashes[$directory] = sha1(serialize($scanhashes));
1315
        }
1316
 
1317
        return $hashes;
1318
    }
1319
 
1320
    /**
1321
     * Invalidate opcode cache for given file, this is intended for
1322
     * php files that are stored in dataroot.
1323
     *
1324
     * Note: we need it here because this class must be self-contained.
1325
     *
1326
     * @param string $file
1327
     */
1328
    public static function invalidate_opcode_php_cache($file) {
1329
        if (function_exists('opcache_invalidate')) {
1330
            if (!file_exists($file)) {
1331
                return;
1332
            }
1333
            opcache_invalidate($file, true);
1334
        }
1335
    }
1336
 
1337
    /**
1338
     * Return true if subsystemname is core subsystem.
1339
     *
1340
     * @param string $subsystemname name of the subsystem.
1341
     * @return bool true if core subsystem.
1342
     */
1343
    public static function is_core_subsystem($subsystemname) {
1344
        return isset(self::$subsystems[$subsystemname]);
1345
    }
1346
 
1347
    /**
1348
     * Return true if apiname is a core API.
1349
     *
1350
     * @param string $apiname name of the API.
1351
     * @return bool true if core API.
1352
     */
1353
    public static function is_core_api($apiname) {
1354
        return isset(self::$apis[$apiname]);
1355
    }
1356
 
1357
    /**
1358
     * Records all class renames that have been made to facilitate autoloading.
1359
     */
1360
    protected static function fill_classmap_renames_cache() {
1361
        global $CFG;
1362
 
1363
        self::$classmaprenames = [];
1364
 
1365
        self::load_renamed_classes("$CFG->dirroot/lib/");
1366
 
1367
        foreach (self::$subsystems as $subsystem => $fulldir) {
1368
            self::load_renamed_classes($fulldir);
1369
        }
1370
 
1371
        foreach (self::$plugins as $plugintype => $plugins) {
1372
            foreach ($plugins as $pluginname => $fulldir) {
1373
                self::load_renamed_classes($fulldir);
1374
            }
1375
        }
1376
    }
1377
 
1378
    /**
1379
     * Loads the db/renamedclasses.php file from the given directory.
1380
     *
1381
     * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1382
     * and the value is the new class name.
1383
     * It is only included when we are populating the component cache. After that is not needed.
1384
     *
1385
     * @param string|null $fulldir The directory to the renamed classes.
1386
     */
1387
    protected static function load_renamed_classes(?string $fulldir) {
1388
        if (is_null($fulldir)) {
1389
            return;
1390
        }
1391
 
1392
        $file = $fulldir . '/db/renamedclasses.php';
1393
        if (is_readable($file)) {
1394
            $renamedclasses = null;
1395
            require($file);
1396
            if (is_array($renamedclasses)) {
1397
                foreach ($renamedclasses as $oldclass => $newclass) {
1398
                    self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1399
                }
1400
            }
1401
        }
1402
    }
1403
 
1404
    /**
1405
     * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1406
     *
1407
     * E.g.
1408
     *  [
1409
     *      'mod' => [
1410
     *          'mod_forum' => FORUM_PLUGIN_PATH,
1411
     *          ...
1412
     *      ],
1413
     *      ...
1414
     *      'core' => [
1415
     *          'core_comment' => COMMENT_SUBSYSTEM_PATH,
1416
     *          ...
1417
     *      ]
1418
     * ]
1419
     *
1420
     * @return array an associative array of components and their corresponding paths.
1421
     */
1422
    public static function get_component_list(): array {
1423
        $components = [];
1424
        // Get all plugins.
1425
        foreach (self::get_plugin_types() as $plugintype => $typedir) {
1426
            $components[$plugintype] = [];
1427
            foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1428
                $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1429
            }
1430
        }
1431
        // Get all subsystems.
1432
        foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1433
            $components['core']['core_' . $subsystemname] = $subsystempath;
1434
        }
1435
        return $components;
1436
    }
1437
 
1438
    /**
1439
     * Returns a list of frankenstyle component names, including all plugins, subplugins, and subsystems.
1440
     *
1441
     * Note: By default the 'core' subsystem is not included.
1442
     *
1443
     * @param bool $includecore Whether to include the 'core' subsystem
1444
     * @return string[] the list of frankenstyle component names.
1445
     */
1446
    public static function get_component_names(
1447
        bool $includecore = false,
1448
    ): array {
1449
        $componentnames = [];
1450
        // Get all plugins.
1451
        foreach (self::get_plugin_types() as $plugintype => $typedir) {
1452
            foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1453
                $componentnames[] = $plugintype . '_' . $pluginname;
1454
            }
1455
        }
1456
        // Get all subsystems.
1457
        foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1458
            $componentnames[] = 'core_' . $subsystemname;
1459
        }
1460
 
1461
        if ($includecore) {
1462
            $componentnames[] = 'core';
1463
        }
1464
 
1465
        return $componentnames;
1466
    }
1467
 
1468
    /**
1469
     * Returns the list of available API names.
1470
     *
1471
     * @return string[] the list of available API names.
1472
     */
1473
    public static function get_core_api_names(): array {
1474
        return array_keys(self::get_core_apis());
1475
    }
1476
 
1477
    /**
1478
     * Checks for the presence of monologo icons within a plugin.
1479
     *
1480
     * Only checks monologo icons in PNG and SVG formats as they are
1481
     * formats that can have transparent background.
1482
     *
1483
     * @param string $plugintype The plugin type.
1484
     * @param string $pluginname The plugin name.
1485
     * @return bool True if the plugin has a monologo icon
1486
     */
1487
    public static function has_monologo_icon(string $plugintype, string $pluginname): bool {
1488
        $plugindir = self::get_plugin_directory($plugintype, $pluginname);
1489
        if ($plugindir === null) {
1490
            return false;
1491
        }
1492
        return file_exists("$plugindir/pix/monologo.svg") || file_exists("$plugindir/pix/monologo.png");
1493
    }
1494
}