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