Proyectos de Subversion Moodle

Rev

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