1<?php
2namespace TYPO3\CMS\Core\Core;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use Composer\Autoload\ClassLoader;
18use Composer\Autoload\ClassMapGenerator;
19use TYPO3\CMS\Core\Package\PackageInterface;
20use TYPO3\CMS\Core\Utility\GeneralUtility;
21
22/**
23 * Generates class loading information (class maps, class aliases etc.) and writes it to files
24 * for further inclusion in the bootstrap
25 * @internal
26 */
27class ClassLoadingInformationGenerator
28{
29    /**
30     * @var PackageInterface[]
31     */
32    protected $activeExtensionPackages;
33
34    /**
35     * @var ClassLoader
36     */
37    protected $classLoader;
38
39    /**
40     * @var string
41     */
42    protected $installationRoot;
43
44    /**
45     * @var bool
46     */
47    protected $isDevMode;
48
49    /**
50     * @param ClassLoader $classLoader
51     * @param array $activeExtensionPackages
52     * @param string $installationRoot
53     * @param bool $isDevMode
54     */
55    public function __construct(ClassLoader $classLoader, array $activeExtensionPackages, $installationRoot, $isDevMode = false)
56    {
57        $this->classLoader = $classLoader;
58        $this->activeExtensionPackages = $activeExtensionPackages;
59        $this->installationRoot = $installationRoot;
60        $this->isDevMode = $isDevMode;
61    }
62
63    /**
64     * Returns class loading information for a single package
65     *
66     * @param PackageInterface $package The package to generate the class loading info for
67     * @param bool $useRelativePaths If set to TRUE, make the path relative to the current TYPO3 public web path
68     * @return array
69     */
70    public function buildClassLoadingInformationForPackage(PackageInterface $package, $useRelativePaths = false)
71    {
72        $classMap = [];
73        $psr4 = [];
74        $packagePath = $package->getPackagePath();
75        $manifest = $package->getValueFromComposerManifest();
76
77        if (empty($manifest->autoload)) {
78            // Legacy mode: Scan the complete extension directory for class files
79            $classMap = $this->createClassMap($packagePath, $useRelativePaths, !$this->isDevMode);
80        } else {
81            $autoloadPsr4 = $this->getAutoloadSectionFromManifest($manifest, 'psr-4');
82            if (!empty($autoloadPsr4)) {
83                foreach ($autoloadPsr4 as $namespacePrefix => $paths) {
84                    foreach ((array)$paths as $path) {
85                        $namespacePath = $packagePath . $path;
86                        $namespaceRealPath = realpath($namespacePath);
87                        if ($useRelativePaths) {
88                            $psr4[$namespacePrefix][] = $this->makePathRelative($namespacePath, $namespaceRealPath);
89                        } else {
90                            $psr4[$namespacePrefix][] = $namespacePath;
91                        }
92                        if (!empty($namespaceRealPath) && is_dir($namespaceRealPath)) {
93                            // Add all prs-4 classes to the class map for improved class loading performance
94                            $classMap = array_merge($classMap, $this->createClassMap($namespacePath, $useRelativePaths, false, $namespacePrefix));
95                        }
96                    }
97                }
98            }
99            $autoloadClassmap = $this->getAutoloadSectionFromManifest($manifest, 'classmap');
100            if (!empty($autoloadClassmap)) {
101                foreach ($autoloadClassmap as $path) {
102                    $classMap = array_merge($classMap, $this->createClassMap($packagePath . $path, $useRelativePaths));
103                }
104            }
105        }
106
107        return ['classMap' => $classMap, 'psr-4' => $psr4];
108    }
109
110    /**
111     * Fetches class loading info from the according section from the manifest file.
112     * Development information will be extracted and merged as well.
113     *
114     * @param \stdClass $manifest
115     * @param string $section
116     * @return array
117     */
118    protected function getAutoloadSectionFromManifest($manifest, $section)
119    {
120        $finalAutoloadSection = [];
121        $autoloadDefinition = json_decode(json_encode($manifest->autoload), true);
122        if (!empty($autoloadDefinition[$section]) && is_array($autoloadDefinition[$section])) {
123            $finalAutoloadSection = $autoloadDefinition[$section];
124        }
125        if ($this->isDevMode) {
126            if (isset($manifest->{'autoload-dev'})) {
127                $autoloadDefinitionDev = json_decode(json_encode($manifest->{'autoload-dev'}), true);
128                if (!empty($autoloadDefinitionDev[$section]) && is_array($autoloadDefinitionDev[$section])) {
129                    $finalAutoloadSection = array_merge($finalAutoloadSection, $autoloadDefinitionDev[$section]);
130                }
131            }
132        }
133
134        return $finalAutoloadSection;
135    }
136
137    /**
138     * Creates a class map for a given (absolute) path
139     *
140     * @param string $classesPath
141     * @param bool $useRelativePaths
142     * @param bool $ignorePotentialTestClasses
143     * @param string $namespace
144     * @return array
145     */
146    protected function createClassMap($classesPath, $useRelativePaths = false, $ignorePotentialTestClasses = false, $namespace = null)
147    {
148        $classMap = [];
149        $blacklistExpression = null;
150        if ($ignorePotentialTestClasses) {
151            $blacklistPathPrefix = realpath($classesPath);
152            $blacklistPathPrefix = str_replace('\\', '/', $blacklistPathPrefix);
153            $blacklistExpression = "{($blacklistPathPrefix/tests/|$blacklistPathPrefix/Tests/|$blacklistPathPrefix/Resources/|$blacklistPathPrefix/res/|$blacklistPathPrefix/class.ext_update.php)}";
154        }
155        foreach (ClassMapGenerator::createMap($classesPath, $blacklistExpression, null, $namespace) as $class => $path) {
156            if ($useRelativePaths) {
157                $classMap[$class] = $this->makePathRelative($classesPath, $path);
158            } else {
159                $classMap[$class] = $path;
160            }
161        }
162        return $classMap;
163    }
164
165    /**
166     * Returns class alias map for given package
167     *
168     * @param PackageInterface $package The package to generate the class alias info for
169     * @throws \TYPO3\CMS\Core\Error\Exception
170     * @return array
171     */
172    public function buildClassAliasMapForPackage(PackageInterface $package)
173    {
174        $aliasToClassNameMapping = [];
175        $classNameToAliasMapping = [];
176        $possibleClassAliasFiles = [];
177        $manifest = $package->getValueFromComposerManifest();
178        if (!empty($manifest->extra->{'typo3/class-alias-loader'}->{'class-alias-maps'})) {
179            $possibleClassAliasFiles = $manifest->extra->{'typo3/class-alias-loader'}->{'class-alias-maps'};
180            if (!is_array($possibleClassAliasFiles)) {
181                throw new \TYPO3\CMS\Core\Error\Exception('"typo3/class-alias-loader"/"class-alias-maps" must return an array!', 1444142481);
182            }
183        } else {
184            $possibleClassAliasFiles[] = 'Migrations/Code/ClassAliasMap.php';
185        }
186        $packagePath = $package->getPackagePath();
187        foreach ($possibleClassAliasFiles as $possibleClassAliasFile) {
188            $possiblePathToClassAliasFile = $packagePath . $possibleClassAliasFile;
189            if (file_exists($possiblePathToClassAliasFile)) {
190                $packageAliasMap = require $possiblePathToClassAliasFile;
191                if (!is_array($packageAliasMap)) {
192                    throw new \TYPO3\CMS\Core\Error\Exception('"class alias maps" must return an array', 1422625075);
193                }
194                foreach ($packageAliasMap as $aliasClassName => $className) {
195                    $lowerCasedAliasClassName = strtolower($aliasClassName);
196                    $aliasToClassNameMapping[$lowerCasedAliasClassName] = $className;
197                    $classNameToAliasMapping[$className][$lowerCasedAliasClassName] = $lowerCasedAliasClassName;
198                }
199            }
200        }
201
202        return ['aliasToClassNameMapping' => $aliasToClassNameMapping, 'classNameToAliasMapping' => $classNameToAliasMapping];
203    }
204
205    /**
206     * Generate the class map file
207     * @return string[]
208     * @internal
209     */
210    public function buildAutoloadInformationFiles()
211    {
212        $psr4File = $classMapFile = <<<EOF
213<?php
214
215// autoload_classmap.php @generated by TYPO3
216
217\$typo3InstallDir = \TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/';
218
219return array(
220
221EOF;
222        $classMap = [];
223        $psr4 = [];
224        foreach ($this->activeExtensionPackages as $package) {
225            $classLoadingInformation = $this->buildClassLoadingInformationForPackage($package, true);
226            $classMap = array_merge($classMap, $classLoadingInformation['classMap']);
227            $psr4 = array_merge($psr4, $classLoadingInformation['psr-4']);
228        }
229
230        ksort($classMap);
231        ksort($psr4);
232        foreach ($classMap as $class => $relativePath) {
233            $classMapFile .= sprintf('    %s => %s,', var_export($class, true), $this->getPathCode($relativePath)) . "\n";
234        }
235        $classMapFile .= ");\n";
236
237        foreach ($psr4 as $prefix => $relativePaths) {
238            $psr4File .= sprintf('    %s => array(%s),', var_export($prefix, true), implode(',', array_map([$this, 'getPathCode'], $relativePaths))) . "\n";
239        }
240        $psr4File .= ");\n";
241
242        return ['classMapFile' => $classMapFile, 'psr-4File' => $psr4File];
243    }
244
245    /**
246     * Generate a relative path string from an absolute path within a give package path
247     *
248     * @param string $packagePath
249     * @param string $realPathOfClassFile
250     * @param bool $relativeToRoot
251     * @return string
252     */
253    protected function makePathRelative($packagePath, $realPathOfClassFile, $relativeToRoot = true)
254    {
255        $realPathOfClassFile = GeneralUtility::fixWindowsFilePath($realPathOfClassFile);
256        $packageRealPath = GeneralUtility::fixWindowsFilePath(realpath($packagePath));
257        $relativePackagePath = rtrim(substr($packagePath, strlen($this->installationRoot)), '/');
258        if ($relativeToRoot) {
259            if ($realPathOfClassFile === $packageRealPath) {
260                $relativePathToClassFile = $relativePackagePath;
261            } else {
262                $relativePathToClassFile = $relativePackagePath . '/' . ltrim(substr($realPathOfClassFile, strlen($packageRealPath)), '/');
263            }
264        } else {
265            $relativePathToClassFile = ltrim(substr($realPathOfClassFile, strlen($packageRealPath)), '/');
266        }
267
268        return $relativePathToClassFile;
269    }
270
271    /**
272     * Generate a relative path string from a relative path
273     *
274     * @param string $relativePathToClassFile
275     * @return string
276     */
277    protected function getPathCode($relativePathToClassFile)
278    {
279        return '$typo3InstallDir . ' . var_export($relativePathToClassFile, true);
280    }
281
282    /**
283     * Build class alias mapping file
284     *
285     * @return string
286     * @throws \Exception
287     * @internal
288     */
289    public function buildClassAliasMapFile()
290    {
291        $aliasToClassNameMapping = [];
292        $classNameToAliasMapping = [];
293        foreach ($this->activeExtensionPackages as $package) {
294            $aliasMappingForPackage = $this->buildClassAliasMapForPackage($package);
295            $aliasToClassNameMapping = array_merge($aliasToClassNameMapping, $aliasMappingForPackage['aliasToClassNameMapping']);
296            $classNameToAliasMapping = array_merge($classNameToAliasMapping, $aliasMappingForPackage['classNameToAliasMapping']);
297        }
298        $exportArray = [
299            'aliasToClassNameMapping' => $aliasToClassNameMapping,
300            'classNameToAliasMapping' => $classNameToAliasMapping
301        ];
302        $fileContent = "<?php\nreturn ";
303        $fileContent .= var_export($exportArray, true);
304        $fileContent .= ";\n";
305        return $fileContent;
306    }
307}
308