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