1 <?php
2 /**
3 * @package Joomla.Platform
4 *
5 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE.txt
7 */
8
9 defined('JPATH_PLATFORM') or die;
10
11 /**
12 * Static class to handle loading of libraries.
13 *
14 * @package Joomla.Platform
15 * @since 11.1
16 */
17 abstract class JLoader
18 {
19 /**
20 * Container for already imported library paths.
21 *
22 * @var array
23 * @since 11.1
24 */
25 protected static $classes = array();
26
27 /**
28 * Container for already imported library paths.
29 *
30 * @var array
31 * @since 11.1
32 */
33 protected static $imported = array();
34
35 /**
36 * Container for registered library class prefixes and path lookups.
37 *
38 * @var array
39 * @since 12.1
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 12.3
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 11.1
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 (empty(self::$classes[$class]) || $force)
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 11.1
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 12.3
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 11.1
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 11.1
245 */
246 public static function load($class)
247 {
248 // Sanitize class name.
249 $class = 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[$class]))
259 {
260 include_once self::$classes[$class];
261
262 return true;
263 }
264
265 return false;
266 }
267
268 /**
269 * Directly register a class to the autoload list.
270 *
271 * @param string $class The class name to register.
272 * @param string $path Full path to the file that holds the class to register.
273 * @param boolean $force True to overwrite the autoload path value for the class if it already exists.
274 *
275 * @return void
276 *
277 * @since 11.1
278 */
279 public static function register($class, $path, $force = true)
280 {
281 // Sanitize class name.
282 $class = strtolower($class);
283
284 // Only attempt to register the class if the name and file exist.
285 if (!empty($class) && is_file($path))
286 {
287 // Register the class with the autoloader if not already registered or the force flag is set.
288 if (empty(self::$classes[$class]) || $force)
289 {
290 self::$classes[$class] = $path;
291 }
292 }
293 }
294
295 /**
296 * Register a class prefix with lookup path. This will allow developers to register library
297 * packages with different class prefixes to the system autoloader. More than one lookup path
298 * may be registered for the same class prefix, but if this method is called with the reset flag
299 * set to true then any registered lookups for the given prefix will be overwritten with the current
300 * lookup path. When loaded, prefix paths are searched in a "last in, first out" order.
301 *
302 * @param string $prefix The class prefix to register.
303 * @param string $path Absolute file path to the library root where classes with the given prefix can be found.
304 * @param boolean $reset True to reset the prefix with only the given lookup path.
305 * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array.
306 *
307 * @return void
308 *
309 * @throws RuntimeException
310 *
311 * @since 12.1
312 */
313 public static function registerPrefix($prefix, $path, $reset = false, $prepend = false)
314 {
315 // Verify the library path exists.
316 if (!file_exists($path))
317 {
318 $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path);
319
320 throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500);
321 }
322
323 // If the prefix is not yet registered or we have an explicit reset flag then set set the path.
324 if (!isset(self::$prefixes[$prefix]) || $reset)
325 {
326 self::$prefixes[$prefix] = array($path);
327 }
328 // Otherwise we want to simply add the path to the prefix.
329 else
330 {
331 if ($prepend)
332 {
333 array_unshift(self::$prefixes[$prefix], $path);
334 }
335 else
336 {
337 self::$prefixes[$prefix][] = $path;
338 }
339 }
340 }
341
342 /**
343 * Offers the ability for "just in time" usage of `class_alias()`.
344 * You cannot overwrite an existing alias.
345 *
346 * @param string $alias The alias name to register.
347 * @param string $original The original class to alias.
348 * @param string|boolean $version The version in which the alias will no longer be present.
349 *
350 * @return boolean True if registration was successful. False if the alias already exists.
351 *
352 * @since 3.2
353 */
354 public static function registerAlias($alias, $original, $version = false)
355 {
356 if (!isset(self::$classAliases[$alias]))
357 {
358 self::$classAliases[$alias] = $original;
359
360 // Remove the root backslash if present.
361 if ($original[0] == '\\')
362 {
363 $original = substr($original, 1);
364 }
365
366 if (!isset(self::$classAliasesInverse[$original]))
367 {
368 self::$classAliasesInverse[$original] = array($alias);
369 }
370 else
371 {
372 self::$classAliasesInverse[$original][] = $alias;
373 }
374
375 // If given a version, log this alias as deprecated
376 if ($version)
377 {
378 self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version);
379 }
380
381 return true;
382 }
383
384 return false;
385 }
386
387 /**
388 * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order.
389 *
390 * @param string $namespace A case sensitive Namespace to register.
391 * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found.
392 * @param boolean $reset True to reset the namespace with only the given lookup path.
393 * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array.
394 * @param string $type Defines the type of namespace, can be prs0 or psr4.
395 *
396 * @return void
397 *
398 * @throws RuntimeException
399 *
400 * @note The default argument of $type will be changed in J4 to be 'psr4'
401 * @since 12.3
402 */
403 public static function registerNamespace($namespace, $path, $reset = false, $prepend = false, $type = 'psr0')
404 {
405 if ($type !== 'psr0' && $type !== 'psr4')
406 {
407 throw new InvalidArgumentException('Type needs to be prs0 or psr4!');
408 }
409
410 // Verify the library path exists.
411 if (!file_exists($path))
412 {
413 $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path);
414
415 throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500);
416 }
417
418 // If the namespace is not yet registered or we have an explicit reset flag then set the path.
419 if (!isset(self::$namespaces[$type][$namespace]) || $reset)
420 {
421 self::$namespaces[$type][$namespace] = array($path);
422 }
423
424 // Otherwise we want to simply add the path to the namespace.
425 else
426 {
427 if ($prepend)
428 {
429 array_unshift(self::$namespaces[$type][$namespace], $path);
430 }
431 else
432 {
433 self::$namespaces[$type][$namespace][] = $path;
434 }
435 }
436 }
437
438 /**
439 * Method to setup the autoloaders for the Joomla Platform.
440 * Since the SPL autoloaders are called in a queue we will add our explicit
441 * class-registration based loader first, then fall back on the autoloader based on conventions.
442 * This will allow people to register a class in a specific location and override platform libraries
443 * as was previously possible.
444 *
445 * @param boolean $enablePsr True to enable autoloading based on PSR-0.
446 * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core).
447 * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core).
448 *
449 * @return void
450 *
451 * @since 12.3
452 */
453 public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true)
454 {
455 if ($enableClasses)
456 {
457 // Register the class map based autoloader.
458 spl_autoload_register(array('JLoader', 'load'));
459 }
460
461 if ($enablePrefixes)
462 {
463 // Register the J prefix and base path for Joomla platform libraries.
464 self::registerPrefix('J', JPATH_PLATFORM . '/joomla');
465
466 // Register the prefix autoloader.
467 spl_autoload_register(array('JLoader', '_autoload'));
468 }
469
470 if ($enablePsr)
471 {
472 // Register the PSR-0 based autoloader.
473 spl_autoload_register(array('JLoader', 'loadByPsr0'));
474 spl_autoload_register(array('JLoader', 'loadByPsr4'));
475 spl_autoload_register(array('JLoader', 'loadByAlias'));
476 }
477 }
478
479 /**
480 * Method to autoload classes that are namespaced to the PSR-4 standard.
481 *
482 * @param string $class The fully qualified class name to autoload.
483 *
484 * @return boolean True on success, false otherwise.
485 *
486 * @since 3.7.0
487 */
488 public static function loadByPsr4($class)
489 {
490 // Remove the root backslash if present.
491 if ($class[0] == '\\')
492 {
493 $class = substr($class, 1);
494 }
495
496 // Find the location of the last NS separator.
497 $pos = strrpos($class, '\\');
498
499 // If one is found, we're dealing with a NS'd class.
500 if ($pos !== false)
501 {
502 $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR;
503 $className = substr($class, $pos + 1);
504 }
505 // If not, no need to parse path.
506 else
507 {
508 $classPath = null;
509 $className = $class;
510 }
511
512 $classPath .= $className . '.php';
513
514 // Loop through registered namespaces until we find a match.
515 foreach (self::$namespaces['psr4'] as $ns => $paths)
516 {
517 $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR);
518
519 if (strpos($class, $ns) === 0)
520 {
521 // Loop through paths registered to this namespace until we find a match.
522 foreach ($paths as $path)
523 {
524 $classFilePath = $path . DIRECTORY_SEPARATOR . str_replace($nsPath, '', $classPath);
525
526 // We check for class_exists to handle case-sensitive file systems
527 if (file_exists($classFilePath) && !class_exists($class, false))
528 {
529 return (bool) include_once $classFilePath;
530 }
531 }
532 }
533 }
534
535 return false;
536 }
537
538 /**
539 * Method to autoload classes that are namespaced to the PSR-0 standard.
540 *
541 * @param string $class The fully qualified class name to autoload.
542 *
543 * @return boolean True on success, false otherwise.
544 *
545 * @since 13.1
546 *
547 * @deprecated 4.0 this method will be removed
548 */
549 public static function loadByPsr0($class)
550 {
551 // Remove the root backslash if present.
552 if ($class[0] == '\\')
553 {
554 $class = substr($class, 1);
555 }
556
557 // Find the location of the last NS separator.
558 $pos = strrpos($class, '\\');
559
560 // If one is found, we're dealing with a NS'd class.
561 if ($pos !== false)
562 {
563 $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR;
564 $className = substr($class, $pos + 1);
565 }
566 // If not, no need to parse path.
567 else
568 {
569 $classPath = null;
570 $className = $class;
571 }
572
573 $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
574
575 // Loop through registered namespaces until we find a match.
576 foreach (self::$namespaces['psr0'] as $ns => $paths)
577 {
578 if (strpos($class, $ns) === 0)
579 {
580 // Loop through paths registered to this namespace until we find a match.
581 foreach ($paths as $path)
582 {
583 $classFilePath = $path . DIRECTORY_SEPARATOR . $classPath;
584
585 // We check for class_exists to handle case-sensitive file systems
586 if (file_exists($classFilePath) && !class_exists($class, false))
587 {
588 return (bool) include_once $classFilePath;
589 }
590 }
591 }
592 }
593
594 return false;
595 }
596
597 /**
598 * Method to autoload classes that have been aliased using the registerAlias method.
599 *
600 * @param string $class The fully qualified class name to autoload.
601 *
602 * @return boolean True on success, false otherwise.
603 *
604 * @since 3.2
605 */
606 public static function loadByAlias($class)
607 {
608 // Remove the root backslash if present.
609 if ($class[0] == '\\')
610 {
611 $class = substr($class, 1);
612 }
613
614 if (isset(self::$classAliases[$class]))
615 {
616 // Force auto-load of the regular class
617 class_exists(self::$classAliases[$class], true);
618
619 // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is
620 // auto-loaded above.
621 if (!class_exists($class, false) && !interface_exists($class, false))
622 {
623 class_alias(self::$classAliases[$class], $class);
624 }
625 }
626 }
627
628 /**
629 * Applies a class alias for an already loaded class, if a class alias was created for it.
630 *
631 * @param string $class We'll look for and register aliases for this (real) class name
632 *
633 * @return void
634 *
635 * @since 3.4
636 */
637 public static function applyAliasFor($class)
638 {
639 // Remove the root backslash if present.
640 if ($class[0] == '\\')
641 {
642 $class = substr($class, 1);
643 }
644
645 if (isset(self::$classAliasesInverse[$class]))
646 {
647 foreach (self::$classAliasesInverse[$class] as $alias)
648 {
649 class_alias($class, $alias);
650 }
651 }
652 }
653
654 /**
655 * Autoload a class based on name.
656 *
657 * @param string $class The class to be loaded.
658 *
659 * @return boolean True if the class was loaded, false otherwise.
660 *
661 * @since 11.3
662 */
663 public static function _autoload($class)
664 {
665 foreach (self::$prefixes as $prefix => $lookup)
666 {
667 $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0;
668
669 if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr)))
670 {
671 return self::_load(substr($class, strlen($prefix)), $lookup);
672 }
673 }
674
675 return false;
676 }
677
678 /**
679 * Load a class based on name and lookup array.
680 *
681 * @param string $class The class to be loaded (wihtout prefix).
682 * @param array $lookup The array of base paths to use for finding the class file.
683 *
684 * @return boolean True if the class was loaded, false otherwise.
685 *
686 * @since 12.1
687 */
688 private static function _load($class, $lookup)
689 {
690 // Split the class name into parts separated by camelCase.
691 $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class);
692 $partsCount = count($parts);
693
694 foreach ($lookup as $base)
695 {
696 // Generate the path based on the class name parts.
697 $path = $base . '/' . implode('/', array_map('strtolower', $parts)) . '.php';
698
699 // Load the file if it exists.
700 if (file_exists($path))
701 {
702 return include $path;
703 }
704
705 // Backwards compatibility patch
706
707 // If there is only one part we want to duplicate that part for generating the path.
708 if ($partsCount === 1)
709 {
710 // Generate the path based on the class name parts.
711 $path = $base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php';
712
713 // Load the file if it exists.
714 if (file_exists($path))
715 {
716 return include $path;
717 }
718 }
719 }
720
721 return false;
722 }
723 }
724
725 // Check if jexit is defined first (our unit tests mock this)
726 if (!function_exists('jexit'))
727 {
728 /**
729 * Global application exit.
730 *
731 * This function provides a single exit point for the platform.
732 *
733 * @param mixed $message Exit code or string. Defaults to zero.
734 *
735 * @return void
736 *
737 * @codeCoverageIgnore
738 * @since 11.1
739 */
740 function jexit($message = 0)
741 {
742 exit($message);
743 }
744 }
745
746 /**
747 * Intelligent file importer.
748 *
749 * @param string $path A dot syntax path.
750 * @param string $base Search this directory for the class.
751 *
752 * @return boolean True on success.
753 *
754 * @since 11.1
755 */
756 function jimport($path, $base = null)
757 {
758 return JLoader::import($path, $base);
759 }
760