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