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