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