[BUGFIX] Invalid class name to file path conversion
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Core / ClassLoader.php
1 <?php
2 namespace TYPO3\CMS\Core\Core;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Thomas Maroschik <tmaroschik@dfau.de>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the text file GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Core\Package\PackageInterface;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32 use TYPO3\CMS\Core\Cache;
33
34 /**
35 * Class Loader implementation which loads .php files found in the classes
36 * directory of an object.
37 */
38 class ClassLoader {
39
40 /**
41 * @var ClassAliasMap
42 */
43 protected $classAliasMap;
44
45 /**
46 * @var ClassAliasMap
47 */
48 static protected $staticAliasMap;
49
50 /**
51 * @var \TYPO3\CMS\Core\Cache\Frontend\StringFrontend
52 */
53 protected $classesCache;
54
55 /**
56 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
57 */
58 protected $coreCache;
59
60 /**
61 * @var string
62 */
63 protected $cacheIdentifier;
64
65 /**
66 * @var \TYPO3\Flow\Package\Package[]
67 */
68 protected $packages = array();
69
70 /**
71 * @var boolean
72 */
73 protected $isEarlyCache = TRUE;
74
75 /**
76 * @var array
77 */
78 protected $runtimeClassLoadingInformationCache = array();
79
80 /**
81 * @var array A list of namespaces this class loader is definitely responsible for
82 */
83 protected $packageNamespaces = array();
84
85 /**
86 * @var array A list of packages and their replaces pointing to class paths
87 */
88 protected $packageClassesPaths = array();
89
90 /**
91 * Constructor
92 *
93 * @param ApplicationContext $context
94 */
95 public function __construct(ApplicationContext $context) {
96 $this->classesCache = new Cache\Frontend\StringFrontend('cache_classes', new Cache\Backend\TransientMemoryBackend($context));
97 }
98
99 /**
100 * Get class alias map list injected
101 *
102 * @param ClassAliasMap
103 */
104 public function injectClassAliasMap(ClassAliasMap $classAliasMap) {
105 $this->classAliasMap = $classAliasMap;
106 static::$staticAliasMap = $classAliasMap;
107 }
108
109 /**
110 * Get core cache injected
111 *
112 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
113 */
114 public function injectCoreCache(Cache\Frontend\PhpFrontend $coreCache) {
115 $this->coreCache = $coreCache;
116 $this->classAliasMap->injectCoreCache($coreCache);
117 }
118
119 /**
120 * Get classes cache injected
121 *
122 * @param \TYPO3\CMS\Core\Cache\Frontend\StringFrontend $classesCache
123 */
124 public function injectClassesCache(Cache\Frontend\StringFrontend $classesCache) {
125 /** @var $earlyClassLoaderBackend Cache\Backend\TransientMemoryBackend */
126 $earlyClassesCache = $this->classesCache;
127 $this->classesCache = $classesCache;
128 $this->isEarlyCache = FALSE;
129 $this->classAliasMap->injectClassesCache($classesCache);
130 foreach ($earlyClassesCache->getByTag('early') as $originalClassLoadingInformation) {
131 $classLoadingInformation = explode("\xff", $originalClassLoadingInformation);
132 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
133 if (!$this->classesCache->has($cacheEntryIdentifier)) {
134 $this->classesCache->set($cacheEntryIdentifier, $originalClassLoadingInformation);
135 }
136 }
137 }
138
139 /**
140 * Loads php files containing classes or interfaces found in the classes directory of
141 * a package and specifically registered classes.
142 *
143 * @param string $className Name of the class/interface to load
144 * @return boolean
145 */
146 public function loadClass($className) {
147 if ($className[0] === '\\') {
148 $className = substr($className, 1);
149 }
150
151 if (!$this->isValidClassName($className)) {
152 return FALSE;
153 }
154
155 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $className));
156 try {
157 if ($this->classesCache->has($cacheEntryIdentifier)) {
158 $classLoadingInformation = explode("\xff", $this->classesCache->get($cacheEntryIdentifier));
159 } else {
160 $classLoadingInformation = $this->buildClassLoadingInformation($className);
161 if ($classLoadingInformation !== NULL) {
162 $this->classesCache->set($cacheEntryIdentifier, implode("\xff", $classLoadingInformation), $this->isEarlyCache ? array('early') : array());
163 }
164 }
165 } catch (\InvalidArgumentException $exception) {
166 return FALSE;
167 }
168
169 // Class loading information structure
170 // array(
171 // 0 => class file path
172 // 1 => original class name
173 // 2 and following => alias class names
174 // )
175
176 $loadingSuccessful = FALSE;
177 if ($classLoadingInformation !== NULL) {
178 $loadingSuccessful = (boolean)require_once $classLoadingInformation[0];
179 }
180 if ($loadingSuccessful && count($classLoadingInformation) > 2) {
181 $originalClassName = $classLoadingInformation[1];
182 foreach (array_slice($classLoadingInformation, 2) as $aliasClassName) {
183 $this->setAliasForClassName($aliasClassName, $originalClassName);
184 }
185 }
186
187 return $loadingSuccessful;
188 }
189
190 /**
191 * @param string $className
192 * @return array|null
193 */
194 public function buildClassLoadingInformation($className) {
195 $classLoadingInformation = $this->buildClassLoadingInformationForClassFromCorePackage($className);
196
197 if ($classLoadingInformation === NULL) {
198 $classLoadingInformation = $this->fetchClassLoadingInformationFromRuntimeCache($className);
199 }
200
201 if ($classLoadingInformation === NULL) {
202 $classLoadingInformation = $this->buildClassLoadingInformationForClassFromRegisteredPackages($className);
203 }
204
205 if ($classLoadingInformation === NULL) {
206 $classLoadingInformation = $this->buildClassLoadingInformationForClassByNamingConvention($className);
207 }
208
209 return $classLoadingInformation;
210 }
211
212 /**
213 * Find out if a class name is valid
214 *
215 * @param string $className
216 * @return boolean
217 */
218 protected function isValidClassName($className) {
219 return strpos($className, ' ') === FALSE;
220 }
221
222 /**
223 * Retrieve class loading information for class from core package
224 *
225 * @param string $className
226 * @return array|null
227 */
228 protected function buildClassLoadingInformationForClassFromCorePackage($className) {
229 if (substr($className, 0, 14) === 'TYPO3\\CMS\\Core') {
230 $classesFolder = substr($className, 15, 5) === 'Tests' ? '' : 'Classes/';
231 $classFilePath = PATH_typo3 . 'sysext/core/' . $classesFolder . str_replace('\\', '/', substr($className, 15)) . '.php';
232 if (@file_exists($classFilePath)) {
233 return array($classFilePath, $className);
234 }
235 }
236 return NULL;
237 }
238
239 /**
240 * Retrieve class loading information from early class name autoload registry cache
241 *
242 * @param string $className
243 * @return array|null
244 */
245 protected function fetchClassLoadingInformationFromRuntimeCache($className) {
246 $lowercasedClassName = strtolower($className);
247 if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
248 return NULL;
249 }
250 $classInformation = $this->runtimeClassLoadingInformationCache[$lowercasedClassName];
251 return @file_exists($classInformation[0]) ? $classInformation : NULL;
252 }
253
254 /**
255 * Retrieve class loading information from registered packages
256 *
257 * @param string $className
258 * @return array|null
259 */
260 protected function buildClassLoadingInformationForClassFromRegisteredPackages($className) {;
261 foreach ($this->packageNamespaces as $packageNamespace => $packageData) {
262 if (substr(str_replace('_', '\\', $className), 0, $packageData['namespaceLength']) === $packageNamespace) {
263 if ($packageData['substituteNamespaceInPath']) {
264 // If it's a TYPO3 package, classes don't comply to PSR-0.
265 // The namespace part is substituted.
266 $classPathAndFilename = '/' . str_replace('\\', '/', ltrim(substr($className, $packageData['namespaceLength']), '\\')) . '.php';
267 } else {
268 // make the classname PSR-0 compliant by replacing underscores only in the classname not in the namespace
269 $classPathAndFilename = '';
270 $lastNamespacePosition = strrpos($className, '\\');
271 if ($lastNamespacePosition !== FALSE) {
272 $namespace = substr($className, 0, $lastNamespacePosition);
273 $className = substr($className, $lastNamespacePosition + 1);
274 $classPathAndFilename = str_replace('\\', '/', $namespace) . '/';
275 }
276 $classPathAndFilename .= str_replace('_', '/', $className) . '.php';
277 }
278 if (strtolower(substr($className, $packageData['namespaceLength'], 5)) === 'tests') {
279 $classPathAndFilename = $packageData['packagePath'] . $classPathAndFilename;
280 } else {
281 $classPathAndFilename = $packageData['classesPath'] . $classPathAndFilename;
282 }
283 if (@file_exists($classPathAndFilename)) {
284 return array($classPathAndFilename, $className);
285 }
286 }
287 }
288 return NULL;
289 }
290
291 /**
292 * Retrieve class loading information based on 'extbase' naming convention into the registry.
293 *
294 * @param string $className Class name to find source file of
295 * @return array|null
296 */
297 protected function buildClassLoadingInformationForClassByNamingConvention($className) {
298 $delimiter = '_';
299 // To handle namespaced class names, split the class name at the
300 // namespace delimiters.
301 if (strpos($className, '\\') !== FALSE) {
302 $delimiter = '\\';
303 }
304
305 $classNameParts = explode($delimiter, $className, 4);
306
307 // We only handle classes that follow the convention Vendor\Product\Classname or is longer
308 // so we won't deal with class names that only have one or two parts
309 if (count($classNameParts) <= 2) {
310 return NULL;
311 }
312
313 if (
314 isset($classNameParts[0])
315 && isset($classNameParts[1])
316 && $classNameParts[0] === 'TYPO3'
317 && $classNameParts[1] === 'CMS'
318 ) {
319 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[2]);
320 $classNameWithoutVendorAndProduct = $classNameParts[3];
321 } else {
322 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[1]);
323 $classNameWithoutVendorAndProduct = $classNameParts[2];
324
325 if (isset($classNameParts[3])) {
326 $classNameWithoutVendorAndProduct .= $delimiter . $classNameParts[3];
327 }
328 }
329
330 if ($extensionKey && isset($this->packageClassesPaths[$extensionKey])) {
331 if (substr(strtolower($classNameWithoutVendorAndProduct), 0, 5) === 'tests') {
332 $classesPath = $this->packages[$extensionKey]->getPackagePath();
333 } else {
334 $classesPath = $this->packageClassesPaths[$extensionKey];
335 }
336 // Naming convention is to capitalize each part of the path
337 $classNameWithoutVendorAndProduct = ucwords(strtr($classNameWithoutVendorAndProduct, $delimiter, LF));
338 $classFilePath = $classesPath . strtr($classNameWithoutVendorAndProduct, LF, '/') . '.php';
339 if (@file_exists($classFilePath)) {
340 return array($classFilePath, $className);
341 }
342 }
343
344 return NULL;
345 }
346
347 /**
348 * Get cache identifier
349 *
350 * @return string identifier
351 */
352 protected function getCacheIdentifier() {
353 return $this->cacheIdentifier;
354 }
355
356 /**
357 * Get cache entry identifier
358 *
359 * @return string|null identifier
360 */
361 protected function getCacheEntryIdentifier() {
362 return $this->getCacheIdentifier() !== NULL
363 ? 'ClassLoader_' . $this->getCacheIdentifier()
364 : NULL;
365 }
366
367 /**
368 * Set cache identifier
369 *
370 * @param string $cacheIdentifier Cache identifier
371 * @return ClassLoader
372 */
373 public function setCacheIdentifier($cacheIdentifier) {
374 $this->cacheIdentifier = $cacheIdentifier;
375 $this->classAliasMap->setCacheIdentifier($cacheIdentifier);
376 return $this;
377 }
378
379 /**
380 * Sets the available packages
381 *
382 * @param array $packages An array of \TYPO3\Flow\Package\Package objects
383 * @return ClassLoader
384 */
385 public function setPackages(array $packages) {
386 $this->packages = $packages;
387 if (!$this->loadPackageNamespacesFromCache() || !$this->classAliasMap->loadEarlyInstanceMappingFromCache()) {
388 $this->buildPackageNamespacesAndClassesPaths();
389 } else {
390 $this->classAliasMap->setPackages($packages);
391 }
392 // Clear the runtime cache for runtime activated packages
393 $this->runtimeClassLoadingInformationCache = array();
394 return $this;
395 }
396
397 /**
398 * Add a package to class loader just during runtime, so classes can be loaded without the need for a new request
399 *
400 * @param \TYPO3\Flow\Package\PackageInterface $package
401 * @return ClassLoader
402 */
403 public function addRuntimeActivatedPackage(\TYPO3\Flow\Package\PackageInterface $package) {
404 $this->packages[] = $package;
405 $this->buildPackageNamespaceAndClassesPath($package);
406 $this->sortPackageNamespaces();
407 $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache(array($package));
408 return $this;
409 }
410
411 /**
412 * Builds the package namespaces and classes paths for the given packages
413 *
414 * @return void
415 */
416 protected function buildPackageNamespacesAndClassesPaths() {
417 foreach ($this->packages as $package) {
418 $this->buildPackageNamespaceAndClassesPath($package);
419 }
420 $this->sortPackageNamespaces();
421 $this->savePackageNamespacesAndClassesPathsToCache();
422 // The class alias map has to be rebuilt first, because ext_autoload files can contain
423 // old class names that need established class aliases.
424 $classNameToAliasMapping = $this->classAliasMap->setPackages($this->packages)->buildMappingAndInitializeEarlyInstanceMapping();
425 $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache($this->packages);
426 $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
427 $this->transferRuntimeClassInformationCacheEntriesToClassesCache();
428 }
429
430 /**
431 * Builds the namespace and class paths for a single package
432 *
433 * @param \TYPO3\Flow\Package\PackageInterface $package
434 * @return void
435 */
436 protected function buildPackageNamespaceAndClassesPath(\TYPO3\Flow\Package\PackageInterface $package) {
437 if ($package instanceof \TYPO3\Flow\Package\PackageInterface) {
438 $this->buildPackageNamespace($package);
439 }
440 if ($package instanceof PackageInterface) {
441 $this->buildPackageClassPathsForLegacyExtension($package);
442 }
443 }
444
445 /**
446 * Load package namespaces from cache
447 *
448 * @return boolean TRUE if package namespaces were loaded
449 */
450 protected function loadPackageNamespacesFromCache() {
451 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
452 if ($cacheEntryIdentifier !== NULL && $this->coreCache->has($cacheEntryIdentifier)) {
453 list($packageNamespaces, $packageClassesPaths) = $this->coreCache->requireOnce($cacheEntryIdentifier);
454 if (is_array($packageNamespaces) && is_array($packageClassesPaths)) {
455 $this->packageNamespaces = $packageNamespaces;
456 $this->packageClassesPaths = $packageClassesPaths;
457 return TRUE;
458 }
459 }
460 return FALSE;
461 }
462
463 /**
464 * Extracts the namespace from a package
465 *
466 * @param \TYPO3\Flow\Package\PackageInterface $package
467 */
468 protected function buildPackageNamespace(\TYPO3\Flow\Package\PackageInterface $package) {
469 $packageNamespace = $package->getNamespace();
470 // Ignore legacy extensions with unkown vendor name
471 if ($packageNamespace[0] !== '*') {
472 $this->packageNamespaces[$packageNamespace] = array(
473 'namespaceLength' => strlen($packageNamespace),
474 'classesPath' => $package->getClassesPath(),
475 'packagePath' => $package->getPackagePath(),
476 'substituteNamespaceInPath' => ($package instanceof PackageInterface)
477 );
478 }
479 }
480
481 /**
482 * Save autoload registry to cache
483 *
484 * @param array $packages
485 * @return void
486 */
487 protected function loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache(array $packages) {
488 $classFileAutoloadRegistry = array();
489 foreach ($packages as $package) {
490 if ($package instanceof PackageInterface) {
491 $classFilesFromAutoloadRegistry = $package->getClassFilesFromAutoloadRegistry();
492 if (is_array($classFilesFromAutoloadRegistry)) {
493 $classFileAutoloadRegistry = array_merge($classFileAutoloadRegistry, $classFilesFromAutoloadRegistry);
494 }
495 }
496 }
497 foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
498 $lowercasedClassName = strtolower($className);
499 if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName]) && @file_exists($classFilePath)) {
500 $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
501 }
502 }
503 }
504
505 /**
506 * Transfers all entries from the early class information cache to
507 * the classes cache in order to make them persistent
508 */
509 protected function transferRuntimeClassInformationCacheEntriesToClassesCache() {
510 foreach ($this->runtimeClassLoadingInformationCache as $classLoadingInformation) {
511 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
512 if (!$this->classesCache->has($cacheEntryIdentifier)) {
513 $this->classesCache->set($cacheEntryIdentifier, implode("\xff", $classLoadingInformation));
514 }
515 }
516 }
517
518 /**
519 * @param PackageInterface $package
520 * @return void
521 */
522 protected function buildPackageClassPathsForLegacyExtension(PackageInterface $package) {
523 $this->packageClassesPaths[$package->getPackageKey()] = $package->getClassesPath();
524 foreach (array_keys($package->getPackageReplacementKeys()) as $packageToReplace) {
525 $this->packageClassesPaths[$packageToReplace] = $package->getClassesPath();
526 }
527 }
528
529 /**
530 * Save package namespaces and classes paths to cache
531 *
532 * @return void
533 */
534 protected function savePackageNamespacesAndClassesPathsToCache() {
535 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
536 if ($cacheEntryIdentifier !== NULL) {
537 $this->coreCache->set(
538 $this->getCacheEntryIdentifier(),
539 'return ' . var_export(array($this->packageNamespaces, $this->packageClassesPaths), TRUE) . ';'
540 );
541 }
542 }
543
544 /**
545 * Sorts longer package namespaces first, to find specific matches before generic ones
546 *
547 * @return void
548 */
549 protected function sortPackageNamespaces() {
550 $sortPackages = function ($a, $b) {
551 if (($lenA = strlen($a)) === ($lenB = strlen($b))) {
552 return strcmp($a, $b);
553 }
554 return $lenA > $lenB ? -1 : 1;
555 };
556 uksort($this->packageNamespaces, $sortPackages);
557 }
558
559 /**
560 * This method is necessary for the early loading of the cores autoload registry
561 *
562 * @param array $classFileAutoloadRegistry
563 * @return void
564 */
565 public function setRuntimeClassLoadingInformationFromAutoloadRegistry(array $classFileAutoloadRegistry) {
566 foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
567 $lowercasedClassName = strtolower($className);
568 if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
569 $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
570 }
571 }
572 }
573
574 /**
575 * Set alias for class name
576 *
577 * @param string $aliasClassName
578 * @param string $originalClassName
579 * @return boolean
580 */
581 public function setAliasForClassName($aliasClassName, $originalClassName) {
582 return $this->classAliasMap->setAliasForClassName($aliasClassName, $originalClassName);
583 }
584
585 /**
586 * Get class name for alias
587 *
588 * @param string $alias
589 * @return mixed
590 */
591 static public function getClassNameForAlias($alias) {
592 return static::$staticAliasMap->getClassNameForAlias($alias);
593 }
594
595 /**
596 * Get alias for class name
597 *
598 * @param string $className
599 * @deprecated since 6.2, use getAliasesForClassName instead. will be removed 2 versions later
600 * @return mixed
601 */
602 static public function getAliasForClassName($className) {
603 $aliases = static::$staticAliasMap->getAliasesForClassName($className);
604 return is_array($aliases) && isset($aliases[0]) ? $aliases[0] : NULL;
605 }
606
607 /**
608 * Get an aliases for a class name
609 *
610 * @param string $className
611 * @return mixed
612 */
613 static public function getAliasesForClassName($className) {
614 return static::$staticAliasMap->getAliasesForClassName($className);
615 }
616
617 }