afb22fa05a394bfa04691fde3c111064e51bd621
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / Action / Ajax / ExtensionScannerScanFile.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Install\Controller\Action\Ajax;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use PhpParser\NodeTraverser;
19 use PhpParser\NodeVisitor\NameResolver;
20 use PhpParser\ParserFactory;
21 use Symfony\Component\Finder\Finder;
22 use Symfony\Component\Finder\SplFileInfo;
23 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Install\ExtensionScanner\Php\CodeStatistics;
26 use TYPO3\CMS\Install\ExtensionScanner\Php\GeneratorClassesResolver;
27 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ArrayDimensionMatcher;
28 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ArrayGlobalMatcher;
29 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ClassConstantMatcher;
30 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ClassNameMatcher;
31 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\InterfaceMethodChangedMatcher;
32 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentDroppedMatcher;
33 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentDroppedStaticMatcher;
34 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentRequiredMatcher;
35 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentUnusedMatcher;
36 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodCallMatcher;
37 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodCallStaticMatcher;
38 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyProtectedMatcher;
39 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyPublicMatcher;
40 use TYPO3\CMS\Install\ExtensionScanner\Php\MatcherFactory;
41 use TYPO3\CMS\Install\UpgradeAnalysis\DocumentationFile;
42
43 /**
44 * Scan a single extension file for breaking / deprecated core code usages
45 */
46 class ExtensionScannerScanFile extends AbstractAjaxAction
47 {
48 /**
49 * @var array Node visitors that implement CodeScannerInterface
50 */
51 protected $matchers = [
52 [
53 'class' => ArrayDimensionMatcher::class,
54 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php',
55 ],
56 [
57 'class' => ArrayGlobalMatcher::class,
58 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ArrayGlobalMatcher.php',
59 ],
60 [
61 'class' => ClassConstantMatcher::class,
62 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ClassConstantMatcher.php',
63 ],
64 [
65 'class' => ClassNameMatcher::class,
66 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php',
67 ],
68 [
69 'class' => InterfaceMethodChangedMatcher::class,
70 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/InterfaceMethodChangedMatcher.php',
71 ],
72 [
73 'class' => MethodArgumentDroppedMatcher::class,
74 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php',
75 ],
76 [
77 'class' => MethodArgumentDroppedStaticMatcher::class,
78 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedStaticMatcher.php',
79 ],
80 [
81 'class' => MethodArgumentRequiredMatcher::class,
82 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentRequiredMatcher.php',
83 ],
84 [
85 'class' => MethodArgumentUnusedMatcher::class,
86 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentUnusedMatcher.php',
87 ],
88 [
89 'class' => MethodCallMatcher::class,
90 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php',
91 ],
92 [
93 'class' => MethodCallStaticMatcher::class,
94 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php',
95 ],
96 [
97 'class' => PropertyProtectedMatcher::class,
98 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyProtectedMatcher.php',
99 ],
100 [
101 'class' => PropertyPublicMatcher::class,
102 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyPublicMatcher.php',
103 ],
104 ];
105
106 /**
107 * Find code violations in a single file
108 *
109 * @return array
110 * @throws \RuntimeException
111 */
112 protected function executeAction(): array
113 {
114 // Get and validate path and file
115 $extension = $this->postValues['extension'];
116 $extensionBasePath = PATH_site . 'typo3conf/ext/' . $extension;
117 if (empty($extension) || !GeneralUtility::isAllowedAbsPath($extensionBasePath)) {
118 throw new \RuntimeException(
119 'Path to extension ' . $extension . ' not allowed.',
120 1499789246
121 );
122 }
123 if (!is_dir($extensionBasePath)) {
124 throw new \RuntimeException(
125 'Extension path ' . $extensionBasePath . ' does not exist or is no directory.',
126 1499789259
127 );
128 }
129 $file = $this->postValues['file'];
130 $absoluteFilePath = $extensionBasePath . '/' . $file;
131 if (empty($file) || !GeneralUtility::isAllowedAbsPath($absoluteFilePath)) {
132 throw new \RuntimeException(
133 'Path to file ' . $file . ' of extension ' . $extension . ' not allowed.',
134 1499789384
135 );
136 }
137 if (!is_file($absoluteFilePath)) {
138 throw new \RuntimeException(
139 'File ' . $file . ' not found or is not a file.',
140 1499789433
141 );
142 }
143
144 $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
145 // Parse PHP file to AST and traverse tree calling visitors
146 $statements = $parser->parse(file_get_contents($absoluteFilePath));
147
148 $traverser = new NodeTraverser();
149 // The built in NameResolver translates class names shortened with 'use' to fully qualified
150 // class names at all places. Incredibly useful for us and added as first visitor.
151 $traverser->addVisitor(new NameResolver());
152 // Understand makeInstance('My\\Package\\Foo\\Bar') as fqdn class name in first argument
153 $traverser->addVisitor(new GeneratorClassesResolver());
154 // Count ignored lines, effective code lines, ...
155 $statistics = new CodeStatistics();
156 $traverser->addVisitor($statistics);
157
158 // Add all configured matcher classes
159 $matcherFactory = new MatcherFactory();
160 $matchers = $matcherFactory->createAll($this->matchers);
161 foreach ($matchers as $matcher) {
162 $traverser->addVisitor($matcher);
163 }
164
165 $traverser->traverse($statements);
166
167 // Gather code matches
168 $matches = [];
169 foreach ($matchers as $matcher) {
170 $matches = array_merge($matches, $matcher->getMatches());
171 }
172
173 // Prepare match output
174 $restFilesBasePath = PATH_site . ExtensionManagementUtility::siteRelPath('core') . 'Documentation/Changelog';
175 $documentationFile = new DocumentationFile();
176 $preparedMatches = [];
177 foreach ($matches as $match) {
178 $preparedHit = [];
179 $preparedHit['uniqueId'] = str_replace('.', '', uniqid((string)mt_rand(), true));
180 $preparedHit['message'] = $match['message'];
181 $preparedHit['line'] = $match['line'];
182 $preparedHit['indicator'] = $match['indicator'];
183 $preparedHit['lineContent'] = $this->getLineFromFile($absoluteFilePath, $match['line']);
184 $preparedHit['restFiles'] = [];
185 foreach ($match['restFiles'] as $fileName) {
186 $finder = new Finder();
187 $restFileLocation = $finder->files()->in($restFilesBasePath)->name($fileName);
188 if ($restFileLocation->count() !== 1) {
189 throw new \RuntimeException(
190 'ResT file ' . $fileName . ' not found or multiple files found.',
191 1499803909
192 );
193 }
194 foreach ($restFileLocation as $restFile) {
195 /** @var SplFileInfo $restFile */
196 $restFileLocation = $restFile->getPathname();
197 break;
198 }
199 $parsedRestFile = array_pop($documentationFile->getListEntry(realpath($restFileLocation)));
200 $version = GeneralUtility::trimExplode('/', $restFileLocation);
201 array_pop($version);
202 // something like "8.2" .. "8.7" .. "master"
203 $parsedRestFile['version'] = array_pop($version);
204 $parsedRestFile['uniqueId'] = str_replace('.', '', uniqid((string)mt_rand(), true));
205 $preparedHit['restFiles'][] = $parsedRestFile;
206 }
207 $preparedMatches[] = $preparedHit;
208 }
209
210 $this->view->assignMultiple([
211 'success' => true,
212 'matches' => $preparedMatches,
213 'isFileIgnored' => $statistics->isFileIgnored(),
214 'effectiveCodeLines' => $statistics->getNumberOfEffectiveCodeLines(),
215 'ignoredLines' => $statistics->getNumberOfIgnoredLines(),
216 ]);
217 return $this->view->render();
218 }
219
220 /**
221 * Find a code line in a file
222 *
223 * @param string $file Absolute path to file
224 * @param int $lineNumber Find this line in file
225 * @return string Code line
226 */
227 protected function getLineFromFile(string $file, int $lineNumber): string
228 {
229 $fileContent = file($file, FILE_IGNORE_NEW_LINES);
230 $line = '';
231 if (isset($fileContent[$lineNumber - 1])) {
232 $line = trim($fileContent[$lineNumber - 1]);
233 }
234 return $line;
235 }
236 }