1250 lines
52 KiB
PHP
1250 lines
52 KiB
PHP
<?php
|
|
declare(strict_types = 1);
|
|
namespace TYPO3\CMS\Install\Controller;
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
use Doctrine\DBAL\DBALException;
|
|
use Doctrine\DBAL\DriverManager;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
|
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
|
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
|
|
use TYPO3\CMS\Core\Core\Environment;
|
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
|
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
|
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface;
|
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash;
|
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash;
|
|
use TYPO3\CMS\Core\Crypto\Random;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
|
|
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
|
|
use TYPO3\CMS\Core\Database\Schema\SqlReader;
|
|
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
|
use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
|
|
use TYPO3\CMS\Core\Http\HtmlResponse;
|
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
|
use TYPO3\CMS\Core\Http\NormalizedParams;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
|
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
|
use TYPO3\CMS\Core\Package\PackageInterface;
|
|
use TYPO3\CMS\Core\Package\PackageManager;
|
|
use TYPO3\CMS\Core\Registry;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Fluid\View\StandaloneView;
|
|
use TYPO3\CMS\Install\Configuration\FeatureManager;
|
|
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
|
|
use TYPO3\CMS\Install\Service\EnableFileService;
|
|
use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
|
|
use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
|
|
use TYPO3\CMS\Install\SystemEnvironment\Check;
|
|
use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
|
|
use TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard;
|
|
use TYPO3\CMS\Install\Updates\RepeatableInterface;
|
|
|
|
/**
|
|
* Install step controller, dispatcher class of step actions.
|
|
* @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
|
|
*/
|
|
class InstallerController
|
|
{
|
|
/**
|
|
* Init action loads <head> 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 (
|
|
<div style="width: 800px; margin: 15% auto;">
|
|
<div style="width: 300px;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 42"><path d="M60.2 14.4v27h-3.8v-27h-6.7v-3.3h17.1v3.3h-6.6zm20.2 12.9v14h-3.9v-14l-7.7-16.2h4.1l5.7 12.2 5.7-12.2h3.9l-7.8 16.2zm19.5 2.6h-3.6v11.4h-3.8V11.1s3.7-.3 7.3-.3c6.6 0 8.5 4.1 8.5 9.4 0 6.5-2.3 9.7-8.4 9.7m.4-16c-2.4 0-4.1.3-4.1.3v12.6h4.1c2.4 0 4.1-1.6 4.1-6.3 0-4.4-1-6.6-4.1-6.6m21.5 27.7c-7.1 0-9-5.2-9-15.8 0-10.2 1.9-15.1 9-15.1s9 4.9 9 15.1c.1 10.6-1.8 15.8-9 15.8m0-27.7c-3.9 0-5.2 2.6-5.2 12.1 0 9.3 1.3 12.4 5.2 12.4 3.9 0 5.2-3.1 5.2-12.4 0-9.4-1.3-12.1-5.2-12.1m19.9 27.7c-2.1 0-5.3-.6-5.7-.7v-3.1c1 .2 3.7.7 5.6.7 2.2 0 3.6-1.9 3.6-5.2 0-3.9-.6-6-3.7-6H138V24h3.1c3.5 0 3.7-3.6 3.7-5.3 0-3.4-1.1-4.8-3.2-4.8-1.9 0-4.1.5-5.3.7v-3.2c.5-.1 3-.7 5.2-.7 4.4 0 7 1.9 7 8.3 0 2.9-1 5.5-3.3 6.3 2.6.2 3.8 3.1 3.8 7.3 0 6.6-2.5 9-7.3 9"/><path fill="#FF8700" d="M31.7 28.8c-.6.2-1.1.2-1.7.2-5.2 0-12.9-18.2-12.9-24.3 0-2.2.5-3 1.3-3.6C12 1.9 4.3 4.2 1.9 7.2 1.3 8 1 9.1 1 10.6c0 9.5 10.1 31 17.3 31 3.3 0 8.8-5.4 13.4-12.8M28.4.5c6.6 0 13.2 1.1 13.2 4.8 0 7.6-4.8 16.7-7.2 16.7-4.4 0-9.9-12.1-9.9-18.2C24.5 1 25.6.5 28.4.5"/></svg>
|
|
</div>
|
|
<h4 style="font-family: sans-serif;">Welcome to a default website made with <a href="https://typo3.org">TYPO3</a></h4>
|
|
</div>
|
|
)
|
|
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' => [],
|
|
]);
|
|
}
|
|
}
|