[TASK] Doctrine: Migrate DatabaseSelect-Step 35/49435/4
authortobiasadolph <mail@tobiasadolph.de>
Tue, 9 Aug 2016 16:22:00 +0000 (18:22 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Sat, 20 Aug 2016 10:34:28 +0000 (12:34 +0200)
Resolves: #77448
Releases: master
Change-Id: Idb5be03b0ac996646fffc0bcd75c439f2a19b05f
Reviewed-on: https://review.typo3.org/49435
Tested-by: Bamboo TYPO3com <info@typo3.com>
Reviewed-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/install/Classes/Controller/Action/Step/DatabaseSelect.php

index ba19393..25cf4a6 100644 (file)
@@ -14,7 +14,13 @@ namespace TYPO3\CMS\Install\Controller\Action\Step;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\DriverManager;
+use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Status\ErrorStatus;
+use TYPO3\CMS\Install\Status\OkStatus;
 
 /**
  * Database select step.
@@ -31,100 +37,28 @@ class DatabaseSelect extends AbstractStepAction
     /**
      * Create database if needed, save selected db name in configuration
      *
-     * @return array<\TYPO3\CMS\Install\Status\StatusInterface>
+     * @return \TYPO3\CMS\Install\Status\StatusInterface[]
      */
     public function execute()
     {
-        $result = array();
-        $this->initializeDatabaseConnection();
         $postValues = $this->postValues['values'];
-        $localConfigurationPathValuePairs = array();
-        /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */
-        $configurationManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
-        $canProceed = true;
         if ($postValues['type'] === 'new') {
-            $newDatabaseName = $postValues['new'];
-            if ($this->isValidDatabaseName($newDatabaseName)) {
-                $createDatabaseResult = $this->databaseConnection->admin_query('CREATE DATABASE ' . $newDatabaseName . ' CHARACTER SET utf8');
-                if ($createDatabaseResult) {
-                    $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $newDatabaseName;
-                } else {
-                    /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
-                    $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
-                    $errorStatus->setTitle('Unable to create database');
-                    $errorStatus->setMessage(
-                        'Database with name ' . $newDatabaseName . ' could not be created.' .
-                        ' Either your database name contains a reserved keyword or your database' .
-                        ' user does not have sufficient permissions to create it.' .
-                        ' Please choose an existing (empty) database or contact administration.'
-                    );
-                    $result[] = $errorStatus;
-                }
-            } else {
-                /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
-                $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
-                $errorStatus->setTitle('Database name not valid');
-                $errorStatus->setMessage(
-                    '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 (_).'
-                );
-                $result[] = $errorStatus;
+            $status = $this->createNewDatabase($postValues['new']);
+            if ($status instanceof ErrorStatus) {
+                return [ $status ];
             }
         } elseif ($postValues['type'] === 'existing' && !empty($postValues['existing'])) {
-            // Only store database information when it's empty
-            $this->databaseConnection->setDatabaseName($postValues['existing']);
-            try {
-                $this->databaseConnection->sql_select_db();
-                $existingTables = $this->databaseConnection->admin_get_tables();
-                if (!empty($existingTables)) {
-                    $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
-                    $errorStatus->setTitle('Selected database is not empty!');
-                    $errorStatus->setMessage(
-                        sprintf('Cannot use database "%s"', $postValues['existing'])
-                        . ', because it has tables already. Please select a different database or choose to create one!'
-                    );
-                    $result[] = $errorStatus;
-                }
-            } catch (\RuntimeException $e) {
-                $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
-                $errorStatus->setTitle('Could not connect to selected database!');
-                $errorStatus->setMessage(
-                    sprintf('Could not connect to database "%s"', $postValues['existing'])
-                    . '! Make sure it really exists and your database user has the permissions to select it!'
-                );
-                $result[] = $errorStatus;
-            }
-            $isInitialInstallation = $configurationManager->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
-            if (!$isInitialInstallation || empty($result)) {
-                $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $postValues['existing'];
-            }
-            // check if database charset is utf-8
-            $defaultDatabaseCharset = $this->getDefaultDatabaseCharset();
-            // also allow utf8mb4
-            if (substr($defaultDatabaseCharset, 0, 4) !== 'utf8') {
-                $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
-                $errorStatus->setTitle('Invalid Charset');
-                $errorStatus->setMessage(
-                    'Your database uses character set "' . $defaultDatabaseCharset . '", ' .
-                    'but only "utf8" is supported with TYPO3. You probably want to change this before proceeding.'
-                );
-                $result[] = $errorStatus;
-                $canProceed = false;
+            $status = $this->checkExistingDatabase($postValues['existing']);
+            if ($status instanceof ErrorStatus) {
+                return [ $status ];
             }
         } else {
-            /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
-            $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
+            $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
             $errorStatus->setTitle('No Database selected');
             $errorStatus->setMessage('You must select a database.');
-            $result[] = $errorStatus;
+            return [ $errorStatus ];
         }
-
-        if ($canProceed && !empty($localConfigurationPathValuePairs)) {
-            $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs);
-        }
-
-        return $result;
+        return [];
     }
 
     /**
@@ -135,16 +69,16 @@ class DatabaseSelect extends AbstractStepAction
      */
     public function needsExecution()
     {
-        $this->initializeDatabaseConnection();
         $result = true;
         if ((string)$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] !== '') {
-            $this->databaseConnection->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
             try {
-                $selectResult = $this->databaseConnection->sql_select_db();
-                if ($selectResult === true) {
+                $pingResult = GeneralUtility::makeInstance(ConnectionPool::class)
+                    ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
+                    ->ping();
+                if ($pingResult === true) {
                     $result = false;
                 }
-            } catch (\RuntimeException $e) {
+            } catch (DBALException $e) {
             }
         }
         return $result;
@@ -157,9 +91,10 @@ class DatabaseSelect extends AbstractStepAction
      */
     protected function executeAction()
     {
-        /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */
-        $configurationManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
-        $isInitialInstallationInProgress = $configurationManager->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
+        /** @var $configurationManager ConfigurationManager */
+        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
+        $isInitialInstallationInProgress = $configurationManager
+            ->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
         $this->view->assign('databaseList', $this->getDatabaseList($isInitialInstallationInProgress));
         $this->view->assign('isInitialInstallationInProgress', $isInitialInstallationInProgress);
         $this->assignSteps();
@@ -174,45 +109,44 @@ class DatabaseSelect extends AbstractStepAction
      */
     protected function getDatabaseList($initialInstallation)
     {
-        $this->initializeDatabaseConnection();
-        $databaseArray = $this->databaseConnection->admin_get_dbs();
-        // Remove mysql organizational tables from database list
-        $reservedDatabaseNames = array('mysql', 'information_schema', 'performance_schema');
+        $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);
 
         // If we are upgrading we show *all* databases the user has access to
         if ($initialInstallation === false) {
             return $allPossibleDatabases;
-        } else {
-            // In first installation we show all databases but disable not empty ones (with tables)
-            $databases = array();
-            foreach ($allPossibleDatabases as $database) {
-                $this->databaseConnection->setDatabaseName($database);
-                $this->databaseConnection->sql_select_db();
-                $existingTables = $this->databaseConnection->admin_get_tables();
-                $databases[] = array(
-                    'name' => $database,
-                    'tables' => count($existingTables),
-                );
-            }
-            return $databases;
         }
-    }
 
-    /**
-     * Initialize database connection
-     *
-     * @return void
-     */
-    protected function initializeDatabaseConnection()
-    {
-        $this->databaseConnection = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $this->databaseConnection->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']);
-        $this->databaseConnection->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']);
-        $this->databaseConnection->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']);
-        $this->databaseConnection->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port']);
-        $this->databaseConnection->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket']);
-        $this->databaseConnection->sql_pconnect();
+        // 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;
     }
 
     /**
@@ -229,21 +163,134 @@ class DatabaseSelect extends AbstractStepAction
     /**
      * 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 compatability with other database servers).
+     *
+     * @param string $dbName
      * @return string
      */
-    protected function getDefaultDatabaseCharset()
+    protected function getDefaultDatabaseCharset(string $dbName): string
     {
-        $result = $this->databaseConnection->admin_query('SHOW VARIABLES LIKE "character_set_database"');
-        $row = $this->databaseConnection->sql_fetch_assoc($result);
+        $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->quote($dbName))
+            )
+            ->setMaxResults(1)
+            ->execute()
+            ->fetchColumn();
 
-        $key = $row['Variable_name'];
-        $value = $row['Value'];
-        $databaseCharset = '';
+        return (string)$defaultDatabaseCharset;
+    }
+
+    /**
+     * Creates a new database on the default connection
+     *
+     * @param string $dbName name of database
+     *
+     * @return \TYPO3\CMS\Install\Status\StatusInterface
+     */
+    protected function createNewDatabase($dbName)
+    {
+        if (!$this->isValidDatabaseName($dbName)) {
+            $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
+            $errorStatus->setTitle('Database name not valid');
+            $errorStatus->setMessage(
+                '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 (_).'
+            );
+
+            return $errorStatus;
+        }
+
+        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) {
+            $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
+            $errorStatus->setTitle('Unable to create database');
+            $errorStatus->setMessage(
+                '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.'
+            );
+            return $errorStatus;
+        }
+
+        return GeneralUtility::makeInstance(OkStatus::class);
+    }
+
+    /**
+     * 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 \TYPO3\CMS\Install\Status\StatusInterface
+     */
+    protected function checkExistingDatabase($dbName)
+    {
+        $result = GeneralUtility::makeInstance(OkStatus::class);
+        $localConfigurationPathValuePairs = [];
+        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
+        $isInitialInstallation = $configurationManager
+            ->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
+
+        $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname'] = $dbName;
+        try {
+            $connection = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
 
-        if ($key == 'character_set_database') {
-            $databaseCharset = $value;
+            if ($isInitialInstallation && !empty($connection->getSchemaManager()->listTableNames())) {
+                $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
+                $errorStatus->setTitle('Selected database is not empty!');
+                $errorStatus->setMessage(
+                    sprintf('Cannot use database "%s"', $dbName)
+                    . ', because it already contains tables. '
+                    . 'Please select a different database or choose to create one!'
+                );
+                $result = $errorStatus;
+            }
+        } catch (\Exception $e) {
+            $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
+            $errorStatus->setTitle('Could not connect to selected database!');
+            $errorStatus->setMessage(
+                sprintf('Could not connect to database "%s"', $dbName)
+                . '! Make sure it really exists and your database user has the permissions to select it!'
+            );
+            $result = $errorStatus;
         }
 
-        return $databaseCharset;
+        if ($result instanceof OkStatus) {
+            $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $dbName;
+        }
+
+        // check if database charset is utf-8 - also allow utf8mb4
+        $defaultDatabaseCharset = $this->getDefaultDatabaseCharset($dbName);
+        if (substr($defaultDatabaseCharset, 0, 4) !== 'utf8') {
+            $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
+            $errorStatus->setTitle('Invalid Charset');
+            $errorStatus->setMessage(
+                'Your database uses character set "' . $defaultDatabaseCharset . '", ' .
+                'but only "utf8" is supported with TYPO3. You probably want to change this before proceeding.'
+            );
+            $result = $errorStatus;
+        }
+
+        if ($result instanceof OkStatus && !empty($localConfigurationPathValuePairs)) {
+            $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs);
+        }
+
+        return $result;
     }
 }