with JS initiating further stuff * * @return ResponseInterface */ public function initAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/Init.html'); return new HtmlResponse( $view->render(), 200, [ 'Cache-Control' => 'no-cache, must-revalidate', 'Pragma' => 'no-cache' ] ); } /** * Main layout with progress bar, header * * @return ResponseInterface */ public function mainLayoutAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/MainLayout.html'); return new JsonResponse([ 'success' => true, 'html' => $view->render(), ]); } /** * Render "FIRST_INSTALL file need to exist" view * * @return ResponseInterface */ public function showInstallerNotAvailableAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/ShowInstallerNotAvailable.html'); return new JsonResponse([ 'success' => true, 'html' => $view->render(), ]); } /** * Check if "environment and folders" should be shown * * @return ResponseInterface */ public function checkEnvironmentAndFoldersAction(): ResponseInterface { return new JsonResponse([ 'success' => @is_file(GeneralUtility::makeInstance(ConfigurationManager::class)->getLocalConfigurationFileLocation()), ]); } /** * Render "environment and folders" * * @return ResponseInterface */ public function showEnvironmentAndFoldersAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/ShowEnvironmentAndFolders.html'); $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 = GeneralUtility::makeInstance(DefaultFactory::class); $structureFacade = $folderStructureFactory->getStructure(); $structureMessageQueue = $structureFacade->getStatus(); return new JsonResponse([ 'success' => true, 'html' => $view->render(), 'environmentStatusErrors' => $systemCheckMessageQueue->getAllMessages(FlashMessage::ERROR), 'environmentStatusWarnings' => $systemCheckMessageQueue->getAllMessages(FlashMessage::WARNING), 'structureErrors' => $structureMessageQueue->getAllMessages(FlashMessage::ERROR), ]); } /** * Create main folder layout, LocalConfiguration, PackageStates * * @return ResponseInterface */ public function executeEnvironmentAndFoldersAction(): ResponseInterface { $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class); $structureFacade = $folderStructureFactory->getStructure(); $structureFixMessageQueue = $structureFacade->fix(); $errorsFromStructure = $structureFixMessageQueue->getAllMessages(FlashMessage::ERROR); if (@is_dir(Environment::getLegacyConfigPath())) { $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); $configurationManager->createLocalConfigurationFromFactoryConfiguration(); // Create a PackageStates.php with all packages activated marked as "part of factory default" if (!file_exists(Environment::getLegacyConfigPath() . '/PackageStates.php')) { $packageManager = GeneralUtility::makeInstance(PackageManager::class); $packages = $packageManager->getAvailablePackages(); foreach ($packages as $package) { if ($package instanceof PackageInterface && $package->isPartOfFactoryDefault() ) { $packageManager->activatePackage($package->getPackageKey()); } } $packageManager->forceSortAndSavePackageStates(); } $extensionConfiguration = new ExtensionConfiguration(); $extensionConfiguration->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions(); return new JsonResponse([ 'success' => true, ]); } return new JsonResponse([ 'success' => false, 'status' => $errorsFromStructure, ]); } /** * Check if trusted hosts pattern needs to be adjusted * * @return ResponseInterface */ public function checkTrustedHostsPatternAction(): ResponseInterface { return new JsonResponse([ 'success' => GeneralUtility::hostHeaderValueMatchesTrustedHostsPattern($_SERVER['HTTP_HOST']), ]); } /** * Adjust trusted hosts pattern to '.*' if it does not match yet * * @return ResponseInterface */ public function executeAdjustTrustedHostsPatternAction(): ResponseInterface { if (!GeneralUtility::hostHeaderValueMatchesTrustedHostsPattern($_SERVER['HTTP_HOST'])) { $configurationManager = new ConfigurationManager(); $configurationManager->setLocalConfigurationValueByPath('SYS/trustedHostsPattern', '.*'); } return new JsonResponse([ 'success' => true, ]); } /** * Execute silent configuration update. May be called multiple times until success = true is returned. * * @return ResponseInterface success = true if no change has been done */ public function executeSilentConfigurationUpdateAction(): ResponseInterface { $silentUpdate = new SilentConfigurationUpgradeService(); $success = true; try { $silentUpdate->execute(); } catch (ConfigurationChangedException $e) { $success = false; } return new JsonResponse([ 'success' => $success, ]); } /** * Check if database connect step needs to be shown * * @return ResponseInterface */ public function checkDatabaseConnectAction(): ResponseInterface { return new JsonResponse([ 'success' => $this->isDatabaseConnectSuccessful() && $this->isDatabaseConfigurationComplete(), ]); } /** * Show database connect step * * @return ResponseInterface */ public function showDatabaseConnectAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/ShowDatabaseConnect.html'); $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); $hasAtLeastOneOption = false; $activeAvailableOption = ''; if (extension_loaded('mysqli')) { $hasAtLeastOneOption = true; $view->assign('hasMysqliManualConfiguration', true); $mysqliManualConfigurationOptions = [ 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '', 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '', 'port' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? 3306, ]; $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '127.0.0.1'; if ($host === 'localhost') { $host = '127.0.0.1'; } $mysqliManualConfigurationOptions['host'] = $host; $view->assign('mysqliManualConfigurationOptions', $mysqliManualConfigurationOptions); $activeAvailableOption = 'mysqliManualConfiguration'; $view->assign('hasMysqliSocketManualConfiguration', true); $view->assign( 'mysqliSocketManualConfigurationOptions', [ 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '', 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '', 'socket' => $this->getDatabaseConfiguredMysqliSocket(), ] ); if ($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'mysqli' && $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] === 'localhost') { $activeAvailableOption = 'mysqliSocketManualConfiguration'; } } if (extension_loaded('pdo_pgsql')) { $hasAtLeastOneOption = true; $view->assign('hasPostgresManualConfiguration', true); $view->assign( 'postgresManualConfigurationOptions', [ 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '', 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '', 'host' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '127.0.0.1', 'port' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? 5432, 'database' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] ?? '', ] ); if ($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'pdo_pgsql') { $activeAvailableOption = 'postgresManualConfiguration'; } } if (extension_loaded('pdo_sqlite')) { $hasAtLeastOneOption = true; $view->assign('hasSqliteManualConfiguration', true); $view->assign( 'sqliteManualConfigurationOptions', [] ); if ($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'pdo_sqlite') { $activeAvailableOption = 'sqliteManualConfiguration'; } } if (!empty($this->getDatabaseConfigurationFromEnvironment())) { $hasAtLeastOneOption = true; $activeAvailableOption = 'configurationFromEnvironment'; $view->assign('hasConfigurationFromEnvironment', true); } $view->assignMultiple([ 'hasAtLeastOneOption' => $hasAtLeastOneOption, 'activeAvailableOption' => $activeAvailableOption, 'executeDatabaseConnectToken' => $formProtection->generateToken('installTool', 'executeDatabaseConnect'), ]); return new JsonResponse([ 'success' => true, 'html' => $view->render(), ]); } /** * Test database connect data * * @param ServerRequestInterface $request * @return ResponseInterface */ public function executeDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface { $messages = []; $postValues = $request->getParsedBody()['install']['values']; $defaultConnectionSettings = []; if ($postValues['availableSet'] === 'configurationFromEnvironment') { $defaultConnectionSettings = $this->getDatabaseConfigurationFromEnvironment(); } else { if (isset($postValues['driver'])) { $validDrivers = [ 'mysqli', 'pdo_mysql', 'pdo_pgsql', 'mssql', 'pdo_sqlite', ]; if (in_array($postValues['driver'], $validDrivers, true)) { $defaultConnectionSettings['driver'] = $postValues['driver']; } else { $messages[] = new FlashMessage( 'Given driver must be one of ' . implode(', ', $validDrivers), 'Database driver unknown', FlashMessage::ERROR ); } } if (isset($postValues['username'])) { $value = $postValues['username']; if (strlen($value) <= 50) { $defaultConnectionSettings['user'] = $value; } else { $messages[] = new FlashMessage( 'Given username must be shorter than fifty characters.', 'Database username not valid', FlashMessage::ERROR ); } } if (isset($postValues['password'])) { $defaultConnectionSettings['password'] = $postValues['password']; } if (isset($postValues['host'])) { $value = $postValues['host']; if (preg_match('/^[a-zA-Z0-9_\\.-]+(:.+)?$/', $value) && strlen($value) <= 255) { $defaultConnectionSettings['host'] = $value; } else { $messages[] = new FlashMessage( 'Given host is not alphanumeric (a-z, A-Z, 0-9 or _-.:) or longer than 255 characters.', 'Database host not valid', FlashMessage::ERROR ); } } if (isset($postValues['port']) && $postValues['host'] !== 'localhost') { $value = $postValues['port']; if (preg_match('/^[0-9]+(:.+)?$/', $value) && $value > 0 && $value <= 65535) { $defaultConnectionSettings['port'] = (int)$value; } else { $messages[] = new FlashMessage( 'Given port is not numeric or within range 1 to 65535.', 'Database port not valid', FlashMessage::ERROR ); } } if (isset($postValues['socket']) && $postValues['socket'] !== '') { if (@file_exists($postValues['socket'])) { $defaultConnectionSettings['unix_socket'] = $postValues['socket']; } else { $messages[] = new FlashMessage( 'Given socket location does not exist on server.', 'Socket does not exist', FlashMessage::ERROR ); } } if (isset($postValues['database'])) { $value = $postValues['database']; if (strlen($value) <= 50) { $defaultConnectionSettings['dbname'] = $value; } else { $messages[] = new FlashMessage( 'Given database name must be shorter than fifty characters.', 'Database name not valid', FlashMessage::ERROR ); } } // For sqlite a db path is automatically calculated if (isset($postValues['driver']) && $postValues['driver'] === 'pdo_sqlite') { $dbFilename = '/cms-' . (new Random())->generateRandomHexString(8) . '.sqlite'; // If the var/ folder exists outside of document root, put it into var/sqlite/ // Otherwise simply into typo3conf/ if (Environment::getProjectPath() !== Environment::getPublicPath()) { GeneralUtility::mkdir_deep(Environment::getVarPath() . '/sqlite'); $defaultConnectionSettings['path'] = Environment::getVarPath() . '/sqlite' . $dbFilename; } else { $defaultConnectionSettings['path'] = Environment::getConfigPath() . $dbFilename; } } // For mysql, set utf8mb4 as default charset if (isset($postValues['driver']) && in_array($postValues['driver'], ['mysqli', 'pdo_mysql'])) { $defaultConnectionSettings['charset'] = 'utf8mb4'; $defaultConnectionSettings['tableoptions'] = [ 'charset' => 'utf8mb4', 'collate' => 'utf8mb4_unicode_ci', ]; } } $success = false; if (!empty($defaultConnectionSettings)) { // Test connection settings and write to config if connect is successful try { $connectionParams = $defaultConnectionSettings; $connectionParams['wrapperClass'] = Connection::class; if (!isset($connectionParams['charset'])) { // utf-8 as default for non mysql $connectionParams['charset'] = 'utf-8'; } DriverManager::getConnection($connectionParams)->ping(); $success = true; } catch (DBALException $e) { $messages[] = new FlashMessage( 'Connecting to the database with given settings failed: ' . $e->getMessage(), 'Database connect not successful', FlashMessage::ERROR ); } $localConfigurationPathValuePairs = []; foreach ($defaultConnectionSettings as $settingsName => $value) { $localConfigurationPathValuePairs['DB/Connections/Default/' . $settingsName] = $value; } $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); // Remove full default connection array $configurationManager->removeLocalConfigurationKeysByPath(['DB/Connections/Default']); // Write new values $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs); } return new JsonResponse([ 'success' => $success, 'status' => $messages, ]); } /** * Check if a database needs to be selected * * @return ResponseInterface */ public function checkDatabaseSelectAction(): ResponseInterface { $success = false; if ((string)$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] !== '' || (string)$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['path'] !== '' ) { try { $success = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME) ->ping(); } catch (DBALException $e) { } } return new JsonResponse([ 'success' => $success, ]); } /** * Render "select a database" * * @return ResponseInterface */ public function showDatabaseSelectAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/ShowDatabaseSelect.html'); $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); $errors = []; try { $view->assign('databaseList', $this->getDatabaseList()); } catch (\Exception $exception) { $errors[] = $exception->getMessage(); } $view->assignMultiple([ 'errors' => $errors, 'executeDatabaseSelectToken' => $formProtection->generateToken('installTool', 'executeDatabaseSelect'), ]); return new JsonResponse([ 'success' => true, 'html' => $view->render(), ]); } /** * Select / create and test a database * * @param ServerRequestInterface $request * @return ResponseInterface */ public function executeDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface { $postValues = $request->getParsedBody()['install']['values']; if ($postValues['type'] === 'new') { $status = $this->createNewDatabase($postValues['new']); if ($status->getSeverity() === FlashMessage::ERROR) { return new JsonResponse([ 'success' => false, 'status' => [$status], ]); } } elseif ($postValues['type'] === 'existing' && !empty($postValues['existing'])) { $status = $this->checkExistingDatabase($postValues['existing']); if ($status->getSeverity() === FlashMessage::ERROR) { return new JsonResponse([ 'success' => false, 'status' => [$status], ]); } } else { return new JsonResponse([ 'success' => false, 'status' => [ new FlashMessage( 'You must select a database.', 'No Database selected', FlashMessage::ERROR ), ], ]); } return new JsonResponse([ 'success' => true, ]); } /** * Check if initial data needs to be imported * * @return ResponseInterface */ public function checkDatabaseDataAction(): ResponseInterface { $existingTables = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionByName('Default') ->getSchemaManager() ->listTableNames(); return new JsonResponse([ 'success' => !empty($existingTables), ]); } /** * Render "import initial data" * * @return ResponseInterface */ public function showDatabaseDataAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/ShowDatabaseData.html'); $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); $view->assignMultiple([ 'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], 'executeDatabaseDataToken' => $formProtection->generateToken('installTool', 'executeDatabaseData'), ]); return new JsonResponse([ 'success' => true, 'html' => $view->render(), ]); } /** * Create main db layout * * @param ServerRequestInterface $request * @return ResponseInterface */ public function executeDatabaseDataAction(ServerRequestInterface $request): ResponseInterface { $messages = []; $configurationManager = new ConfigurationManager(); $postValues = $request->getParsedBody()['install']['values']; $username = (string)$postValues['username'] !== '' ? $postValues['username'] : 'admin'; // Check password and return early if not good enough $password = $postValues['password']; if (empty($password) || strlen($password) < 8) { $messages[] = new FlashMessage( 'You are setting an important password here! It gives an attacker full control over your instance if cracked.' . ' It should be strong (include lower and upper case characters, special characters and numbers) and must be at least eight characters long.', 'Administrator password not secure enough!', FlashMessage::ERROR ); return new JsonResponse([ 'success' => false, 'status' => $messages, ]); } // Set site name if (!empty($postValues['sitename'])) { $configurationManager->setLocalConfigurationValueByPath('SYS/sitename', $postValues['sitename']); } try { $messages = $this->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', FlashMessage::ERROR ); return new JsonResponse([ 'success' => false, 'status' => $messages, ]); } // Insert admin user $adminUserFields = [ 'username' => $username, 'password' => $this->getHashedPassword($password), 'admin' => 1, 'tstamp' => $GLOBALS['EXEC_TIME'], 'crdate' => $GLOBALS['EXEC_TIME'] ]; $databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users'); try { $databaseConnection->insert('be_users', $adminUserFields); $adminUserUid = (int)$databaseConnection->lastInsertId('be_users'); } catch (DBALException $exception) { $messages[] = new FlashMessage( 'The administrator account could not be created. The following error occurred:' . LF . $exception->getPrevious()->getMessage(), 'Administrator account not created!', FlashMessage::ERROR ); return new JsonResponse([ 'success' => false, 'status' => $messages, ]); } // Set password as install tool password, add admin user to system maintainers $configurationManager->setLocalConfigurationValuesByPathValuePairs([ 'BE/installToolPassword' => $this->getHashedPassword($password), 'SYS/systemMaintainers' => [$adminUserUid] ]); return new JsonResponse([ 'success' => true, 'status' => $messages, ]); } /** * Show last "create empty site / install distribution" * * @return ResponseInterface */ public function showDefaultConfigurationAction(): ResponseInterface { $view = $this->initializeStandaloneView('Installer/ShowDefaultConfiguration.html'); $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); $view->assignMultiple([ 'composerMode' => Environment::isComposerMode(), 'executeDefaultConfigurationToken' => $formProtection->generateToken('installTool', 'executeDefaultConfiguration'), ]); return new JsonResponse([ 'success' => true, 'html' => $view->render(), ]); } /** * Last step execution: clean up, remove FIRST_INSTALL file, ... * * @param ServerRequestInterface $request * @return ResponseInterface */ public function executeDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface { $featureManager = new FeatureManager(); // Get best matching configuration presets $configurationValues = $featureManager->getBestMatchingConfigurationForAllFeatures(); $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); // Let the admin user redirect to the distributions page on first login switch ($request->getParsedBody()['install']['values']['sitesetup']) { // Update the admin backend user to show the distribution management on login case 'loaddistribution': $adminUserFirstLogin = [ 'startModuleOnFirstLogin' => 'tools_ExtensionmanagerExtensionmanager->tx_extensionmanager_tools_extensionmanagerextensionmanager%5Baction%5D=distributions&tx_extensionmanager_tools_extensionmanagerextensionmanager%5Bcontroller%5D=List', 'ucSetByInstallTool' => '1', ]; $connectionPool->getConnectionForTable('be_users')->update( 'be_users', ['uc' => serialize($adminUserFirstLogin)], ['admin' => 1] ); break; // Create a page with UID 1 and PID1 and fluid_styled_content for page TS config, respect ownership case 'createsite': $databaseConnectionForPages = $connectionPool->getConnectionForTable('pages'); $databaseConnectionForPages->insert( 'pages', [ 'pid' => 0, 'crdate' => time(), 'cruser_id' => 1, 'tstamp' => time(), 'title' => 'Home', 'slug' => '/', 'doktype' => 1, 'is_siteroot' => 1, 'perms_userid' => 1, 'perms_groupid' => 1, 'perms_user' => 31, 'perms_group' => 31, 'perms_everybody' => 1 ] ); $pageUid = $databaseConnectionForPages->lastInsertId('pages'); // add a root sys_template with fluid_styled_content and a default PAGE typoscript snippet $connectionPool->getConnectionForTable('sys_template')->insert( 'sys_template', [ 'pid' => $pageUid, 'crdate' => time(), 'cruser_id' => 1, 'tstamp' => time(), 'title' => 'Main TypoScript Rendering', 'sitetitle' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], 'root' => 1, 'clear' => 1, 'include_static_file' => 'EXT:fluid_styled_content/Configuration/TypoScript/,EXT:fluid_styled_content/Configuration/TypoScript/Styling/', 'constants' => '', 'config' => 'page = PAGE page.10 = TEXT page.10.value (

Welcome to a default website made with TYPO3

) page.100 =< styles.content.get', 'description' => 'This is an Empty Site Package TypoScript template. For each website you need a TypoScript template on the main page of your website (on the top level). For better maintenance all TypoScript should be extracted into external files via @import \'EXT:site_myproject/Configuration/TypoScript/setup.typoscript\'' ] ); $this->createSiteConfiguration('main', (int)$pageUid, $request); break; } // Mark upgrade wizards as done $this->loadExtLocalconfDatabaseAndExtTables(); $registry = GeneralUtility::makeInstance(Registry::class); if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) { foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $updateClassName) { if (!in_array(RepeatableInterface::class, class_implements($updateClassName), true)) { $registry->set('installUpdate', $updateClassName, 1); } } } $registry->set('installUpdateRows', 'rowUpdatersDone', GeneralUtility::makeInstance(DatabaseRowsUpdateWizard::class)->getAvailableRowUpdater()); $configurationManager = new ConfigurationManager(); $configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationValues); $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class); $formProtection->clean(); EnableFileService::removeFirstInstallFile(); return new JsonResponse([ 'success' => true, 'redirect' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir . 'index.php', ]); } /** * Helper method to initialize a standalone view instance. * * @param string $templatePath * @return StandaloneView * @internal param string $template */ protected function initializeStandaloneView(string $templatePath): StandaloneView { $viewRootPath = GeneralUtility::getFileAbsFileName('EXT:install/Resources/Private/'); $view = GeneralUtility::makeInstance(StandaloneView::class); $view->getRequest()->setControllerExtensionName('Install'); $view->setTemplatePathAndFilename($viewRootPath . 'Templates/' . $templatePath); $view->setLayoutRootPaths([$viewRootPath . 'Layouts/']); $view->setPartialRootPaths([$viewRootPath . 'Partials/']); return $view; } /** * Test connection with given credentials and return exception message if exception trown * * @return bool */ protected function isDatabaseConnectSuccessful(): bool { try { GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName('Default')->ping(); } catch (DBALException $e) { return false; } return true; } /** * Check LocalConfiguration.php for required database settings: * - 'username' and 'password' are mandatory, but may be empty * - if 'driver' is pdo_sqlite and 'path' is set, its ok, too * * @return bool TRUE if required settings are present */ protected function isDatabaseConfigurationComplete() { $configurationComplete = true; if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'])) { $configurationComplete = false; } if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'])) { $configurationComplete = false; } if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver']) && $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'pdo_sqlite' && !empty($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['path']) ) { $configurationComplete = true; } return $configurationComplete; } /** * Returns configured socket, if set. * * @return string */ protected function getDatabaseConfiguredMysqliSocket() { $socket = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'] ?? ''; if ($socket === '') { // If no configured socket, use default php socket $defaultSocket = (string)ini_get('mysqli.default_socket'); if ($defaultSocket !== '') { $socket = $defaultSocket; } } return $socket; } /** * Try to fetch db credentials from a .env file and see if connect works * * @return array Empty array if no file is found or connect is not successful, else working credentials */ protected function getDatabaseConfigurationFromEnvironment(): array { $envCredentials = []; foreach (['driver', 'host', 'user', 'password', 'port', 'dbname', 'unix_socket'] as $value) { $envVar = 'TYPO3_INSTALL_DB_' . strtoupper($value); if (getenv($envVar) !== false) { $envCredentials[$value] = getenv($envVar); } } if (!empty($envCredentials)) { $connectionParams = $envCredentials; $connectionParams['wrapperClass'] = Connection::class; $connectionParams['charset'] = 'utf-8'; try { DriverManager::getConnection($connectionParams)->ping(); return $envCredentials; } catch (DBALException $e) { return []; } } return []; } /** * Returns list of available databases (with access-check based on username/password) * * @return array List of available databases */ protected function getDatabaseList() { $connectionParams = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]; unset($connectionParams['dbname']); // Establishing the connection using the Doctrine DriverManager directly // as we need a connection without selecting a database right away. Otherwise // an invalid database name would lead to exceptions which would prevent // changing the currently configured database. $connection = DriverManager::getConnection($connectionParams); $databaseArray = $connection->getSchemaManager()->listDatabases(); $connection->close(); // Remove organizational tables from database list $reservedDatabaseNames = ['mysql', 'information_schema', 'performance_schema']; $allPossibleDatabases = array_diff($databaseArray, $reservedDatabaseNames); // In first installation we show all databases but disable not empty ones (with tables) $databases = []; foreach ($allPossibleDatabases as $databaseName) { // Reestablising the connection for each database since there is no // portable way to switch databases on the same Doctrine connection. // Directly using the Doctrine DriverManager here to avoid messing with // the $GLOBALS database configuration array. $connectionParams['dbname'] = $databaseName; $connection = DriverManager::getConnection($connectionParams); $databases[] = [ 'name' => $databaseName, 'tables' => count($connection->getSchemaManager()->listTableNames()), ]; $connection->close(); } return $databases; } /** * Creates a new database on the default connection * * @param string $dbName name of database * @return FlashMessage */ protected function createNewDatabase($dbName) { if (!$this->isValidDatabaseName($dbName)) { return new FlashMessage( 'Given database name must be shorter than fifty characters' . ' and consist solely of basic latin letters (a-z), digits (0-9), dollar signs ($)' . ' and underscores (_).', 'Database name not valid', FlashMessage::ERROR ); } try { GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME) ->getSchemaManager() ->createDatabase($dbName); GeneralUtility::makeInstance(ConfigurationManager::class) ->setLocalConfigurationValueByPath('DB/Connections/Default/dbname', $dbName); } catch (DBALException $e) { return new FlashMessage( 'Database with name "' . $dbName . '" could not be created.' . ' Either your database name contains a reserved keyword or your database' . ' user does not have sufficient permissions to create it or the database already exists.' . ' Please choose an existing (empty) database, choose another name or contact administration.', 'Unable to create database', FlashMessage::ERROR ); } return new FlashMessage( '', 'Database created' ); } /** * Validate the database name against the lowest common denominator of valid identifiers across different DBMS * * @param string $databaseName * @return bool */ protected function isValidDatabaseName($databaseName) { return strlen($databaseName) <= 50 && preg_match('/^[a-zA-Z0-9\$_]*$/', $databaseName); } /** * Checks whether an existing database on the default connection * can be used for a TYPO3 installation. The database name is only * persisted to the local configuration if the database is empty. * * @param string $dbName name of the database * @return FlashMessage */ protected function checkExistingDatabase($dbName) { $result = new FlashMessage(''); $localConfigurationPathValuePairs = []; $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname'] = $dbName; try { $connection = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME); if (!empty($connection->getSchemaManager()->listTableNames())) { $result = new FlashMessage( sprintf('Cannot use database "%s"', $dbName) . ', because it already contains tables. Please select a different database or choose to create one!', 'Selected database is not empty!', FlashMessage::ERROR ); } } catch (\Exception $e) { $result = new FlashMessage( sprintf('Could not connect to database "%s"', $dbName) . '! Make sure it really exists and your database user has the permissions to select it!', 'Could not connect to selected database!', FlashMessage::ERROR ); } if ($result->getSeverity() === FlashMessage::OK) { $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $dbName; } // check if database charset is utf-8 - also allow utf8mb4 $defaultDatabaseCharset = $this->getDefaultDatabaseCharset($dbName); if (strpos($defaultDatabaseCharset, 'utf8') !== 0) { $result = new FlashMessage( 'Your database uses character set "' . $defaultDatabaseCharset . '", ' . 'but only "utf8" and "utf8mb4" are supported with TYPO3. You probably want to change this before proceeding.', 'Invalid Charset', FlashMessage::ERROR ); } if ($result->getSeverity() === FlashMessage::OK && !empty($localConfigurationPathValuePairs)) { $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs); } return $result; } /** * Retrieves the default character set of the database. * * @todo this function is MySQL specific. If the core has migrated to Doctrine it should be reexamined * whether this function and the check in $this->checkExistingDatabase could be deleted and utf8 otherwise * enforced (guaranteeing compatibility with other database servers). * * @param string $dbName * @return string */ protected function getDefaultDatabaseCharset(string $dbName): string { $connection = GeneralUtility::makeInstance(ConnectionPool::class) ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME); $queryBuilder = $connection->createQueryBuilder(); $defaultDatabaseCharset = $queryBuilder->select('DEFAULT_CHARACTER_SET_NAME') ->from('information_schema.SCHEMATA') ->where( $queryBuilder->expr()->eq( 'SCHEMA_NAME', $queryBuilder->createNamedParameter($dbName, \PDO::PARAM_STR) ) ) ->setMaxResults(1) ->execute() ->fetchColumn(); return (string)$defaultDatabaseCharset; } /** * This function returns a salted hashed key for new backend user password and install tool password. * * This method is executed during installation *before* the preset did set up proper hash method * selection in LocalConfiguration. So PasswordHashFactory is not usable at this point. We thus loop through * the four default hash mechanisms and select the first one that works. The preset calculation of step * executeDefaultConfigurationAction() basically does the same later. * * @param string $password Plain text password * @return string Hashed password * @throws \LogicException If no hash method has been found, should never happen PhpassPasswordHash is always available */ protected function getHashedPassword($password) { $okHashMethods = [ Argon2iPasswordHash::class, BcryptPasswordHash::class, Pbkdf2PasswordHash::class, PhpassPasswordHash::class, ]; foreach ($okHashMethods as $className) { /** @var PasswordHashInterface $instance */ $instance = GeneralUtility::makeInstance($className); if ($instance->isAvailable()) { return $instance->getHashedPassword($password); } } throw new \LogicException('No suitable hash method found', 1533988846); } /** * Create tables and import static rows * * @return FlashMessage[] */ protected function importDatabaseData() { // Will load ext_localconf and ext_tables. This is pretty safe here since we are // in first install (database empty), so it is very likely that no extension is loaded // that could trigger a fatal at this point. $this->loadExtLocalconfDatabaseAndExtTables(); $sqlReader = GeneralUtility::makeInstance(SqlReader::class); $sqlCode = $sqlReader->getTablesDefinitionString(true); $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class); $createTableStatements = $sqlReader->getCreateTableStatementArray($sqlCode); $results = $schemaMigrationService->install($createTableStatements); // Only keep statements with error messages $results = array_filter($results); if (count($results) === 0) { $insertStatements = $sqlReader->getInsertStatementArray($sqlCode); $results = $schemaMigrationService->importStaticData($insertStatements); } foreach ($results as $statement => &$message) { if ($message === '') { unset($results[$statement]); continue; } $message = new FlashMessage( 'Query:' . LF . ' ' . $statement . LF . 'Error:' . LF . ' ' . $message, 'Database query failed!', FlashMessage::ERROR ); } return array_values($results); } /** * Some actions like the database analyzer and the upgrade wizards need additional * bootstrap actions performed. * * Those actions can potentially fatal if some old extension is loaded that triggers * a fatal in ext_localconf or ext_tables code! Use only if really needed. */ protected function loadExtLocalconfDatabaseAndExtTables() { \TYPO3\CMS\Core\Core\Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false); \TYPO3\CMS\Core\Core\Bootstrap::unsetReservedGlobalVariables(); \TYPO3\CMS\Core\Core\Bootstrap::loadBaseTca(false); \TYPO3\CMS\Core\Core\Bootstrap::loadExtTables(false); } /** * Creates a site configuration with one language "English" which is the de-facto default language for TYPO3 in general. * * @param string $identifier * @param int $rootPageId * @param ServerRequestInterface $request * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException */ protected function createSiteConfiguration(string $identifier, int $rootPageId, ServerRequestInterface $request) { $normalizedParams = $request->getAttribute('normalizedParams', null); if (!($normalizedParams instanceof NormalizedParams)) { $normalizedParams = new NormalizedParams( $request->getServerParams(), $GLOBALS['TYPO3_CONF_VARS']['SYS'], Environment::getCurrentScript(), Environment::getPublicPath() ); } // Create a default site configuration called "main" as best practice $siteConfiguration = GeneralUtility::makeInstance( SiteConfiguration::class, Environment::getConfigPath() . '/sites' ); $siteConfiguration->write($identifier, [ 'rootPageId' => $rootPageId, 'base' => $normalizedParams->getSiteUrl(), 'languages' => [ 0 => [ 'title' => 'English', 'enabled' => true, 'languageId' => 0, 'base' => '/en/', 'typo3Language' => 'default', 'locale' => 'en_US.UTF-8', 'iso-639-1' => 'en', 'navigationTitle' => 'English', 'hreflang' => 'en-us', 'direction' => 'ltr', 'flag' => 'us', ], ], 'errorHandling' => [], 'routes' => [], ]); } }