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