with JS initiating further stuff */ public function initAction(ServerRequestInterface $request): ResponseInterface { $bust = $GLOBALS['EXEC_TIME']; if (!Environment::getContext()->isDevelopment()) { $bust = $this->hashService->hmac((new Typo3Version()) . Environment::getProjectPath(), self::class); } $sitePath = $request->getAttribute('normalizedParams')->getSitePath(); $importMap = $this->importMapFactory->create($sitePath); $initModule = $importMap->resolveImport('@typo3/install/init-installer.js', true, $sitePath); $view = $this->initializeView($request); $view->assign('bust', $bust); $view->assign('initModule', $initModule); $view->assign('iconCacheIdentifier', sha1($this->iconRegistry->getBackendIconsCacheIdentifier())); $nonce = new ConsumableNonce(); $view->assign('importmap', $importMap->render($sitePath, $nonce)); return new HtmlResponse( $view->render('Installer/Init'), 200, [ 'Content-Security-Policy' => $this->createContentSecurityPolicy()->compile(new PolicyBag(Scope::backend(), new Map(), new Behavior(), $nonce, $this->directiveHashCollection)), 'Cache-Control' => 'no-cache, no-store', 'Pragma' => 'no-cache', ] ); } /** * Main layout with progress bar, header */ public function mainLayoutAction(ServerRequestInterface $request): ResponseInterface { $view = $this->initializeView($request); return new JsonResponse([ 'success' => true, 'html' => $view->render('Installer/MainLayout'), ]); } /** * Render "FIRST_INSTALL file need to exist" view */ public function showInstallerNotAvailableAction(ServerRequestInterface $request): ResponseInterface { $view = $this->initializeView($request); return new JsonResponse([ 'success' => true, 'html' => $view->render('Installer/ShowInstallerNotAvailable'), ]); } /** * Check if "environment and folders" should be shown */ public function checkEnvironmentAndFoldersAction(): ResponseInterface { return new JsonResponse([ 'success' => @is_file($this->configurationManager->getSystemConfigurationFileLocation()), ]); } /** * Render "environment and folders" */ public function showEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface { $view = $this->initializeView($request); $systemCheckMessageQueue = new FlashMessageQueue('install'); $checkMessages = (new Check())->getStatus(); foreach ($checkMessages as $message) { $systemCheckMessageQueue->enqueue($message); } $setupCheckMessages = (new SetupCheck())->getStatus(); foreach ($setupCheckMessages as $message) { $systemCheckMessageQueue->enqueue($message); } $folderStructureFactory = new DefaultFactory(); $structureFacade = $folderStructureFactory->getStructure(WebserverType::fromRequest($request)); $structureMessageQueue = $structureFacade->getStatus(); return new JsonResponse([ 'success' => true, 'html' => $view->render('Installer/ShowEnvironmentAndFolders'), 'environmentStatusErrors' => $systemCheckMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR), 'environmentStatusWarnings' => $systemCheckMessageQueue->getAllMessages(ContextualFeedbackSeverity::WARNING), 'structureErrors' => $structureMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR), ]); } /** * Create main folder layout, LocalConfiguration, PackageStates */ public function executeEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface { $errorsFromStructure = $this->setupService->createDirectoryStructure(WebserverType::fromRequest($request)); try { $this->setupService->prepareSystemSettings(); } catch (ConfigurationDirectoryDoesNotExistException) { return new JsonResponse([ 'success' => false, 'status' => $errorsFromStructure, ]); } return new JsonResponse([ 'success' => true, ]); } /** * Check if trusted hosts pattern needs to be adjusted */ public function checkTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface { $serverParams = $request->getServerParams(); $host = $serverParams['HTTP_HOST'] ?? ''; return new JsonResponse([ 'success' => $this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams), ]); } /** * Adjust trusted hosts pattern to '.*' if it does not match yet */ public function executeAdjustTrustedHostsPatternAction(ServerRequestInterface $request): ResponseInterface { $serverParams = $request->getServerParams(); $host = $serverParams['HTTP_HOST'] ?? ''; if (!$this->verifyHostHeader->isAllowedHostHeaderValue($host, $serverParams)) { $this->configurationManager->setLocalConfigurationValueByPath('SYS/trustedHostsPattern', '.*'); } return new JsonResponse([ 'success' => true, ]); } /** * Check if database connect step needs to be shown */ public function checkDatabaseConnectAction(): ResponseInterface { return new JsonResponse([ 'success' => $this->setupDatabaseService->isDatabaseConfigurationComplete() && $this->setupDatabaseService->isDatabaseConnectSuccessful(), ]); } /** * Show database connect step */ public function showDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface { $view = $this->initializeView($request); $driverOptions = $this->setupDatabaseService->getDriverOptions(); $formProtection = $this->formProtectionFactory->createFromRequest($request); $driverOptions['executeDatabaseConnectToken'] = $formProtection->generateToken('installTool', 'executeDatabaseConnect'); $view->assignMultiple($driverOptions); return new JsonResponse([ 'success' => true, 'html' => $view->render('Installer/ShowDatabaseConnect'), ]); } /** * Test database connect data */ public function executeDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface { $postValues = $request->getParsedBody()['install']['values']; [$success, $messages] = $this->setupDatabaseService->setDefaultConnectionSettings($postValues); return new JsonResponse([ 'success' => $success, 'status' => $messages, ]); } /** * Check if a database needs to be selected */ public function checkDatabaseSelectAction(): ResponseInterface { return new JsonResponse([ 'success' => $this->setupDatabaseService->checkDatabaseSelect(), ]); } /** * Render "select a database" */ public function showDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface { $view = $this->initializeView($request); $formProtection = $this->formProtectionFactory->createFromRequest($request); $errors = []; try { $view->assign('databaseList', $this->setupDatabaseService->getDatabaseList()); } catch (\Exception $exception) { $errors[] = $exception->getMessage(); } $view->assignMultiple([ 'errors' => $errors, 'executeDatabaseSelectToken' => $formProtection->generateToken('installTool', 'executeDatabaseSelect'), 'executeCheckDatabaseRequirementsToken' => $formProtection->generateToken('installTool', 'checkDatabaseRequirements'), ]); return new JsonResponse([ 'success' => true, 'html' => $view->render('Installer/ShowDatabaseSelect'), ]); } /** * Pre-check whether all requirements for the installed database driver and platform are fulfilled */ public function checkDatabaseRequirementsAction(ServerRequestInterface $request): ResponseInterface { $success = true; $messages = []; $databaseDriverName = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['driver']; $databaseName = $this->retrieveDatabaseNameFromRequest($request); if ($databaseName === '') { return new JsonResponse([ 'success' => false, 'status' => [ new FlashMessage( 'You must select a database.', 'No Database selected', ContextualFeedbackSeverity::ERROR ), ], ]); } $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname'] = $databaseName; foreach ($this->setupDatabaseService->checkDatabaseRequirementsForDriver($databaseDriverName) as $message) { if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) { $success = false; $messages[] = $message; } } // Check create and drop permissions $statusMessages = []; foreach ($this->setupDatabaseService->checkRequiredDatabasePermissions() as $checkRequiredPermission) { $statusMessages[] = new FlashMessage( $checkRequiredPermission, 'Missing required permissions', ContextualFeedbackSeverity::ERROR ); } if ($statusMessages !== []) { return new JsonResponse([ 'success' => false, 'status' => $statusMessages, ]); } // if requirements are not fulfilled if ($success === false) { // remove the database again if we created it if ($request->getParsedBody()['install']['values']['type'] === 'new') { $connection = $this->connectionPool ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME); $connection ->createSchemaManager() ->dropDatabase($connection->quoteIdentifier($databaseName)); } $this->configurationManager->removeLocalConfigurationKeysByPath(['DB/Connections/Default/dbname']); $message = new FlashMessage( sprintf( 'Database with name "%s" has been removed due to the following errors. ' . 'Please solve them first and try again. If you tried to create a new database make also sure, that the DBMS charset is to use UTF-8', $databaseName ), '', ContextualFeedbackSeverity::INFO ); array_unshift($messages, $message); } unset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname']); return new JsonResponse([ 'success' => $success, 'status' => $messages, ]); } private function retrieveDatabaseNameFromRequest(ServerRequestInterface $request): string { $postValues = $request->getParsedBody()['install']['values']; if ($postValues['type'] === 'new') { return $postValues['new']; } if ($postValues['type'] === 'existing' && !empty($postValues['existing'])) { return $postValues['existing']; } return ''; } /** * Select / create and test a database */ public function executeDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface { $databaseName = $this->retrieveDatabaseNameFromRequest($request); if ($databaseName === '') { return new JsonResponse([ 'success' => false, 'status' => [ new FlashMessage( 'You must select a database.', 'No Database selected', ContextualFeedbackSeverity::ERROR ), ], ]); } $postValues = $request->getParsedBody()['install']['values']; if ($postValues['type'] === 'new') { $status = $this->setupDatabaseService->createNewDatabase($databaseName); if ($status->getSeverity() === ContextualFeedbackSeverity::ERROR) { return new JsonResponse([ 'success' => false, 'status' => [$status], ]); } } elseif ($postValues['type'] === 'existing') { $status = $this->setupDatabaseService->checkExistingDatabase($databaseName); if ($status->getSeverity() === ContextualFeedbackSeverity::ERROR) { return new JsonResponse([ 'success' => false, 'status' => [$status], ]); } } return new JsonResponse([ 'success' => true, ]); } /** * Check if initial data needs to be imported */ public function checkDatabaseDataAction(): ResponseInterface { $existingTables = $this->connectionPool ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME) ->createSchemaManager() ->listTableNames(); return new JsonResponse([ 'success' => !empty($existingTables), ]); } /** * Render "import initial data" */ public function showDatabaseDataAction(ServerRequestInterface $request): ResponseInterface { $view = $this->initializeView($request); $formProtection = $this->formProtectionFactory->createFromRequest($request); $view->assignMultiple([ 'executeDatabaseDataToken' => $formProtection->generateToken('installTool', 'executeDatabaseData'), ]); return new JsonResponse([ 'success' => true, 'html' => $view->render('Installer/ShowDatabaseData'), ]); } /** * Create main db layout */ public function executeDatabaseDataAction(ServerRequestInterface $request): ResponseInterface { $messages = []; $postValues = $request->getParsedBody()['install']['values']; $username = (string)$postValues['username'] !== '' ? $postValues['username'] : 'admin'; // Check password and return early if not good enough $password = (string)($postValues['password'] ?? ''); $email = $postValues['email'] ?? ''; $passwordValidationErrors = $this->setupDatabaseService->getBackendUserPasswordValidationErrors($password); if (!empty($passwordValidationErrors)) { $messages[] = new FlashMessage( 'Administrator password not secure enough!', '', ContextualFeedbackSeverity::ERROR ); // Add all password validation errors to the messages array foreach ($passwordValidationErrors as $error) { $messages[] = new FlashMessage( $error, '', ContextualFeedbackSeverity::ERROR ); } return new JsonResponse([ 'success' => false, 'status' => $messages, ]); } // Set site name if (!empty($postValues['sitename'])) { $this->setupService->setSiteName($postValues['sitename']); } try { $messages = $this->setupDatabaseService->importDatabaseData(); if (!empty($messages)) { return new JsonResponse([ 'success' => false, 'status' => $messages, ]); } } catch (StatementException $exception) { $messages[] = new FlashMessage( 'Error detected in SQL statement:' . LF . $exception->getMessage(), 'Import of database data could not be performed', ContextualFeedbackSeverity::ERROR ); return new JsonResponse([ 'success' => false, 'status' => $messages, ]); } $this->commandLineUserCreation->ensureCliUserExists(); $this->setupService->createUser($username, $password, $email); $this->setupService->setInstallToolPassword($password); return new JsonResponse([ 'success' => true, 'status' => $messages, ]); } /** * Show last "create site with theme / install distribution" */ public function showDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface { $view = $this->initializeView($request); $formProtection = $this->formProtectionFactory->createFromRequest($request); $distributions = []; if ($this->packageManager->isPackageActive('impexp')) { $distributions = $this->setupService->getAvailableDistributions(); } $view->assignMultiple([ 'composerMode' => Environment::isComposerMode(), 'offerToCreateBasicSite' => $this->packageManager->isPackageActive('fluid_styled_content'), 'distributions' => $distributions, 'executeDefaultConfigurationToken' => $formProtection->generateToken('installTool', 'executeDefaultConfiguration'), ]); return new JsonResponse([ 'success' => true, 'html' => $view->render('Installer/ShowDefaultConfiguration'), ]); } /** * Last step execution: clean up, remove FIRST_INSTALL file, ... */ public function executeDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface { // Let the admin user redirect to the distributions page on first login $siteSetup = $request->getParsedBody()['install']['values']['sitesetup'] ?? ''; $selectedDistribution = ''; if (str_starts_with($siteSetup, 'createsite:')) { $selectedDistribution = substr($siteSetup, strlen('createsite:')); $siteSetup = 'activateDistribution'; } // It is crucial to activate the package *before* loading the container if ($siteSetup === 'activateDistribution') { // Distribution handles all site creation (pages, content, site configuration) $this->setupService->activateDistributionPackage($selectedDistribution); } $nextStepUrl = $this->uriBuilder->buildUriFromRoute('login'); if ($siteSetup === 'createsite') { $siteUrl = $request->getAttribute('normalizedParams')->getSiteUrl(); $this->setupService->createSite('main', $siteUrl); } elseif ($siteSetup === 'loaddistribution' && !Environment::isComposerMode() && $this->packageManager->isPackageActive('extensionmanager') ) { // Update the URL to redirect after login to the extension manager distributions list $nextStepUrl = $this->uriBuilder->buildUriWithRedirect( 'login', [], RouteRedirect::create( 'extensionmanager', [ 'action' => 'distributions', ] ) ); } if (($request->getParsedBody()['install']['values']['backendgroups'] ?? '') === 'creategroups') { $this->setupService->createBackendUserGroups(); } $this->bootService->unsetInternalContainerInstance(); $container = $this->bootService->loadExtLocalconfDatabase(true); // Mark upgrade wizards as done $this->setupDatabaseService->markWizardsDone($container); // Set up all installed extensions // (includes e.g. publishing of assets, importing distribution data) $this->setupService->setupExtensions($container); $formProtection = $this->formProtectionFactory->createFromRequest($request); $formProtection->clean(); EnableFileService::removeFirstInstallFile(); return new JsonResponse([ 'success' => true, 'redirect' => (string)$nextStepUrl, ]); } /** * Helper method to initialize a standalone view instance. */ private function initializeView(ServerRequestInterface $request): ViewInterface { $templatePaths = [ 'templateRootPaths' => ['EXT:install/Resources/Private/Templates'], ]; $renderingContext = $this->renderingContextFactory->create($templatePaths, $request); $fluidView = new FluidTemplateView($renderingContext); return new FluidViewAdapter($fluidView); } }