Commit 3d993153 authored by Frank Nägler's avatar Frank Nägler Committed by Andreas Fernandez
Browse files

[BUGFIX] Optimize database pre checks within install process

During the install process, the user has to choose a database in step 3
from a list of available databases.
If the database user has read access but no write access to the database,
the install tool used to throw an exception.
Now the system catches this exception, calculates permissions more accurately
and also provides more meaningful messages to the user.

Resolves: #78885
Releases: master, 10.4
Change-Id: Ib9aa632e8ca161acf2de628806386bd6607afd74
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64564


Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Reviewed-by: default avatarJörg Bösche <typo3@joergboesche.de>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
parent d1e7e8af
......@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Install\Controller;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception\ConnectionException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
......@@ -52,6 +53,7 @@ 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\Database\PermissionsCheck;
use TYPO3\CMS\Install\Exception;
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
use TYPO3\CMS\Install\Service\EnableFileService;
......@@ -100,13 +102,19 @@ class InstallerController
*/
private $packageManager;
/**
* @var PermissionsCheck
*/
private $databasePermissionsCheck;
public function __construct(
LateBootService $lateBootService,
SilentConfigurationUpgradeService $silentConfigurationUpgradeService,
ConfigurationManager $configurationManager,
SiteConfiguration $siteConfiguration,
Registry $registry,
FailsafePackageManager $packageManager
FailsafePackageManager $packageManager,
PermissionsCheck $databasePermissionsCheck
) {
$this->lateBootService = $lateBootService;
$this->silentConfigurationUpgradeService = $silentConfigurationUpgradeService;
......@@ -114,6 +122,7 @@ class InstallerController
$this->siteConfiguration = $siteConfiguration;
$this->registry = $registry;
$this->packageManager = $packageManager;
$this->databasePermissionsCheck = $databasePermissionsCheck;
}
/**
......@@ -665,6 +674,22 @@ class InstallerController
}
}
// Check create and drop permissions
$statusMessages = [];
foreach ($this->checkRequiredDatabasePermissions() as $checkRequiredPermission) {
$statusMessages[] = new FlashMessage(
$checkRequiredPermission,
'Missing required permissions',
FlashMessage::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
......@@ -698,6 +723,25 @@ class InstallerController
]);
}
private function checkRequiredDatabasePermissions(): array
{
try {
return $this->databasePermissionsCheck
->checkCreateAndDrop()
->checkAlter()
->checkIndex()
->checkCreateTemporaryTable()
->checkLockTable()
->checkInsert()
->checkSelect()
->checkUpdate()
->checkDelete()
->getMessages();
} catch (\TYPO3\CMS\Install\Configuration\Exception $exception) {
return $this->databasePermissionsCheck->getMessages();
}
}
private function checkDatabaseRequirementsForDriver(string $databaseDriverName): FlashMessageQueue
{
$databaseCheck = GeneralUtility::makeInstance(DatabaseCheck::class);
......@@ -1190,14 +1234,26 @@ For each website you need a TypoScript template on the main page of your website
// 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);
try {
$connectionParams['dbname'] = $databaseName;
$connection = DriverManager::getConnection($connectionParams);
$databases[] = [
'name' => $databaseName,
'tables' => count($connection->getSchemaManager()->listTableNames()),
];
$connection->close();
$databases[] = [
'name' => $databaseName,
'tables' => count($connection->getSchemaManager()->listTableNames()),
'readonly' => false
];
$connection->close();
} catch (ConnectionException $exception) {
$databases[] = [
'name' => $databaseName,
'tables' => 0,
'readonly' => true
];
// we ignore a connection exception here.
// if this happens here, the show tables was successful
// but the connection failed because of missing permissions.
}
}
return $databases;
......
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Install\Database;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Configuration\Exception;
/**
* Check all required permissions within the install process.
* @internal This is NOT an API class, it is for internal use in the install tool only.
*/
class PermissionsCheck
{
private $testTableName = 't3install_test_table';
private $messages = [];
public function checkCreateAndDrop(): self
{
$tableCreated = $this->checkCreateTable($this->testTableName);
if (!$tableCreated) {
$this->messages[] = 'The database user needs CREATE permissions.';
}
$tableDropped = $this->checkDropTable($this->testTableName);
if (!$tableDropped) {
$this->messages[] = 'The database user needs DROP permissions.';
}
if ($tableCreated && !$tableDropped) {
$this->messages[] = sprintf('Attention: A test table with name "%s" was created but could not be deleted, please remove the table manually!', $this->testTableName);
}
if (!$tableCreated || !$tableDropped) {
throw new Exception('A test table could not be created or dropped, skipping all further checks now', 1590850369);
}
return $this;
}
public function checkAlter(): self
{
$this->checkCreateTable($this->testTableName);
$connection = $this->getConnection();
$schemaCurrent = $this->getSchemaManager()->createSchema();
$schemaNew = $this->getSchemaManager()->createSchema();
$schemaCurrent
->getTable($this->testTableName)
->addColumn('index_test', 'integer', ['unsigned' => true]);
$platform = $connection->getDatabasePlatform();
try {
foreach ($schemaNew->getMigrateToSql($schemaCurrent, $platform) as $query) {
$connection->executeQuery($query);
}
} catch (\Exception $e) {
$this->messages[] = 'The database user needs ALTER permission';
}
$this->checkDropTable($this->testTableName);
return $this;
}
public function checkIndex(): self
{
if ($this->checkCreateTable($this->testTableName)) {
$connection = $this->getConnection();
$schemaCurrent = $this->getSchemaManager()->createSchema();
$schemaNew = $this->getSchemaManager()->createSchema();
$testTable = $schemaCurrent->getTable($this->testTableName);
$testTable->addColumn('index_test', 'integer', ['unsigned' => true]);
$testTable->addIndex(['index_test'], 'test_index');
$platform = $connection->getDatabasePlatform();
try {
foreach ($schemaNew->getMigrateToSql($schemaCurrent, $platform) as $query) {
$connection->executeQuery($query);
}
} catch (\Exception $e) {
$this->checkDropTable($this->testTableName);
$this->messages[] = 'The database user needs INDEX permission';
}
$this->checkDropTable($this->testTableName);
}
return $this;
}
public function checkCreateTemporaryTable(): self
{
$this->checkCreateTable($this->testTableName);
$connection = $this->getConnection();
try {
$sql = 'CREATE TEMPORARY TABLE %s AS (SELECT id FROM %s )';
$connection->exec(sprintf($sql, $this->testTableName . '_tmp', $this->testTableName));
} catch (\Exception $e) {
$this->messages[] = 'The database user needs CREATE TEMPORARY TABLE permission';
}
$this->checkDropTable($this->testTableName);
return $this;
}
public function checkLockTable(): self
{
$this->checkCreateTable($this->testTableName);
$connection = $this->getConnection();
try {
$connection->exec(sprintf('LOCK TABLES %s WRITE', $this->testTableName));
$connection->exec('UNLOCK TABLES;');
} catch (\Exception $e) {
$this->messages[] = 'The database user needs LOCK TABLE permission';
}
$this->checkDropTable($this->testTableName);
return $this;
}
public function checkSelect(): self
{
$this->checkCreateTable($this->testTableName);
$connection = $this->getConnection();
try {
$connection->insert($this->testTableName, ['id' => 1]);
$connection->select(['id'], $this->testTableName);
} catch (\Exception $e) {
$this->messages[] = 'The database user needs SELECT permission';
}
$this->checkDropTable($this->testTableName);
return $this;
}
public function checkInsert(): self
{
$this->checkCreateTable($this->testTableName);
$connection = $this->getConnection();
try {
$connection->insert($this->testTableName, ['id' => 1]);
} catch (\Exception $e) {
$this->messages[] = 'The database user needs INSERT permission';
}
$this->checkDropTable($this->testTableName);
return $this;
}
public function checkUpdate(): self
{
$this->checkCreateTable($this->testTableName);
$connection = $this->getConnection();
try {
$connection->insert($this->testTableName, ['id' => 1]);
$connection->update($this->testTableName, ['id' => 2], ['id' => 1]);
} catch (\Exception $e) {
$this->messages[] = 'The database user needs UPDATE permission';
}
$this->checkDropTable($this->testTableName);
return $this;
}
public function checkDelete(): self
{
$this->checkCreateTable($this->testTableName);
$connection = $this->getConnection();
try {
$connection->insert($this->testTableName, ['id' => 1]);
$connection->delete($this->testTableName, ['id' => 1]);
} catch (\Exception $e) {
$this->messages[] = 'The database user needs DELETE permission';
}
$this->checkDropTable($this->testTableName);
return $this;
}
public function getMessages(): array
{
return $this->messages;
}
private function checkCreateTable(string $tablename): bool
{
$connection = $this->getConnection();
$schema = $connection->getSchemaManager()->createSchema();
$testTable = $schema->createTable($tablename);
$testTable->addColumn('id', 'integer', ['unsigned' => true]);
$testTable->setPrimaryKey(['id']);
$platform = $connection->getDatabasePlatform();
try {
foreach ($schema->toSql($platform) as $query) {
$connection->executeQuery($query);
}
} catch (\Exception $e) {
return false;
}
return true;
}
private function checkDropTable(string $tablename): bool
{
$connection = $this->getConnection();
try {
$schemaCurrent = $connection->getSchemaManager()->createSchema();
$schemaNew = $connection->getSchemaManager()->createSchema();
$schemaNew->dropTable($tablename);
$platform = $connection->getDatabasePlatform();
foreach ($schemaCurrent->getMigrateToSql($schemaNew, $platform) as $query) {
$connection->executeQuery($query);
}
} catch (\Exception $e) {
return false;
}
return true;
}
private function getConnection(): Connection
{
return GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
}
private function getSchemaManager(): AbstractSchemaManager
{
return $this->getConnection()->getSchemaManager();
}
}
......@@ -39,6 +39,7 @@ use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\TypoScript\Parser\ConstantConfigurationParser;
use TYPO3\CMS\Install\Database\PermissionsCheck;
/**
* @internal
......@@ -78,6 +79,7 @@ class ServiceProvider extends AbstractServiceProvider
Command\LanguagePackCommand::class => [ static::class, 'getLanguagePackCommand' ],
Command\UpgradeWizardRunCommand::class => [ static::class, 'getUpgradeWizardRunCommand' ],
Command\UpgradeWizardListCommand::class => [ static::class, 'getUpgradeWizardListCommand' ],
Database\PermissionsCheck::class => [ static::class, 'getPermissionsCheck' ],
];
}
......@@ -214,7 +216,8 @@ class ServiceProvider extends AbstractServiceProvider
$container->get(ConfigurationManager::class),
$container->get(SiteConfiguration::class),
$container->get(Registry::class),
$container->get(FailsafePackageManager::class)
$container->get(FailsafePackageManager::class),
$container->get(PermissionsCheck::class)
);
}
......@@ -284,6 +287,11 @@ class ServiceProvider extends AbstractServiceProvider
);
}
public static function getPermissionsCheck(ContainerInterface $container): Database\PermissionsCheck
{
return new Database\PermissionsCheck();
}
public static function configureCommands(ContainerInterface $container, CommandRegistry $commandRegistry): CommandRegistry
{
$commandRegistry->addLazyCommand('language:update', Command\LanguagePackCommand::class);
......
......@@ -34,9 +34,9 @@
>
<option value="">-- Select database --</option>
<f:for each="{databaseList}" as="database">
<f:if condition="{database.tables}">
<f:if condition="{database.tables} || {database.readonly}">
<f:then>
<option value="{database.name}" disabled="disabled">{database.name} ({database.tables} Tables)</option>
<option value="{database.name}" disabled="disabled">{database.name} ({f:if(condition: database.readonly, then: 'readonly', else: '{database.tables} Tables')})</option>
</f:then>
<f:else>
<option value="{database.name}">{database.name}</option>
......
......@@ -19,6 +19,7 @@
"sort-packages": true
},
"require": {
"doctrine/dbal": "^2.10",
"nikic/php-parser": "^4.3",
"symfony/finder": "^4.4 || ^5.0",
"typo3/cms-core": "11.0.*@dev",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment