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