1<?php 2 3declare(strict_types=1); 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 18namespace TYPO3\CMS\Install\Controller; 19 20use Psr\Http\Message\ResponseInterface; 21use Psr\Http\Message\ServerRequestInterface; 22use TYPO3\CMS\Core\Cache\CacheManager; 23use TYPO3\CMS\Core\Configuration\ConfigurationManager; 24use TYPO3\CMS\Core\Core\ClassLoadingInformation; 25use TYPO3\CMS\Core\Core\Environment; 26use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; 27use TYPO3\CMS\Core\Database\ConnectionPool; 28use TYPO3\CMS\Core\Database\Schema\Exception\StatementException; 29use TYPO3\CMS\Core\Database\Schema\SchemaMigrator; 30use TYPO3\CMS\Core\Database\Schema\SqlReader; 31use TYPO3\CMS\Core\FormProtection\FormProtectionFactory; 32use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection; 33use TYPO3\CMS\Core\Http\JsonResponse; 34use TYPO3\CMS\Core\Localization\Locales; 35use TYPO3\CMS\Core\Messaging\FlashMessage; 36use TYPO3\CMS\Core\Messaging\FlashMessageQueue; 37use TYPO3\CMS\Core\Service\OpcodeCacheService; 38use TYPO3\CMS\Core\Utility\GeneralUtility; 39use TYPO3\CMS\Install\Service\ClearCacheService; 40use TYPO3\CMS\Install\Service\ClearTableService; 41use TYPO3\CMS\Install\Service\LanguagePackService; 42use TYPO3\CMS\Install\Service\LateBootService; 43use TYPO3\CMS\Install\Service\Typo3tempFileService; 44 45/** 46 * Maintenance controller 47 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API. 48 */ 49class MaintenanceController extends AbstractController 50{ 51 /** 52 * @var LateBootService 53 */ 54 private $lateBootService; 55 56 /** 57 * @var ClearCacheService 58 */ 59 private $clearCacheService; 60 61 /** 62 * @var ConfigurationManager 63 */ 64 private $configurationManager; 65 66 /** 67 * @var PasswordHashFactory 68 */ 69 private $passwordHashFactory; 70 71 /** 72 * @var Locales 73 */ 74 private $locales; 75 76 public function __construct( 77 LateBootService $lateBootService, 78 ClearCacheService $clearCacheService, 79 ConfigurationManager $configurationManager, 80 PasswordHashFactory $passwordHashFactory, 81 Locales $locales 82 ) { 83 $this->lateBootService = $lateBootService; 84 $this->clearCacheService = $clearCacheService; 85 $this->configurationManager = $configurationManager; 86 $this->passwordHashFactory = $passwordHashFactory; 87 $this->locales = $locales; 88 } 89 /** 90 * Main "show the cards" view 91 * 92 * @param ServerRequestInterface $request 93 * @return ResponseInterface 94 */ 95 public function cardsAction(ServerRequestInterface $request): ResponseInterface 96 { 97 $view = $this->initializeStandaloneView($request, 'Maintenance/Cards.html'); 98 return new JsonResponse([ 99 'success' => true, 100 'html' => $view->render(), 101 ]); 102 } 103 104 /** 105 * Clear cache framework and opcode caches 106 * 107 * @return ResponseInterface 108 */ 109 public function cacheClearAllAction(): ResponseInterface 110 { 111 $this->clearCacheService->clearAll(); 112 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive(); 113 $messageQueue = (new FlashMessageQueue('install'))->enqueue( 114 new FlashMessage('Successfully cleared all caches and all available opcode caches.', 'Caches cleared') 115 ); 116 return new JsonResponse([ 117 'success' => true, 118 'status' => $messageQueue, 119 ]); 120 } 121 122 /** 123 * Clear typo3temp files statistics action 124 * 125 * @param ServerRequestInterface $request 126 * @return ResponseInterface 127 */ 128 public function clearTypo3tempFilesStatsAction(ServerRequestInterface $request): ResponseInterface 129 { 130 $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false); 131 $typo3tempFileService = $container->get(Typo3tempFileService::class); 132 133 $view = $this->initializeStandaloneView($request, 'Maintenance/ClearTypo3tempFiles.html'); 134 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); 135 $view->assignMultiple([ 136 'clearTypo3tempFilesToken' => $formProtection->generateToken('installTool', 'clearTypo3tempFiles'), 137 ]); 138 return new JsonResponse( 139 [ 140 'success' => true, 141 'stats' => $typo3tempFileService->getDirectoryStatistics(), 142 'html' => $view->render(), 143 'buttons' => [ 144 [ 145 'btnClass' => 'btn-default t3js-clearTypo3temp-stats', 146 'text' => 'Scan again', 147 ], 148 ], 149 ] 150 ); 151 } 152 153 /** 154 * Clear typo3temp/assets or FAL processed Files 155 * 156 * @param ServerRequestInterface $request 157 * @return ResponseInterface 158 */ 159 public function clearTypo3tempFilesAction(ServerRequestInterface $request): ResponseInterface 160 { 161 $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false); 162 $typo3tempFileService = $container->get(Typo3tempFileService::class); 163 $messageQueue = new FlashMessageQueue('install'); 164 $folder = $request->getParsedBody()['install']['folder']; 165 // storageUid is an optional post param if FAL storages should be cleaned 166 $storageUid = $request->getParsedBody()['install']['storageUid'] ?? null; 167 if ($storageUid === null) { 168 $typo3tempFileService->clearAssetsFolder($folder); 169 $messageQueue->enqueue(new FlashMessage('The directory "' . $folder . '" has been cleared successfully', 'Directory cleared')); 170 } else { 171 $storageUid = (int)$storageUid; 172 // We have to get the stats before deleting files, otherwise we're not able to retrieve the amount of files anymore 173 $stats = $typo3tempFileService->getStatsFromStorageByUid($storageUid); 174 $failedDeletions = $typo3tempFileService->clearProcessedFiles($storageUid); 175 if ($failedDeletions) { 176 $messageQueue->enqueue(new FlashMessage( 177 'Failed to delete ' . $failedDeletions . ' processed files. See TYPO3 log (by default typo3temp/var/log/typo3_*.log)', 178 'Failed to delete files', 179 FlashMessage::ERROR 180 )); 181 } else { 182 $messageQueue->enqueue(new FlashMessage( 183 sprintf('Removed %d files from directory "%s"', $stats['numberOfFiles'], $stats['directory']), 184 'Deleted processed files' 185 )); 186 } 187 } 188 return new JsonResponse([ 189 'success' => true, 190 'status' => $messageQueue, 191 ]); 192 } 193 194 /** 195 * Dump autoload information 196 * 197 * @return ResponseInterface 198 */ 199 public function dumpAutoloadAction(): ResponseInterface 200 { 201 $messageQueue = new FlashMessageQueue('install'); 202 if (Environment::isComposerMode()) { 203 $messageQueue->enqueue(new FlashMessage( 204 'Skipped generating additional class loading information in Composer mode.', 205 'Autoloader not dumped', 206 FlashMessage::NOTICE 207 )); 208 } else { 209 ClassLoadingInformation::dumpClassLoadingInformation(); 210 $messageQueue->enqueue(new FlashMessage( 211 'Successfully dumped class loading information for extensions.', 212 'Dumped autoloader' 213 )); 214 } 215 return new JsonResponse([ 216 'success' => true, 217 'status' => $messageQueue, 218 ]); 219 } 220 221 /** 222 * Get main database analyzer modal HTML 223 * 224 * @param ServerRequestInterface $request 225 * @return ResponseInterface 226 */ 227 public function databaseAnalyzerAction(ServerRequestInterface $request): ResponseInterface 228 { 229 $view = $this->initializeStandaloneView($request, 'Maintenance/DatabaseAnalyzer.html'); 230 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); 231 $view->assignMultiple([ 232 'databaseAnalyzerExecuteToken' => $formProtection->generateToken('installTool', 'databaseAnalyzerExecute'), 233 ]); 234 return new JsonResponse([ 235 'success' => true, 236 'html' => $view->render(), 237 'buttons' => [ 238 [ 239 'btnClass' => 'btn-default t3js-databaseAnalyzer-analyze', 240 'text' => 'Run database compare again', 241 ], [ 242 'btnClass' => 'btn-warning t3js-databaseAnalyzer-execute', 243 'text' => 'Apply selected changes', 244 ], 245 ], 246 ]); 247 } 248 249 /** 250 * Analyze current database situation 251 * 252 * @param ServerRequestInterface $request 253 * @return ResponseInterface 254 */ 255 public function databaseAnalyzerAnalyzeAction(ServerRequestInterface $request): ResponseInterface 256 { 257 $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(); 258 $messageQueue = new FlashMessageQueue('install'); 259 $suggestions = []; 260 try { 261 $sqlReader = $container->get(SqlReader::class); 262 $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString()); 263 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class); 264 $addCreateChange = $schemaMigrationService->getUpdateSuggestions($sqlStatements); 265 266 // Aggregate the per-connection statements into one flat array 267 $addCreateChange = array_merge_recursive(...array_values($addCreateChange)); 268 if (!empty($addCreateChange['create_table'])) { 269 $suggestion = [ 270 'key' => 'addTable', 271 'label' => 'Add tables', 272 'enabled' => true, 273 'children' => [], 274 ]; 275 foreach ($addCreateChange['create_table'] as $hash => $statement) { 276 $suggestion['children'][] = [ 277 'hash' => $hash, 278 'statement' => $statement, 279 ]; 280 } 281 $suggestions[] = $suggestion; 282 } 283 if (!empty($addCreateChange['add'])) { 284 $suggestion = [ 285 'key' => 'addField', 286 'label' => 'Add fields to tables', 287 'enabled' => true, 288 'children' => [], 289 ]; 290 foreach ($addCreateChange['add'] as $hash => $statement) { 291 $suggestion['children'][] = [ 292 'hash' => $hash, 293 'statement' => $statement, 294 ]; 295 } 296 $suggestions[] = $suggestion; 297 } 298 if (!empty($addCreateChange['change'])) { 299 $suggestion = [ 300 'key' => 'change', 301 'label' => 'Change fields', 302 'enabled' => false, 303 'children' => [], 304 ]; 305 foreach ($addCreateChange['change'] as $hash => $statement) { 306 $child = [ 307 'hash' => $hash, 308 'statement' => $statement, 309 ]; 310 if (isset($addCreateChange['change_currentValue'][$hash])) { 311 $child['current'] = $addCreateChange['change_currentValue'][$hash]; 312 } 313 $suggestion['children'][] = $child; 314 } 315 $suggestions[] = $suggestion; 316 } 317 318 // Difference from current to expected 319 $dropRename = $schemaMigrationService->getUpdateSuggestions($sqlStatements, true); 320 321 // Aggregate the per-connection statements into one flat array 322 $dropRename = array_merge_recursive(...array_values($dropRename)); 323 if (!empty($dropRename['change_table'])) { 324 $suggestion = [ 325 'key' => 'renameTableToUnused', 326 'label' => 'Remove tables (rename with prefix)', 327 'enabled' => false, 328 'children' => [], 329 ]; 330 foreach ($dropRename['change_table'] as $hash => $statement) { 331 $child = [ 332 'hash' => $hash, 333 'statement' => $statement, 334 ]; 335 if (!empty($dropRename['tables_count'][$hash])) { 336 $child['rowCount'] = $dropRename['tables_count'][$hash]; 337 } 338 $suggestion['children'][] = $child; 339 } 340 $suggestions[] = $suggestion; 341 } 342 if (!empty($dropRename['change'])) { 343 $suggestion = [ 344 'key' => 'renameTableFieldToUnused', 345 'label' => 'Remove unused fields (rename with prefix)', 346 'enabled' => false, 347 'children' => [], 348 ]; 349 foreach ($dropRename['change'] as $hash => $statement) { 350 $suggestion['children'][] = [ 351 'hash' => $hash, 352 'statement' => $statement, 353 ]; 354 } 355 $suggestions[] = $suggestion; 356 } 357 if (!empty($dropRename['drop'])) { 358 $suggestion = [ 359 'key' => 'deleteField', 360 'label' => 'Drop fields (really!)', 361 'enabled' => false, 362 'children' => [], 363 ]; 364 foreach ($dropRename['drop'] as $hash => $statement) { 365 $suggestion['children'][] = [ 366 'hash' => $hash, 367 'statement' => $statement, 368 ]; 369 } 370 $suggestions[] = $suggestion; 371 } 372 if (!empty($dropRename['drop_table'])) { 373 $suggestion = [ 374 'key' => 'deleteTable', 375 'label' => 'Drop tables (really!)', 376 'enabled' => false, 377 'children' => [], 378 ]; 379 foreach ($dropRename['drop_table'] as $hash => $statement) { 380 $child = [ 381 'hash' => $hash, 382 'statement' => $statement, 383 ]; 384 if (!empty($dropRename['tables_count'][$hash])) { 385 $child['rowCount'] = $dropRename['tables_count'][$hash]; 386 } 387 $suggestion['children'][] = $child; 388 } 389 $suggestions[] = $suggestion; 390 } 391 } catch (StatementException $e) { 392 $messageQueue->enqueue(new FlashMessage( 393 $e->getMessage(), 394 'Database analysis failed', 395 FlashMessage::ERROR 396 )); 397 } 398 return new JsonResponse([ 399 'success' => true, 400 'status' => $messageQueue, 401 'suggestions' => $suggestions, 402 ]); 403 } 404 405 /** 406 * Apply selected database changes 407 * 408 * @param ServerRequestInterface $request 409 * @return ResponseInterface 410 */ 411 public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface 412 { 413 $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(); 414 $messageQueue = new FlashMessageQueue('install'); 415 $selectedHashes = $request->getParsedBody()['install']['hashes'] ?? []; 416 if (empty($selectedHashes)) { 417 $messageQueue->enqueue(new FlashMessage( 418 'Please select any change by activating their respective checkboxes.', 419 'No database changes selected', 420 FlashMessage::WARNING 421 )); 422 } else { 423 $sqlReader = $container->get(SqlReader::class); 424 $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString()); 425 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class); 426 $statementHashesToPerform = array_flip($selectedHashes); 427 $results = $schemaMigrationService->migrate($sqlStatements, $statementHashesToPerform); 428 // Create error flash messages if any 429 foreach ($results as $errorMessage) { 430 $messageQueue->enqueue(new FlashMessage( 431 'Error: ' . $errorMessage, 432 'Database update failed', 433 FlashMessage::ERROR 434 )); 435 } 436 $messageQueue->enqueue(new FlashMessage( 437 'Executed database updates', 438 'Executed database updates' 439 )); 440 } 441 return new JsonResponse([ 442 'success' => true, 443 'status' => $messageQueue, 444 ]); 445 } 446 447 /** 448 * Clear table overview statistics action 449 * 450 * @param ServerRequestInterface $request 451 * @return ResponseInterface 452 */ 453 public function clearTablesStatsAction(ServerRequestInterface $request): ResponseInterface 454 { 455 $view = $this->initializeStandaloneView($request, 'Maintenance/ClearTables.html'); 456 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); 457 $view->assignMultiple([ 458 'clearTablesClearToken' => $formProtection->generateToken('installTool', 'clearTablesClear'), 459 ]); 460 return new JsonResponse([ 461 'success' => true, 462 'stats' => (new ClearTableService())->getTableStatistics(), 463 'html' => $view->render(), 464 'buttons' => [ 465 [ 466 'btnClass' => 'btn-default t3js-clearTables-stats', 467 'text' => 'Scan again', 468 ], 469 ], 470 ]); 471 } 472 473 /** 474 * Truncate a specific table 475 * 476 * @param ServerRequestInterface $request 477 * @return ResponseInterface 478 * @throws \RuntimeException 479 */ 480 public function clearTablesClearAction(ServerRequestInterface $request): ResponseInterface 481 { 482 $table = $request->getParsedBody()['install']['table']; 483 if (empty($table)) { 484 throw new \RuntimeException( 485 'No table name given', 486 1501944076 487 ); 488 } 489 (new ClearTableService())->clearSelectedTable($table); 490 $messageQueue = (new FlashMessageQueue('install'))->enqueue( 491 new FlashMessage('The table ' . $table . ' has been cleared.', 'Table cleared') 492 ); 493 return new JsonResponse([ 494 'success' => true, 495 'status' => $messageQueue, 496 ]); 497 } 498 /** 499 * Create Admin Get Data action 500 * 501 * @param ServerRequestInterface $request 502 * @return ResponseInterface 503 */ 504 public function createAdminGetDataAction(ServerRequestInterface $request): ResponseInterface 505 { 506 $view = $this->initializeStandaloneView($request, 'Maintenance/CreateAdmin.html'); 507 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); 508 $view->assignMultiple([ 509 'createAdminToken' => $formProtection->generateToken('installTool', 'createAdmin'), 510 ]); 511 return new JsonResponse([ 512 'success' => true, 513 'html' => $view->render(), 514 'buttons' => [ 515 [ 516 'btnClass' => 'btn-default t3js-createAdmin-create', 517 'text' => 'Create administrator user', 518 ], 519 ], 520 ]); 521 } 522 523 /** 524 * Create a backend administrator from given username and password 525 * 526 * @param ServerRequestInterface $request 527 * @return ResponseInterface 528 */ 529 public function createAdminAction(ServerRequestInterface $request): ResponseInterface 530 { 531 $username = preg_replace('/\\s/i', '', $request->getParsedBody()['install']['userName']); 532 $password = $request->getParsedBody()['install']['userPassword']; 533 $passwordCheck = $request->getParsedBody()['install']['userPasswordCheck']; 534 $email = $request->getParsedBody()['install']['userEmail'] ?? ''; 535 $isSystemMaintainer = ((bool)$request->getParsedBody()['install']['userSystemMaintainer'] == '1') ? true : false; 536 537 $messages = new FlashMessageQueue('install'); 538 539 if ($username === '') { 540 $messages->enqueue(new FlashMessage( 541 'No username given.', 542 'Administrator user not created', 543 FlashMessage::ERROR 544 )); 545 } elseif ($password !== $passwordCheck) { 546 $messages->enqueue(new FlashMessage( 547 'Passwords do not match.', 548 'Administrator user not created', 549 FlashMessage::ERROR 550 )); 551 } elseif (strlen($password) < 8) { 552 $messages->enqueue(new FlashMessage( 553 'Password must be at least eight characters long.', 554 'Administrator user not created', 555 FlashMessage::ERROR 556 )); 557 } else { 558 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 559 $userExists = $connectionPool->getConnectionForTable('be_users') 560 ->count( 561 'uid', 562 'be_users', 563 ['username' => $username] 564 ); 565 if ($userExists) { 566 $messages->enqueue(new FlashMessage( 567 'A user with username "' . $username . '" exists already.', 568 'Administrator user not created', 569 FlashMessage::ERROR 570 )); 571 } else { 572 $hashInstance = $this->passwordHashFactory->getDefaultHashInstance('BE'); 573 $hashedPassword = $hashInstance->getHashedPassword($password); 574 $adminUserFields = [ 575 'username' => $username, 576 'password' => $hashedPassword, 577 'admin' => 1, 578 'tstamp' => $GLOBALS['EXEC_TIME'], 579 'crdate' => $GLOBALS['EXEC_TIME'], 580 ]; 581 if (GeneralUtility::validEmail($email)) { 582 $adminUserFields['email'] = $email; 583 } 584 $connectionPool->getConnectionForTable('be_users')->insert('be_users', $adminUserFields); 585 586 if ($isSystemMaintainer) { 587 588 // Get the new admin user uid just created 589 $newAdminUserUid = (int)$connectionPool->getConnectionForTable('be_users')->lastInsertId('be_users'); 590 591 // Get the list of the existing systemMaintainer 592 $existingSystemMaintainersList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []; 593 594 // Add the new admin user to the existing systemMaintainer list 595 $newSystemMaintainersList = $existingSystemMaintainersList; 596 $newSystemMaintainersList[] = $newAdminUserUid; 597 598 // Update the LocalConfiguration.php file with the new list 599 $this->configurationManager->setLocalConfigurationValuesByPathValuePairs( 600 ['SYS/systemMaintainers' => $newSystemMaintainersList] 601 ); 602 } 603 604 $messages->enqueue(new FlashMessage( 605 'Administrator created', 606 'An administrator with username "' . $username . '" has been created successfully.' 607 )); 608 } 609 } 610 return new JsonResponse([ 611 'success' => true, 612 'status' => $messages, 613 ]); 614 } 615 616 /** 617 * Entry action of language packs module gets 618 * * list of available languages with details like active or not and last update 619 * * list of loaded extensions 620 * 621 * @param ServerRequestInterface $request 622 * @return ResponseInterface 623 */ 624 public function languagePacksGetDataAction(ServerRequestInterface $request): ResponseInterface 625 { 626 $view = $this->initializeStandaloneView($request, 'Maintenance/LanguagePacks.html'); 627 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); 628 $view->assignMultiple([ 629 'languagePacksActivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksActivateLanguage'), 630 'languagePacksDeactivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksDeactivateLanguage'), 631 'languagePacksUpdatePackToken' => $formProtection->generateToken('installTool', 'languagePacksUpdatePack'), 632 'languagePacksUpdateIsoTimesToken' => $formProtection->generateToken('installTool', 'languagePacksUpdateIsoTimes'), 633 ]); 634 // This action needs TYPO3_CONF_VARS for full GeneralUtility::getUrl() config 635 $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false, true); 636 $languagePackService = $container->get(LanguagePackService::class); 637 $extensions = $languagePackService->getExtensionLanguagePackDetails(); 638 return new JsonResponse([ 639 'success' => true, 640 'languages' => $languagePackService->getLanguageDetails(), 641 'extensions' => $extensions, 642 'activeLanguages' => $languagePackService->getActiveLanguages(), 643 'activeExtensions' => array_column($extensions, 'key'), 644 'html' => $view->render(), 645 ]); 646 } 647 648 /** 649 * Activate a language and any possible dependency it may have 650 * 651 * @param ServerRequestInterface $request 652 * @return ResponseInterface 653 */ 654 public function languagePacksActivateLanguageAction(ServerRequestInterface $request): ResponseInterface 655 { 656 $messageQueue = new FlashMessageQueue('install'); 657 $languagePackService = GeneralUtility::makeInstance(LanguagePackService::class); 658 $availableLanguages = $languagePackService->getAvailableLanguages(); 659 $activeLanguages = $languagePackService->getActiveLanguages(); 660 $iso = $request->getParsedBody()['install']['iso']; 661 $activateArray = []; 662 foreach ($availableLanguages as $availableIso => $name) { 663 if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) { 664 $activateArray[] = $iso; 665 $dependencies = $this->locales->getLocaleDependencies($availableIso); 666 if (!empty($dependencies)) { 667 foreach ($dependencies as $dependency) { 668 if (!in_array($dependency, $activeLanguages, true)) { 669 $activateArray[] = $dependency; 670 } 671 } 672 } 673 } 674 } 675 if (!empty($activateArray)) { 676 $activeLanguages = array_merge($activeLanguages, $activateArray); 677 sort($activeLanguages); 678 $this->configurationManager->setLocalConfigurationValueByPath( 679 'EXTCONF/lang', 680 ['availableLanguages' => $activeLanguages] 681 ); 682 $activationArray = []; 683 foreach ($activateArray as $activateIso) { 684 $activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')'; 685 } 686 $messageQueue->enqueue( 687 new FlashMessage( 688 'These languages have been activated: ' . implode(', ', $activationArray) 689 ) 690 ); 691 } else { 692 $messageQueue->enqueue( 693 new FlashMessage('Language with ISO code "' . $iso . '" not found or already active.', '', FlashMessage::ERROR) 694 ); 695 } 696 return new JsonResponse([ 697 'success' => true, 698 'status' => $messageQueue, 699 ]); 700 } 701 702 /** 703 * Deactivate a language if no other active language depends on it 704 * 705 * @param ServerRequestInterface $request 706 * @return ResponseInterface 707 * @throws \RuntimeException 708 */ 709 public function languagePacksDeactivateLanguageAction(ServerRequestInterface $request): ResponseInterface 710 { 711 $messageQueue = new FlashMessageQueue('install'); 712 $languagePackService = GeneralUtility::makeInstance(LanguagePackService::class); 713 $availableLanguages = $languagePackService->getAvailableLanguages(); 714 $activeLanguages = $languagePackService->getActiveLanguages(); 715 $iso = $request->getParsedBody()['install']['iso']; 716 if (empty($iso)) { 717 throw new \RuntimeException('No iso code given', 1520109807); 718 } 719 $otherActiveLanguageDependencies = []; 720 foreach ($activeLanguages as $activeLanguage) { 721 if ($activeLanguage === $iso) { 722 continue; 723 } 724 $dependencies = $this->locales->getLocaleDependencies($activeLanguage); 725 if (in_array($iso, $dependencies, true)) { 726 $otherActiveLanguageDependencies[] = $activeLanguage; 727 } 728 } 729 if (!empty($otherActiveLanguageDependencies)) { 730 // Error: Must disable dependencies first 731 $dependentArray = []; 732 foreach ($otherActiveLanguageDependencies as $dependency) { 733 $dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')'; 734 } 735 $messageQueue->enqueue( 736 new FlashMessage( 737 'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These' 738 . ' other languages depend on it and need to be deactivated before:' 739 . implode(', ', $dependentArray), 740 '', 741 FlashMessage::ERROR 742 ) 743 ); 744 } else { 745 if (in_array($iso, $activeLanguages, true)) { 746 // Deactivate this language 747 $newActiveLanguages = []; 748 foreach ($activeLanguages as $activeLanguage) { 749 if ($activeLanguage === $iso) { 750 continue; 751 } 752 $newActiveLanguages[] = $activeLanguage; 753 } 754 $this->configurationManager->setLocalConfigurationValueByPath( 755 'EXTCONF/lang', 756 ['availableLanguages' => $newActiveLanguages] 757 ); 758 $messageQueue->enqueue( 759 new FlashMessage( 760 'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated' 761 ) 762 ); 763 } else { 764 $messageQueue->enqueue( 765 new FlashMessage( 766 'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated', 767 '', 768 FlashMessage::ERROR 769 ) 770 ); 771 } 772 } 773 return new JsonResponse([ 774 'success' => true, 775 'status' => $messageQueue, 776 ]); 777 } 778 779 /** 780 * Update a pack of one extension and one language 781 * 782 * @param ServerRequestInterface $request 783 * @return ResponseInterface 784 * @throws \RuntimeException 785 */ 786 public function languagePacksUpdatePackAction(ServerRequestInterface $request): ResponseInterface 787 { 788 $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false, true); 789 $iso = $request->getParsedBody()['install']['iso']; 790 $key = $request->getParsedBody()['install']['extension']; 791 792 $languagePackService = $container->get(LanguagePackService::class); 793 794 return new JsonResponse([ 795 'success' => true, 796 'packResult' => $languagePackService->languagePackDownload($key, $iso), 797 ]); 798 } 799 800 /** 801 * Set "last updated" time in registry for fully updated language packs. 802 * 803 * @param ServerRequestInterface $request 804 * @return ResponseInterface 805 */ 806 public function languagePacksUpdateIsoTimesAction(ServerRequestInterface $request): ResponseInterface 807 { 808 $isos = $request->getParsedBody()['install']['isos']; 809 $languagePackService = GeneralUtility::makeInstance(LanguagePackService::class); 810 $languagePackService->setLastUpdatedIsoCode($isos); 811 812 // The cache manager is already instantiated in the install tool 813 // with some hacked settings to disable caching of extbase and fluid. 814 // We want a "fresh" object here to operate on a different cache setup. 815 // cacheManager implements SingletonInterface, so the only way to get a "fresh" 816 // instance is by circumventing makeInstance and using new directly! 817 $cacheManager = new CacheManager(); 818 $cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']); 819 $cacheManager->getCache('l10n')->flush(); 820 821 return new JsonResponse(['success' => true]); 822 } 823 824 /** 825 * Set 'uc' field of all backend users to empty string 826 * 827 * @return ResponseInterface 828 */ 829 public function resetBackendUserUcAction(): ResponseInterface 830 { 831 GeneralUtility::makeInstance(ConnectionPool::class) 832 ->getQueryBuilderForTable('be_users') 833 ->update('be_users') 834 ->set('uc', '') 835 ->executeStatement(); 836 $messageQueue = new FlashMessageQueue('install'); 837 $messageQueue->enqueue(new FlashMessage( 838 'Preferences of all backend users have been reset', 839 'Reset preferences of all backend users' 840 )); 841 return new JsonResponse([ 842 'success' => true, 843 'status' => $messageQueue, 844 ]); 845 } 846} 847