Passed
Branch master (6c65a4)
by Christian
27:15 queued 11:09
created

extensionScannerGetLineFromFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
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\Bootstrap;
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\PropertyProtectedMatcher;
55
use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyPublicMatcher;
56
use TYPO3\CMS\Install\ExtensionScanner\Php\MatcherFactory;
57
use TYPO3\CMS\Install\Service\CoreUpdateService;
58
use TYPO3\CMS\Install\Service\CoreVersionService;
59
use TYPO3\CMS\Install\Service\LoadTcaService;
60
use TYPO3\CMS\Install\Service\UpgradeWizardsService;
61
use TYPO3\CMS\Install\UpgradeAnalysis\DocumentationFile;
62
63
/**
64
 * Upgrade controller
65
 */
66
class UpgradeController extends AbstractController
67
{
68
    /**
69
     * @var CoreUpdateService
70
     */
71
    protected $coreUpdateService;
72
73
    /**
74
     * @var CoreVersionService
75
     */
76
    protected $coreVersionService;
77
78
    /**
79
     * Matcher registry of extension scanner.
80
     * Node visitors that implement CodeScannerInterface
81
     *
82
     * @var array
83
     */
84
    protected $matchers = [
85
        [
86
            'class' => ArrayDimensionMatcher::class,
87
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php',
88
        ],
89
        [
90
            'class' => ArrayGlobalMatcher::class,
91
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ArrayGlobalMatcher.php',
92
        ],
93
        [
94
            'class' => ClassConstantMatcher::class,
95
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ClassConstantMatcher.php',
96
        ],
97
        [
98
            'class' => ClassNameMatcher::class,
99
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php',
100
        ],
101
        [
102
            'class' => ConstantMatcher::class,
103
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/ConstantMatcher.php',
104
        ],
105
        [
106
            'class' => PropertyAnnotationMatcher::class,
107
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyAnnotationMatcher.php',
108
        ],
109
        [
110
            'class' => MethodAnnotationMatcher::class,
111
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodAnnotationMatcher.php',
112
        ],
113
        [
114
            'class' => FunctionCallMatcher::class,
115
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/FunctionCallMatcher.php',
116
        ],
117
        [
118
            'class' => InterfaceMethodChangedMatcher::class,
119
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/InterfaceMethodChangedMatcher.php',
120
        ],
121
        [
122
            'class' => MethodArgumentDroppedMatcher::class,
123
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php',
124
        ],
125
        [
126
            'class' => MethodArgumentDroppedStaticMatcher::class,
127
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedStaticMatcher.php',
128
        ],
129
        [
130
            'class' => MethodArgumentRequiredMatcher::class,
131
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentRequiredMatcher.php',
132
        ],
133
        [
134
            'class' => MethodArgumentRequiredStaticMatcher::class,
135
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentRequiredStaticMatcher.php',
136
        ],
137
        [
138
            'class' => MethodArgumentUnusedMatcher::class,
139
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodArgumentUnusedMatcher.php',
140
        ],
141
        [
142
            'class' => MethodCallMatcher::class,
143
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php',
144
        ],
145
        [
146
            'class' => MethodCallStaticMatcher::class,
147
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php',
148
        ],
149
        [
150
            'class' => PropertyProtectedMatcher::class,
151
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyProtectedMatcher.php',
152
        ],
153
        [
154
            'class' => PropertyPublicMatcher::class,
155
            'configurationFile' => 'EXT:install/Configuration/ExtensionScanner/Php/PropertyPublicMatcher.php',
156
        ],
157
    ];
158
159
    /**
160
     * Main "show the cards" view
161
     *
162
     * @param ServerRequestInterface $request
163
     * @return ResponseInterface
164
     */
165
    public function cardsAction(ServerRequestInterface $request): ResponseInterface
166
    {
167
        $view = $this->initializeStandaloneView($request, 'Upgrade/Cards.html');
168
        $extensionsInTypo3conf = (new Finder())->directories()->in(PATH_site . 'typo3conf/ext')->depth('== 0')->sortByName();
169
        $coreUpdateService = GeneralUtility::makeInstance(CoreUpdateService::class);
170
        $coreVersionService = GeneralUtility::makeInstance(CoreVersionService::class);
171
        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
172
        $view->assignMultiple([
173
            'extensionCompatTesterLoadExtLocalconfToken' => $formProtection->generateToken('installTool', 'extensionCompatTesterLoadExtLocalconf'),
174
            'extensionCompatTesterLoadExtTablesToken' => $formProtection->generateToken('installTool', 'extensionCompatTesterLoadExtTables'),
175
            'extensionCompatTesterUninstallToken' => $formProtection->generateToken('installTool', 'extensionCompatTesterUninstallExtension'),
176
177
            'coreUpdateEnabled' => $coreUpdateService->isCoreUpdateEnabled(),
178
            'coreUpdateComposerMode' => Bootstrap::usesComposerClassLoading(),
179
            'coreUpdateIsReleasedVersion' => $coreVersionService->isInstalledVersionAReleasedVersion(),
180
            'coreUpdateIsSymLinkedCore' => is_link(PATH_site . 'typo3_src'),
181
182
            'extensionScannerExtensionList' => $extensionsInTypo3conf,
183
            'extensionScannerFilesToken' => $formProtection->generateToken('installTool', 'extensionScannerFiles'),
184
            'extensionScannerScanFileToken' => $formProtection->generateToken('installTool', 'extensionScannerScanFile'),
185
            'extensionScannerMarkFullyScannedRestFilesToken' => $formProtection->generateToken('installTool', 'extensionScannerMarkFullyScannedRestFiles'),
186
187
            'upgradeWizardsMarkUndoneToken' => $formProtection->generateToken('installTool', 'upgradeWizardsMarkUndone'),
188
            'upgradeWizardsInputToken' => $formProtection->generateToken('installTool', 'upgradeWizardsInput'),
189
            'upgradeWizardsExecuteToken' => $formProtection->generateToken('installTool', 'upgradeWizardsExecute'),
190
        ]);
191
        return new JsonResponse([
192
            'success' => true,
193
            'html' => $view->render(),
194
        ]);
195
    }
196
197
    /**
198
     * Activate a new core
199
     *
200
     * @param ServerRequestInterface $request
201
     * @return ResponseInterface
202
     */
203
    public function coreUpdateActivateAction(ServerRequestInterface $request): ResponseInterface
204
    {
205
        $this->coreUpdateInitialize();
206
        return new JsonResponse([
207
            'success' => $this->coreUpdateService->activateVersion($this->coreUpdateGetVersionToHandle($request)),
208
            'status' => $this->coreUpdateService->getMessages(),
209
        ]);
210
    }
211
212
    /**
213
     * Check if core update is possible
214
     *
215
     * @param ServerRequestInterface $request
216
     * @return ResponseInterface
217
     */
218
    public function coreUpdateCheckPreConditionsAction(ServerRequestInterface $request): ResponseInterface
219
    {
220
        $this->coreUpdateInitialize();
221
        return new JsonResponse([
222
            'success' => $this->coreUpdateService->checkPreConditions($this->coreUpdateGetVersionToHandle($request)),
223
            'status' => $this->coreUpdateService->getMessages(),
224
        ]);
225
    }
226
227
    /**
228
     * Download new core
229
     *
230
     * @param ServerRequestInterface $request
231
     * @return ResponseInterface
232
     */
233
    public function coreUpdateDownloadAction(ServerRequestInterface $request): ResponseInterface
234
    {
235
        $this->coreUpdateInitialize();
236
        return new JsonResponse([
237
            'success' => $this->coreUpdateService->downloadVersion($this->coreUpdateGetVersionToHandle($request)),
238
            'status' => $this->coreUpdateService->getMessages(),
239
        ]);
240
    }
241
242
    /**
243
     * Check for new core
244
     *
245
     * @return ResponseInterface
246
     */
247
    public function coreUpdateIsUpdateAvailableAction(): ResponseInterface
248
    {
249
        $this->coreUpdateInitialize();
250
        $messageQueue = new FlashMessageQueue('install');
251
        if ($this->coreVersionService->isInstalledVersionAReleasedVersion()) {
252
            $isDevelopmentUpdateAvailable = $this->coreVersionService->isYoungerPatchDevelopmentReleaseAvailable();
253
            $isUpdateAvailable = $this->coreVersionService->isYoungerPatchReleaseAvailable();
254
            $isUpdateSecurityRelevant = $this->coreVersionService->isUpdateSecurityRelevant();
255
            if (!$isUpdateAvailable && !$isDevelopmentUpdateAvailable) {
256
                $messageQueue->enqueue(new FlashMessage(
257
                    '',
258
                    'No regular update available',
259
                    FlashMessage::NOTICE
260
                ));
261
            } elseif ($isUpdateAvailable) {
262
                $newVersion = $this->coreVersionService->getYoungestPatchRelease();
263
                if ($isUpdateSecurityRelevant) {
264
                    $messageQueue->enqueue(new FlashMessage(
265
                        '',
266
                        'Update to security relevant released version ' . $newVersion . ' is available!',
267
                        FlashMessage::WARNING
268
                    ));
269
                    $action = ['title' => 'Update now', 'action' => 'updateRegular'];
270
                } else {
271
                    $messageQueue->enqueue(new FlashMessage(
272
                        '',
273
                        'Update to regular released version ' . $newVersion . ' is available!',
274
                        FlashMessage::INFO
275
                    ));
276
                    $action = ['title' => 'Update now', 'action' => 'updateRegular'];
277
                }
278
            } elseif ($isDevelopmentUpdateAvailable) {
279
                $newVersion = $this->coreVersionService->getYoungestPatchDevelopmentRelease();
280
                $messageQueue->enqueue(new FlashMessage(
281
                    '',
282
                    'Update to development release ' . $newVersion . ' is available!',
283
                    FlashMessage::INFO
284
                ));
285
                $action = ['title' => 'Update now', 'action' => 'updateDevelopment'];
286
            }
287
        } else {
288
            $messageQueue->enqueue(new FlashMessage(
289
                '',
290
                'Current version is a development version and can not be updated',
291
                FlashMessage::WARNING
292
            ));
293
        }
294
        $responseData = [
295
            'success' => true,
296
            'status' => $messageQueue,
297
        ];
298
        if (isset($action)) {
299
            $responseData['action'] = $action;
300
        }
301
        return new JsonResponse($responseData);
302
    }
303
304
    /**
305
     * Move core to new location
306
     *
307
     * @param ServerRequestInterface $request
308
     * @return ResponseInterface
309
     */
310
    public function coreUpdateMoveAction(ServerRequestInterface $request): ResponseInterface
311
    {
312
        $this->coreUpdateInitialize();
313
        return new JsonResponse([
314
            'success' => $this->coreUpdateService->moveVersion($this->coreUpdateGetVersionToHandle($request)),
315
            'status' => $this->coreUpdateService->getMessages(),
316
        ]);
317
    }
318
319
    /**
320
     * Unpack a downloaded core
321
     *
322
     * @param ServerRequestInterface $request
323
     * @return ResponseInterface
324
     */
325
    public function coreUpdateUnpackAction(ServerRequestInterface $request): ResponseInterface
326
    {
327
        $this->coreUpdateInitialize();
328
        return new JsonResponse([
329
            'success' => $this->coreUpdateService->unpackVersion($this->coreUpdateGetVersionToHandle($request)),
330
            'status' => $this->coreUpdateService->getMessages(),
331
        ]);
332
    }
333
334
    /**
335
     * Update available core version list
336
     *
337
     * @return ResponseInterface
338
     */
339
    public function coreUpdateUpdateVersionMatrixAction(): ResponseInterface
340
    {
341
        $this->coreUpdateInitialize();
342
        return new JsonResponse([
343
            'success' => $this->coreUpdateService->updateVersionMatrix(),
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
     * @return ResponseInterface
367
     */
368
    public function extensionCompatTesterLoadedExtensionListAction(): ResponseInterface
369
    {
370
        return new JsonResponse([
371
            'success' => true,
372
            'extensions' => array_keys($GLOBALS['TYPO3_LOADED_EXT']),
373
        ]);
374
    }
375
376
    /**
377
     * Load all ext_localconf files in order until given extension name
378
     *
379
     * @param ServerRequestInterface $request
380
     * @return ResponseInterface
381
     */
382
    public function extensionCompatTesterLoadExtLocalconfAction(ServerRequestInterface $request): ResponseInterface
383
    {
384
        $extension = $request->getParsedBody()['install']['extension'];
385
        foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $extKey => $extDetails) {
386
            $this->extensionCompatTesterLoadExtLocalconfForExtension($extKey, $extDetails);
387
            if ($extKey === $extension) {
388
                break;
389
            }
390
        }
391
        return new JsonResponse([
392
            'success' => true,
393
        ]);
394
    }
395
396
    /**
397
     * Load all ext_localconf files in order until given extension name
398
     *
399
     * @param ServerRequestInterface $request
400
     * @return ResponseInterface
401
     */
402
    public function extensionCompatTesterLoadExtTablesAction(ServerRequestInterface $request): ResponseInterface
403
    {
404
        $extension = $request->getParsedBody()['install']['extension'];
405
        foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $extKey => $extDetails) {
406
            // Load all ext_localconf files first
407
            $this->extensionCompatTesterLoadExtLocalconfForExtension($extKey, $extDetails);
408
        }
409
        foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $extKey => $extDetails) {
410
            $this->extensionCompatTesterLoadExtTablesForExtension($extKey, $extDetails);
411
            if ($extKey === $extension) {
412
                break;
413
            }
414
        }
415
        return new JsonResponse([
416
            'success' => true,
417
        ]);
418
    }
419
420
    /**
421
     * Unload one extension
422
     *
423
     * @param ServerRequestInterface $request
424
     * @return ResponseInterface
425
     */
426
    public function extensionCompatTesterUninstallExtensionAction(ServerRequestInterface $request): ResponseInterface
427
    {
428
        $extension = $request->getParsedBody()['install']['extension'];
429
        if (empty($extension)) {
430
            throw new \RuntimeException(
431
                'No extension given',
432
                1505407269
433
            );
434
        }
435
        $messageQueue = new FlashMessageQueue('install');
436
        if (ExtensionManagementUtility::isLoaded($extension)) {
437
            try {
438
                ExtensionManagementUtility::unloadExtension($extension);
439
                $messageQueue->enqueue(new FlashMessage(
440
                    'Extension "' . $extension . '" unloaded.',
441
                    '',
442
                    FlashMessage::ERROR
443
                ));
444
            } catch (\Exception $e) {
445
                $messageQueue->enqueue(new FlashMessage(
446
                    $e->getMessage(),
447
                    '',
448
                    FlashMessage::ERROR
449
                ));
450
            }
451
        }
452
        return new JsonResponse([
453
            'success' => true,
454
            'status' => $messageQueue,
455
        ]);
456
    }
457
458
    /**
459
     * Return a list of files of an extension
460
     *
461
     * @param ServerRequestInterface $request
462
     * @return ResponseInterface
463
     */
464
    public function extensionScannerFilesAction(ServerRequestInterface $request): ResponseInterface
465
    {
466
        // Get and validate path
467
        $extension = $request->getParsedBody()['install']['extension'];
468
        $extensionBasePath = PATH_site . 'typo3conf/ext/' . $extension;
469
        if (empty($extension) || !GeneralUtility::isAllowedAbsPath($extensionBasePath)) {
470
            throw new \RuntimeException(
471
                'Path to extension ' . $extension . ' not allowed.',
472
                1499777261
473
            );
474
        }
475
        if (!is_dir($extensionBasePath)) {
476
            throw new \RuntimeException(
477
                'Extension path ' . $extensionBasePath . ' does not exist or is no directory.',
478
                1499777330
479
            );
480
        }
481
482
        $finder = new Finder();
483
        $files = $finder->files()->in($extensionBasePath)->name('*.php')->sortByName();
484
        // A list of file names relative to extension directory
485
        $relativeFileNames = [];
486
        foreach ($files as $file) {
487
            /** @var $file SplFileInfo */
488
            $relativeFileNames[] = GeneralUtility::fixWindowsFilePath($file->getRelativePathname());
489
        }
490
        return new JsonResponse([
491
            'success' => true,
492
            'files' => $relativeFileNames,
493
        ]);
494
    }
495
496
    /**
497
     * Ajax controller, part of "extension scanner". Called at the end of "scan all"
498
     * as last action. Gets a list of RST file hashes that matched, goes through all
499
     * existing RST files, finds those marked as "FullyScanned" and marks those that
500
     * did not had any matches as "you are not affected".
501
     *
502
     * @param ServerRequestInterface $request
503
     * @return ResponseInterface
504
     */
505
    public function extensionScannerMarkFullyScannedRestFilesAction(ServerRequestInterface $request): ResponseInterface
506
    {
507
        $foundRestFileHashes = (array)$request->getParsedBody()['install']['hashes'];
508
        // First un-mark files marked as scanned-ok
509
        $registry = new Registry();
510
        $registry->removeAllByNamespace('extensionScannerNotAffected');
511
        // Find all .rst files (except those from v8), see if they are tagged with "FullyScanned"
512
        // and if their content is not in incoming "hashes" array, mark as "not affected"
513
        $documentationFile = new DocumentationFile();
514
        $finder = new Finder();
515
        $restFilesBasePath = ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog';
516
        $restFiles = $finder->files()->in($restFilesBasePath);
517
        $fullyScannedRestFilesNotAffected = [];
518
        foreach ($restFiles as $restFile) {
519
            // Skip files in "8.x" directory
520
            /** @var $restFile SplFileInfo */
521
            if (substr($restFile->getRelativePath(), 0, 1) === '8') {
522
                continue;
523
            }
524
525
            // Build array of file (hashes) not affected by current scan, if they are tagged as "FullyScanned"
526
            $parsedRestFile = array_pop($documentationFile->getListEntry(strtr(realpath($restFile->getPathname()), '\\', '/')));
0 ignored issues
show
Bug introduced by
$documentationFile->getL...Pathname()), '\', '/')) cannot be passed to array_pop() as the parameter $array expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

526
            $parsedRestFile = array_pop(/** @scrutinizer ignore-type */ $documentationFile->getListEntry(strtr(realpath($restFile->getPathname()), '\\', '/')));
Loading history...
527
            if (!in_array($parsedRestFile['file_hash'], $foundRestFileHashes, true)
528
                && in_array('FullyScanned', $parsedRestFile['tags'], true)
529
            ) {
530
                $fullyScannedRestFilesNotAffected[] = $parsedRestFile['file_hash'];
531
            }
532
        }
533
        foreach ($fullyScannedRestFilesNotAffected as $fileHash) {
534
            $registry->set('extensionScannerNotAffected', $fileHash, $fileHash);
535
        }
536
        return new JsonResponse([
537
            'success' => true,
538
            'markedAsNotAffected' => count($fullyScannedRestFilesNotAffected),
539
        ]);
540
    }
541
542
    /**
543
     * Scan a single extension file for breaking / deprecated core code usages
544
     *
545
     * @param ServerRequestInterface $request
546
     * @return ResponseInterface
547
     */
548
    public function extensionScannerScanFileAction(ServerRequestInterface $request): ResponseInterface
549
    {
550
        // Get and validate path and file
551
        $extension = $request->getParsedBody()['install']['extension'];
552
        $extensionBasePath = PATH_site . 'typo3conf/ext/' . $extension;
553
        if (empty($extension) || !GeneralUtility::isAllowedAbsPath($extensionBasePath)) {
554
            throw new \RuntimeException(
555
                'Path to extension ' . $extension . ' not allowed.',
556
                1499789246
557
            );
558
        }
559
        if (!is_dir($extensionBasePath)) {
560
            throw new \RuntimeException(
561
                'Extension path ' . $extensionBasePath . ' does not exist or is no directory.',
562
                1499789259
563
            );
564
        }
565
        $file = $request->getParsedBody()['install']['file'];
566
        $absoluteFilePath = $extensionBasePath . '/' . $file;
567
        if (empty($file) || !GeneralUtility::isAllowedAbsPath($absoluteFilePath)) {
568
            throw new \RuntimeException(
569
                'Path to file ' . $file . ' of extension ' . $extension . ' not allowed.',
570
                1499789384
571
            );
572
        }
573
        if (!is_file($absoluteFilePath)) {
574
            throw new \RuntimeException(
575
                'File ' . $file . ' not found or is not a file.',
576
                1499789433
577
            );
578
        }
579
580
        $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
581
        // Parse PHP file to AST and traverse tree calling visitors
582
        $statements = $parser->parse(file_get_contents($absoluteFilePath));
583
584
        $traverser = new NodeTraverser();
585
        // The built in NameResolver translates class names shortened with 'use' to fully qualified
586
        // class names at all places. Incredibly useful for us and added as first visitor.
587
        $traverser->addVisitor(new NameResolver());
588
        // Understand makeInstance('My\\Package\\Foo\\Bar') as fqdn class name in first argument
589
        $traverser->addVisitor(new GeneratorClassesResolver());
590
        // Count ignored lines, effective code lines, ...
591
        $statistics = new CodeStatistics();
592
        $traverser->addVisitor($statistics);
593
594
        // Add all configured matcher classes
595
        $matcherFactory = new MatcherFactory();
596
        $matchers = $matcherFactory->createAll($this->matchers);
597
        foreach ($matchers as $matcher) {
598
            $traverser->addVisitor($matcher);
599
        }
600
601
        $traverser->traverse($statements);
602
603
        // Gather code matches
604
        $matches = [];
605
        foreach ($matchers as $matcher) {
606
            /** @var \TYPO3\CMS\Install\ExtensionScanner\CodeScannerInterface $matcher */
607
            $matches = array_merge($matches, $matcher->getMatches());
608
        }
609
610
        // Prepare match output
611
        $restFilesBasePath = ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog';
612
        $documentationFile = new DocumentationFile();
613
        $preparedMatches = [];
614
        foreach ($matches as $match) {
615
            $preparedHit = [];
616
            $preparedHit['uniqueId'] = str_replace('.', '', uniqid((string)mt_rand(), true));
0 ignored issues
show
Bug introduced by
The call to mt_rand() has too few arguments starting with min. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

616
            $preparedHit['uniqueId'] = str_replace('.', '', uniqid((string)/** @scrutinizer ignore-call */ mt_rand(), true));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
617
            $preparedHit['message'] = $match['message'];
618
            $preparedHit['line'] = $match['line'];
619
            $preparedHit['indicator'] = $match['indicator'];
620
            $preparedHit['lineContent'] = $this->extensionScannerGetLineFromFile($absoluteFilePath, $match['line']);
621
            $preparedHit['restFiles'] = [];
622
            foreach ($match['restFiles'] as $fileName) {
623
                $finder = new Finder();
624
                $restFileLocation = $finder->files()->in($restFilesBasePath)->name($fileName);
625
                if ($restFileLocation->count() !== 1) {
626
                    throw new \RuntimeException(
627
                        'ResT file ' . $fileName . ' not found or multiple files found.',
628
                        1499803909
629
                    );
630
                }
631
                foreach ($restFileLocation as $restFile) {
632
                    /** @var SplFileInfo $restFile */
633
                    $restFileLocation = $restFile->getPathname();
634
                    break;
635
                }
636
                $parsedRestFile = array_pop($documentationFile->getListEntry(strtr(realpath($restFileLocation), '\\', '/')));
0 ignored issues
show
Bug introduced by
It seems like $restFileLocation can also be of type Symfony\Component\Finder\Finder; however, parameter $path of realpath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

636
                $parsedRestFile = array_pop($documentationFile->getListEntry(strtr(realpath(/** @scrutinizer ignore-type */ $restFileLocation), '\\', '/')));
Loading history...
Bug introduced by
$documentationFile->getL...leLocation), '\', '/')) cannot be passed to array_pop() as the parameter $array expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

636
                $parsedRestFile = array_pop(/** @scrutinizer ignore-type */ $documentationFile->getListEntry(strtr(realpath($restFileLocation), '\\', '/')));
Loading history...
637
                $version = GeneralUtility::trimExplode(DIRECTORY_SEPARATOR, $restFileLocation);
0 ignored issues
show
Bug introduced by
It seems like $restFileLocation can also be of type Symfony\Component\Finder\Finder; however, parameter $string of TYPO3\CMS\Core\Utility\G...lUtility::trimExplode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

637
                $version = GeneralUtility::trimExplode(DIRECTORY_SEPARATOR, /** @scrutinizer ignore-type */ $restFileLocation);
Loading history...
638
                array_pop($version);
639
                // something like "8.2" .. "8.7" .. "master"
640
                $parsedRestFile['version'] = array_pop($version);
641
                $parsedRestFile['uniqueId'] = str_replace('.', '', uniqid((string)mt_rand(), true));
642
                $preparedHit['restFiles'][] = $parsedRestFile;
643
            }
644
            $preparedMatches[] = $preparedHit;
645
        }
646
        return new JsonResponse([
647
            'success' => true,
648
            'matches' => $preparedMatches,
649
            'isFileIgnored' => $statistics->isFileIgnored(),
650
            'effectiveCodeLines' => $statistics->getNumberOfEffectiveCodeLines(),
651
            'ignoredLines' => $statistics->getNumberOfIgnoredLines(),
652
        ]);
653
    }
654
655
    /**
656
     * Check if loading ext_tables.php files still changes TCA
657
     *
658
     * @return ResponseInterface
659
     */
660
    public function tcaExtTablesCheckAction(): ResponseInterface
661
    {
662
        $messageQueue = new FlashMessageQueue('install');
663
        $loadTcaService = GeneralUtility::makeInstance(LoadTcaService::class);
664
        $loadTcaService->loadExtensionTablesWithoutMigration();
665
        $baseTca = $GLOBALS['TCA'];
666
        foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $extensionKey => $extensionInformation) {
667
            if ((is_array($extensionInformation) || $extensionInformation instanceof \ArrayAccess)
668
                && $extensionInformation['ext_tables.php']
669
            ) {
670
                $loadTcaService->loadSingleExtTablesFile($extensionKey);
671
                $newTca = $GLOBALS['TCA'];
672
                if ($newTca !== $baseTca) {
673
                    $messageQueue->enqueue(new FlashMessage(
674
                        '',
675
                        $extensionKey,
676
                        FlashMessage::NOTICE
677
                    ));
678
                }
679
                $baseTca = $newTca;
680
            }
681
        }
682
        return new JsonResponse([
683
            'success' => true,
684
            'status' => $messageQueue,
685
        ]);
686
    }
687
688
    /**
689
     * Check TCA for needed migrations
690
     *
691
     * @return ResponseInterface
692
     */
693
    public function tcaMigrationsCheckAction(): ResponseInterface
694
    {
695
        $messageQueue = new FlashMessageQueue('install');
696
        GeneralUtility::makeInstance(LoadTcaService::class)->loadExtensionTablesWithoutMigration();
697
        $tcaMigration = GeneralUtility::makeInstance(TcaMigration::class);
698
        $GLOBALS['TCA'] = $tcaMigration->migrate($GLOBALS['TCA']);
699
        $tcaMessages = $tcaMigration->getMessages();
700
        foreach ($tcaMessages as $tcaMessage) {
701
            $messageQueue->enqueue(new FlashMessage(
702
                '',
703
                $tcaMessage,
704
                FlashMessage::NOTICE
705
            ));
706
        }
707
        return new JsonResponse([
708
            'success' => true,
709
            'status' => $messageQueue,
710
        ]);
711
    }
712
713
    /**
714
     * Render list of .rst files
715
     *
716
     * @param ServerRequestInterface $request
717
     * @return ResponseInterface
718
     */
719
    public function upgradeDocsGetContentAction(ServerRequestInterface $request): ResponseInterface
720
    {
721
        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
722
        $documentationFiles = $this->getDocumentationFiles();
723
        $view = $this->initializeStandaloneView($request, 'Upgrade/UpgradeDocsGetContent.html');
724
        $view->assignMultiple([
725
            'upgradeDocsMarkReadToken' => $formProtection->generateToken('installTool', 'upgradeDocsMarkRead'),
726
            'upgradeDocsUnmarkReadToken' => $formProtection->generateToken('installTool', 'upgradeDocsUnmarkRead'),
727
            'upgradeDocsFiles' => $documentationFiles['normalFiles'],
728
            'upgradeDocsReadFiles' => $documentationFiles['readFiles'],
729
            'upgradeDocsNotAffectedFiles' => $documentationFiles['notAffectedFiles'],
730
        ]);
731
        return new JsonResponse([
732
            'success' => true,
733
            'html' => $view->render(),
734
        ]);
735
    }
736
737
    /**
738
     * Mark a .rst file as read
739
     *
740
     * @param ServerRequestInterface $request
741
     * @return ResponseInterface
742
     */
743
    public function upgradeDocsMarkReadAction(ServerRequestInterface $request): ResponseInterface
744
    {
745
        $registry = new Registry();
746
        $filePath = $request->getParsedBody()['install']['ignoreFile'];
747
        $fileHash = md5_file($filePath);
748
        $registry->set('upgradeAnalysisIgnoredFiles', $fileHash, $filePath);
749
        return new JsonResponse([
750
            'success' => true,
751
        ]);
752
    }
753
754
    /**
755
     * Mark a .rst file as not read
756
     *
757
     * @param ServerRequestInterface $request
758
     * @return ResponseInterface
759
     */
760
    public function upgradeDocsUnmarkReadAction(ServerRequestInterface $request): ResponseInterface
761
    {
762
        $registry = new Registry();
763
        $filePath = $request->getParsedBody()['install']['ignoreFile'];
764
        $fileHash = md5_file($filePath);
765
        $registry->remove('upgradeAnalysisIgnoredFiles', $fileHash);
766
        return new JsonResponse([
767
            'success' => true,
768
        ]);
769
    }
770
771
    /**
772
     * Check if new tables and fields should be added before executing wizards
773
     *
774
     * @return ResponseInterface
775
     */
776
    public function upgradeWizardsBlockingDatabaseAddsAction(): ResponseInterface
777
    {
778
        // ext_localconf, db and ext_tables must be loaded for the updates :(
779
        $this->loadExtLocalconfDatabaseAndExtTables();
780
        $upgradeWizardsService = new UpgradeWizardsService();
781
        $adds = $upgradeWizardsService->getBlockingDatabaseAdds();
782
        $needsUpdate = false;
783
        if (!empty($adds)) {
784
            $needsUpdate = true;
785
        }
786
        return new JsonResponse([
787
            'success' => true,
788
            'needsUpdate' => $needsUpdate,
789
            'adds' => $adds,
790
        ]);
791
    }
792
793
    /**
794
     * Add new tables and fields
795
     *
796
     * @return ResponseInterface
797
     */
798
    public function upgradeWizardsBlockingDatabaseExecuteAction(): ResponseInterface
799
    {
800
        // ext_localconf, db and ext_tables must be loaded for the updates :(
801
        $this->loadExtLocalconfDatabaseAndExtTables();
802
        $upgradeWizardsService = new UpgradeWizardsService();
803
        $upgradeWizardsService->addMissingTablesAndFields();
804
        $messages = new FlashMessageQueue('install');
805
        $messages->enqueue(new FlashMessage(
806
            '',
807
            'Added missing database fields and tables'
808
        ));
809
        return new JsonResponse([
810
            'success' => true,
811
            'status' => $messages,
812
        ]);
813
    }
814
815
    /**
816
     * Fix a broken DB charset setting
817
     *
818
     * @return ResponseInterface
819
     */
820
    public function upgradeWizardsBlockingDatabaseCharsetFixAction(): ResponseInterface
821
    {
822
        $upgradeWizardsService = new UpgradeWizardsService();
823
        $upgradeWizardsService->setDatabaseCharsetUtf8();
824
        $messages = new FlashMessageQueue('install');
825
        $messages->enqueue(new FlashMessage(
826
            '',
827
            'Default connection database has been set to utf8'
828
        ));
829
        return new JsonResponse([
830
            'success' => true,
831
            'status' => $messages,
832
        ]);
833
    }
834
835
    /**
836
     * Test if database charset is ok
837
     *
838
     * @return ResponseInterface
839
     */
840
    public function upgradeWizardsBlockingDatabaseCharsetTestAction(): ResponseInterface
841
    {
842
        $upgradeWizardsService = new UpgradeWizardsService();
843
        $result = !$upgradeWizardsService->isDatabaseCharsetUtf8();
844
        return new JsonResponse([
845
            'success' => true,
846
            'needsUpdate' => $result,
847
        ]);
848
    }
849
850
    /**
851
     * Get list of upgrade wizards marked as done
852
     *
853
     * @return ResponseInterface
854
     */
855
    public function upgradeWizardsDoneUpgradesAction(): ResponseInterface
856
    {
857
        $this->loadExtLocalconfDatabaseAndExtTables();
858
        $upgradeWizardsService = new UpgradeWizardsService();
859
        $wizardsDone = $upgradeWizardsService->listOfWizardsDoneInRegistry();
860
        $rowUpdatersDone = $upgradeWizardsService->listOfRowUpdatersDoneInRegistry();
861
        $messages = new FlashMessageQueue('install');
862
        if (empty($wizardsDone) && empty($rowUpdatersDone)) {
863
            $messages->enqueue(new FlashMessage(
864
                '',
865
                'No wizards are marked as done'
866
            ));
867
        }
868
        return new JsonResponse([
869
            'success' => true,
870
            'status' => $messages,
871
            'wizardsDone' => $wizardsDone,
872
            'rowUpdatersDone' => $rowUpdatersDone,
873
        ]);
874
    }
875
876
    /**
877
     * Execute one upgrade wizard
878
     *
879
     * @param ServerRequestInterface $request
880
     * @return ResponseInterface
881
     */
882
    public function upgradeWizardsExecuteAction(ServerRequestInterface $request): ResponseInterface
883
    {
884
        // ext_localconf, db and ext_tables must be loaded for the updates :(
885
        $this->loadExtLocalconfDatabaseAndExtTables();
886
        $upgradeWizardsService = new UpgradeWizardsService();
887
        $identifier = $request->getParsedBody()['install']['identifier'];
888
        $messages = $upgradeWizardsService->executeWizard($identifier);
889
        return new JsonResponse([
890
            'success' => true,
891
            'status' => $messages,
892
        ]);
893
    }
894
895
    /**
896
     * Input stage of a specific upgrade wizard
897
     *
898
     * @param ServerRequestInterface $request
899
     * @return ResponseInterface
900
     */
901
    public function upgradeWizardsInputAction(ServerRequestInterface $request): ResponseInterface
902
    {
903
        // ext_localconf, db and ext_tables must be loaded for the updates :(
904
        $this->loadExtLocalconfDatabaseAndExtTables();
905
        $upgradeWizardsService = new UpgradeWizardsService();
906
        $identifier = $request->getParsedBody()['install']['identifier'];
907
        $result = $upgradeWizardsService->getWizardUserInput($identifier);
908
        return new JsonResponse([
909
            'success' => true,
910
            'status' => [],
911
            'userInput' => $result,
912
        ]);
913
    }
914
915
    /**
916
     * List available upgrade wizards
917
     *
918
     * @return ResponseInterface
919
     */
920
    public function upgradeWizardsListAction(): ResponseInterface
921
    {
922
        // ext_localconf, db and ext_tables must be loaded for the updates :(
923
        $this->loadExtLocalconfDatabaseAndExtTables();
924
        $upgradeWizardsService = new UpgradeWizardsService();
925
        $wizards = $upgradeWizardsService->getUpgradeWizardsList();
926
        return new JsonResponse([
927
            'success' => true,
928
            'status' => [],
929
            'wizards' => $wizards,
930
        ]);
931
    }
932
933
    /**
934
     * Mark a wizard as "not done"
935
     *
936
     * @param ServerRequestInterface $request
937
     * @return ResponseInterface
938
     */
939
    public function upgradeWizardsMarkUndoneAction(ServerRequestInterface $request): ResponseInterface
940
    {
941
        $this->loadExtLocalconfDatabaseAndExtTables();
942
        $wizardToBeMarkedAsUndoneIdentifier = $request->getParsedBody()['install']['identifier'];
943
        $upgradeWizardsService = new UpgradeWizardsService();
944
        $result = $upgradeWizardsService->markWizardUndoneInRegistry($wizardToBeMarkedAsUndoneIdentifier);
945
        $messages = new FlashMessageQueue('install');
946
        if ($result) {
947
            $messages->enqueue(new FlashMessage(
948
                '',
949
                'Wizard has been marked undone'
950
            ));
951
        } else {
952
            $messages->enqueue(new FlashMessage(
953
                '',
954
                'Wizard has not been marked undone',
955
                FlashMessage::ERROR
956
            ));
957
        }
958
        return new JsonResponse([
959
            'success' => true,
960
            'status' => $messages,
961
        ]);
962
    }
963
964
    /**
965
     * Execute silent database field adds like cache framework tables
966
     *
967
     * @return ResponseInterface
968
     */
969
    public function upgradeWizardsSilentUpgradesAction(): ResponseInterface
970
    {
971
        $this->loadExtLocalconfDatabaseAndExtTables();
972
        // Perform silent cache framework table upgrade
973
        $upgradeWizardsService = new UpgradeWizardsService();
974
        $statements = $upgradeWizardsService->silentCacheFrameworkTableSchemaMigration();
975
        $messages = new FlashMessageQueue('install');
976
        if (!empty($statements)) {
977
            $messages->enqueue(new FlashMessage(
978
                '',
979
                'Created some database cache tables.'
980
            ));
981
        }
982
        return new JsonResponse([
983
            'success' => true,
984
            'status' => $messages,
985
        ]);
986
    }
987
988
    /**
989
     * Initialize the core upgrade actions
990
     *
991
     * @throws \RuntimeException
992
     */
993
    protected function coreUpdateInitialize()
994
    {
995
        $this->coreUpdateService = GeneralUtility::makeInstance(CoreUpdateService::class);
996
        $this->coreVersionService = GeneralUtility::makeInstance(CoreVersionService::class);
997
        if (!$this->coreUpdateService->isCoreUpdateEnabled()) {
998
            throw new \RuntimeException(
999
                'Core Update disabled in this environment',
1000
                1381609294
1001
            );
1002
        }
1003
        // @todo: Does the core updater really depend on loaded ext_* files?
1004
        $this->loadExtLocalconfDatabaseAndExtTables();
1005
    }
1006
1007
    /**
1008
     * Find out which version upgrade should be handled. This may
1009
     * be different depending on whether development or regular release.
1010
     *
1011
     * @param ServerRequestInterface $request
1012
     * @throws \RuntimeException
1013
     * @return string Version to handle, eg. 6.2.2
1014
     */
1015
    protected function coreUpdateGetVersionToHandle(ServerRequestInterface $request): string
1016
    {
1017
        $type = $request->getQueryParams()['install']['type'];
1018
        if (!isset($type) || empty($type)) {
1019
            throw new \RuntimeException(
1020
                'Type must be set to either "regular" or "development"',
1021
                1380975303
1022
            );
1023
        }
1024
        if ($type === 'development') {
1025
            $versionToHandle = $this->coreVersionService->getYoungestPatchDevelopmentRelease();
1026
        } else {
1027
            $versionToHandle = $this->coreVersionService->getYoungestPatchRelease();
1028
        }
1029
        return $versionToHandle;
1030
    }
1031
1032
    /**
1033
     * Loads ext_localconf.php for a single extension. Method is a modified copy of
1034
     * the original bootstrap method.
1035
     *
1036
     * @param string $extensionKey
1037
     * @param array $extension
1038
     */
1039
    protected function extensionCompatTesterLoadExtLocalconfForExtension($extensionKey, array $extension)
1040
    {
1041
        // This is the main array meant to be manipulated in the ext_localconf.php files
1042
        // In general it is recommended to not rely on it to be globally defined in that
1043
        // scope but to use $GLOBALS['TYPO3_CONF_VARS'] instead.
1044
        // Nevertheless we define it here as global for backwards compatibility.
1045
        global $TYPO3_CONF_VARS;
1046
        $_EXTKEY = $extensionKey;
1047
        if (isset($extension['ext_localconf.php']) && $extension['ext_localconf.php']) {
1048
            // $_EXTKEY and $_EXTCONF are available in ext_localconf.php
1049
            // and are explicitly set in cached file as well
1050
            $_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null;
1051
            require $extension['ext_localconf.php'];
1052
        }
1053
    }
1054
1055
    /**
1056
     * Loads ext_tables.php for a single extension. Method is a modified copy of
1057
     * the original bootstrap method.
1058
     *
1059
     * @param string $extensionKey
1060
     * @param array $extension
1061
     */
1062
    protected function extensionCompatTesterLoadExtTablesForExtension($extensionKey, array $extension)
1063
    {
1064
        // In general it is recommended to not rely on it to be globally defined in that
1065
        // scope, but we can not prohibit this without breaking backwards compatibility
1066
        global $T3_SERVICES, $T3_VAR, $TYPO3_CONF_VARS;
1067
        global $TBE_MODULES, $TBE_MODULES_EXT, $TCA;
1068
        global $PAGES_TYPES, $TBE_STYLES;
1069
        global $_EXTKEY;
1070
        // Load each ext_tables.php file of loaded extensions
1071
        $_EXTKEY = $extensionKey;
1072
        if (isset($extension['ext_tables.php']) && $extension['ext_tables.php']) {
1073
            // $_EXTKEY and $_EXTCONF are available in ext_tables.php
1074
            // and are explicitly set in cached file as well
1075
            $_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null;
1076
            require $extension['ext_tables.php'];
1077
        }
1078
    }
1079
1080
    /**
1081
     * Get a list of '.rst' files and their details for "Upgrade documentation" view.
1082
     *
1083
     * @return array
1084
     */
1085
    protected function getDocumentationFiles(): array
1086
    {
1087
        $documentationFileService = new DocumentationFile();
1088
        $documentationFiles = $documentationFileService->findDocumentationFiles(
1089
            strtr(realpath(ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog'), '\\', '/')
1090
        );
1091
        $documentationFiles = array_reverse($documentationFiles);
1092
1093
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_registry');
1094
        $filesMarkedAsRead = $queryBuilder
1095
            ->select('*')
1096
            ->from('sys_registry')
1097
            ->where(
1098
                $queryBuilder->expr()->eq(
1099
                    'entry_namespace',
1100
                    $queryBuilder->createNamedParameter('upgradeAnalysisIgnoredFiles', \PDO::PARAM_STR)
1101
                )
1102
            )
1103
            ->execute()
1104
            ->fetchAll();
1105
        $hashesMarkedAsRead = [];
1106
        foreach ($filesMarkedAsRead as $file) {
1107
            $hashesMarkedAsRead[] = $file['entry_key'];
1108
        }
1109
1110
        $fileMarkedAsNotAffected = $queryBuilder
1111
            ->select('*')
1112
            ->from('sys_registry')
1113
            ->where(
1114
                $queryBuilder->expr()->eq(
1115
                    'entry_namespace',
1116
                    $queryBuilder->createNamedParameter('extensionScannerNotAffected', \PDO::PARAM_STR)
1117
                )
1118
            )
1119
            ->execute()
1120
            ->fetchAll();
1121
        $hashesMarkedAsNotAffected = [];
1122
        foreach ($fileMarkedAsNotAffected as $file) {
1123
            $hashesMarkedAsNotAffected[] = $file['entry_key'];
1124
        }
1125
1126
        $readFiles = [];
1127
        foreach ($documentationFiles as $section => &$files) {
1128
            foreach ($files as $fileId => $fileData) {
1129
                if (in_array($fileData['file_hash'], $hashesMarkedAsRead, true)) {
1130
                    $fileData['section'] = $section;
1131
                    $readFiles[$fileId] = $fileData;
1132
                    unset($files[$fileId]);
1133
                }
1134
            }
1135
        }
1136
1137
        $notAffectedFiles = [];
1138
        foreach ($documentationFiles as $section => &$files) {
1139
            foreach ($files as $fileId => $fileData) {
1140
                if (in_array($fileData['file_hash'], $hashesMarkedAsNotAffected, true)) {
1141
                    $fileData['section'] = $section;
1142
                    $notAffectedFiles[$fileId] = $fileData;
1143
                    unset($files[$fileId]);
1144
                }
1145
            }
1146
        }
1147
1148
        return [
1149
            'normalFiles' => $documentationFiles,
1150
            'readFiles' => $readFiles,
1151
            'notAffectedFiles' => $notAffectedFiles,
1152
        ];
1153
    }
1154
1155
    /**
1156
     * Find a code line in a file
1157
     *
1158
     * @param string $file Absolute path to file
1159
     * @param int $lineNumber Find this line in file
1160
     * @return string Code line
1161
     */
1162
    protected function extensionScannerGetLineFromFile(string $file, int $lineNumber): string
1163
    {
1164
        $fileContent = file($file, FILE_IGNORE_NEW_LINES);
1165
        $line = '';
1166
        if (isset($fileContent[$lineNumber - 1])) {
1167
            $line = trim($fileContent[$lineNumber - 1]);
1168
        }
1169
        return $line;
1170
    }
1171
}
1172