Proyectos de Subversion Moodle

Rev

Rev 11 | | 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
 
1441 ariadna 25
namespace core;
26
 
27
use core\exception\coding_exception;
28
use core\output\theme_config;
29
use stdClass;
30
use ArrayIterator;
31
use DirectoryIterator;
32
use Exception;
33
use RegexIterator;
34
 
1 efrain 35
// Constants used in version.php files, these must exist when core_component executes.
36
 
37
// We make use of error_log as debugging is not always available.
38
// phpcs:disable moodle.PHP.ForbiddenFunctions.FoundWithAlternative
39
// We make use of empty if statements to make complex decisions clearer.
40
// phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedIf
41
 
42
/** Software maturity level - internals can be tested using white box techniques. */
43
define('MATURITY_ALPHA', 50);
44
/** Software maturity level - feature complete, ready for preview and testing. */
45
define('MATURITY_BETA', 100);
46
/** Software maturity level - tested, will be released unless there are fatal bugs. */
47
define('MATURITY_RC', 150);
48
/** Software maturity level - ready for production deployment. */
49
define('MATURITY_STABLE', 200);
50
/** Any version - special value that can be used in $plugin->dependencies in version.php files. */
51
define('ANY_VERSION', 'any');
52
 
53
/**
54
 * Collection of components related methods.
55
 */
1441 ariadna 56
class component {
1 efrain 57
    /** @var array list of ignored directories in plugin type roots - watch out for auth/db exception */
58
    protected static $ignoreddirs = [
59
        'CVS' => true,
60
        '_vti_cnf' => true,
61
        'amd' => true,
62
        'classes' => true,
63
        'db' => true,
64
        'fonts' => true,
65
        'lang' => true,
66
        'pix' => true,
67
        'simpletest' => true,
68
        'templates' => true,
69
        'tests' => true,
70
        'yui' => true,
71
    ];
72
    /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
73
    protected static $supportsubplugins = ['mod', 'editor', 'tool', 'local'];
74
 
75
    /** @var object JSON source of the component data */
76
    protected static $componentsource = null;
77
    /** @var array cache of plugin types */
78
    protected static $plugintypes = null;
79
    /** @var array cache of plugin locations */
80
    protected static $plugins = null;
81
    /** @var array cache of core subsystems */
82
    protected static $subsystems = null;
83
    /** @var array subplugin type parents */
84
    protected static $parents = null;
85
    /** @var array subplugins */
86
    protected static $subplugins = null;
1441 ariadna 87
    /** @var array deprecated plugins  */
88
    protected static $deprecatedplugins = null;
89
    /** @var array deleted plugins */
90
    protected static $deletedplugins = null;
91
    /** @var array deprecated plugin types */
92
    protected static $deprecatedplugintypes = null;
93
    /** @var array deleted plugin types */
94
    protected static $deletedplugintypes = null;
95
    /** @var array deprecated sub plugins */
96
    protected static $deprecatedsubplugins = null;
97
    /** @var array deleted sub plugins */
98
    protected static $deletedsubplugins = null;
1 efrain 99
    /** @var array cache of core APIs */
100
    protected static $apis = null;
101
    /** @var array list of all known classes that can be autoloaded */
102
    protected static $classmap = null;
103
    /** @var array list of all classes that have been renamed to be autoloaded */
104
    protected static $classmaprenames = null;
105
    /** @var array list of some known files that can be included. */
106
    protected static $filemap = null;
107
    /** @var int|float core version. */
108
    protected static $version = null;
109
    /** @var array list of the files to map. */
110
    protected static $filestomap = ['lib.php', 'settings.php'];
111
    /** @var array associative array of PSR-0 namespaces and corresponding paths. */
112
    protected static $psr0namespaces = [
113
        'Mustache' => 'lib/mustache/src/Mustache',
114
    ];
115
    /** @var array<string|array<string>> associative array of PRS-4 namespaces and corresponding paths. */
116
    protected static $psr4namespaces = [
1441 ariadna 117
        \Aws::class => 'lib/aws-sdk/src',
118
        \CFPropertyList::class => 'lib/plist/src/CFPropertyList',
119
        \Complex::class => 'lib/phpspreadsheet/markbaker/complex/classes/src',
120
        \Composer\Pcre::class => 'lib/composer/pcre/src',
121
        \DI::class => 'lib/php-di/php-di/src',
122
        \GeoIp2::class => 'lib/maxmind/GeoIp2/src',
123
        \FastRoute::class => 'lib/nikic/fast-route/src',
124
        \Firebase\JWT::class => 'lib/php-jwt/src',
125
        \GuzzleHttp::class => 'lib/guzzlehttp/guzzle/src',
126
        \GuzzleHttp\Promise::class => 'lib/guzzlehttp/promises/src',
127
        \GuzzleHttp\Psr7::class => 'lib/guzzlehttp/psr7/src',
128
        \Html2Text::class => 'lib/html2text/src',
129
        \IMSGlobal\LTI::class => 'lib/ltiprovider/src',
130
        \Invoker::class => 'lib/php-di/invoker/src',
131
        \JmesPath::class => 'lib/jmespath/src',
132
        \Kevinrob\GuzzleCache::class => 'lib/guzzlehttp/kevinrob/guzzlecache/src',
133
        \Laravel\SerializableClosure::class => 'lib/laravel/serializable-closure/src',
134
        \lbuchs\WebAuthn::class => 'lib/webauthn/src',
135
        \libphonenumber::class => 'lib/giggsey/libphonenumber-for-php-lite/src',
136
        \Matrix::class => 'lib/phpspreadsheet/markbaker/matrix/classes/src',
137
        \MatthiasMullie\Minify::class => 'lib/minify/matthiasmullie-minify/src',
138
        \MatthiasMullie\PathConverter::class => 'lib/minify/matthiasmullie-pathconverter/src',
139
        \MaxMind\Db::class => 'lib/maxmind/MaxMind/src/MaxMind/Db',
140
        \Michelf::class => 'lib/markdown/Michelf',
141
        \MoodleHQ::class => [
142
            'lib/rtlcss/src/MoodleHQ',
143
        ],
144
        \OpenSpout::class => 'lib/openspout/src',
145
        \Packback\Lti1p3::class => 'lib/lti1p3/src',
146
        \PHPMailer\PHPMailer::class => 'lib/phpmailer/src',
147
        \PhpOffice\PhpSpreadsheet::class => 'lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet',
148
        \PhpXmlRpc::class => 'lib/phpxmlrpc/src',
149
        \Phpml::class => 'lib/mlbackend/php/phpml/src/Phpml',
150
        \Psr\Clock::class => 'lib/psr/clock/src',
151
        \Psr\Container::class => 'lib/psr/container/src',
152
        \Psr\EventDispatcher::class => 'lib/psr/event-dispatcher/src',
153
        \Psr\Http\Client::class => 'lib/psr/http-client/src',
154
        \Psr\Http\Message::class => [
155
            'lib/psr/http-factory/src',
1 efrain 156
            'lib/psr/http-message/src',
157
        ],
1441 ariadna 158
        \Psr\Http\Server::class => [
159
            "lib/psr/http-server-handler/src",
160
            "lib/psr/http-server-middleware/src",
161
        ],
162
        \Psr\Log::class => "lib/psr/log/src",
163
        \Psr\SimpleCache::class => 'lib/psr/simple-cache/src',
164
        \RedeyeVentures::class => 'lib/geopattern-php/src',
165
        \Sabberworm\CSS::class => 'lib/php-css-parser/src',
166
        \ScssPhp\ScssPhp::class => 'lib/scssphp/src',
167
        \SimplePie::class => 'lib/simplepie/src',
168
        \Slim::class => 'lib/slim/slim/Slim',
169
        \Spatie\Cloneable::class => 'lib/spatie/php-cloneable/src',
170
        \ZipStream::class => 'lib/zipstream/src',
1 efrain 171
    ];
172
 
173
    /**
1441 ariadna 174
     *  An array containing files which are normally in a package's composer/autoload.files section.
175
     *
176
     * PHP does not provide a mechanism for automatically including the files that methods are in.
177
     *
178
     * The Composer autoloader includes all files in this section of the composer.json file during the instantiation of the loader.
179
     *
180
     * @var array<string>
181
     */
182
    protected static $composerautoloadfiles = [
183
        'lib/aws-sdk/src/functions.php',
184
        'lib/guzzlehttp/guzzle/src/functions_include.php',
185
        'lib/jmespath/src/JmesPath.php',
186
        'lib/nikic/fast-route/src/functions.php',
187
        'lib/php-di/php-di/src/functions.php',
188
        'lib/ralouphie/getallheaders/src/getallheaders.php',
189
        'lib/symfony/deprecation-contracts/function.php',
190
    ];
191
 
192
    /**
193
     * Register the Moodle class autoloader.
194
     */
195
    public static function register_autoloader(): void {
196
        if (defined('COMPONENT_CLASSLOADER')) {
197
            spl_autoload_register(COMPONENT_CLASSLOADER);
198
        } else {
199
            spl_autoload_register([self::class, 'classloader']);
200
        }
201
 
202
        // Load any composer-driven autoload files.
203
        // This is intended to mimic the behaviour of the standard Composer Autoloader.
204
        foreach (static::$composerautoloadfiles as $file) {
205
            $path = dirname(__DIR__, 2) . '/' . $file;
206
            if (file_exists($path)) {
207
                require_once($path);
208
            }
209
        }
210
    }
211
 
212
    /**
1 efrain 213
     * Class loader for Frankenstyle named classes in standard locations.
214
     * Frankenstyle namespaces are supported.
215
     *
216
     * The expected location for core classes is:
217
     *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
218
     *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
219
     *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
220
     *
221
     * The expected location for plugin classes is:
222
     *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
223
     *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
224
     *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
225
     *
226
     * @param string $classname
227
     */
228
    public static function classloader($classname) {
229
        self::init();
230
 
231
        if (isset(self::$classmap[$classname])) {
232
            // Global $CFG is expected in included scripts.
233
            global $CFG;
234
            // Function include would be faster, but for BC it is better to include only once.
235
            include_once(self::$classmap[$classname]);
236
            return;
237
        }
238
        if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
239
            $newclassname = self::$classmaprenames[$classname];
240
            $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
241
            debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
1441 ariadna 242
            if (preg_match('#\\\null(\\\|$)#', $classname)) {
243
                throw new coding_exception("Cannot alias $classname to $newclassname");
1 efrain 244
            }
245
            class_alias($newclassname, $classname);
246
            return;
247
        }
248
 
249
        $file = self::psr_classloader($classname);
250
        // If the file is found, require it.
251
        if (!empty($file)) {
252
            require($file);
253
            return;
254
        }
11 efrain 255
 
256
        if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
257
            // For unit tests we support classes in `\frankenstyle_component\tests\` to be loaded from
258
            // `path/to/frankenstyle/component/tests/classes` directory.
259
            // Note: We do *not* support the legacy `\frankenstyle_component_tests_style_classnames`.
260
            if ($component = self::get_component_from_classname($classname)) {
261
                $pathoptions = [
262
                    '/tests/classes' => "{$component}\\tests\\",
263
                    '/tests/behat' => "{$component}\\behat\\",
264
                ];
265
                foreach ($pathoptions as $path => $testnamespace) {
266
                    if (preg_match("#^" . preg_quote($testnamespace) . "#", $classname)) {
267
                        $path = self::get_component_directory($component) . $path;
268
                        $relativeclassname = str_replace(
269
                            $testnamespace,
270
                            '',
271
                            $classname,
272
                        );
273
                        $file = sprintf(
274
                            "%s/%s.php",
275
                            $path,
276
                            str_replace('\\', '/', $relativeclassname),
277
                        );
278
                        if (!empty($file) && file_exists($file)) {
279
                            require($file);
280
                            return;
281
                        }
282
                        break;
283
                    }
284
                }
285
            }
286
        }
1 efrain 287
    }
288
 
289
    /**
290
     * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
291
     * demand. Only returns paths to files that exist.
292
     *
293
     * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
294
     * compatible.
295
     *
296
     * @param string $class the name of the class.
297
     * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
298
     */
299
    protected static function psr_classloader($class) {
300
        // Iterate through each PSR-4 namespace prefix.
301
        foreach (self::$psr4namespaces as $prefix => $paths) {
302
            if (!is_array($paths)) {
303
                $paths = [$paths];
304
            }
305
            foreach ($paths as $path) {
306
                $file = self::get_class_file($class, $prefix, $path, ['\\']);
307
                if (!empty($file) && file_exists($file)) {
308
                    return $file;
309
                }
310
            }
311
        }
312
 
313
        // Iterate through each PSR-0 namespace prefix.
314
        foreach (self::$psr0namespaces as $prefix => $path) {
315
            $file = self::get_class_file($class, $prefix, $path, ['\\', '_']);
316
            if (!empty($file) && file_exists($file)) {
317
                return $file;
318
            }
319
        }
320
 
321
        return false;
322
    }
323
 
324
    /**
325
     * Return the path to the class based on the given namespace prefix and path it corresponds to.
326
     *
327
     * Will return the path even if the file does not exist. Check the file esists before requiring.
328
     *
329
     * @param string $class the name of the class.
330
     * @param string $prefix The namespace prefix used to identify the base directory of the source files.
331
     * @param string $path The relative path to the base directory of the source files.
332
     * @param string[] $separators The characters that should be used for separating.
333
     * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
334
     */
335
    protected static function get_class_file($class, $prefix, $path, $separators) {
336
        global $CFG;
337
 
338
        // Does the class use the namespace prefix?
339
        $len = strlen($prefix);
340
        if (strncmp($prefix, $class, $len) !== 0) {
341
            // No, move to the next prefix.
342
            return false;
343
        }
344
        $path = $CFG->dirroot . '/' . $path;
345
 
346
        // Get the relative class name.
347
        $relativeclass = substr($class, $len);
348
 
349
        // Replace the namespace prefix with the base directory, replace namespace
350
        // separators with directory separators in the relative class name, append
351
        // with .php.
352
        $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
353
 
354
        return $file;
355
    }
356
 
357
    /**
358
     * Initialise caches, always call before accessing self:: caches.
359
     */
360
    protected static function init() {
361
        global $CFG;
362
 
363
        // Init only once per request/CLI execution, we ignore changes done afterwards.
364
        if (isset(self::$plugintypes)) {
365
            return;
366
        }
367
 
368
        if (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE) {
369
            self::fill_all_caches();
370
            return;
371
        }
372
 
373
        if (!empty($CFG->alternative_component_cache)) {
374
            // Hack for heavily clustered sites that want to manage component cache invalidation manually.
375
            $cachefile = $CFG->alternative_component_cache;
376
 
377
            if (file_exists($cachefile)) {
378
                if (CACHE_DISABLE_ALL) {
379
                    // Verify the cache state only on upgrade pages.
380
                    $content = self::get_cache_content();
381
                    if (sha1_file($cachefile) !== sha1($content)) {
382
                        die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
383
                    }
384
                    return;
385
                }
386
                $cache = [];
387
                include($cachefile);
1441 ariadna 388
                self::$plugintypes              = $cache['plugintypes'];
389
                self::$deprecatedplugintypes    = $cache['deprecatedplugintypes'];
390
                self::$deletedplugintypes       = $cache['deletedplugintypes'];
391
                self::$plugins                  = $cache['plugins'];
392
                self::$deprecatedplugins        = $cache['deprecatedplugins'];
393
                self::$deletedplugins           = $cache['deletedplugins'];
394
                self::$subsystems               = $cache['subsystems'];
395
                self::$parents                  = $cache['parents'];
396
                self::$subplugins               = $cache['subplugins'];
397
                self::$deprecatedsubplugins     = $cache['deprecatedsubplugins'];
398
                self::$deletedsubplugins        = $cache['deletedsubplugins'];
399
                self::$apis                     = $cache['apis'];
400
                self::$classmap                 = $cache['classmap'];
401
                self::$classmaprenames          = $cache['classmaprenames'];
402
                self::$filemap                  = $cache['filemap'];
1 efrain 403
                return;
404
            }
405
 
406
            if (!is_writable(dirname($cachefile))) {
407
                die(
408
                    'Can not create alternative component cache file defined in ' .
409
                    '$CFG->alternative_component_cache, can not continue'
410
                );
411
            }
412
 
413
            // Lets try to create the file, it might be in some writable directory or a local cache dir.
414
        } else {
415
            // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
416
            // use $CFG->alternative_component_cache if you do not like it.
417
            $cachefile = "$CFG->cachedir/core_component.php";
418
        }
419
 
420
        if (!CACHE_DISABLE_ALL && !self::is_developer()) {
421
            // 1/ Use the cache only outside of install and upgrade.
422
            // 2/ Let developers add/remove classes in developer mode.
423
            if (is_readable($cachefile)) {
424
                $cache = false;
425
                include($cachefile);
1441 ariadna 426
                if (is_array($cache) && self::is_cache_valid($cache)) {
1 efrain 427
                    // The cache looks ok, let's use it.
1441 ariadna 428
                    self::$plugintypes              = $cache['plugintypes'];
429
                    self::$deprecatedplugintypes    = $cache['deprecatedplugintypes'];
430
                    self::$deletedplugintypes       = $cache['deletedplugintypes'];
431
                    self::$plugins                  = $cache['plugins'];
432
                    self::$deprecatedplugins        = $cache['deprecatedplugins'];
433
                    self::$deletedplugins           = $cache['deletedplugins'];
434
                    self::$subsystems               = $cache['subsystems'];
435
                    self::$parents                  = $cache['parents'];
436
                    self::$subplugins               = $cache['subplugins'];
437
                    self::$deprecatedsubplugins     = $cache['deprecatedsubplugins'];
438
                    self::$deletedsubplugins        = $cache['deletedsubplugins'];
439
                    self::$apis                     = $cache['apis'];
440
                    self::$classmap                 = $cache['classmap'];
441
                    self::$classmaprenames          = $cache['classmaprenames'];
442
                    self::$filemap                  = $cache['filemap'];
1 efrain 443
                    return;
444
                }
445
                // Note: we do not verify $CFG->admin here intentionally,
446
                // they must visit admin/index.php after any change.
447
            }
448
        }
449
 
450
        if (!isset(self::$plugintypes)) {
451
            // This needs to be atomic and self-fixing as much as possible.
452
 
453
            $content = self::get_cache_content();
454
            if (file_exists($cachefile)) {
455
                if (sha1_file($cachefile) === sha1($content)) {
456
                    return;
457
                }
458
                // Stale cache detected!
459
                unlink($cachefile);
460
            }
461
 
462
            // Permissions might not be setup properly in installers.
463
            $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
464
            $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
465
 
466
            clearstatcache();
467
            $cachedir = dirname($cachefile);
468
            if (!is_dir($cachedir)) {
469
                mkdir($cachedir, $dirpermissions, true);
470
            }
471
 
472
            if ($fp = @fopen($cachefile . '.tmp', 'xb')) {
473
                fwrite($fp, $content);
474
                fclose($fp);
475
                @rename($cachefile . '.tmp', $cachefile);
476
                @chmod($cachefile, $filepermissions);
477
            }
478
            @unlink($cachefile . '.tmp'); // Just in case anything fails (race condition).
479
            self::invalidate_opcode_php_cache($cachefile);
480
        }
481
    }
482
 
483
    /**
11 efrain 484
     * Reset the initialisation of the component utility.
485
     *
486
     * Note: It should not be necessary to call this in regular code.
487
     * Please only use it where strictly required.
488
     */
1441 ariadna 489
    public static function reset(
490
        bool $fullreset = false,
491
    ): void {
11 efrain 492
        // The autoloader will re-initialise if plugintypes is null.
493
        self::$plugintypes = null;
1441 ariadna 494
 
495
        // Reset all caches to ensure they are reloaded.
496
        self::$plugins = null;
497
        self::$subsystems = null;
498
        self::$parents = null;
499
        self::$subplugins = null;
500
        self::$deprecatedplugins = null;
501
        self::$deletedplugins = null;
502
        self::$deprecatedplugintypes = null;
503
        self::$deletedplugintypes = null;
504
        self::$deprecatedsubplugins = null;
505
        self::$deletedsubplugins = null;
506
        self::$apis = null;
507
        self::$classmap = null;
508
        self::$classmaprenames = null;
509
        self::$filemap = null;
510
 
511
        if ($fullreset) {
512
            self::$componentsource = null;
513
            self::$version = null;
514
            self::$supportsubplugins = ['mod', 'editor', 'tool', 'local'];
515
        }
11 efrain 516
    }
517
 
518
    /**
1441 ariadna 519
     * Check whether the cache content in the supplied cache is valid.
520
     *
521
     * @param array $cache The content being loaded
522
     * @return bool Whether it is valid
523
     */
524
    protected static function is_cache_valid(array $cache): bool {
525
        global $CFG;
526
 
527
        if (!isset($cache['version'])) {
528
            // Something is very wrong.
529
            return false;
530
        }
531
 
532
        if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
533
            // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
534
            error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
535
            return false;
536
        }
537
 
538
        if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
539
            // phpcs:ignore moodle.Commenting.InlineComment.NotCapital
540
            // $CFG->dirroot was changed.
541
            return false;
542
        }
543
 
544
        // Check for key classes which block access to the upgrade in some way.
545
        // Note: This list should be kept _extremely_ minimal and generally
546
        // when adding a newly discovered classes older ones should be removed.
547
        // Always keep moodle_exception in place.
548
        $keyclasses = [
549
            \core\exception\moodle_exception::class,
550
            \core\output\bootstrap_renderer::class,
551
            \core_cache\cache::class,
552
        ];
553
        foreach ($keyclasses as $classname) {
554
            if (!array_key_exists($classname, $cache['classmap'])) {
555
                // The cache is missing some key classes. This is likely before the upgrade has run.
556
                error_log(
557
                    "The '{$classname}' class was not found in the component class cache. Resetting the classmap.",
558
                );
559
                return false;
560
            }
561
        }
562
 
563
        return true;
564
    }
565
 
566
    /**
1 efrain 567
     * Are we in developer debug mode?
568
     *
1441 ariadna 569
     * Note: You need to set "$CFG->debug = (E_ALL);" in config.php,
1 efrain 570
     *       the reason is we need to use this before we setup DB connection or caches for CFG.
571
     *
572
     * @return bool
573
     */
574
    protected static function is_developer() {
575
        global $CFG;
576
 
577
        // Note we can not rely on $CFG->debug here because DB is not initialised yet.
578
        if (isset($CFG->config_php_settings['debug'])) {
579
            $debug = (int)$CFG->config_php_settings['debug'];
580
        } else {
581
            return false;
582
        }
583
 
1441 ariadna 584
        if ($debug & E_ALL) {
1 efrain 585
            return true;
586
        }
587
 
588
        return false;
589
    }
590
 
591
    /**
592
     * Create cache file content.
593
     *
594
     * @private this is intended for $CFG->alternative_component_cache only.
595
     *
596
     * @return string
597
     */
598
    public static function get_cache_content() {
599
        if (!isset(self::$plugintypes)) {
600
            self::fill_all_caches();
601
        }
602
 
603
        $cache = [
1441 ariadna 604
            'subsystems'            => self::$subsystems,
605
            'plugintypes'           => self::$plugintypes,
606
            'deprecatedplugintypes' => self::$deprecatedplugintypes,
607
            'deletedplugintypes'    => self::$deletedplugintypes,
608
            'plugins'               => self::$plugins,
609
            'deprecatedplugins'     => self::$deprecatedplugins,
610
            'deletedplugins'        => self::$deletedplugins,
611
            'subplugins'            => self::$subplugins,
612
            'deprecatedsubplugins'  => self::$deprecatedsubplugins,
613
            'deletedsubplugins'     => self::$deletedsubplugins,
614
            'parents'               => self::$parents,
615
            'apis'                  => self::$apis,
616
            'classmap'              => self::$classmap,
617
            'classmaprenames'       => self::$classmaprenames,
618
            'filemap'               => self::$filemap,
619
            'version'               => self::$version,
1 efrain 620
        ];
621
 
622
        return '<?php
623
$cache = ' . var_export($cache, true) . ';
624
';
625
    }
626
 
627
    /**
628
     * Fill all caches.
629
     */
630
    protected static function fill_all_caches() {
631
        self::$subsystems = self::fetch_subsystems();
632
 
1441 ariadna 633
        [
634
            'plugintypes' => self::$plugintypes,
635
            'parents' => self::$parents,
636
            'subplugins' => self::$subplugins,
637
            'deprecatedplugintypes' => self::$deprecatedplugintypes,
638
            'deletedplugintypes' => self::$deletedplugintypes,
639
            'deprecatedsubplugins' => self::$deprecatedsubplugins,
640
            'deletedsubplugin' => self::$deletedsubplugins
641
        ] = self::fetch_plugintypes();
1 efrain 642
 
643
        self::$plugins = [];
644
        foreach (self::$plugintypes as $type => $fulldir) {
645
            self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
646
        }
647
 
1441 ariadna 648
        self::$deprecatedplugins = [];
649
        foreach (self::$deprecatedplugintypes as $type => $fulldir) {
650
            self::$deprecatedplugins[$type] = self::fetch_plugins($type, $fulldir);
651
        }
652
 
653
        self::$deletedplugins = [];
654
        foreach (self::$deletedplugintypes as $type => $fulldir) {
655
            self::$deletedplugins[$type] = self::fetch_plugins($type, $fulldir);
656
        }
657
 
1 efrain 658
        self::$apis = self::fetch_apis();
659
 
660
        self::fill_classmap_cache();
661
        self::fill_classmap_renames_cache();
662
        self::fill_filemap_cache();
663
        self::fetch_core_version();
664
    }
665
 
666
    /**
667
     * Get the core version.
668
     *
669
     * In order for this to work properly, opcache should be reset beforehand.
670
     *
671
     * @return float core version.
672
     */
673
    protected static function fetch_core_version() {
674
        global $CFG;
675
        if (self::$version === null) {
676
            $version = null; // Prevent IDE complaints.
677
            require($CFG->dirroot . '/version.php');
678
            self::$version = $version;
679
        }
680
        return self::$version;
681
    }
682
 
683
    /**
684
     * Returns list of core subsystems.
685
     * @return array
686
     */
687
    protected static function fetch_subsystems() {
688
        global $CFG;
689
 
690
        // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
691
        $info = [];
692
        foreach (self::fetch_component_source('subsystems') as $subsystem => $path) {
693
            // Replace admin/ directory with the config setting.
694
            if ($CFG->admin !== 'admin') {
695
                if ($path === 'admin') {
696
                    $path = $CFG->admin;
697
                }
698
                if (strpos($path, 'admin/') === 0) {
699
                    $path = $CFG->admin . substr($path, 5);
700
                }
701
            }
702
 
703
            $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
704
        }
705
 
706
        return $info;
707
    }
708
 
709
    /**
710
     * Returns list of core APIs.
711
     * @return stdClass[]
712
     */
713
    protected static function fetch_apis() {
714
        return (array) json_decode(file_get_contents(__DIR__ . '/../apis.json'));
715
    }
716
 
717
    /**
718
     * Returns list of known plugin types.
719
     * @return array
720
     */
721
    protected static function fetch_plugintypes() {
722
        global $CFG;
723
 
1441 ariadna 724
        // Top level plugin types.
725
        $plugintypesmap = [
726
            'plugintypes' => [],
727
            'deprecatedplugintypes' => [],
728
            'deletedplugintypes' => [],
729
        ];
730
 
731
        $subplugintypesmap = [
732
            'plugintypes' => [],
733
            'deprecatedplugintypes' => [],
734
            'deletedplugintypes' => [],
735
        ];
736
 
737
        $parents = [];
738
 
739
        foreach ($plugintypesmap as $sourcekey => $typesarr) {
740
            /** @var string $plugintype */
741
            foreach (self::fetch_component_source($sourcekey) as $plugintype => $path) {
742
                // Replace admin/ with the config setting.
743
                if ($CFG->admin !== 'admin' && strpos($path, 'admin/') === 0) {
744
                    $path = $CFG->admin . substr($path, 5);
745
                }
746
                $plugintypesmap[$sourcekey][$plugintype] = "{$CFG->dirroot}/{$path}";
1 efrain 747
            }
748
        }
749
 
1441 ariadna 750
        // Prevent deprecation of plugin types supporting subplugins (those in self::$supportsubplugins).
751
        foreach (['deprecatedplugintypes', 'deletedplugintypes'] as $key) {
752
            $illegaltypes = array_intersect(self::$supportsubplugins, array_keys($plugintypesmap[$key]));
753
            if (!empty($illegaltypes)) {
754
                debugging("Deprecation of a plugin type which supports subplugins is not supported. These plugin types will ".
755
                    "continue to be treated as active.", DEBUG_DEVELOPER);
756
                foreach ($illegaltypes as $plugintype) {
757
                    $plugintypesmap['plugintypes'][$plugintype] = $plugintypesmap[$key][$plugintype];
758
                    unset($plugintypesmap[$key][$plugintype]);
759
                }
760
            }
761
        }
1 efrain 762
 
763
        if (!empty($CFG->themedir) && is_dir($CFG->themedir)) {
1441 ariadna 764
            $plugintypesmap['plugintypes']['theme'] = $CFG->themedir;
1 efrain 765
        } else {
1441 ariadna 766
            $plugintypesmap['plugintypes']['theme'] = $CFG->dirroot . '/theme';
1 efrain 767
        }
768
 
769
        foreach (self::$supportsubplugins as $type) {
770
            if ($type === 'local') {
771
                // Local subplugins must be after local plugins.
772
                continue;
773
            }
1441 ariadna 774
 
775
            $plugins = self::fetch_plugins($type, $plugintypesmap['plugintypes'][$type]);
1 efrain 776
            foreach ($plugins as $plugin => $fulldir) {
1441 ariadna 777
                $allsubtypes = self::fetch_subtypes($fulldir);
778
                $subplugintypesdata = [
779
                    'plugintypes' => $allsubtypes['plugintypes'] ?? [],
780
                    'deprecatedplugintypes' => $allsubtypes['deprecatedplugintypes'] ?? [],
781
                    'deletedplugintypes' => $allsubtypes['deletedplugintypes'] ?? [],
782
                ];
783
 
784
                if (!$subplugintypesdata['plugintypes'] && !$subplugintypesdata['deprecatedplugintypes']
785
                        && !$subplugintypesdata['deletedplugintypes']) {
1 efrain 786
                    continue;
787
                }
1441 ariadna 788
                $subplugintypesmap['plugintypes'][$type . '_' . $plugin] = [];
789
                $subplugintypesmap['deprecatedplugintypes'][$type . '_' . $plugin] = [];
790
                $subplugintypesmap['deletedplugintypes'][$type . '_' . $plugin] = [];
791
 
792
                foreach ($subplugintypesdata as $key => $subplugintypes) {
793
                    foreach ($subplugintypes as $subtype => $subdir) {
794
                        if (isset($plugintypesmap['plugintypes'][$subtype])
795
                                || isset($plugintypesmap['deprecatedplugintypes'][$subtype])
796
                                || isset($plugintypesmap['deletedplugintypes'][$subtype])) {
797
                            error_log("Invalid subtype '$subtype', duplicate detected.");
798
                            continue;
799
                        }
800
                        $plugintypesmap[$key][$subtype] = $subdir;
801
                        $parents[$subtype] = $type . '_' . $plugin;
802
                        $subplugintypesmap[$key][$type . '_' . $plugin][$subtype] = array_keys(
803
                            self::fetch_plugins($subtype, $subdir)
804
                        );
1 efrain 805
                    }
806
                }
807
            }
808
        }
809
        // Local is always last!
1441 ariadna 810
        $plugintypesmap['plugintypes']['local'] = $CFG->dirroot . '/local';
1 efrain 811
 
812
        if (in_array('local', self::$supportsubplugins)) {
813
            $type = 'local';
1441 ariadna 814
            $plugins = self::fetch_plugins($type, $plugintypesmap['plugintypes'][$type]);
1 efrain 815
            foreach ($plugins as $plugin => $fulldir) {
1441 ariadna 816
                $allsubtypes = self::fetch_subtypes($fulldir);
817
                $subplugintypesdata = [
818
                    'plugintypes' => $allsubtypes['plugintypes'] ?? [],
819
                    'deprecatedplugintypes' => $allsubtypes['deprecatedplugintypes'] ?? [],
820
                    'deletedplugintypes' => $allsubtypes['deletedplugintypes'] ?? [],
821
                ];
822
                if (!$subplugintypesdata['plugintypes'] && !$subplugintypesdata['deprecatedplugintypes']
823
                        && !$subplugintypesdata['deletedplugintypes']) {
1 efrain 824
                    continue;
825
                }
1441 ariadna 826
                $subplugintypesmap['plugintypes'][$type . '_' . $plugin] = [];
827
                $subplugintypesmap['deprecatedplugintypes'][$type . '_' . $plugin] = [];
828
                $subplugintypesmap['deletedplugintypes'][$type . '_' . $plugin] = [];
829
 
830
                foreach ($subplugintypesdata as $key => $subplugintypes) {
831
                    foreach ($subplugintypes as $subtype => $subdir) {
832
                        if (isset($plugintypesmap['plugintypes'][$subtype])
833
                                || isset($plugintypesmap['deprecatedplugintypes'][$subtype])
834
                                || isset($plugintypesmap['deletedplugintypes'][$subtype])) {
835
                            error_log("Invalid subtype '$subtype', duplicate detected.");
836
                            continue;
837
                        }
838
                        $plugintypesmap[$key][$subtype] = $subdir;
839
                        $parents[$subtype] = $type . '_' . $plugin;
840
                        $subplugintypesmap[$key][$type . '_' . $plugin][$subtype] = array_keys(
841
                            self::fetch_plugins($subtype, $subdir)
842
                        );
1 efrain 843
                    }
844
                }
845
            }
846
        }
847
 
1441 ariadna 848
        return [
849
            'plugintypes' => $plugintypesmap['plugintypes'],
850
            'parents' => $parents,
851
            'subplugins' => $subplugintypesmap['plugintypes'],
852
            'deprecatedplugintypes' => $plugintypesmap['deprecatedplugintypes'],
853
            'deletedplugintypes' => $plugintypesmap['deletedplugintypes'],
854
            'deprecatedsubplugins' => $subplugintypesmap['deprecatedplugintypes'],
855
            'deletedsubplugin' => $subplugintypesmap['deletedplugintypes'],
856
        ];
1 efrain 857
    }
858
 
859
    /**
860
     * Returns the component source content as loaded from /lib/components.json.
861
     *
862
     * @return array
863
     */
864
    protected static function fetch_component_source(string $key) {
865
        if (null === self::$componentsource) {
866
            self::$componentsource = (array) json_decode(file_get_contents(__DIR__ . '/../components.json'));
867
        }
868
 
1441 ariadna 869
        return !empty(self::$componentsource[$key]) ? (array) self::$componentsource[$key] : [];
1 efrain 870
    }
871
 
872
    /**
873
     * Returns list of subtypes.
874
     * @param string $ownerdir
875
     * @return array
876
     */
877
    protected static function fetch_subtypes($ownerdir) {
878
        global $CFG;
879
 
880
        $types = [];
881
        $subplugins = [];
1441 ariadna 882
        if (str_contains($ownerdir, $CFG->dirroot)) {
883
            $plugindir = substr($ownerdir, strlen($CFG->dirroot) + 1);
884
        } else {
885
            $realownerdir = realpath($ownerdir);
886
            $realroot = realpath(dirname(__DIR__, 2));
887
            $plugindir = substr($realownerdir, strlen($realroot) + 1);
888
        }
889
 
890
        $subtypesregister = [
891
            'plugintypes' => [],
892
            'deprecatedplugintypes' => [],
893
            'deletedplugintypes' => [],
894
        ];
1 efrain 895
        if (file_exists("$ownerdir/db/subplugins.json")) {
1441 ariadna 896
            $subpluginpathformatter = fn (string $value): string => "{$plugindir}/{$value}";
1 efrain 897
            $subpluginsjson = json_decode(file_get_contents("$ownerdir/db/subplugins.json"));
898
            if (json_last_error() === JSON_ERROR_NONE) {
1441 ariadna 899
                $subplugins = [];
900
                if (!empty($subpluginsjson->subplugintypes)) {
901
                    // If the newer subplugintypes is defined, use it.
902
                    // The value here is relative to the plugin's owner directory.
903
                    $subplugins = array_map($subpluginpathformatter, (array) $subpluginsjson->subplugintypes);
904
                } else if (!empty($subpluginsjson->plugintypes)) {
905
                    error_log(
906
                        "No subplugintypes defined in $ownerdir/db/subplugins.json. " .
907
                        "Falling back to deprecated plugintypes value. " .
908
                        "See MDL-83705 for further information.",
909
                    );
1 efrain 910
                    $subplugins = (array) $subpluginsjson->plugintypes;
1441 ariadna 911
                } else if (empty($subpluginjson->deprecatedplugintypes) && empty($subpluginsjson->deletedplugintypes)) {
1 efrain 912
                    error_log("No plugintypes defined in $ownerdir/db/subplugins.json");
913
                }
1441 ariadna 914
 
915
                $subtypesregister['plugintypes'] = $subplugins;
916
 
917
                // The deprecated and deleted subplugintypes are optional and are always relative to the plugin's root directory.
918
                $subtypesregister['deprecatedplugintypes'] = array_map(
919
                    $subpluginpathformatter,
920
                    (array) ($subpluginsjson->deprecatedsubplugintypes ?? []),
921
                );
922
                $subtypesregister['deletedplugintypes'] = array_map(
923
                    $subpluginpathformatter,
924
                    (array) ($subpluginsjson->deletedsubplugintypes ?? []),
925
                );
1 efrain 926
            } else {
927
                $jsonerror = json_last_error_msg();
928
                error_log("$ownerdir/db/subplugins.json is invalid ($jsonerror)");
929
            }
1441 ariadna 930
 
931
            if (function_exists('debugging') && debugging()) {
932
                if (property_exists($subpluginsjson, 'subplugintypes') && property_exists($subpluginsjson, 'plugintypes')) {
933
                    $subplugintypes = (array) $subpluginsjson->subplugintypes;
934
                    $plugintypes = (array) $subpluginsjson->plugintypes;
935
                    if (count($subplugintypes) !== count(($plugintypes))) {
936
                        error_log("Subplugintypes and plugintypes are not in sync in $ownerdir/db/subplugins.json");
937
                    }
938
                    foreach ($subplugintypes as $type => $path) {
939
                        if (!isset($plugintypes[$type])) {
940
                            error_log("Subplugintypes and plugintypes are not in sync for '$type' in $ownerdir/db/subplugins.json");
941
 
942
                            continue;
943
                        }
944
 
945
                        if ($plugintypes[$type] !== $subplugins[$type]) {
946
                            error_log("Subplugintypes and plugintypes are not in sync for '$type' in $ownerdir/db/subplugins.json");
947
                        }
948
                    }
949
                }
950
            }
1 efrain 951
        } else if (file_exists("$ownerdir/db/subplugins.php")) {
1441 ariadna 952
            throw new coding_exception(
953
                'Use of subplugins.php has been deprecated and is no longer supported. ' .
954
                "Please update your '$ownerdir' plugin to provide a subplugins.json file instead.",
955
            );
1 efrain 956
        }
957
 
1441 ariadna 958
        foreach ($subtypesregister as $key => $subtypes) {
959
            foreach ($subtypes as $subtype => $dir) {
960
                if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
961
                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
962
                    continue;
963
                }
964
                if (isset(self::$subsystems[$subtype])) {
965
                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
966
                    continue;
967
                }
968
                if ($CFG->admin !== 'admin' && strpos($dir, 'admin/') === 0) {
969
                    $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
970
                }
971
                if (!is_dir("$CFG->dirroot/$dir")) {
972
                    error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
973
                    continue;
974
                }
975
                $types[$key][$subtype] = "$CFG->dirroot/$dir";
1 efrain 976
            }
977
        }
978
 
979
        return $types;
980
    }
981
 
982
    /**
983
     * Returns list of plugins of given type in given directory.
984
     * @param string $plugintype
985
     * @param string $fulldir
986
     * @return array
987
     */
988
    protected static function fetch_plugins($plugintype, $fulldir) {
989
        global $CFG;
990
 
991
        $fulldirs = (array)$fulldir;
992
        if ($plugintype === 'theme') {
993
            if (realpath($fulldir) !== realpath($CFG->dirroot . '/theme')) {
994
                // Include themes in standard location too.
995
                array_unshift($fulldirs, $CFG->dirroot . '/theme');
996
            }
997
        }
998
 
999
        $result = [];
1000
 
1001
        foreach ($fulldirs as $fulldir) {
1002
            if (!is_dir($fulldir)) {
1003
                continue;
1004
            }
1441 ariadna 1005
            $items = new DirectoryIterator($fulldir);
1 efrain 1006
            foreach ($items as $item) {
1007
                if ($item->isDot() || !$item->isDir()) {
1008
                    continue;
1009
                }
1010
                $pluginname = $item->getFilename();
1011
                if ($plugintype === 'auth' && $pluginname === 'db') {
1012
                    // Special exception for this wrong plugin name.
1013
                } else if (isset(self::$ignoreddirs[$pluginname])) {
1014
                    continue;
1015
                }
1016
                if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
1017
                    // Always ignore plugins with problematic names here.
1018
                    continue;
1019
                }
1020
                $result[$pluginname] = $fulldir . '/' . $pluginname;
1021
                unset($item);
1022
            }
1023
            unset($items);
1024
        }
1025
 
1026
        ksort($result);
1027
        return $result;
1028
    }
1029
 
1030
    /**
1031
     * Find all classes that can be autoloaded including frankenstyle namespaces.
1032
     */
1033
    protected static function fill_classmap_cache() {
1034
        global $CFG;
1035
 
1036
        self::$classmap = [];
1037
 
1038
        self::load_classes('core', "$CFG->dirroot/lib/classes");
1441 ariadna 1039
        self::load_legacy_classes($CFG->libdir, true);
1 efrain 1040
 
1041
        foreach (self::$subsystems as $subsystem => $fulldir) {
1042
            if (!$fulldir) {
1043
                continue;
1044
            }
1045
            self::load_classes('core_' . $subsystem, "$fulldir/classes");
1046
        }
1047
 
1048
        foreach (self::$plugins as $plugintype => $plugins) {
1049
            foreach ($plugins as $pluginname => $fulldir) {
1050
                self::load_classes($plugintype . '_' . $pluginname, "$fulldir/classes");
1441 ariadna 1051
                self::load_legacy_classes($fulldir);
1 efrain 1052
            }
1053
        }
1441 ariadna 1054
 
1055
        // Include deprecated plugins in the classmap, to facilitate migration code which uses existing plugin classes.
1056
        foreach (self::$deprecatedplugins as $plugintype => $plugins) {
1057
            foreach ($plugins as $pluginname => $fulldir) {
1058
                self::load_classes($plugintype . '_' . $pluginname, "$fulldir/classes");
1059
            }
1060
        }
1061
 
1 efrain 1062
        ksort(self::$classmap);
1063
    }
1064
 
1065
    /**
1066
     * Fills up the cache defining what plugins have certain files.
1067
     *
1068
     * @see self::get_plugin_list_with_file
1069
     * @return void
1070
     */
1071
    protected static function fill_filemap_cache() {
1072
        global $CFG;
1073
 
1074
        self::$filemap = [];
1075
 
1076
        foreach (self::$filestomap as $file) {
1077
            if (!isset(self::$filemap[$file])) {
1078
                self::$filemap[$file] = [];
1079
            }
1080
            foreach (self::$plugins as $plugintype => $plugins) {
1081
                if (!isset(self::$filemap[$file][$plugintype])) {
1082
                    self::$filemap[$file][$plugintype] = [];
1083
                }
1084
                foreach ($plugins as $pluginname => $fulldir) {
1085
                    if (file_exists("$fulldir/$file")) {
1086
                        self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
1087
                    }
1088
                }
1089
            }
1090
        }
1091
    }
1092
 
1093
    /**
1094
     * Find classes in directory and recurse to subdirs.
1095
     * @param string $component
1096
     * @param string $fulldir
1097
     * @param string $namespace
1098
     */
1099
    protected static function load_classes($component, $fulldir, $namespace = '') {
1100
        if (!is_dir($fulldir)) {
1101
            return;
1102
        }
1103
 
1104
        if (!is_readable($fulldir)) {
1105
            // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
1106
            // because its pretty likely to lead to a missing class error further down the line.
1107
            // But our early setup code can't handle errors this early at the moment.
1108
            return;
1109
        }
1110
 
1441 ariadna 1111
        $items = new DirectoryIterator($fulldir);
1 efrain 1112
        foreach ($items as $item) {
1113
            if ($item->isDot()) {
1114
                continue;
1115
            }
1116
            if ($item->isDir()) {
1117
                $dirname = $item->getFilename();
1118
                self::load_classes($component, "$fulldir/$dirname", $namespace . '\\' . $dirname);
1119
                continue;
1120
            }
1121
 
1122
            $filename = $item->getFilename();
1123
            $classname = preg_replace('/\.php$/', '', $filename);
1124
 
1125
            if ($filename === $classname) {
1126
                // Not a php file.
1127
                continue;
1128
            }
1129
            if ($namespace === '') {
1130
                // Legacy long frankenstyle class name.
1131
                self::$classmap[$component . '_' . $classname] = "$fulldir/$filename";
1132
            }
1133
            // New namespaced classes.
1134
            self::$classmap[$component . $namespace . '\\' . $classname] = "$fulldir/$filename";
1135
        }
1136
        unset($item);
1137
        unset($items);
1138
    }
1139
 
1140
 
1141
    /**
1142
     * List all core subsystems and their location
1143
     *
1144
     * This is a list of components that are part of the core and their
1145
     * language strings are defined in /lang/en/<<subsystem>>.php. If a given
1146
     * plugin is not listed here and it does not have proper plugintype prefix,
1147
     * then it is considered as course activity module.
1148
     *
1149
     * The location is absolute file path to dir. NULL means there is no special
1150
     * directory for this subsystem. If the location is set, the subsystem's
1151
     * renderer.php is expected to be there.
1152
     *
1153
     * @return array of (string)name => (string|null)full dir location
1154
     */
1155
    public static function get_core_subsystems() {
1156
        self::init();
1157
        return self::$subsystems;
1158
    }
1159
 
1160
    /**
1161
     * List all core APIs and their attributes.
1162
     *
1163
     * This is a list of all the existing / allowed APIs in moodle, each one with the
1164
     * following attributes:
1165
     *   - component: the component, usually a subsystem or core, the API belongs to.
1166
     *   - allowedlevel2: if the API is allowed as level2 namespace or no.
1167
     *   - allowedspread: if the API can spread out from its component or no.
1168
     *
1169
     * @return stdClass[] array of APIs (as keys) with their attributes as object instances.
1170
     */
1171
    public static function get_core_apis() {
1172
        self::init();
1173
        return self::$apis;
1174
    }
1175
 
1176
    /**
1177
     * Get list of available plugin types together with their location.
1178
     *
1179
     * @return array as (string)plugintype => (string)fulldir
1180
     */
1181
    public static function get_plugin_types() {
1182
        self::init();
1183
        return self::$plugintypes;
1184
    }
1185
 
1186
    /**
1441 ariadna 1187
     * Get a list of deprecated plugin types and their locations.
1188
     *
1189
     * @return array as (string)plugintype => (string)fulldir
1190
     */
1191
    public static function get_deprecated_plugin_types(): array {
1192
        self::init();
1193
        return self::$deprecatedplugintypes;
1194
    }
1195
 
1196
    /**
1197
     * Get a list of all deleted plugin types and their locations.
1198
     *
1199
     * @return array as (string)plugintype => (string)fulldir
1200
     */
1201
    public static function get_deleted_plugin_types(): array {
1202
        self::init();
1203
        return self::$deletedplugintypes;
1204
    }
1205
 
1206
    /**
1207
     * Gets list of all plugin types, comprising available plugin types as well as any plugin types currently in deprecation.
1208
     *
1209
     * @return array as (string)plugintype => (string)fulldir
1210
     */
1211
    public static function get_all_plugin_types(): array {
1212
        self::init();
1213
        return array_merge(self::$plugintypes, self::$deprecatedplugintypes, self::$deletedplugintypes);
1214
    }
1215
 
1216
    /**
1217
     * Is the plugintype deprecated.
1218
     *
1219
     * @param string $plugintype
1220
     * @return bool true if deprecated, false otherwise.
1221
     */
1222
    public static function is_deprecated_plugin_type(string $plugintype): bool {
1223
        self::init();
1224
        return array_key_exists($plugintype, self::$deprecatedplugintypes);
1225
    }
1226
 
1227
    /**
1228
     * Is the plugintype deleted.
1229
     *
1230
     * @param string $plugintype
1231
     * @return bool true if deleted, false otherwise.
1232
     */
1233
    public static function is_deleted_plugin_type(string $plugintype): bool {
1234
        self::init();
1235
        return array_key_exists($plugintype, self::$deletedplugintypes);
1236
    }
1237
 
1238
    /**
1239
     * Is the plugintype in deprecation.
1240
     *
1241
     * @param string $plugintype
1242
     * @return bool true if in either phase 1 (deprecated) or phase 2 (deleted) or deprecation.
1243
     */
1244
    public static function is_plugintype_in_deprecation(string $plugintype): bool {
1245
        self::init();
1246
        return array_key_exists($plugintype, array_merge(self::$deprecatedplugintypes, self::$deletedplugintypes));
1247
    }
1248
 
1249
    /**
1 efrain 1250
     * Get list of plugins of given type.
1251
     *
1252
     * @param string $plugintype
1253
     * @return array as (string)pluginname => (string)fulldir
1254
     */
1255
    public static function get_plugin_list($plugintype) {
1256
        self::init();
1257
 
1258
        if (!isset(self::$plugins[$plugintype])) {
1259
            return [];
1260
        }
1261
        return self::$plugins[$plugintype];
1262
    }
1263
 
1264
    /**
1441 ariadna 1265
     * Get list of deprecated plugins of a given type.
1266
     *
1267
     * @param string $plugintype
1268
     * @return array as (string)pluginname => (string)fulldir
1269
     */
1270
    public static function get_deprecated_plugin_list($plugintype): array {
1271
        self::init();
1272
        return self::$deprecatedplugins[$plugintype] ?? [];
1273
    }
1274
 
1275
    /**
1276
     * Get list of deleted plugins of a given type.
1277
     *
1278
     * @param string $plugintype
1279
     * @return array as (string)pluginname => (string)fulldir
1280
     */
1281
    public static function get_deleted_plugin_list($plugintype): array {
1282
        self::init();
1283
        return self::$deletedplugins[$plugintype] ?? [];
1284
    }
1285
 
1286
    /**
1287
     * Get list of all plugins of a given type, comprising all available plugins as well as any plugins in deprecation.
1288
     *
1289
     * @param string $plugintype
1290
     * @return array as (string)pluginname => (string)fulldir
1291
     */
1292
    public static function get_all_plugins_list(string $plugintype): array {
1293
        self::init();
1294
        return array_merge(
1295
            self::$plugins[$plugintype] ?? [],
1296
            self::$deprecatedplugins[$plugintype] ?? [],
1297
            self::$deletedplugins[$plugintype] ?? []
1298
        );
1299
    }
1300
 
1301
    /**
1 efrain 1302
     * Get a list of all the plugins of a given type that define a certain class
1303
     * in a certain file. The plugin component names and class names are returned.
1304
     *
1305
     * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
1306
     * @param string $class the part of the name of the class after the
1307
     *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
1308
     *      names like report_courselist_thing. If you are looking for classes with
1309
     *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
1310
     *      Frankenstyle namespaces are also supported.
1311
     * @param string $file the name of file within the plugin that defines the class.
1312
     * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
1313
     *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
1314
     */
1315
    public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
1316
        global $CFG; // Necessary in case it is referenced by included PHP scripts.
1317
 
1318
        if ($class) {
1319
            $suffix = '_' . $class;
1320
        } else {
1321
            $suffix = '';
1322
        }
1323
 
1324
        $pluginclasses = [];
1325
        $plugins = self::get_plugin_list($plugintype);
1326
        foreach ($plugins as $plugin => $fulldir) {
1327
            // Try class in frankenstyle namespace.
1328
            if ($class) {
1329
                $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
1330
                if (class_exists($classname, true)) {
1331
                    $pluginclasses[$plugintype . '_' . $plugin] = $classname;
1332
                    continue;
1333
                }
1334
            }
1335
 
1336
            // Try autoloading of class with frankenstyle prefix.
1337
            $classname = $plugintype . '_' . $plugin . $suffix;
1338
            if (class_exists($classname, true)) {
1339
                $pluginclasses[$plugintype . '_' . $plugin] = $classname;
1340
                continue;
1341
            }
1342
 
1343
            // Fall back to old file location and class name.
1344
            if ($file && file_exists("$fulldir/$file")) {
1345
                include_once("$fulldir/$file");
1346
                if (class_exists($classname, false)) {
1347
                    $pluginclasses[$plugintype . '_' . $plugin] = $classname;
1348
                    continue;
1349
                }
1350
            }
1351
        }
1352
 
1353
        return $pluginclasses;
1354
    }
1355
 
1356
    /**
1357
     * Get a list of all the plugins of a given type that contain a particular file.
1358
     *
1359
     * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
1360
     * @param string $file the name of file that must be present in the plugin.
1361
     *                     (e.g. 'view.php', 'db/install.xml').
1362
     * @param bool $include if true (default false), the file will be include_once-ed if found.
1363
     * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
1364
     *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
1365
     */
1366
    public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
1367
        global $CFG; // Necessary in case it is referenced by included PHP scripts.
1368
        $pluginfiles = [];
1369
 
1370
        if (isset(self::$filemap[$file])) {
1371
            // If the file was supposed to be mapped, then it should have been set in the array.
1372
            if (isset(self::$filemap[$file][$plugintype])) {
1373
                $pluginfiles = self::$filemap[$file][$plugintype];
1374
            }
1375
        } else {
1376
            // Old-style search for non-cached files.
1377
            $plugins = self::get_plugin_list($plugintype);
1378
            foreach ($plugins as $plugin => $fulldir) {
1379
                $path = $fulldir . '/' . $file;
1380
                if (file_exists($path)) {
1381
                    $pluginfiles[$plugin] = $path;
1382
                }
1383
            }
1384
        }
1385
 
1386
        if ($include) {
1387
            foreach ($pluginfiles as $path) {
1388
                include_once($path);
1389
            }
1390
        }
1391
 
1392
        return $pluginfiles;
1393
    }
1394
 
1395
    /**
1396
     * Returns all classes in a component matching the provided namespace.
1397
     *
1398
     * It checks that the class exists.
1399
     *
1400
     * e.g. get_component_classes_in_namespace('mod_forum', 'event')
1401
     *
1402
     * @param string|null $component A valid moodle component (frankenstyle) or null if searching all components
1403
     * @param string $namespace Namespace from the component name or empty string if all $component classes.
1404
     * @return array The full class name as key and the class path as value, empty array if $component is `null`
1405
     * and $namespace is empty.
1406
     */
1407
    public static function get_component_classes_in_namespace($component = null, $namespace = '') {
1408
 
1409
        $classes = [];
1410
 
1411
        // Only look for components if a component name is set or a namespace is set.
1412
        if (isset($component) || !empty($namespace)) {
1413
            // If a component parameter value is set we only want to look in that component.
1414
            // Otherwise we want to check all components.
1415
            $component = (isset($component)) ? self::normalize_componentname($component) : '\w+';
1416
            if ($namespace) {
1417
                // We will add them later.
1418
                $namespace = trim($namespace, '\\');
1419
 
1420
                // We need add double backslashes as it is how classes are stored into self::$classmap.
1421
                $namespace = implode('\\\\', explode('\\', $namespace));
1422
                $namespace = $namespace . '\\\\';
1423
            }
1424
            $regex = '|^' . $component . '\\\\' . $namespace . '|';
1425
            $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
1426
 
1427
            // We want to be sure that they exist.
1428
            foreach ($it as $classname => $classpath) {
1429
                if (class_exists($classname)) {
1430
                    $classes[$classname] = $classpath;
1431
                }
1432
            }
1433
        }
1434
 
1435
        return $classes;
1436
    }
1437
 
1438
    /**
1439
     * Returns the exact absolute path to plugin directory.
1440
     *
1441
     * @param string $plugintype type of plugin
1442
     * @param string $pluginname name of the plugin
1443
     * @return string full path to plugin directory; null if not found
1444
     */
1445
    public static function get_plugin_directory($plugintype, $pluginname) {
1446
        if (empty($pluginname)) {
1447
            // Invalid plugin name, sorry.
1448
            return null;
1449
        }
1450
 
1451
        self::init();
1452
 
1441 ariadna 1453
        if (!isset(self::$plugins[$plugintype][$pluginname]) && !isset(self::$deprecatedplugins[$plugintype][$pluginname])) {
1 efrain 1454
            return null;
1455
        }
1441 ariadna 1456
        return self::$plugins[$plugintype][$pluginname] ?? self::$deprecatedplugins[$plugintype][$pluginname];
1 efrain 1457
    }
1458
 
1459
    /**
1460
     * Returns the exact absolute path to plugin directory.
1461
     *
1462
     * @param string $subsystem type of core subsystem
1463
     * @return string full path to subsystem directory; null if not found
1464
     */
1465
    public static function get_subsystem_directory($subsystem) {
1466
        self::init();
1467
 
1468
        if (!isset(self::$subsystems[$subsystem])) {
1469
            return null;
1470
        }
1471
        return self::$subsystems[$subsystem];
1472
    }
1473
 
1474
    /**
1475
     * This method validates a plug name. It is much faster than calling clean_param.
1476
     *
1477
     * @param string $plugintype type of plugin
1478
     * @param string $pluginname a string that might be a plugin name.
1479
     * @return bool if this string is a valid plugin name.
1480
     */
1481
    public static function is_valid_plugin_name($plugintype, $pluginname) {
1482
        if ($plugintype === 'mod') {
1483
            // Modules must not have the same name as core subsystems.
1484
            if (!isset(self::$subsystems)) {
1485
                // Watch out, this is called from init!
1486
                self::init();
1487
            }
1488
            if (isset(self::$subsystems[$pluginname])) {
1489
                return false;
1490
            }
1491
            // Modules MUST NOT have any underscores,
1492
            // component normalisation would break very badly otherwise!
1493
            return !is_null($pluginname) && (bool) preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
1494
        } else {
1495
            return !is_null($pluginname) && (bool) preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
1496
        }
1497
    }
1498
 
1499
    /**
1500
     * Normalize the component name.
1501
     *
1502
     * Note: this does not verify the validity of the plugin or component.
1503
     *
1504
     * @param string $component
1505
     * @return string
1506
     */
1507
    public static function normalize_componentname($componentname) {
1508
        [$plugintype, $pluginname] = self::normalize_component($componentname);
1509
        if ($plugintype === 'core' && is_null($pluginname)) {
1510
            return $plugintype;
1511
        }
1512
        return $plugintype . '_' . $pluginname;
1513
    }
1514
 
1515
    /**
1516
     * Normalize the component name using the "frankenstyle" rules.
1517
     *
1518
     * Note: this does not verify the validity of plugin or type names.
1519
     *
1520
     * @param string $component
1521
     * @return array two-items list of [(string)type, (string|null)name]
1522
     */
1523
    public static function normalize_component($component) {
1524
        if ($component === 'moodle' || $component === 'core' || $component === '') {
1525
            return ['core', null];
1526
        }
1527
 
1528
        if (strpos($component, '_') === false) {
1529
            self::init();
1530
            if (array_key_exists($component, self::$subsystems)) {
1531
                $type   = 'core';
1532
                $plugin = $component;
1533
            } else {
1534
                // Everything else without underscore is a module.
1535
                $type   = 'mod';
1536
                $plugin = $component;
1537
            }
1538
        } else {
1539
            [$type, $plugin] = explode('_', $component, 2);
1540
            if ($type === 'moodle') {
1541
                $type = 'core';
1542
            }
1543
            // Any unknown type must be a subplugin.
1544
        }
1545
 
1546
        return [$type, $plugin];
1547
    }
1548
 
1549
    /**
1550
     * Fetch the component name from a Moodle PSR-like namespace.
1551
     *
1552
     * Note: Classnames in the flat underscore_class_name_format are not supported.
1553
     *
1554
     * @param string $classname
1555
     * @return null|string The component name, or null if a matching component was not found
1556
     */
1557
    public static function get_component_from_classname(string $classname): ?string {
1441 ariadna 1558
        $components = static::get_component_names(true, true);
1 efrain 1559
 
1560
        $classname = ltrim($classname, '\\');
1561
 
1562
        // Prefer PSR-4 classnames.
1563
        $parts = explode('\\', $classname);
1564
        if ($parts) {
1565
            $component = array_shift($parts);
1566
            if (array_search($component, $components) !== false) {
1567
                return $component;
1568
            }
1569
        }
1570
 
1571
        // Note: Frankenstyle classnames are not supported as they lead to false positives, for example:
1572
        // \core_typo\example => \core instead of \core_typo because it does not exist
1573
        // Please *do not* add support for Frankenstyle classnames. They will break other things.
1574
 
1575
        return null;
1576
    }
1577
 
1578
    /**
1579
     * Return exact absolute path to a plugin directory.
1580
     *
1581
     * @param string $component name such as 'moodle', 'mod_forum'
1582
     * @return string full path to component directory; NULL if not found
1583
     */
1584
    public static function get_component_directory($component) {
1585
        global $CFG;
1586
 
1587
        [$type, $plugin] = self::normalize_component($component);
1588
 
1589
        if ($type === 'core') {
1590
            if ($plugin === null) {
1591
                return $path = $CFG->libdir;
1592
            }
1593
            return self::get_subsystem_directory($plugin);
1594
        }
1595
 
1596
        return self::get_plugin_directory($type, $plugin);
1597
    }
1598
 
1599
    /**
1600
     * Returns list of plugin types that allow subplugins.
1601
     * @return array as (string)plugintype => (string)fulldir
1602
     */
1603
    public static function get_plugin_types_with_subplugins() {
1604
        self::init();
1605
 
1606
        $return = [];
1607
        foreach (self::$supportsubplugins as $type) {
1608
            $return[$type] = self::$plugintypes[$type];
1609
        }
1610
        return $return;
1611
    }
1612
 
1613
    /**
1614
     * Returns parent of this subplugin type.
1615
     *
1441 ariadna 1616
     * No filtering is done on deprecated/deleted subtypes. Calling code should check this if needed.
1617
     *
1 efrain 1618
     * @param string $type
1619
     * @return string parent component or null
1620
     */
1621
    public static function get_subtype_parent($type) {
1622
        self::init();
1623
 
1624
        if (isset(self::$parents[$type])) {
1625
            return self::$parents[$type];
1626
        }
1627
 
1628
        return null;
1629
    }
1630
 
1631
    /**
1441 ariadna 1632
     * Return all available subplugins of this component.
1 efrain 1633
     * @param string $component.
1634
     * @return array $subtype=>array($component, ..), null if no subtypes defined
1635
     */
1636
    public static function get_subplugins($component) {
1637
        self::init();
1638
 
1639
        if (isset(self::$subplugins[$component])) {
1640
            return self::$subplugins[$component];
1641
        }
1642
 
1643
        return null;
1644
    }
1645
 
1646
    /**
1441 ariadna 1647
     * Return all subplugins for the component, comprising all available subplugins plus any in deprecation.
1648
     *
1649
     * @param string $component
1650
     * @return array|null $subtype=>array($component, ..), null if no subtypes defined
1651
     */
1652
    public static function get_all_subplugins($component): ?array {
1653
        self::init();
1654
        $subplugins = array_merge(
1655
            self::$subplugins[$component] ?? [],
1656
            self::$deprecatedsubplugins[$component] ?? [],
1657
            self::$deletedsubplugins[$component] ?? [],
1658
        );
1659
        return $subplugins ?: null;
1660
    }
1661
 
1662
    /**
1 efrain 1663
     * Returns hash of all versions including core and all plugins.
1664
     *
1665
     * This is relatively slow and not fully cached, use with care!
1666
     *
1667
     * @return string sha1 hash
1668
     */
1669
    public static function get_all_versions_hash() {
1670
        return sha1(serialize(self::get_all_versions()));
1671
    }
1672
 
1673
    /**
1674
     * Returns hash of all versions including core and all plugins.
1675
     *
1676
     * This is relatively slow and not fully cached, use with care!
1677
     *
1678
     * @return array as (string)plugintype_pluginname => (int)version
1679
     */
1680
    public static function get_all_versions(): array {
1681
        global $CFG;
1682
 
1683
        self::init();
1684
 
1685
        $versions = [];
1686
 
1687
        // Main version first.
1688
        $versions['core'] = self::fetch_core_version();
1689
 
1690
        // The problem here is tha the component cache might be stable,
1691
        // we want this to work also on frontpage without resetting the component cache.
1692
        $usecache = false;
1693
        if (CACHE_DISABLE_ALL || (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE)) {
1694
            $usecache = true;
1695
        }
1696
 
1697
        // Now all plugins.
1698
        $plugintypes = self::get_plugin_types();
1699
        foreach ($plugintypes as $type => $typedir) {
1700
            if ($usecache) {
1701
                $plugs = self::get_plugin_list($type);
1702
            } else {
1703
                $plugs = self::fetch_plugins($type, $typedir);
1704
            }
1705
            foreach ($plugs as $plug => $fullplug) {
1706
                $plugin = new stdClass();
1707
                $plugin->version = null;
1708
                $module = $plugin;
1709
                include($fullplug . '/version.php');
1710
                $versions[$type . '_' . $plug] = $plugin->version;
1711
            }
1712
        }
1713
 
1714
        return $versions;
1715
    }
1716
 
1717
    /**
1718
     * Returns hash of all core + plugin /db/ directories.
1719
     *
1720
     * This is relatively slow and not fully cached, use with care!
1721
     *
1722
     * @param array|null $components optional component directory => hash array to use. Only used in PHPUnit.
1723
     * @return string sha1 hash.
1724
     */
1725
    public static function get_all_component_hash(?array $components = null): string {
1726
        $tohash = $components ?? self::get_all_directory_hashes();
1727
        return sha1(serialize($tohash));
1728
    }
1729
 
1730
    /**
1731
     * Get the hashes of all core + plugin /db/ directories.
1732
     *
1733
     * @param array|null $directories optional component directory array to hash. Only used in PHPUnit.
1734
     * @return array of directory => hash.
1735
     */
1736
    public static function get_all_directory_hashes(?array $directories = null): array {
1737
        global $CFG;
1738
 
1739
        self::init();
1740
 
1741
        // The problem here is that the component cache might be stale,
1742
        // we want this to work also on frontpage without resetting the component cache.
1743
        $usecache = false;
1744
        if (CACHE_DISABLE_ALL || (defined('IGNORE_COMPONENT_CACHE') && IGNORE_COMPONENT_CACHE)) {
1745
            $usecache = true;
1746
        }
1747
 
1748
        if (empty($directories)) {
1749
            $directories = [
1750
                $CFG->libdir . '/db',
1751
            ];
1752
            // For all components, get the directory of the /db directory.
1753
            $plugintypes = self::get_plugin_types();
1754
            foreach ($plugintypes as $type => $typedir) {
1755
                if ($usecache) {
1756
                    $plugs = self::get_plugin_list($type);
1757
                } else {
1758
                    $plugs = self::fetch_plugins($type, $typedir);
1759
                }
1760
                foreach ($plugs as $plug) {
1761
                    $directories[] = $plug . '/db';
1762
                }
1763
            }
1764
        }
1765
 
1766
        // Create a mapping of directories to their hash.
1767
        $hashes = [];
1768
        foreach ($directories as $directory) {
1769
            if (!is_dir($directory)) {
1770
                // Just hash an empty string as the non-existing representation.
1771
                $hashes[$directory] = sha1('');
1772
                continue;
1773
            }
1774
 
1775
            $scan = scandir($directory);
1776
            if ($scan) {
1777
                sort($scan);
1778
            }
1779
            $scanhashes = [];
1780
            foreach ($scan as $file) {
1781
                $file = $directory . '/' . $file;
1782
                // Moodle ignores directories.
1783
                if (!is_dir($file)) {
1784
                    $scanhashes[] = hash_file('sha1', $file);
1785
                }
1786
            }
1787
            // Finally we can serialize and hash the whole dir.
1788
            $hashes[$directory] = sha1(serialize($scanhashes));
1789
        }
1790
 
1791
        return $hashes;
1792
    }
1793
 
1794
    /**
1795
     * Invalidate opcode cache for given file, this is intended for
1796
     * php files that are stored in dataroot.
1797
     *
1798
     * Note: we need it here because this class must be self-contained.
1799
     *
1800
     * @param string $file
1801
     */
1802
    public static function invalidate_opcode_php_cache($file) {
1803
        if (function_exists('opcache_invalidate')) {
1804
            if (!file_exists($file)) {
1805
                return;
1806
            }
1807
            opcache_invalidate($file, true);
1808
        }
1809
    }
1810
 
1811
    /**
1812
     * Return true if subsystemname is core subsystem.
1813
     *
1814
     * @param string $subsystemname name of the subsystem.
1815
     * @return bool true if core subsystem.
1816
     */
1817
    public static function is_core_subsystem($subsystemname) {
1818
        return isset(self::$subsystems[$subsystemname]);
1819
    }
1820
 
1821
    /**
1822
     * Return true if apiname is a core API.
1823
     *
1824
     * @param string $apiname name of the API.
1825
     * @return bool true if core API.
1826
     */
1827
    public static function is_core_api($apiname) {
1828
        return isset(self::$apis[$apiname]);
1829
    }
1830
 
1831
    /**
1832
     * Records all class renames that have been made to facilitate autoloading.
1833
     */
1834
    protected static function fill_classmap_renames_cache() {
1835
        global $CFG;
1836
 
1837
        self::$classmaprenames = [];
1838
 
1839
        self::load_renamed_classes("$CFG->dirroot/lib/");
1840
 
1841
        foreach (self::$subsystems as $subsystem => $fulldir) {
1842
            self::load_renamed_classes($fulldir);
1843
        }
1844
 
1845
        foreach (self::$plugins as $plugintype => $plugins) {
1846
            foreach ($plugins as $pluginname => $fulldir) {
1847
                self::load_renamed_classes($fulldir);
1848
            }
1849
        }
1850
    }
1851
 
1852
    /**
1853
     * Loads the db/renamedclasses.php file from the given directory.
1854
     *
1855
     * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1856
     * and the value is the new class name.
1857
     * It is only included when we are populating the component cache. After that is not needed.
1858
     *
1859
     * @param string|null $fulldir The directory to the renamed classes.
1860
     */
1861
    protected static function load_renamed_classes(?string $fulldir) {
1862
        if (is_null($fulldir)) {
1863
            return;
1864
        }
1865
 
1866
        $file = $fulldir . '/db/renamedclasses.php';
1867
        if (is_readable($file)) {
1868
            $renamedclasses = null;
1869
            require($file);
1870
            if (is_array($renamedclasses)) {
1871
                foreach ($renamedclasses as $oldclass => $newclass) {
1872
                    self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1873
                }
1874
            }
1875
        }
1876
    }
1877
 
1878
    /**
1441 ariadna 1879
     * Load legacy classes based upon the db/legacyclasses.php file.
1880
     *
1881
     * The legacyclasses.php should contain a key => value array ($legacyclasses) where the key is the class name,
1882
     * and the value is the path to the class file within the relative ../classes/ directory.
1883
     *
1884
     * @param string|null $fulldir The directory to the legacy classes.
1885
     * @param bool $allowsubsystems Whether to allow the specification of alternative subsystems for this path.
1886
     */
1887
    protected static function load_legacy_classes(
1888
        ?string $fulldir,
1889
        bool $allowsubsystems = false,
1890
    ): void {
1891
        if (is_null($fulldir)) {
1892
            return;
1893
        }
1894
 
1895
        $file = $fulldir . '/db/legacyclasses.php';
1896
        if (is_readable($file)) {
1897
            $legacyclasses = null;
1898
            require($file);
1899
            if (is_array($legacyclasses)) {
1900
                foreach ($legacyclasses as $classname => $path) {
1901
                    if (is_array($path)) {
1902
                        if (!$allowsubsystems) {
1903
                            throw new Exception(
1904
                                "Invalid legacy classes path entry for {$classname}. " .
1905
                                    "Only files within the component can be specified.",
1906
                            );
1907
                        }
1908
                        if (count($path) !== 2) {
1909
                            throw new Exception(
1910
                                "Invalid legacy classes path entry for {$classname}. " .
1911
                                    "Entries must be in the format [subsystem, path].",
1912
                            );
1913
                        }
1914
                        [$subsystem, $path] = $path;
1915
                        $subsystem = substr($subsystem, 5);
1916
                        if (!array_key_exists($subsystem, self::$subsystems)) {
1917
                            throw new Exception(
1918
                                "Unknown subsystem '{$subsystem}' for legacy classes entry of '{$classname}'",
1919
                            );
1920
                        }
1921
 
1922
                        $subsystemfulldir = self::$subsystems[$subsystem];
1923
                        self::$classmap[$classname] = "{$subsystemfulldir}/classes/{$path}";
1924
                    } else {
1925
                        self::$classmap[$classname] = "{$fulldir}/classes/{$path}";
1926
                    }
1927
                }
1928
            }
1929
        }
1930
    }
1931
 
1932
    /**
1 efrain 1933
     * Returns a list of frankenstyle component names and their paths, for all components (plugins and subsystems).
1934
     *
1935
     * E.g.
1936
     *  [
1937
     *      'mod' => [
1938
     *          'mod_forum' => FORUM_PLUGIN_PATH,
1939
     *          ...
1940
     *      ],
1941
     *      ...
1942
     *      'core' => [
1943
     *          'core_comment' => COMMENT_SUBSYSTEM_PATH,
1944
     *          ...
1945
     *      ]
1946
     * ]
1947
     *
1948
     * @return array an associative array of components and their corresponding paths.
1949
     */
1950
    public static function get_component_list(): array {
1951
        $components = [];
1952
        // Get all plugins.
1953
        foreach (self::get_plugin_types() as $plugintype => $typedir) {
1954
            $components[$plugintype] = [];
1955
            foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1956
                $components[$plugintype][$plugintype . '_' . $pluginname] = $plugindir;
1957
            }
1958
        }
1959
        // Get all subsystems.
1960
        foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1961
            $components['core']['core_' . $subsystemname] = $subsystempath;
1962
        }
1963
        return $components;
1964
    }
1965
 
1966
    /**
1967
     * Returns a list of frankenstyle component names, including all plugins, subplugins, and subsystems.
1968
     *
1969
     * Note: By default the 'core' subsystem is not included.
1970
     *
1971
     * @param bool $includecore Whether to include the 'core' subsystem
1441 ariadna 1972
     * @param bool $includedeprecated Whether to include deprecated components
1 efrain 1973
     * @return string[] the list of frankenstyle component names.
1974
     */
1975
    public static function get_component_names(
1976
        bool $includecore = false,
1441 ariadna 1977
        bool $includedeprecated = false
1 efrain 1978
    ): array {
1979
        $componentnames = [];
1980
        // Get all plugins.
1981
        foreach (self::get_plugin_types() as $plugintype => $typedir) {
1982
            foreach (self::get_plugin_list($plugintype) as $pluginname => $plugindir) {
1983
                $componentnames[] = $plugintype . '_' . $pluginname;
1984
            }
1985
        }
1986
        // Get all subsystems.
1987
        foreach (self::get_core_subsystems() as $subsystemname => $subsystempath) {
1988
            $componentnames[] = 'core_' . $subsystemname;
1989
        }
1990
 
1991
        if ($includecore) {
1992
            $componentnames[] = 'core';
1993
        }
1994
 
1441 ariadna 1995
        if ($includedeprecated) {
1996
            foreach (self::get_deprecated_plugin_types() as $plugintype => $typedir) {
1997
                foreach (self::get_deprecated_plugin_list($plugintype) as $pluginname => $plugindir) {
1998
                    $componentnames[] = $plugintype . '_' . $pluginname;
1999
                }
2000
            }
2001
        }
2002
 
1 efrain 2003
        return $componentnames;
2004
    }
2005
 
2006
    /**
2007
     * Returns the list of available API names.
2008
     *
2009
     * @return string[] the list of available API names.
2010
     */
2011
    public static function get_core_api_names(): array {
2012
        return array_keys(self::get_core_apis());
2013
    }
2014
 
2015
    /**
2016
     * Checks for the presence of monologo icons within a plugin.
2017
     *
2018
     * Only checks monologo icons in PNG and SVG formats as they are
2019
     * formats that can have transparent background.
2020
     *
2021
     * @param string $plugintype The plugin type.
2022
     * @param string $pluginname The plugin name.
2023
     * @return bool True if the plugin has a monologo icon
2024
     */
2025
    public static function has_monologo_icon(string $plugintype, string $pluginname): bool {
1441 ariadna 2026
        global $PAGE;
1 efrain 2027
        $plugindir = self::get_plugin_directory($plugintype, $pluginname);
2028
        if ($plugindir === null) {
2029
            return false;
2030
        }
1441 ariadna 2031
        $theme = theme_config::load($PAGE->theme->name);
2032
        $component = self::normalize_componentname("{$plugintype}_{$pluginname}");
2033
        $hassvgmonologo = $theme->resolve_image_location('monologo', $component, true) !== null;
2034
        $haspngmonologo = $theme->resolve_image_location('monologo', $component) !== null;
2035
        return $haspngmonologo || $hassvgmonologo;
1 efrain 2036
    }
2037
}
1441 ariadna 2038
 
2039
// Alias this class to the old name.
2040
// This should be kept here because we use this class in external tooling.
2041
class_alias(component::class, \core_component::class);