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