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