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