6fd14d490ffce2b801055b2ebc236d2eb7d316f1
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / UpgradeController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Install\Controller;
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 Psr\Http\Message\ResponseInterface;
22 use Psr\Http\Message\ServerRequestInterface;
23 use Symfony\Component\Finder\Finder;
24 use Symfony\Component\Finder\SplFileInfo;
25 use TYPO3\CMS\Core\Core\Environment;
26 use TYPO3\CMS\Core\Database\ConnectionPool;
27 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
28 use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
29 use TYPO3\CMS\Core\Http\JsonResponse;
30 use TYPO3\CMS\Core\Messaging\FlashMessage;
31 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
32 use TYPO3\CMS\Core\Migrations\TcaMigration;
33 use TYPO3\CMS\Core\Package\Package;
34 use TYPO3\CMS\Core\Package\PackageManager;
35 use TYPO3\CMS\Core\Registry;
36 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
37 use TYPO3\CMS\Core\Utility\GeneralUtility;
38 use TYPO3\CMS\Install\ExtensionScanner\Php\CodeStatistics;
39 use TYPO3\CMS\Install\ExtensionScanner\Php\GeneratorClassesResolver;
40 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ArrayDimensionMatcher;
41 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ArrayGlobalMatcher;
42 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ClassConstantMatcher;
43 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ClassNameMatcher;
44 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ConstantMatcher;
45 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\FunctionCallMatcher;
46 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\InterfaceMethodChangedMatcher;
47 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodAnnotationMatcher;
48 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentDroppedMatcher;
49 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentDroppedStaticMatcher;
50 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentRequiredMatcher;
51 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentRequiredStaticMatcher;
52 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodArgumentUnusedMatcher;
53 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodCallMatcher;
54 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\MethodCallStaticMatcher;
55 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyAnnotationMatcher;
56 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyExistsStaticMatcher;
57 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyProtectedMatcher;
58 use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyPublicMatcher;
59 use TYPO3\CMS\Install\ExtensionScanner\Php\MatcherFactory;
60 use TYPO3\CMS\Install\Service\CoreUpdateService;
61 use TYPO3\CMS\Install\Service\CoreVersionService;
62 use TYPO3\CMS\Install\Service\LoadTcaService;
63 use TYPO3\CMS\Install\Service\UpgradeWizardsService;
64 use TYPO3\CMS\Install\UpgradeAnalysis\DocumentationFile;
65
66 /**
67 * Upgrade controller
68 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
69 */
70 class UpgradeController extends AbstractController
71 {
72 /**
73 * @var CoreUpdateService
74 */
75 protected $coreUpdateService;
76
77 /**
78 * @var CoreVersionService
79 */
80 protected $coreVersionService;
81
82 /**
83 * @var PackageManager
84 */
85 protected $packageManager;
86
87 /**
88 * @param PackageManager|null $packageManager
89 */
90 public function __construct(PackageManager $packageManager = null)
91 {
92 $this->packageManager = $packageManager ?? GeneralUtility::makeInstance(PackageManager::class);
93 }
94
95 /**
96 * Matcher registry of extension scanner.
97 * Node visitors that implement CodeScannerInterface
98 *
99 * @var array
100 */
101 protected $matchers = [
102 [
103 'class' => ArrayDimensionMatcher::class,
104 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php',
105 ],
106 [
107 'class' => ArrayGlobalMatcher::class,
108 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ArrayGlobalMatcher.php',
109 ],
110 [
111 'class' => ClassConstantMatcher::class,
112 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ClassConstantMatcher.php',
113 ],
114 [
115 'class' => ClassNameMatcher::class,
116 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php',
117 ],
118 [
119 'class' => ConstantMatcher::class,
120 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ConstantMatcher.php',
121 ],
122 [
123 'class' => PropertyAnnotationMatcher::class,
124 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyAnnotationMatcher.php',
125 ],
126 [
127 'class' => MethodAnnotationMatcher::class,
128 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodAnnotationMatcher.php',
129 ],
130 [
131 'class' => FunctionCallMatcher::class,
132 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/FunctionCallMatcher.php',
133 ],
134 [
135 'class' => InterfaceMethodChangedMatcher::class,
136 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/InterfaceMethodChangedMatcher.php',
137 ],
138 [
139 'class' => MethodArgumentDroppedMatcher::class,
140 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php',
141 ],
142 [
143 'class' => MethodArgumentDroppedStaticMatcher::class,
144 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedStaticMatcher.php',
145 ],
146 [
147 'class' => MethodArgumentRequiredMatcher::class,
148 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentRequiredMatcher.php',
149 ],
150 [
151 'class' => MethodArgumentRequiredStaticMatcher::class,
152 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentRequiredStaticMatcher.php',
153 ],
154 [
155 'class' => MethodArgumentUnusedMatcher::class,
156 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentUnusedMatcher.php',
157 ],
158 [
159 'class' => MethodCallMatcher::class,
160 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php',
161 ],
162 [
163 'class' => MethodCallStaticMatcher::class,
164 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php',
165 ],
166 [
167 'class' => PropertyExistsStaticMatcher::class,
168 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyExistsStaticMatcher.php'
169 ],
170 [
171 'class' => PropertyProtectedMatcher::class,
172 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyProtectedMatcher.php',
173 ],
174 [
175 'class' => PropertyPublicMatcher::class,
176 'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyPublicMatcher.php',
177 ],
178 ];
179
180 /**
181 * Main "show the cards" view
182 *
183 * @param ServerRequestInterface $request
184 * @return ResponseInterface
185 */
186 public function cardsAction(ServerRequestInterface $request): ResponseInterface
187 {
188 $view = $this->initializeStandaloneView($request, 'Upgrade/Cards.html');
189 return new JsonResponse([
190 'success' => true,
191 'html' => $view->render(),
192 ]);
193 }
194
195 /**
196 * Activate a new core
197 *
198 * @param ServerRequestInterface $request
199 * @return ResponseInterface
200 */
201 public function coreUpdateActivateAction(ServerRequestInterface $request): ResponseInterface
202 {
203 $this->coreUpdateInitialize();
204 return new JsonResponse([
205 'success' => $this->coreUpdateService->activateVersion($this->coreUpdateGetVersionToHandle($request)),
206 'status' => $this->coreUpdateService->getMessages(),
207 ]);
208 }
209
210 /**
211 * Check if core update is possible
212 *
213 * @param ServerRequestInterface $request
214 * @return ResponseInterface
215 */
216 public function coreUpdateCheckPreConditionsAction(ServerRequestInterface $request): ResponseInterface
217 {
218 $this->coreUpdateInitialize();
219 return new JsonResponse([
220 'success' => $this->coreUpdateService->checkPreConditions($this->coreUpdateGetVersionToHandle($request)),
221 'status' => $this->coreUpdateService->getMessages(),
222 ]);
223 }
224
225 /**
226 * Download new core
227 *
228 * @param ServerRequestInterface $request
229 * @return ResponseInterface
230 */
231 public function coreUpdateDownloadAction(ServerRequestInterface $request): ResponseInterface
232 {
233 $this->coreUpdateInitialize();
234 return new JsonResponse([
235 'success' => $this->coreUpdateService->downloadVersion($this->coreUpdateGetVersionToHandle($request)),
236 'status' => $this->coreUpdateService->getMessages(),
237 ]);
238 }
239
240 /**
241 * Core Update Get Data Action
242 *
243 * @param ServerRequestInterface $request
244 * @return ResponseInterface
245 */
246 public function coreUpdateGetDataAction(ServerRequestInterface $request): ResponseInterface
247 {
248 $view = $this->initializeStandaloneView($request, 'Upgrade/CoreUpdate.html');
249 $coreUpdateService = GeneralUtility::makeInstance(CoreUpdateService::class);
250 $coreVersionService = GeneralUtility::makeInstance(CoreVersionService::class);
251 $view->assignMultiple([
252 'coreUpdateEnabled' => $coreUpdateService->isCoreUpdateEnabled(),
253 'coreUpdateComposerMode' => Environment::isComposerMode(),
254 'coreUpdateIsReleasedVersion' => $coreVersionService->isInstalledVersionAReleasedVersion(),
255 'coreUpdateIsSymLinkedCore' => is_link(Environment::getPublicPath() . '/typo3_src'),
256 ]);
257 return new JsonResponse([
258 'success' => true,
259 'html' => $view->render(),
260 ]);
261 }
262
263 /**
264 * Check for new core
265 *
266 * @return ResponseInterface
267 */
268 public function coreUpdateIsUpdateAvailableAction(): ResponseInterface
269 {
270 $this->coreUpdateInitialize();
271 $messageQueue = new FlashMessageQueue('install');
272 if ($this->coreVersionService->isInstalledVersionAReleasedVersion()) {
273 $isDevelopmentUpdateAvailable = $this->coreVersionService->isYoungerPatchDevelopmentReleaseAvailable();
274 $isUpdateAvailable = $this->coreVersionService->isYoungerPatchReleaseAvailable();
275 $isUpdateSecurityRelevant = $this->coreVersionService->isUpdateSecurityRelevant();
276 if (!$isUpdateAvailable && !$isDevelopmentUpdateAvailable) {
277 $messageQueue->enqueue(new FlashMessage(
278 '',
279 'No regular update available',
280 FlashMessage::NOTICE
281 ));
282 } elseif ($isUpdateAvailable) {
283 $newVersion = $this->coreVersionService->getYoungestPatchRelease();
284 if ($isUpdateSecurityRelevant) {
285 $messageQueue->enqueue(new FlashMessage(
286 '',
287 'Update to security relevant released version ' . $newVersion . ' is available!',
288 FlashMessage::WARNING
289 ));
290 $action = ['title' => 'Update now', 'action' => 'updateRegular'];
291 } else {
292 $messageQueue->enqueue(new FlashMessage(
293 '',
294 'Update to regular released version ' . $newVersion . ' is available!',
295 FlashMessage::INFO
296 ));
297 $action = ['title' => 'Update now', 'action' => 'updateRegular'];
298 }
299 } elseif ($isDevelopmentUpdateAvailable) {
300 $newVersion = $this->coreVersionService->getYoungestPatchDevelopmentRelease();
301 $messageQueue->enqueue(new FlashMessage(
302 '',
303 'Update to development release ' . $newVersion . ' is available!',
304 FlashMessage::INFO
305 ));
306 $action = ['title' => 'Update now', 'action' => 'updateDevelopment'];
307 }
308 } else {
309 $messageQueue->enqueue(new FlashMessage(
310 '',
311 'Current version is a development version and can not be updated',
312 FlashMessage::WARNING
313 ));
314 }
315 $responseData = [
316 'success' => true,
317 'status' => $messageQueue,
318 ];
319 if (isset($action)) {
320 $responseData['action'] = $action;
321 }
322 return new JsonResponse($responseData);
323 }
324
325 /**
326 * Move core to new location
327 *
328 * @param ServerRequestInterface $request
329 * @return ResponseInterface
330 */
331 public function coreUpdateMoveAction(ServerRequestInterface $request): ResponseInterface
332 {
333 $this->coreUpdateInitialize();
334 return new JsonResponse([
335 'success' => $this->coreUpdateService->moveVersion($this->coreUpdateGetVersionToHandle($request)),
336 'status' => $this->coreUpdateService->getMessages(),
337 ]);
338 }
339
340 /**
341 * Unpack a downloaded core
342 *
343 * @param ServerRequestInterface $request
344 * @return ResponseInterface
345 */
346 public function coreUpdateUnpackAction(ServerRequestInterface $request): ResponseInterface
347 {
348 $this->coreUpdateInitialize();
349 return new JsonResponse([
350 'success' => $this->coreUpdateService->unpackVersion($this->coreUpdateGetVersionToHandle($request)),
351 'status' => $this->coreUpdateService->getMessages(),
352 ]);
353 }
354
355 /**
356 * Verify downloaded core checksum
357 *
358 * @param ServerRequestInterface $request
359 * @return ResponseInterface
360 */
361 public function coreUpdateVerifyChecksumAction(ServerRequestInterface $request): ResponseInterface
362 {
363 $this->coreUpdateInitialize();
364 return new JsonResponse([
365 'success' => $this->coreUpdateService->verifyFileChecksum($this->coreUpdateGetVersionToHandle($request)),
366 'status' => $this->coreUpdateService->getMessages(),
367 ]);
368 }
369
370 /**
371 * Get list of loaded extensions
372 *
373 * @param ServerRequestInterface $request
374 * @return ResponseInterface
375 */
376 public function extensionCompatTesterLoadedExtensionListAction(ServerRequestInterface $request): ResponseInterface
377 {
378 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
379 $view = $this->initializeStandaloneView($request, 'Upgrade/ExtensionCompatTester.html');
380 $view->assignMultiple([
381 'extensionCompatTesterLoadExtLocalconfToken' => $formProtection->generateToken('installTool', 'extensionCompatTesterLoadExtLocalconf'),
382 'extensionCompatTesterLoadExtTablesToken' => $formProtection->generateToken('installTool', 'extensionCompatTesterLoadExtTables'),
383 'extensionCompatTesterUninstallToken' => $formProtection->generateToken('installTool', 'extensionCompatTesterUninstallExtension'),
384 ]);
385
386 return new JsonResponse([
387 'success' => true,
388 'extensions' => array_keys($this->packageManager->getActivePackages()),
389 'html' => $view->render(),
390 ]);
391 }
392
393 /**
394 * Load all ext_localconf files in order until given extension name
395 *
396 * @param ServerRequestInterface $request
397 * @return ResponseInterface
398 */
399 public function extensionCompatTesterLoadExtLocalconfAction(ServerRequestInterface $request): ResponseInterface
400 {
401 $extension = $request->getParsedBody()['install']['extension'];
402 foreach ($this->packageManager->getActivePackages() as $package) {
403 $this->extensionCompatTesterLoadExtLocalconfForExtension($package);
404 if ($package->getPackageKey() === $extension) {
405 break;
406 }
407 }
408 return new JsonResponse([
409 'success' => true,
410 ]);
411 }
412
413 /**
414 * Load all ext_localconf files in order until given extension name
415 *
416 * @param ServerRequestInterface $request
417 * @return ResponseInterface
418 */
419 public function extensionCompatTesterLoadExtTablesAction(ServerRequestInterface $request): ResponseInterface
420 {
421 $extension = $request->getParsedBody()['install']['extension'];
422 $activePackages = $this->packageManager->getActivePackages();
423 foreach ($activePackages as $package) {
424 // Load all ext_localconf files first
425 $this->extensionCompatTesterLoadExtLocalconfForExtension($package);
426 }
427 foreach ($activePackages as $package) {
428 $this->extensionCompatTesterLoadExtTablesForExtension($package);
429 if ($package->getPackageKey() === $extension) {
430 break;
431 }
432 }
433 return new JsonResponse([
434 'success' => true,
435 ]);
436 }
437
438 /**
439 * Unload one extension
440 *
441 * @param ServerRequestInterface $request
442 * @return ResponseInterface
443 * @throws \RuntimeException
444 */
445 public function extensionCompatTesterUninstallExtensionAction(ServerRequestInterface $request): ResponseInterface
446 {
447 $extension = $request->getParsedBody()['install']['extension'];
448 if (empty($extension)) {
449 throw new \RuntimeException(
450 'No extension given',
451 1505407269
452 );
453 }
454 $messageQueue = new FlashMessageQueue('install');
455 if (ExtensionManagementUtility::isLoaded($extension)) {
456 try {
457 ExtensionManagementUtility::unloadExtension($extension);
458 $messageQueue->enqueue(new FlashMessage(
459 'Extension "' . $extension . '" unloaded.',
460 '',
461 FlashMessage::ERROR
462 ));
463 } catch (\Exception $e) {
464 $messageQueue->enqueue(new FlashMessage(
465 $e->getMessage(),
466 '',
467 FlashMessage::ERROR
468 ));
469 }
470 }
471 return new JsonResponse([
472 'success' => true,
473 'status' => $messageQueue,
474 ]);
475 }
476
477 /**
478 * Create Extension Scanner Data action
479 *
480 * @param ServerRequestInterface $request
481 * @return ResponseInterface
482 */
483 public function extensionScannerGetDataAction(ServerRequestInterface $request): ResponseInterface
484 {
485 $extensionsInTypo3conf = (new Finder())->directories()->in(Environment::getExtensionsPath())->depth(0)->sortByName();
486 $view = $this->initializeStandaloneView($request, 'Upgrade/ExtensionScanner.html');
487 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
488 $view->assignMultiple([
489 'extensionScannerExtensionList' => $extensionsInTypo3conf,
490 'extensionScannerFilesToken' => $formProtection->generateToken('installTool', 'extensionScannerFiles'),
491 'extensionScannerScanFileToken' => $formProtection->generateToken('installTool', 'extensionScannerScanFile'),
492 'extensionScannerMarkFullyScannedRestFilesToken' => $formProtection->generateToken('installTool', 'extensionScannerMarkFullyScannedRestFiles'),
493 ]);
494 return new JsonResponse([
495 'success' => true,
496 'html' => $view->render(),
497 ]);
498 }
499
500 /**
501 * Return a list of files of an extension
502 *
503 * @param ServerRequestInterface $request
504 * @return ResponseInterface
505 */
506 public function extensionScannerFilesAction(ServerRequestInterface $request): ResponseInterface
507 {
508 // Get and validate path
509 $extension = $request->getParsedBody()['install']['extension'];
510 $extensionBasePath = Environment::getExtensionsPath() . '/' . $extension;
511 if (empty($extension) || !GeneralUtility::isAllowedAbsPath($extensionBasePath)) {
512 throw new \RuntimeException(
513 'Path to extension ' . $extension . ' not allowed.',
514 1499777261
515 );
516 }
517 if (!is_dir($extensionBasePath)) {
518 throw new \RuntimeException(
519 'Extension path ' . $extensionBasePath . ' does not exist or is no directory.',
520 1499777330
521 );
522 }
523
524 $finder = new Finder();
525 $files = $finder->files()->in($extensionBasePath)->name('*.php')->sortByName();
526 // A list of file names relative to extension directory
527 $relativeFileNames = [];
528 foreach ($files as $file) {
529 /** @var SplFileInfo $file */
530 $relativeFileNames[] = GeneralUtility::fixWindowsFilePath($file->getRelativePathname());
531 }
532 return new JsonResponse([
533 'success' => true,
534 'files' => $relativeFileNames,
535 ]);
536 }
537
538 /**
539 * Ajax controller, part of "extension scanner". Called at the end of "scan all"
540 * as last action. Gets a list of RST file hashes that matched, goes through all
541 * existing RST files, finds those marked as "FullyScanned" and marks those that
542 * did not had any matches as "you are not affected".
543 *
544 * @param ServerRequestInterface $request
545 * @return ResponseInterface
546 */
547 public function extensionScannerMarkFullyScannedRestFilesAction(ServerRequestInterface $request): ResponseInterface
548 {
549 $foundRestFileHashes = (array)$request->getParsedBody()['install']['hashes'];
550 // First un-mark files marked as scanned-ok
551 $registry = new Registry();
552 $registry->removeAllByNamespace('extensionScannerNotAffected');
553 // Find all .rst files (except those from v8), see if they are tagged with "FullyScanned"
554 // and if their content is not in incoming "hashes" array, mark as "not affected"
555 $documentationFile = new DocumentationFile();
556 $finder = new Finder();
557 $restFilesBasePath = ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog';
558 $restFiles = $finder->files()->in($restFilesBasePath);
559 $fullyScannedRestFilesNotAffected = [];
560 foreach ($restFiles as $restFile) {
561 // Skip files in "8.x" directory
562 /** @var SplFileInfo $restFile */
563 if (strpos($restFile->getRelativePath(), '8') === 0) {
564 continue;
565 }
566
567 // Build array of file (hashes) not affected by current scan, if they are tagged as "FullyScanned"
568 $parsedRestFile = array_pop($documentationFile->getListEntry(str_replace(
569 '\\',
570 '/',
571 realpath($restFile->getPathname())
572 )));
573 if (!in_array($parsedRestFile['file_hash'], $foundRestFileHashes, true)
574 && in_array('FullyScanned', $parsedRestFile['tags'], true)
575 ) {
576 $fullyScannedRestFilesNotAffected[] = $parsedRestFile['file_hash'];
577 }
578 }
579 foreach ($fullyScannedRestFilesNotAffected as $fileHash) {
580 $registry->set('extensionScannerNotAffected', $fileHash, $fileHash);
581 }
582 return new JsonResponse([
583 'success' => true,
584 'markedAsNotAffected' => count($fullyScannedRestFilesNotAffected),
585 ]);
586 }
587
588 /**
589 * Scan a single extension file for breaking / deprecated core code usages
590 *
591 * @param ServerRequestInterface $request
592 * @return ResponseInterface
593 */
594 public function extensionScannerScanFileAction(ServerRequestInterface $request): ResponseInterface
595 {
596 // Get and validate path and file
597 $extension = $request->getParsedBody()['install']['extension'];
598 $extensionBasePath = Environment::getExtensionsPath() . '/' . $extension;
599 if (empty($extension) || !GeneralUtility::isAllowedAbsPath($extensionBasePath)) {
600 throw new \RuntimeException(
601 'Path to extension ' . $extension . ' not allowed.',
602 1499789246
603 );
604 }
605 if (!is_dir($extensionBasePath)) {
606 throw new \RuntimeException(
607 'Extension path ' . $extensionBasePath . ' does not exist or is no directory.',
608 1499789259
609 );
610 }
611 $file = $request->getParsedBody()['install']['file'];
612 $absoluteFilePath = $extensionBasePath . '/' . $file;
613 if (empty($file) || !GeneralUtility::isAllowedAbsPath($absoluteFilePath)) {
614 throw new \RuntimeException(
615 'Path to file ' . $file . ' of extension ' . $extension . ' not allowed.',
616 1499789384
617 );
618 }
619 if (!is_file($absoluteFilePath)) {
620 throw new \RuntimeException(
621 'File ' . $file . ' not found or is not a file.',
622 1499789433
623 );
624 }
625
626 $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
627 // Parse PHP file to AST and traverse tree calling visitors
628 $statements = $parser->parse(file_get_contents($absoluteFilePath));
629
630 $traverser = new NodeTraverser();
631 // The built in NameResolver translates class names shortened with 'use' to fully qualified
632 // class names at all places. Incredibly useful for us and added as first visitor.
633 $traverser->addVisitor(new NameResolver());
634 // Understand makeInstance('My\\Package\\Foo\\Bar') as fqdn class name in first argument
635 $traverser->addVisitor(new GeneratorClassesResolver());
636 // Count ignored lines, effective code lines, ...
637 $statistics = new CodeStatistics();
638 $traverser->addVisitor($statistics);
639
640 // Add all configured matcher classes
641 $matcherFactory = new MatcherFactory();
642 $matchers = $matcherFactory->createAll($this->matchers);
643 foreach ($matchers as $matcher) {
644 $traverser->addVisitor($matcher);
645 }
646
647 $traverser->traverse($statements);
648
649 // Gather code matches
650 $matches = [[]];
651 foreach ($matchers as $matcher) {
652 /** @var \TYPO3\CMS\Install\ExtensionScanner\CodeScannerInterface $matcher */
653 $matches[] = $matcher->getMatches();
654 }
655 $matches = array_merge(...$matches);
656
657 // Prepare match output
658 $restFilesBasePath = ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog';
659 $documentationFile = new DocumentationFile();
660 $preparedMatches = [];
661 foreach ($matches as $match) {
662 $preparedHit = [];
663 $preparedHit['uniqueId'] = str_replace('.', '', uniqid((string)mt_rand(), true));
664 $preparedHit['message'] = $match['message'];
665 $preparedHit['line'] = $match['line'];
666 $preparedHit['indicator'] = $match['indicator'];
667 $preparedHit['lineContent'] = $this->extensionScannerGetLineFromFile($absoluteFilePath, $match['line']);
668 $preparedHit['restFiles'] = [];
669 foreach ($match['restFiles'] as $fileName) {
670 $finder = new Finder();
671 $restFileLocation = $finder->files()->in($restFilesBasePath)->name($fileName);
672 if ($restFileLocation->count() !== 1) {
673 throw new \RuntimeException(
674 'ResT file ' . $fileName . ' not found or multiple files found.',
675 1499803909
676 );
677 }
678 foreach ($restFileLocation as $restFile) {
679 /** @var SplFileInfo $restFile */
680 $restFileLocation = $restFile->getPathname();
681 break;
682 }
683 $parsedRestFile = array_pop($documentationFile->getListEntry(str_replace(
684 '\\',
685 '/',
686 realpath($restFileLocation)
687 )));
688 $version = GeneralUtility::trimExplode(DIRECTORY_SEPARATOR, $restFileLocation);
689 array_pop($version);
690 // something like "8.2" .. "8.7" .. "master"
691 $parsedRestFile['version'] = array_pop($version);
692 $parsedRestFile['uniqueId'] = str_replace('.', '', uniqid((string)mt_rand(), true));
693 $preparedHit['restFiles'][] = $parsedRestFile;
694 }
695 $preparedMatches[] = $preparedHit;
696 }
697 return new JsonResponse([
698 'success' => true,
699 'matches' => $preparedMatches,
700 'isFileIgnored' => $statistics->isFileIgnored(),
701 'effectiveCodeLines' => $statistics->getNumberOfEffectiveCodeLines(),
702 'ignoredLines' => $statistics->getNumberOfIgnoredLines(),
703 ]);
704 }
705
706 /**
707 * Check if loading ext_tables.php files still changes TCA
708 *
709 * @param ServerRequestInterface $request
710 * @return ResponseInterface
711 */
712 public function tcaExtTablesCheckAction(ServerRequestInterface $request): ResponseInterface
713 {
714 $view = $this->initializeStandaloneView($request, 'Upgrade/TcaExtTablesCheck.html');
715 $messageQueue = new FlashMessageQueue('install');
716 $loadTcaService = GeneralUtility::makeInstance(LoadTcaService::class);
717 $loadTcaService->loadExtensionTablesWithoutMigration();
718 $baseTca = $GLOBALS['TCA'];
719 foreach ($this->packageManager->getActivePackages() as $package) {
720 $extensionKey = $package->getPackageKey();
721 $extTablesPath = $package->getPackagePath() . 'ext_tables.php';
722 if (@file_exists($extTablesPath)) {
723 $loadTcaService->loadSingleExtTablesFile($extensionKey);
724 $newTca = $GLOBALS['TCA'];
725 if ($newTca !== $baseTca) {
726 $messageQueue->enqueue(new FlashMessage(
727 '',
728 $extensionKey,
729 FlashMessage::NOTICE
730 ));
731 }
732 $baseTca = $newTca;
733 }
734 }
735 return new JsonResponse([
736 'success' => true,
737 'status' => $messageQueue,
738 'html' => $view->render(),
739 ]);
740 }
741
742 /**
743 * Check TCA for needed migrations
744 *
745 * @param ServerRequestInterface $request
746 * @return ResponseInterface
747 */
748 public function tcaMigrationsCheckAction(ServerRequestInterface $request): ResponseInterface
749 {
750 $view = $this->initializeStandaloneView($request, 'Upgrade/TcaMigrationsCheck.html');
751 $messageQueue = new FlashMessageQueue('install');
752 GeneralUtility::makeInstance(LoadTcaService::class)->loadExtensionTablesWithoutMigration();
753 $tcaMigration = GeneralUtility::makeInstance(TcaMigration::class);
754 $GLOBALS['TCA'] = $tcaMigration->migrate($GLOBALS['TCA']);
755 $tcaMessages = $tcaMigration->getMessages();
756 foreach ($tcaMessages as $tcaMessage) {
757 $messageQueue->enqueue(new FlashMessage(
758 '',
759 $tcaMessage,
760 FlashMessage::NOTICE
761 ));
762 }
763 return new JsonResponse([
764 'success' => true,
765 'status' => $messageQueue,
766 'html' => $view->render(),
767 ]);
768 }
769
770 /**
771 * Render list of versions
772 *
773 * @param ServerRequestInterface $request
774 * @return ResponseInterface
775 */
776 public function upgradeDocsGetContentAction(ServerRequestInterface $request): ResponseInterface
777 {
778 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
779 $documentationDirectories = $this->getDocumentationDirectories();
780 $view = $this->initializeStandaloneView($request, 'Upgrade/UpgradeDocsGetContent.html');
781 $view->assignMultiple([
782 'upgradeDocsMarkReadToken' => $formProtection->generateToken('installTool', 'upgradeDocsMarkRead'),
783 'upgradeDocsUnmarkReadToken' => $formProtection->generateToken('installTool', 'upgradeDocsUnmarkRead'),
784 'upgradeDocsVersions' => $documentationDirectories,
785 ]);
786 return new JsonResponse([
787 'success' => true,
788 'html' => $view->render(),
789 ]);
790 }
791
792 /**
793 * Render list of .rst files
794 *
795 * @param ServerRequestInterface $request
796 * @return ResponseInterface
797 */
798 public function upgradeDocsGetChangelogForVersionAction(ServerRequestInterface $request): ResponseInterface
799 {
800 $version = $request->getQueryParams()['install']['version'] ?? '';
801 $this->assertValidVersion($version);
802
803 $documentationFiles = $this->getDocumentationFiles($version);
804 $view = $this->initializeStandaloneView($request, 'Upgrade/UpgradeDocsGetChangelogForVersion.html');
805 $view->assignMultiple([
806 'upgradeDocsFiles' => $documentationFiles['normalFiles'],
807 'upgradeDocsReadFiles' => $documentationFiles['readFiles'],
808 'upgradeDocsNotAffectedFiles' => $documentationFiles['notAffectedFiles'],
809 ]);
810 return new JsonResponse([
811 'success' => true,
812 'html' => $view->render(),
813 ]);
814 }
815
816 /**
817 * Mark a .rst file as read
818 *
819 * @param ServerRequestInterface $request
820 * @return ResponseInterface
821 */
822 public function upgradeDocsMarkReadAction(ServerRequestInterface $request): ResponseInterface
823 {
824 $registry = new Registry();
825 $filePath = $request->getParsedBody()['install']['ignoreFile'];
826 $fileHash = md5_file($filePath);
827 $registry->set('upgradeAnalysisIgnoredFiles', $fileHash, $filePath);
828 return new JsonResponse([
829 'success' => true,
830 ]);
831 }
832
833 /**
834 * Mark a .rst file as not read
835 *
836 * @param ServerRequestInterface $request
837 * @return ResponseInterface
838 */
839 public function upgradeDocsUnmarkReadAction(ServerRequestInterface $request): ResponseInterface
840 {
841 $registry = new Registry();
842 $filePath = $request->getParsedBody()['install']['ignoreFile'];
843 $fileHash = md5_file($filePath);
844 $registry->remove('upgradeAnalysisIgnoredFiles', $fileHash);
845 return new JsonResponse([
846 'success' => true,
847 ]);
848 }
849
850 /**
851 * Check if new tables and fields should be added before executing wizards
852 *
853 * @return ResponseInterface
854 */
855 public function upgradeWizardsBlockingDatabaseAddsAction(): ResponseInterface
856 {
857 // ext_localconf, db and ext_tables must be loaded for the updates :(
858 $this->loadExtLocalconfDatabaseAndExtTables();
859 $upgradeWizardsService = new UpgradeWizardsService();
860 $adds = $upgradeWizardsService->getBlockingDatabaseAdds();
861 $needsUpdate = false;
862 if (!empty($adds)) {
863 $needsUpdate = true;
864 }
865 return new JsonResponse([
866 'success' => true,
867 'needsUpdate' => $needsUpdate,
868 'adds' => $adds,
869 ]);
870 }
871
872 /**
873 * Add new tables and fields
874 *
875 * @return ResponseInterface
876 */
877 public function upgradeWizardsBlockingDatabaseExecuteAction(): ResponseInterface
878 {
879 // ext_localconf, db and ext_tables must be loaded for the updates :(
880 $this->loadExtLocalconfDatabaseAndExtTables();
881 $upgradeWizardsService = new UpgradeWizardsService();
882 $upgradeWizardsService->addMissingTablesAndFields();
883 $messages = new FlashMessageQueue('install');
884 $messages->enqueue(new FlashMessage(
885 '',
886 'Added missing database fields and tables'
887 ));
888 return new JsonResponse([
889 'success' => true,
890 'status' => $messages,
891 ]);
892 }
893
894 /**
895 * Fix a broken DB charset setting
896 *
897 * @return ResponseInterface
898 */
899 public function upgradeWizardsBlockingDatabaseCharsetFixAction(): ResponseInterface
900 {
901 $upgradeWizardsService = new UpgradeWizardsService();
902 $upgradeWizardsService->setDatabaseCharsetUtf8();
903 $messages = new FlashMessageQueue('install');
904 $messages->enqueue(new FlashMessage(
905 '',
906 'Default connection database has been set to utf8'
907 ));
908 return new JsonResponse([
909 'success' => true,
910 'status' => $messages,
911 ]);
912 }
913
914 /**
915 * Test if database charset is ok
916 *
917 * @return ResponseInterface
918 */
919 public function upgradeWizardsBlockingDatabaseCharsetTestAction(): ResponseInterface
920 {
921 $upgradeWizardsService = new UpgradeWizardsService();
922 $result = !$upgradeWizardsService->isDatabaseCharsetUtf8();
923 return new JsonResponse([
924 'success' => true,
925 'needsUpdate' => $result,
926 ]);
927 }
928
929 /**
930 * Get list of upgrade wizards marked as done
931 *
932 * @return ResponseInterface
933 */
934 public function upgradeWizardsDoneUpgradesAction(): ResponseInterface
935 {
936 $this->loadExtLocalconfDatabaseAndExtTables();
937 $upgradeWizardsService = new UpgradeWizardsService();
938 $wizardsDone = $upgradeWizardsService->listOfWizardsDone();
939 $rowUpdatersDone = $upgradeWizardsService->listOfRowUpdatersDone();
940 $messages = new FlashMessageQueue('install');
941 if (empty($wizardsDone) && empty($rowUpdatersDone)) {
942 $messages->enqueue(new FlashMessage(
943 '',
944 'No wizards are marked as done'
945 ));
946 }
947 return new JsonResponse([
948 'success' => true,
949 'status' => $messages,
950 'wizardsDone' => $wizardsDone,
951 'rowUpdatersDone' => $rowUpdatersDone,
952 ]);
953 }
954
955 /**
956 * Execute one upgrade wizard
957 *
958 * @param ServerRequestInterface $request
959 * @return ResponseInterface
960 */
961 public function upgradeWizardsExecuteAction(ServerRequestInterface $request): ResponseInterface
962 {
963 // ext_localconf, db and ext_tables must be loaded for the updates :(
964 $this->loadExtLocalconfDatabaseAndExtTables();
965 $upgradeWizardsService = new UpgradeWizardsService();
966 $identifier = $request->getParsedBody()['install']['identifier'];
967 $messages = $upgradeWizardsService->executeWizard($identifier);
968 return new JsonResponse([
969 'success' => true,
970 'status' => $messages,
971 ]);
972 }
973
974 /**
975 * Input stage of a specific upgrade wizard
976 *
977 * @param ServerRequestInterface $request
978 * @return ResponseInterface
979 */
980 public function upgradeWizardsInputAction(ServerRequestInterface $request): ResponseInterface
981 {
982 // ext_localconf, db and ext_tables must be loaded for the updates :(
983 $this->loadExtLocalconfDatabaseAndExtTables();
984 $upgradeWizardsService = new UpgradeWizardsService();
985 $identifier = $request->getParsedBody()['install']['identifier'];
986 $result = $upgradeWizardsService->getWizardUserInput($identifier);
987 return new JsonResponse([
988 'success' => true,
989 'status' => [],
990 'userInput' => $result,
991 ]);
992 }
993
994 /**
995 * List available upgrade wizards
996 *
997 * @return ResponseInterface
998 */
999 public function upgradeWizardsListAction(): ResponseInterface
1000 {
1001 // ext_localconf, db and ext_tables must be loaded for the updates :(
1002 $this->loadExtLocalconfDatabaseAndExtTables();
1003 $upgradeWizardsService = new UpgradeWizardsService();
1004 $wizards = $upgradeWizardsService->getUpgradeWizardsList();
1005 return new JsonResponse([
1006 'success' => true,
1007 'status' => [],
1008 'wizards' => $wizards,
1009 ]);
1010 }
1011
1012 /**
1013 * Mark a wizard as "not done"
1014 *
1015 * @param ServerRequestInterface $request
1016 * @return ResponseInterface
1017 */
1018 public function upgradeWizardsMarkUndoneAction(ServerRequestInterface $request): ResponseInterface
1019 {
1020 $this->loadExtLocalconfDatabaseAndExtTables();
1021 $wizardToBeMarkedAsUndoneIdentifier = $request->getParsedBody()['install']['identifier'];
1022 $upgradeWizardsService = new UpgradeWizardsService();
1023 $result = $upgradeWizardsService->markWizardUndone($wizardToBeMarkedAsUndoneIdentifier);
1024 $messages = new FlashMessageQueue('install');
1025 if ($result) {
1026 $messages->enqueue(new FlashMessage(
1027 'Wizard has been marked undone'
1028 ));
1029 } else {
1030 $messages->enqueue(new FlashMessage(
1031 'Wizard has not been marked undone',
1032 '',
1033 FlashMessage::ERROR
1034 ));
1035 }
1036 return new JsonResponse([
1037 'success' => true,
1038 'status' => $messages,
1039 ]);
1040 }
1041
1042 /**
1043 * Change install tool password
1044 *
1045 * @param ServerRequestInterface $request
1046 * @return ResponseInterface
1047 */
1048 public function upgradeWizardsGetDataAction(ServerRequestInterface $request): ResponseInterface
1049 {
1050 $view = $this->initializeStandaloneView($request, 'Upgrade/UpgradeWizards.html');
1051 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
1052 $view->assignMultiple([
1053 'upgradeWizardsMarkUndoneToken' => $formProtection->generateToken('installTool', 'upgradeWizardsMarkUndone'),
1054 'upgradeWizardsInputToken' => $formProtection->generateToken('installTool', 'upgradeWizardsInput'),
1055 'upgradeWizardsExecuteToken' => $formProtection->generateToken('installTool', 'upgradeWizardsExecute'),
1056 ]);
1057 return new JsonResponse([
1058 'success' => true,
1059 'html' => $view->render(),
1060 ]);
1061 }
1062
1063 /**
1064 * Initialize the core upgrade actions
1065 *
1066 * @throws \RuntimeException
1067 */
1068 protected function coreUpdateInitialize()
1069 {
1070 $this->coreUpdateService = GeneralUtility::makeInstance(CoreUpdateService::class);
1071 $this->coreVersionService = GeneralUtility::makeInstance(CoreVersionService::class);
1072 if (!$this->coreUpdateService->isCoreUpdateEnabled()) {
1073 throw new \RuntimeException(
1074 'Core Update disabled in this environment',
1075 1381609294
1076 );
1077 }
1078 // @todo: Does the core updater really depend on loaded ext_* files?
1079 $this->loadExtLocalconfDatabaseAndExtTables();
1080 }
1081
1082 /**
1083 * Find out which version upgrade should be handled. This may
1084 * be different depending on whether development or regular release.
1085 *
1086 * @param ServerRequestInterface $request
1087 * @throws \RuntimeException
1088 * @return string Version to handle, eg. 6.2.2
1089 */
1090 protected function coreUpdateGetVersionToHandle(ServerRequestInterface $request): string
1091 {
1092 $type = $request->getQueryParams()['install']['type'];
1093 if (!isset($type) || empty($type)) {
1094 throw new \RuntimeException(
1095 'Type must be set to either "regular" or "development"',
1096 1380975303
1097 );
1098 }
1099 if ($type === 'development') {
1100 $versionToHandle = $this->coreVersionService->getYoungestPatchDevelopmentRelease();
1101 } else {
1102 $versionToHandle = $this->coreVersionService->getYoungestPatchRelease();
1103 }
1104 return $versionToHandle;
1105 }
1106
1107 /**
1108 * Loads ext_localconf.php for a single extension. Method is a modified copy of
1109 * the original bootstrap method.
1110 *
1111 * @param Package $package
1112 */
1113 protected function extensionCompatTesterLoadExtLocalconfForExtension(Package $package)
1114 {
1115 $extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php';
1116 // This is the main array meant to be manipulated in the ext_localconf.php files
1117 // In general it is recommended to not rely on it to be globally defined in that
1118 // scope but to use $GLOBALS['TYPO3_CONF_VARS'] instead.
1119 // Nevertheless we define it here as global for backwards compatibility.
1120 global $TYPO3_CONF_VARS;
1121 if (@file_exists($extLocalconfPath)) {
1122 // $_EXTKEY and $_EXTCONF are available in ext_localconf.php
1123 // and are explicitly set in cached file as well
1124 $_EXTKEY = $package->getPackageKey();
1125 $_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null;
1126 require $extLocalconfPath;
1127 }
1128 }
1129
1130 /**
1131 * Loads ext_tables.php for a single extension. Method is a modified copy of
1132 * the original bootstrap method.
1133 *
1134 * @param Package $package
1135 */
1136 protected function extensionCompatTesterLoadExtTablesForExtension(Package $package)
1137 {
1138 $extTablesPath = $package->getPackagePath() . 'ext_tables.php';
1139 // In general it is recommended to not rely on it to be globally defined in that
1140 // scope, but we can not prohibit this without breaking backwards compatibility
1141 global $T3_SERVICES, $T3_VAR, $TYPO3_CONF_VARS;
1142 global $TBE_MODULES, $TBE_MODULES_EXT, $TCA;
1143 global $PAGES_TYPES, $TBE_STYLES;
1144 global $_EXTKEY;
1145 // Load each ext_tables.php file of loaded extensions
1146 $_EXTKEY = $package->getPackageKey();
1147 if (@file_exists($extTablesPath)) {
1148 // $_EXTKEY and $_EXTCONF are available in ext_tables.php
1149 // and are explicitly set in cached file as well
1150 $_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null;
1151 require $extTablesPath;
1152 }
1153 }
1154
1155 /**
1156 * @return string[]
1157 */
1158 protected function getDocumentationDirectories(): array
1159 {
1160 $documentationFileService = new DocumentationFile();
1161 $documentationDirectories = $documentationFileService->findDocumentationDirectories(
1162 str_replace('\\', '/', realpath(ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog'))
1163 );
1164 return array_reverse($documentationDirectories);
1165 }
1166
1167 /**
1168 * Get a list of '.rst' files and their details for "Upgrade documentation" view.
1169 *
1170 * @param string $version
1171 * @return array
1172 */
1173 protected function getDocumentationFiles(string $version): array
1174 {
1175 $documentationFileService = new DocumentationFile();
1176 $documentationFiles = $documentationFileService->findDocumentationFiles(
1177 str_replace('\\', '/', realpath(ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog/' . $version))
1178 );
1179 $documentationFiles = array_reverse($documentationFiles);
1180
1181 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_registry');
1182 $filesMarkedAsRead = $queryBuilder
1183 ->select('*')
1184 ->from('sys_registry')
1185 ->where(
1186 $queryBuilder->expr()->eq(
1187 'entry_namespace',
1188 $queryBuilder->createNamedParameter('upgradeAnalysisIgnoredFiles', \PDO::PARAM_STR)
1189 )
1190 )
1191 ->execute()
1192 ->fetchAll();
1193 $hashesMarkedAsRead = [];
1194 foreach ($filesMarkedAsRead as $file) {
1195 $hashesMarkedAsRead[] = $file['entry_key'];
1196 }
1197
1198 $fileMarkedAsNotAffected = $queryBuilder
1199 ->select('*')
1200 ->from('sys_registry')
1201 ->where(
1202 $queryBuilder->expr()->eq(
1203 'entry_namespace',
1204 $queryBuilder->createNamedParameter('extensionScannerNotAffected', \PDO::PARAM_STR)
1205 )
1206 )
1207 ->execute()
1208 ->fetchAll();
1209 $hashesMarkedAsNotAffected = [];
1210 foreach ($fileMarkedAsNotAffected as $file) {
1211 $hashesMarkedAsNotAffected[] = $file['entry_key'];
1212 }
1213
1214 $readFiles = [];
1215 $notAffectedFiles = [];
1216 foreach ($documentationFiles as $fileId => $fileData) {
1217 if (in_array($fileData['file_hash'], $hashesMarkedAsRead, true)) {
1218 $readFiles[$fileId] = $fileData;
1219 unset($documentationFiles[$fileId]);
1220 } elseif (in_array($fileData['file_hash'], $hashesMarkedAsNotAffected, true)) {
1221 $notAffectedFiles[$fileId] = $fileData;
1222 unset($documentationFiles[$fileId]);
1223 }
1224 }
1225
1226 return [
1227 'normalFiles' => $documentationFiles,
1228 'readFiles' => $readFiles,
1229 'notAffectedFiles' => $notAffectedFiles,
1230 ];
1231 }
1232
1233 /**
1234 * Find a code line in a file
1235 *
1236 * @param string $file Absolute path to file
1237 * @param int $lineNumber Find this line in file
1238 * @return string Code line
1239 */
1240 protected function extensionScannerGetLineFromFile(string $file, int $lineNumber): string
1241 {
1242 $fileContent = file($file, FILE_IGNORE_NEW_LINES);
1243 $line = '';
1244 if (isset($fileContent[$lineNumber - 1])) {
1245 $line = trim($fileContent[$lineNumber - 1]);
1246 }
1247 return $line;
1248 }
1249
1250 /**
1251 * Asserts that the given version is valid
1252 *
1253 * @param string $version
1254 * @throws \InvalidArgumentException
1255 */
1256 protected function assertValidVersion(string $version): void
1257 {
1258 if ($version !== 'master' && !preg_match('/^\d+.\d+(?:.(?:\d+|x))?$/', $version)) {
1259 throw new \InvalidArgumentException('Given version "' . $version . '" is invalid', 1537209128);
1260 }
1261 }
1262 }