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