[TASK] Doctrine: Migrate DatabaseSelect-Step
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / Action / Step / DatabaseSelect.php
1 <?php
2 namespace TYPO3\CMS\Install\Controller\Action\Step;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Doctrine\DBAL\DBALException;
18 use Doctrine\DBAL\DriverManager;
19 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Install\Status\ErrorStatus;
23 use TYPO3\CMS\Install\Status\OkStatus;
24
25 /**
26 * Database select step.
27 * This step is only rendered if database is mysql. With dbal,
28 * database name is submitted by previous step already.
29 */
30 class DatabaseSelect extends AbstractStepAction
31 {
32 /**
33 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
34 */
35 protected $databaseConnection = null;
36
37 /**
38 * Create database if needed, save selected db name in configuration
39 *
40 * @return \TYPO3\CMS\Install\Status\StatusInterface[]
41 */
42 public function execute()
43 {
44 $postValues = $this->postValues['values'];
45 if ($postValues['type'] === 'new') {
46 $status = $this->createNewDatabase($postValues['new']);
47 if ($status instanceof ErrorStatus) {
48 return [ $status ];
49 }
50 } elseif ($postValues['type'] === 'existing' && !empty($postValues['existing'])) {
51 $status = $this->checkExistingDatabase($postValues['existing']);
52 if ($status instanceof ErrorStatus) {
53 return [ $status ];
54 }
55 } else {
56 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
57 $errorStatus->setTitle('No Database selected');
58 $errorStatus->setMessage('You must select a database.');
59 return [ $errorStatus ];
60 }
61 return [];
62 }
63
64 /**
65 * Step needs to be executed if database is not set or can
66 * not be selected.
67 *
68 * @return bool
69 */
70 public function needsExecution()
71 {
72 $result = true;
73 if ((string)$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] !== '') {
74 try {
75 $pingResult = GeneralUtility::makeInstance(ConnectionPool::class)
76 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
77 ->ping();
78 if ($pingResult === true) {
79 $result = false;
80 }
81 } catch (DBALException $e) {
82 }
83 }
84 return $result;
85 }
86
87 /**
88 * Executes the step
89 *
90 * @return string Rendered content
91 */
92 protected function executeAction()
93 {
94 /** @var $configurationManager ConfigurationManager */
95 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
96 $isInitialInstallationInProgress = $configurationManager
97 ->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
98 $this->view->assign('databaseList', $this->getDatabaseList($isInitialInstallationInProgress));
99 $this->view->assign('isInitialInstallationInProgress', $isInitialInstallationInProgress);
100 $this->assignSteps();
101 return $this->view->render();
102 }
103
104 /**
105 * Returns list of available databases (with access-check based on username/password)
106 *
107 * @param bool $initialInstallation TRUE if first installation is in progress, FALSE if upgrading or usual access
108 * @return array List of available databases
109 */
110 protected function getDatabaseList($initialInstallation)
111 {
112 $connectionParams = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME];
113 unset($connectionParams['dbname']);
114
115 // Establishing the connection using the Doctrine DriverManager directly
116 // as we need a connection without selecting a database right away. Otherwise
117 // an invalid database name would lead to exceptions which would prevent
118 // changing the currently configured database.
119 $connection = DriverManager::getConnection($connectionParams);
120 $databaseArray = $connection->getSchemaManager()->listDatabases();
121 $connection->close();
122
123 // Remove organizational tables from database list
124 $reservedDatabaseNames = ['mysql', 'information_schema', 'performance_schema'];
125 $allPossibleDatabases = array_diff($databaseArray, $reservedDatabaseNames);
126
127 // If we are upgrading we show *all* databases the user has access to
128 if ($initialInstallation === false) {
129 return $allPossibleDatabases;
130 }
131
132 // In first installation we show all databases but disable not empty ones (with tables)
133 $databases = [];
134 foreach ($allPossibleDatabases as $databaseName) {
135 // Reestablising the connection for each database since there is no
136 // portable way to switch databases on the same Doctrine connection.
137 // Directly using the Doctrine DriverManager here to avoid messing with
138 // the $GLOBALS database configuration array.
139 $connectionParams['dbname'] = $databaseName;
140 $connection = DriverManager::getConnection($connectionParams);
141
142 $databases[] = [
143 'name' => $databaseName,
144 'tables' => count($connection->getSchemaManager()->listTableNames()),
145 ];
146 $connection->close();
147 }
148
149 return $databases;
150 }
151
152 /**
153 * Validate the database name against the lowest common denominator of valid identifiers across different DBMS
154 *
155 * @param string $databaseName
156 * @return bool
157 */
158 protected function isValidDatabaseName($databaseName)
159 {
160 return strlen($databaseName) <= 50 && preg_match('/^[a-zA-Z0-9\$_]*$/', $databaseName);
161 }
162
163 /**
164 * Retrieves the default character set of the database.
165 *
166 * @todo this function is MySQL specific. If the core has migrated to Doctrine it should be reexamined
167 * whether this function and the check in $this->checkExistingDatabase could be deleted and utf8 otherwise
168 * enforced (guaranteeing compatability with other database servers).
169 *
170 * @param string $dbName
171 * @return string
172 */
173 protected function getDefaultDatabaseCharset(string $dbName): string
174 {
175 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
176 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
177 $queryBuilder = $connection->createQueryBuilder();
178 $defaultDatabaseCharset = $queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
179 ->from('information_schema.SCHEMATA')
180 ->where(
181 $queryBuilder->expr()->eq('SCHEMA_NAME', $queryBuilder->quote($dbName))
182 )
183 ->setMaxResults(1)
184 ->execute()
185 ->fetchColumn();
186
187 return (string)$defaultDatabaseCharset;
188 }
189
190 /**
191 * Creates a new database on the default connection
192 *
193 * @param string $dbName name of database
194 *
195 * @return \TYPO3\CMS\Install\Status\StatusInterface
196 */
197 protected function createNewDatabase($dbName)
198 {
199 if (!$this->isValidDatabaseName($dbName)) {
200 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
201 $errorStatus->setTitle('Database name not valid');
202 $errorStatus->setMessage(
203 'Given database name must be shorter than fifty characters' .
204 ' and consist solely of basic latin letters (a-z), digits (0-9), dollar signs ($)' .
205 ' and underscores (_).'
206 );
207
208 return $errorStatus;
209 }
210
211 try {
212 GeneralUtility::makeInstance(ConnectionPool::class)
213 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
214 ->getSchemaManager()
215 ->createDatabase($dbName);
216 GeneralUtility::makeInstance(ConfigurationManager::class)
217 ->setLocalConfigurationValueByPath('DB/Connections/Default/dbname', $dbName);
218 } catch (DBALException $e) {
219 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
220 $errorStatus->setTitle('Unable to create database');
221 $errorStatus->setMessage(
222 'Database with name "' . $dbName . '" could not be created.' .
223 ' Either your database name contains a reserved keyword or your database' .
224 ' user does not have sufficient permissions to create it or the database already exists.' .
225 ' Please choose an existing (empty) database, choose another name or contact administration.'
226 );
227 return $errorStatus;
228 }
229
230 return GeneralUtility::makeInstance(OkStatus::class);
231 }
232
233 /**
234 * Checks whether an existing database on the default connection
235 * can be used for a TYPO3 installation. The database name is only
236 * persisted to the local configuration if the database is empty.
237 *
238 * @param string $dbName name of the database
239 * @return \TYPO3\CMS\Install\Status\StatusInterface
240 */
241 protected function checkExistingDatabase($dbName)
242 {
243 $result = GeneralUtility::makeInstance(OkStatus::class);
244 $localConfigurationPathValuePairs = [];
245 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
246 $isInitialInstallation = $configurationManager
247 ->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
248
249 $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname'] = $dbName;
250 try {
251 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
252 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
253
254 if ($isInitialInstallation && !empty($connection->getSchemaManager()->listTableNames())) {
255 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
256 $errorStatus->setTitle('Selected database is not empty!');
257 $errorStatus->setMessage(
258 sprintf('Cannot use database "%s"', $dbName)
259 . ', because it already contains tables. '
260 . 'Please select a different database or choose to create one!'
261 );
262 $result = $errorStatus;
263 }
264 } catch (\Exception $e) {
265 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
266 $errorStatus->setTitle('Could not connect to selected database!');
267 $errorStatus->setMessage(
268 sprintf('Could not connect to database "%s"', $dbName)
269 . '! Make sure it really exists and your database user has the permissions to select it!'
270 );
271 $result = $errorStatus;
272 }
273
274 if ($result instanceof OkStatus) {
275 $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $dbName;
276 }
277
278 // check if database charset is utf-8 - also allow utf8mb4
279 $defaultDatabaseCharset = $this->getDefaultDatabaseCharset($dbName);
280 if (substr($defaultDatabaseCharset, 0, 4) !== 'utf8') {
281 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
282 $errorStatus->setTitle('Invalid Charset');
283 $errorStatus->setMessage(
284 'Your database uses character set "' . $defaultDatabaseCharset . '", ' .
285 'but only "utf8" is supported with TYPO3. You probably want to change this before proceeding.'
286 );
287 $result = $errorStatus;
288 }
289
290 if ($result instanceof OkStatus && !empty($localConfigurationPathValuePairs)) {
291 $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs);
292 }
293
294 return $result;
295 }
296 }