1<?php 2/** 3 * @package Joomla.Platform 4 * 5 * @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE.txt 7 */ 8 9defined('JPATH_PLATFORM') or die; 10 11/** 12 * Static class to handle loading of libraries. 13 * 14 * @package Joomla.Platform 15 * @since 1.7.0 16 */ 17abstract class JLoader 18{ 19 /** 20 * Container for already imported library paths. 21 * 22 * @var array 23 * @since 1.7.0 24 */ 25 protected static $classes = array(); 26 27 /** 28 * Container for already imported library paths. 29 * 30 * @var array 31 * @since 1.7.0 32 */ 33 protected static $imported = array(); 34 35 /** 36 * Container for registered library class prefixes and path lookups. 37 * 38 * @var array 39 * @since 3.0.0 40 */ 41 protected static $prefixes = array(); 42 43 /** 44 * Holds proxy classes and the class names the proxy. 45 * 46 * @var array 47 * @since 3.2 48 */ 49 protected static $classAliases = array(); 50 51 /** 52 * Holds the inverse lookup for proxy classes and the class names the proxy. 53 * 54 * @var array 55 * @since 3.4 56 */ 57 protected static $classAliasesInverse = array(); 58 59 /** 60 * Container for namespace => path map. 61 * 62 * @var array 63 * @since 3.1.4 64 */ 65 protected static $namespaces = array('psr0' => array(), 'psr4' => array()); 66 67 /** 68 * Holds a reference for all deprecated aliases (mainly for use by a logging platform). 69 * 70 * @var array 71 * @since 3.6.3 72 */ 73 protected static $deprecatedAliases = array(); 74 75 /** 76 * Method to discover classes of a given type in a given path. 77 * 78 * @param string $classPrefix The class name prefix to use for discovery. 79 * @param string $parentPath Full path to the parent folder for the classes to discover. 80 * @param boolean $force True to overwrite the autoload path value for the class if it already exists. 81 * @param boolean $recurse Recurse through all child directories as well as the parent path. 82 * 83 * @return void 84 * 85 * @since 1.7.0 86 */ 87 public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) 88 { 89 try 90 { 91 if ($recurse) 92 { 93 $iterator = new RecursiveIteratorIterator( 94 new RecursiveDirectoryIterator($parentPath), 95 RecursiveIteratorIterator::SELF_FIRST 96 ); 97 } 98 else 99 { 100 $iterator = new DirectoryIterator($parentPath); 101 } 102 103 /** @type $file DirectoryIterator */ 104 foreach ($iterator as $file) 105 { 106 $fileName = $file->getFilename(); 107 108 // Only load for php files. 109 if ($file->isFile() && $file->getExtension() === 'php') 110 { 111 // Get the class name and full path for each file. 112 $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); 113 114 // Register the class with the autoloader if not already registered or the force flag is set. 115 if ($force || empty(self::$classes[$class])) 116 { 117 self::register($class, $file->getPath() . '/' . $fileName); 118 } 119 } 120 } 121 } 122 catch (UnexpectedValueException $e) 123 { 124 // Exception will be thrown if the path is not a directory. Ignore it. 125 } 126 } 127 128 /** 129 * Method to get the list of registered classes and their respective file paths for the autoloader. 130 * 131 * @return array The array of class => path values for the autoloader. 132 * 133 * @since 1.7.0 134 */ 135 public static function getClassList() 136 { 137 return self::$classes; 138 } 139 140 /** 141 * Method to get the list of deprecated class aliases. 142 * 143 * @return array An associative array with deprecated class alias data. 144 * 145 * @since 3.6.3 146 */ 147 public static function getDeprecatedAliases() 148 { 149 return self::$deprecatedAliases; 150 } 151 152 /** 153 * Method to get the list of registered namespaces. 154 * 155 * @param string $type Defines the type of namespace, can be prs0 or psr4. 156 * 157 * @return array The array of namespace => path values for the autoloader. 158 * 159 * @since 3.1.4 160 */ 161 public static function getNamespaces($type = 'psr0') 162 { 163 if ($type !== 'psr0' && $type !== 'psr4') 164 { 165 throw new InvalidArgumentException('Type needs to be prs0 or psr4!'); 166 } 167 168 return self::$namespaces[$type]; 169 } 170 171 /** 172 * Loads a class from specified directories. 173 * 174 * @param string $key The class name to look for (dot notation). 175 * @param string $base Search this directory for the class. 176 * 177 * @return boolean True on success. 178 * 179 * @since 1.7.0 180 */ 181 public static function import($key, $base = null) 182 { 183 // Only import the library if not already attempted. 184 if (!isset(self::$imported[$key])) 185 { 186 // Setup some variables. 187 $success = false; 188 $parts = explode('.', $key); 189 $class = array_pop($parts); 190 $base = (!empty($base)) ? $base : __DIR__; 191 $path = str_replace('.', DIRECTORY_SEPARATOR, $key); 192 193 // Handle special case for helper classes. 194 if ($class === 'helper') 195 { 196 $class = ucfirst(array_pop($parts)) . ucfirst($class); 197 } 198 // Standard class. 199 else 200 { 201 $class = ucfirst($class); 202 } 203 204 // If we are importing a library from the Joomla namespace set the class to autoload. 205 if (strpos($path, 'joomla') === 0) 206 { 207 // Since we are in the Joomla namespace prepend the classname with J. 208 $class = 'J' . $class; 209 210 // Only register the class for autoloading if the file exists. 211 if (is_file($base . '/' . $path . '.php')) 212 { 213 self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; 214 $success = true; 215 } 216 } 217 /* 218 * If we are not importing a library from the Joomla namespace directly include the 219 * file since we cannot assert the file/folder naming conventions. 220 */ 221 else 222 { 223 // If the file exists attempt to include it. 224 if (is_file($base . '/' . $path . '.php')) 225 { 226 $success = (bool) include_once $base . '/' . $path . '.php'; 227 } 228 } 229 230 // Add the import key to the memory cache container. 231 self::$imported[$key] = $success; 232 } 233 234 return self::$imported[$key]; 235 } 236 237 /** 238 * Load the file for a class. 239 * 240 * @param string $class The class to be loaded. 241 * 242 * @return boolean True on success 243 * 244 * @since 1.7.0 245 */ 246 public static function load($class) 247 { 248 // Sanitize class name. 249 $key = strtolower($class); 250 251 // If the class already exists do nothing. 252 if (class_exists($class, false)) 253 { 254 return true; 255 } 256 257 // If the class is registered include the file. 258 if (isset(self::$classes[$key])) 259 { 260 $found = (bool) include_once self::$classes[$key]; 261 262 if ($found) 263 { 264 self::loadAliasFor($class); 265 } 266 267 // If the class doesn't exists, we probably have a class alias available 268 if (!class_exists($class, false)) 269 { 270 // Search the alias class, first none namespaced and then namespaced 271 $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); 272 273 // When we have an original and the class exists an alias should be created 274 if ($original && class_exists($original, false)) 275 { 276 class_alias($original, $class); 277 } 278 } 279 280 return true; 281 } 282 283 return false; 284 } 285 286 /** 287 * Directly register a class to the autoload list. 288 * 289 * @param string $class The class name to register. 290 * @param string $path Full path to the file that holds the class to register. 291 * @param boolean $force True to overwrite the autoload path value for the class if it already exists. 292 * 293 * @return void 294 * 295 * @since 1.7.0 296 */ 297 public static function register($class, $path, $force = true) 298 { 299 // When an alias exists, register it as well 300 if (key_exists(strtolower($class), self::$classAliases)) 301 { 302 self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); 303 } 304 305 // Sanitize class name. 306 $class = strtolower($class); 307 308 // Only attempt to register the class if the name and file exist. 309 if (!empty($class) && is_file($path)) 310 { 311 // Register the class with the autoloader if not already registered or the force flag is set. 312 if ($force || empty(self::$classes[$class])) 313 { 314 self::$classes[$class] = $path; 315 } 316 } 317 } 318 319 /** 320 * Register a class prefix with lookup path. This will allow developers to register library 321 * packages with different class prefixes to the system autoloader. More than one lookup path 322 * may be registered for the same class prefix, but if this method is called with the reset flag 323 * set to true then any registered lookups for the given prefix will be overwritten with the current 324 * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. 325 * 326 * @param string $prefix The class prefix to register. 327 * @param string $path Absolute file path to the library root where classes with the given prefix can be found. 328 * @param boolean $reset True to reset the prefix with only the given lookup path. 329 * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. 330 * 331 * @return void 332 * 333 * @throws RuntimeException 334 * 335 * @since 3.0.0 336 */ 337 public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) 338 { 339 // Verify the library path exists. 340 if (!file_exists($path)) 341 { 342 $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); 343 344 throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); 345 } 346 347 // If the prefix is not yet registered or we have an explicit reset flag then set set the path. 348 if ($reset || !isset(self::$prefixes[$prefix])) 349 { 350 self::$prefixes[$prefix] = array($path); 351 } 352 // Otherwise we want to simply add the path to the prefix. 353 else 354 { 355 if ($prepend) 356 { 357 array_unshift(self::$prefixes[$prefix], $path); 358 } 359 else 360 { 361 self::$prefixes[$prefix][] = $path; 362 } 363 } 364 } 365 366 /** 367 * Offers the ability for "just in time" usage of `class_alias()`. 368 * You cannot overwrite an existing alias. 369 * 370 * @param string $alias The alias name to register. 371 * @param string $original The original class to alias. 372 * @param string|boolean $version The version in which the alias will no longer be present. 373 * 374 * @return boolean True if registration was successful. False if the alias already exists. 375 * 376 * @since 3.2 377 */ 378 public static function registerAlias($alias, $original, $version = false) 379 { 380 // PHP is case insensitive so support all kind of alias combination 381 $lowercasedAlias = strtolower($alias); 382 383 if (!isset(self::$classAliases[$lowercasedAlias])) 384 { 385 self::$classAliases[$lowercasedAlias] = $original; 386 387 $original = self::stripFirstBackslash($original); 388 389 if (!isset(self::$classAliasesInverse[$original])) 390 { 391 self::$classAliasesInverse[$original] = array($lowercasedAlias); 392 } 393 else 394 { 395 self::$classAliasesInverse[$original][] = $lowercasedAlias; 396 } 397 398 // If given a version, log this alias as deprecated 399 if ($version) 400 { 401 self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); 402 } 403 404 return true; 405 } 406 407 return false; 408 } 409 410 /** 411 * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. 412 * 413 * @param string $namespace A case sensitive Namespace to register. 414 * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. 415 * @param boolean $reset True to reset the namespace with only the given lookup path. 416 * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. 417 * @param string $type Defines the type of namespace, can be prs0 or psr4. 418 * 419 * @return void 420 * 421 * @throws RuntimeException 422 * 423 * @note The default argument of $type will be changed in J4 to be 'psr4' 424 * @since 3.1.4 425 */ 426 public static function registerNamespace($namespace, $path, $reset = false, $prepend = false, $type = 'psr0') 427 { 428 if ($type !== 'psr0' && $type !== 'psr4') 429 { 430 throw new InvalidArgumentException('Type needs to be prs0 or psr4!'); 431 } 432 433 // Verify the library path exists. 434 if (!file_exists($path)) 435 { 436 $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); 437 438 throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); 439 } 440 441 // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. 442 $namespace = trim($namespace, '\\'); 443 444 // If the namespace is not yet registered or we have an explicit reset flag then set the path. 445 if ($reset || !isset(self::$namespaces[$type][$namespace])) 446 { 447 self::$namespaces[$type][$namespace] = array($path); 448 } 449 450 // Otherwise we want to simply add the path to the namespace. 451 else 452 { 453 if ($prepend) 454 { 455 array_unshift(self::$namespaces[$type][$namespace], $path); 456 } 457 else 458 { 459 self::$namespaces[$type][$namespace][] = $path; 460 } 461 } 462 } 463 464 /** 465 * Method to setup the autoloaders for the Joomla Platform. 466 * Since the SPL autoloaders are called in a queue we will add our explicit 467 * class-registration based loader first, then fall back on the autoloader based on conventions. 468 * This will allow people to register a class in a specific location and override platform libraries 469 * as was previously possible. 470 * 471 * @param boolean $enablePsr True to enable autoloading based on PSR-0. 472 * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). 473 * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). 474 * 475 * @return void 476 * 477 * @since 3.1.4 478 */ 479 public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) 480 { 481 if ($enableClasses) 482 { 483 // Register the class map based autoloader. 484 spl_autoload_register(array('JLoader', 'load')); 485 } 486 487 if ($enablePrefixes) 488 { 489 // Register the J prefix and base path for Joomla platform libraries. 490 self::registerPrefix('J', JPATH_PLATFORM . '/joomla'); 491 492 // Register the prefix autoloader. 493 spl_autoload_register(array('JLoader', '_autoload')); 494 } 495 496 if ($enablePsr) 497 { 498 // Register the PSR based autoloader. 499 spl_autoload_register(array('JLoader', 'loadByPsr0')); 500 spl_autoload_register(array('JLoader', 'loadByPsr4')); 501 spl_autoload_register(array('JLoader', 'loadByAlias')); 502 } 503 } 504 505 /** 506 * Method to autoload classes that are namespaced to the PSR-4 standard. 507 * 508 * @param string $class The fully qualified class name to autoload. 509 * 510 * @return boolean True on success, false otherwise. 511 * 512 * @since 3.7.0 513 */ 514 public static function loadByPsr4($class) 515 { 516 $class = self::stripFirstBackslash($class); 517 518 // Find the location of the last NS separator. 519 $pos = strrpos($class, '\\'); 520 521 // If one is found, we're dealing with a NS'd class. 522 if ($pos !== false) 523 { 524 $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; 525 $className = substr($class, $pos + 1); 526 } 527 // If not, no need to parse path. 528 else 529 { 530 $classPath = null; 531 $className = $class; 532 } 533 534 $classPath .= $className . '.php'; 535 536 // Loop through registered namespaces until we find a match. 537 foreach (self::$namespaces['psr4'] as $ns => $paths) 538 { 539 if (strpos($class, "{$ns}\\") === 0) 540 { 541 $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); 542 543 // Loop through paths registered to this namespace until we find a match. 544 foreach ($paths as $path) 545 { 546 $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); 547 548 // We do not allow files outside the namespace root to be loaded 549 if (strpos($classFilePath, realpath($path)) !== 0) 550 { 551 continue; 552 } 553 554 // We check for class_exists to handle case-sensitive file systems 555 if (file_exists($classFilePath) && !class_exists($class, false)) 556 { 557 $found = (bool) include_once $classFilePath; 558 559 if ($found) 560 { 561 self::loadAliasFor($class); 562 } 563 564 return $found; 565 } 566 } 567 } 568 } 569 570 return false; 571 } 572 573 /** 574 * Method to autoload classes that are namespaced to the PSR-0 standard. 575 * 576 * @param string $class The fully qualified class name to autoload. 577 * 578 * @return boolean True on success, false otherwise. 579 * 580 * @since 3.2.0 581 * 582 * @deprecated 4.0 this method will be removed 583 */ 584 public static function loadByPsr0($class) 585 { 586 $class = self::stripFirstBackslash($class); 587 588 // Find the location of the last NS separator. 589 $pos = strrpos($class, '\\'); 590 591 // If one is found, we're dealing with a NS'd class. 592 if ($pos !== false) 593 { 594 $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; 595 $className = substr($class, $pos + 1); 596 } 597 // If not, no need to parse path. 598 else 599 { 600 $classPath = null; 601 $className = $class; 602 } 603 604 $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; 605 606 // Loop through registered namespaces until we find a match. 607 foreach (self::$namespaces['psr0'] as $ns => $paths) 608 { 609 if (strpos($class, $ns) === 0) 610 { 611 // Loop through paths registered to this namespace until we find a match. 612 foreach ($paths as $path) 613 { 614 $classFilePath = realpath($path . DIRECTORY_SEPARATOR . $classPath); 615 616 // We do not allow files outside the namespace root to be loaded 617 if (strpos($classFilePath, realpath($path)) !== 0) 618 { 619 continue; 620 } 621 622 // We check for class_exists to handle case-sensitive file systems 623 if (file_exists($classFilePath) && !class_exists($class, false)) 624 { 625 $found = (bool) include_once $classFilePath; 626 627 if ($found) 628 { 629 self::loadAliasFor($class); 630 } 631 632 return $found; 633 } 634 } 635 } 636 } 637 638 return false; 639 } 640 641 /** 642 * Method to autoload classes that have been aliased using the registerAlias method. 643 * 644 * @param string $class The fully qualified class name to autoload. 645 * 646 * @return boolean True on success, false otherwise. 647 * 648 * @since 3.2 649 */ 650 public static function loadByAlias($class) 651 { 652 $class = strtolower(self::stripFirstBackslash($class)); 653 654 if (isset(self::$classAliases[$class])) 655 { 656 // Force auto-load of the regular class 657 class_exists(self::$classAliases[$class], true); 658 659 // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is 660 // auto-loaded above. 661 if (!class_exists($class, false) && !interface_exists($class, false)) 662 { 663 class_alias(self::$classAliases[$class], $class); 664 } 665 } 666 } 667 668 /** 669 * Applies a class alias for an already loaded class, if a class alias was created for it. 670 * 671 * @param string $class We'll look for and register aliases for this (real) class name 672 * 673 * @return void 674 * 675 * @since 3.4 676 */ 677 public static function applyAliasFor($class) 678 { 679 $class = self::stripFirstBackslash($class); 680 681 if (isset(self::$classAliasesInverse[$class])) 682 { 683 foreach (self::$classAliasesInverse[$class] as $alias) 684 { 685 class_alias($class, $alias); 686 } 687 } 688 } 689 690 /** 691 * Autoload a class based on name. 692 * 693 * @param string $class The class to be loaded. 694 * 695 * @return boolean True if the class was loaded, false otherwise. 696 * 697 * @since 1.7.3 698 */ 699 public static function _autoload($class) 700 { 701 foreach (self::$prefixes as $prefix => $lookup) 702 { 703 $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; 704 705 if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) 706 { 707 return self::_load(substr($class, strlen($prefix)), $lookup); 708 } 709 } 710 711 return false; 712 } 713 714 /** 715 * Load a class based on name and lookup array. 716 * 717 * @param string $class The class to be loaded (without prefix). 718 * @param array $lookup The array of base paths to use for finding the class file. 719 * 720 * @return boolean True if the class was loaded, false otherwise. 721 * 722 * @since 3.0.0 723 */ 724 private static function _load($class, $lookup) 725 { 726 // Split the class name into parts separated by camelCase. 727 $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); 728 $partsCount = count($parts); 729 730 foreach ($lookup as $base) 731 { 732 // Generate the path based on the class name parts. 733 $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); 734 735 // Load the file if it exists and is in the lookup path. 736 if (strpos($path, realpath($base)) === 0 && file_exists($path)) 737 { 738 $found = (bool) include_once $path; 739 740 if ($found) 741 { 742 self::loadAliasFor($class); 743 } 744 745 return $found; 746 } 747 748 // Backwards compatibility patch 749 750 // If there is only one part we want to duplicate that part for generating the path. 751 if ($partsCount === 1) 752 { 753 // Generate the path based on the class name parts. 754 $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); 755 756 // Load the file if it exists and is in the lookup path. 757 if (strpos($path, realpath($base)) === 0 && file_exists($path)) 758 { 759 $found = (bool) include_once $path; 760 761 if ($found) 762 { 763 self::loadAliasFor($class); 764 } 765 766 return $found; 767 } 768 } 769 } 770 771 return false; 772 } 773 774 /** 775 * Loads the aliases for the given class. 776 * 777 * @param string $class The class. 778 * 779 * @return void 780 * 781 * @since 3.8.0 782 */ 783 private static function loadAliasFor($class) 784 { 785 if (!key_exists($class, self::$classAliasesInverse)) 786 { 787 return; 788 } 789 790 foreach (self::$classAliasesInverse[$class] as $alias) 791 { 792 // Force auto-load of the alias class 793 class_exists($alias, true); 794 } 795 } 796 797 /** 798 * Strips the first backslash from the given class if present. 799 * 800 * @param string $class The class to strip the first prefix from. 801 * 802 * @return string The striped class name. 803 * 804 * @since 3.8.0 805 */ 806 private static function stripFirstBackslash($class) 807 { 808 return $class && $class[0] === '\\' ? substr($class, 1) : $class; 809 } 810} 811 812// Check if jexit is defined first (our unit tests mock this) 813if (!function_exists('jexit')) 814{ 815 /** 816 * Global application exit. 817 * 818 * This function provides a single exit point for the platform. 819 * 820 * @param mixed $message Exit code or string. Defaults to zero. 821 * 822 * @return void 823 * 824 * @codeCoverageIgnore 825 * @since 1.7.0 826 */ 827 function jexit($message = 0) 828 { 829 exit($message); 830 } 831} 832 833/** 834 * Intelligent file importer. 835 * 836 * @param string $path A dot syntax path. 837 * @param string $base Search this directory for the class. 838 * 839 * @return boolean True on success. 840 * 841 * @since 1.7.0 842 */ 843function jimport($path, $base = null) 844{ 845 return JLoader::import($path, $base); 846} 847