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