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