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