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