[BUGFIX] Harden database select 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 TYPO3\CMS\Core\Utility\GeneralUtility;
18
19 /**
20 * Database select step.
21 * This step is only rendered if database is mysql. With dbal,
22 * database name is submitted by previous step already.
23 */
24 class DatabaseSelect extends AbstractStepAction
25 {
26 /**
27 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
28 */
29 protected $databaseConnection = null;
30
31 /**
32 * Create database if needed, save selected db name in configuration
33 *
34 * @return array<\TYPO3\CMS\Install\Status\StatusInterface>
35 */
36 public function execute()
37 {
38 $result = array();
39 $this->initializeDatabaseConnection();
40 $postValues = $this->postValues['values'];
41 $localConfigurationPathValuePairs = array();
42 /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */
43 $configurationManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
44 $canProceed = true;
45 if ($postValues['type'] === 'new') {
46 $newDatabaseName = $postValues['new'];
47 if ($this->isValidDatabaseName($newDatabaseName)) {
48 $createDatabaseResult = $this->databaseConnection->admin_query('CREATE DATABASE ' . $newDatabaseName . ' CHARACTER SET utf8');
49 if ($createDatabaseResult) {
50 $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $newDatabaseName;
51 } else {
52 /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
53 $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
54 $errorStatus->setTitle('Unable to create database');
55 $errorStatus->setMessage(
56 'Database with name ' . $newDatabaseName . ' could not be created.' .
57 ' Either your database name contains a reserved keyword or your database' .
58 ' user does not have sufficient permissions to create it.' .
59 ' Please choose an existing (empty) database or contact administration.'
60 );
61 $result[] = $errorStatus;
62 }
63 } else {
64 /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
65 $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
66 $errorStatus->setTitle('Database name not valid');
67 $errorStatus->setMessage(
68 'Given database name must be shorter than fifty characters' .
69 ' and consist solely of basic latin letters (a-z), digits (0-9), dollar signs ($)' .
70 ' and underscores (_).'
71 );
72 $result[] = $errorStatus;
73 }
74 } elseif ($postValues['type'] === 'existing' && !empty($postValues['existing'])) {
75 // Only store database information when it's empty
76 $this->databaseConnection->setDatabaseName($postValues['existing']);
77 try {
78 $this->databaseConnection->sql_select_db();
79 $existingTables = $this->databaseConnection->admin_get_tables();
80 if (!empty($existingTables)) {
81 $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
82 $errorStatus->setTitle('Selected database is not empty!');
83 $errorStatus->setMessage(
84 sprintf('Cannot use database "%s"', $postValues['existing'])
85 . ', because it has tables already. Please select a different database or choose to create one!'
86 );
87 $result[] = $errorStatus;
88 }
89 } catch (\RuntimeException $e) {
90 $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
91 $errorStatus->setTitle('Could not connect to selected database!');
92 $errorStatus->setMessage(
93 sprintf('Could not connect to database "%s"', $postValues['existing'])
94 . '! Make sure it really exists and your database user has the permissions to select it!'
95 );
96 $result[] = $errorStatus;
97 }
98 $isInitialInstallation = $configurationManager->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
99 if (!$isInitialInstallation || empty($result)) {
100 $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $postValues['existing'];
101 }
102 // check if database charset is utf-8
103 $defaultDatabaseCharset = $this->getDefaultDatabaseCharset();
104 // also allow utf8mb4
105 if (substr($defaultDatabaseCharset, 0, 4) !== 'utf8') {
106 $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
107 $errorStatus->setTitle('Invalid Charset');
108 $errorStatus->setMessage(
109 'Your database uses character set "' . $defaultDatabaseCharset . '", ' .
110 'but only "utf8" is supported with TYPO3. You probably want to change this before proceeding.'
111 );
112 $result[] = $errorStatus;
113 $canProceed = false;
114 }
115 } else {
116 /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
117 $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
118 $errorStatus->setTitle('No Database selected');
119 $errorStatus->setMessage('You must select a database.');
120 $result[] = $errorStatus;
121 }
122
123 if ($canProceed && !empty($localConfigurationPathValuePairs)) {
124 $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs);
125 }
126
127 return $result;
128 }
129
130 /**
131 * Step needs to be executed if database is not set or can
132 * not be selected.
133 *
134 * @return bool
135 */
136 public function needsExecution()
137 {
138 $this->initializeDatabaseConnection();
139 $result = true;
140 if ((string)$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] !== '') {
141 $this->databaseConnection->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
142 try {
143 $selectResult = $this->databaseConnection->sql_select_db();
144 if ($selectResult === true) {
145 $result = false;
146 }
147 } catch (\RuntimeException $e) {
148 }
149 }
150 return $result;
151 }
152
153 /**
154 * Executes the step
155 *
156 * @return string Rendered content
157 */
158 protected function executeAction()
159 {
160 /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */
161 $configurationManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
162 $isInitialInstallationInProgress = $configurationManager->getConfigurationValueByPath('SYS/isInitialInstallationInProgress');
163 $this->view->assign('databaseList', $this->getDatabaseList($isInitialInstallationInProgress));
164 $this->view->assign('isInitialInstallationInProgress', $isInitialInstallationInProgress);
165 $this->assignSteps();
166 return $this->view->render();
167 }
168
169 /**
170 * Returns list of available databases (with access-check based on username/password)
171 *
172 * @param bool $initialInstallation TRUE if first installation is in progress, FALSE if upgrading or usual access
173 * @return array List of available databases
174 */
175 protected function getDatabaseList($initialInstallation)
176 {
177 $this->initializeDatabaseConnection();
178 $databaseArray = $this->databaseConnection->admin_get_dbs();
179 // Remove mysql organizational tables from database list
180 $reservedDatabaseNames = array('mysql', 'information_schema', 'performance_schema');
181 $allPossibleDatabases = array_diff($databaseArray, $reservedDatabaseNames);
182
183 // If we are upgrading we show *all* databases the user has access to
184 if ($initialInstallation === false) {
185 return $allPossibleDatabases;
186 } else {
187 // In first installation we show all databases but disable not empty ones (with tables)
188 $databases = array();
189 foreach ($allPossibleDatabases as $database) {
190 $this->databaseConnection->setDatabaseName($database);
191 $this->databaseConnection->sql_select_db();
192 $existingTables = $this->databaseConnection->admin_get_tables();
193 $databases[] = array(
194 'name' => $database,
195 'tables' => count($existingTables),
196 );
197 }
198 return $databases;
199 }
200 }
201
202 /**
203 * Initialize database connection
204 *
205 * @return void
206 */
207 protected function initializeDatabaseConnection()
208 {
209 $this->databaseConnection = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
210 $this->databaseConnection->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']);
211 $this->databaseConnection->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']);
212 $this->databaseConnection->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']);
213 $this->databaseConnection->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port']);
214 $this->databaseConnection->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket']);
215 $this->databaseConnection->sql_pconnect();
216 }
217
218 /**
219 * Validate the database name against the lowest common denominator of valid identifiers across different DBMS
220 *
221 * @param string $databaseName
222 * @return bool
223 */
224 protected function isValidDatabaseName($databaseName)
225 {
226 return strlen($databaseName) <= 50 && preg_match('/^[a-zA-Z0-9\$_]*$/', $databaseName);
227 }
228
229 /**
230 * Retrieves the default character set of the database.
231 *
232 * @return string
233 */
234 protected function getDefaultDatabaseCharset()
235 {
236 $result = $this->databaseConnection->admin_query('SHOW VARIABLES LIKE "character_set_database"');
237 $row = $this->databaseConnection->sql_fetch_assoc($result);
238
239 $key = $row['Variable_name'];
240 $value = $row['Value'];
241 $databaseCharset = '';
242
243 if ($key == 'character_set_database') {
244 $databaseCharset = $value;
245 }
246
247 return $databaseCharset;
248 }
249 }