[BUGFIX] Replace .env parsing with reading from environment
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / Action / Step / DatabaseConnect.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\Connection;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Install\Status\ErrorStatus;
24
25 /**
26 * Database connect step:
27 * - Needs execution if database credentials are not set or fail to connect
28 * - Renders fields for database connection fields
29 * - Sets database credentials in LocalConfiguration
30 */
31 class DatabaseConnect extends AbstractStepAction
32 {
33 /**
34 * Execute database step:
35 * - Set database connect credentials in LocalConfiguration
36 *
37 * @return array<\TYPO3\CMS\Install\Status\StatusInterface>
38 */
39 public function execute()
40 {
41 $result = [];
42 $postValues = $this->postValues['values'];
43 $defaultConnectionSettings = [];
44
45 if ($postValues['availableSet'] === 'configurationFromEnvironment') {
46 $defaultConnectionSettings = $this->getConfigurationFromEnvironment();
47 } else {
48 if (isset($postValues['driver'])) {
49 $validDrivers = [
50 'mysqli',
51 'pdo_mysql',
52 'pdo_pgsql',
53 'mssql',
54 ];
55 if (in_array($postValues['driver'], $validDrivers, true)) {
56 $defaultConnectionSettings['driver'] = $postValues['driver'];
57 } else {
58 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
59 $errorStatus->setTitle('Database driver unknown');
60 $errorStatus->setMessage('Given driver must be one of ' . implode(', ', $validDrivers));
61 $result[] = $errorStatus;
62 }
63 }
64 if (isset($postValues['username'])) {
65 $value = $postValues['username'];
66 if (strlen($value) <= 50) {
67 $defaultConnectionSettings['user'] = $value;
68 } else {
69 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
70 $errorStatus->setTitle('Database username not valid');
71 $errorStatus->setMessage('Given username must be shorter than fifty characters.');
72 $result[] = $errorStatus;
73 }
74 }
75 if (isset($postValues['password'])) {
76 $value = $postValues['password'];
77 if (strlen($value) <= 50) {
78 $defaultConnectionSettings['password'] = $value;
79 } else {
80 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
81 $errorStatus->setTitle('Database password not valid');
82 $errorStatus->setMessage('Given password must be shorter than fifty characters.');
83 $result[] = $errorStatus;
84 }
85 }
86 if (isset($postValues['host'])) {
87 $value = $postValues['host'];
88 if (preg_match('/^[a-zA-Z0-9_\\.-]+(:.+)?$/', $value) && strlen($value) <= 255) {
89 $defaultConnectionSettings['host'] = $value;
90 } else {
91 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
92 $errorStatus->setTitle('Database host not valid');
93 $errorStatus->setMessage('Given host is not alphanumeric (a-z, A-Z, 0-9 or _-.:) or longer than 255 characters.');
94 $result[] = $errorStatus;
95 }
96 }
97 if (isset($postValues['port']) && $postValues['host'] !== 'localhost') {
98 $value = $postValues['port'];
99 if (preg_match('/^[0-9]+(:.+)?$/', $value) && $value > 0 && $value <= 65535) {
100 $defaultConnectionSettings['port'] = (int)$value;
101 } else {
102 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
103 $errorStatus->setTitle('Database port not valid');
104 $errorStatus->setMessage('Given port is not numeric or within range 1 to 65535.');
105 $result[] = $errorStatus;
106 }
107 }
108 if (isset($postValues['socket']) && $postValues['socket'] !== '') {
109 if (@file_exists($postValues['socket'])) {
110 $defaultConnectionSettings['unix_socket'] = $postValues['socket'];
111 } else {
112 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
113 $errorStatus->setTitle('Socket does not exist');
114 $errorStatus->setMessage('Given socket location does not exist on server.');
115 $result[] = $errorStatus;
116 }
117 }
118 if (isset($postValues['database'])) {
119 $value = $postValues['database'];
120 if (strlen($value) <= 50) {
121 $defaultConnectionSettings['dbname'] = $value;
122 } else {
123 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
124 $errorStatus->setTitle('Database name not valid');
125 $errorStatus->setMessage('Given database name must be shorter than fifty characters.');
126 $result[] = $errorStatus;
127 }
128 }
129 }
130
131 if (!empty($defaultConnectionSettings)) {
132 // Test connection settings and write to config if connect is successful
133 try {
134 $connectionParams = $defaultConnectionSettings;
135 $connectionParams['wrapperClass'] = Connection::class;
136 $connectionParams['charset'] = 'utf-8';
137 DriverManager::getConnection($connectionParams)->ping();
138 } catch (DBALException $e) {
139 $errorStatus = GeneralUtility::makeInstance(ErrorStatus::class);
140 $errorStatus->setTitle('Database connect not successful');
141 $errorStatus->setMessage('Connecting to the database with given settings failed: ' . $e->getMessage());
142 $result[] = $errorStatus;
143 }
144 $localConfigurationPathValuePairs = [];
145 foreach ($defaultConnectionSettings as $settingsName => $value) {
146 $localConfigurationPathValuePairs['DB/Connections/Default/' . $settingsName] = $value;
147 }
148 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
149 // Remove full default connection array
150 $configurationManager->removeLocalConfigurationKeysByPath([ 'DB/Connections/Default' ]);
151 // Write new values
152 $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs);
153 }
154
155 return $result;
156 }
157
158 /**
159 * Step needs to be executed if database connection is not successful.
160 *
161 * @throws \TYPO3\CMS\Install\Controller\Exception\RedirectException
162 * @return bool
163 */
164 public function needsExecution()
165 {
166 if ($this->isConnectSuccessful() && $this->isConfigurationComplete()) {
167 return false;
168 }
169 return true;
170 }
171
172 /**
173 * Executes the step
174 *
175 * @return string Rendered content
176 */
177 protected function executeAction()
178 {
179 $hasAtLeastOneOption = false;
180 $activeAvailableOption = '';
181 if (extension_loaded('mysqli')) {
182 $hasAtLeastOneOption = true;
183 $this->view->assign('hasMysqliManualConfiguration', true);
184 $mysqliManualConfigurationOptions = [
185 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '',
186 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '',
187 'port' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? 3306,
188 ];
189 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '127.0.0.1';
190 if ($host === 'localhost') {
191 $host = '127.0.0.1';
192 }
193 $mysqliManualConfigurationOptions['host'] = $host;
194 $this->view->assign('mysqliManualConfigurationOptions', $mysqliManualConfigurationOptions);
195 $activeAvailableOption = 'mysqliManualConfiguration';
196
197 $this->view->assign('hasMysqliSocketManualConfiguration', true);
198 $this->view->assign(
199 'mysqliSocketManualConfigurationOptions',
200 [
201 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '',
202 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '',
203 'socket' => $this->getConfiguredMysqliSocket(),
204 ]
205 );
206 if ($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'mysqli'
207 && $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] === 'localhost') {
208 $activeAvailableOption = 'mysqliSocketManualConfiguration';
209 }
210 }
211 if (extension_loaded('pdo_pgsql')) {
212 $hasAtLeastOneOption = true;
213 $this->view->assign('hasPostgresManualConfiguration', true);
214 $this->view->assign(
215 'postgresManualConfigurationOptions',
216 [
217 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '',
218 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '',
219 'host' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '127.0.0.1',
220 'port' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? 5432,
221 'database' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] ?? '',
222 ]
223 );
224 if ($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'pdo_pgsql') {
225 $activeAvailableOption = 'postgresManualConfiguration';
226 }
227 }
228
229 if (!empty($this->getConfigurationFromEnvironment())) {
230 $hasAtLeastOneOption = true;
231 $activeAvailableOption = 'configurationFromEnvironment';
232 $this->view->assign('hasConfigurationFromEnvironment', true);
233 }
234
235 $this->view->assign('hasAtLeastOneOption', $hasAtLeastOneOption);
236 $this->view->assign('activeAvailableOption', $activeAvailableOption);
237
238 $this->assignSteps();
239
240 return $this->view->render();
241 }
242
243 /**
244 * Test connection with given credentials
245 *
246 * @return bool true if connect was successful
247 */
248 protected function isConnectSuccessful()
249 {
250 return empty($this->isConnectSuccessfulWithExceptionMessage());
251 }
252
253 /**
254 * Test connection with given credentials and return exception message if exception trown
255 *
256 * @return string
257 */
258 protected function isConnectSuccessfulWithExceptionMessage(): string
259 {
260 try {
261 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName('Default')->ping();
262 } catch (DBALException $e) {
263 return $e->getMessage();
264 }
265 return '';
266 }
267
268 /**
269 * Check LocalConfiguration.php for required database settings:
270 * - 'username' and 'password' are mandatory, but may be empty
271 *
272 * @return bool TRUE if required settings are present
273 */
274 protected function isConfigurationComplete()
275 {
276 $configurationComplete = true;
277 if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'])) {
278 $configurationComplete = false;
279 }
280 if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'])) {
281 $configurationComplete = false;
282 }
283 return $configurationComplete;
284 }
285
286 /**
287 * Returns configured socket, if set.
288 *
289 * @return string
290 */
291 protected function getConfiguredMysqliSocket()
292 {
293 $socket = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'] ?? '';
294 if ($socket === '') {
295 // If no configured socket, use default php socket
296 $defaultSocket = (string)ini_get('mysqli.default_socket');
297 if ($defaultSocket !== '') {
298 $socket = $defaultSocket;
299 }
300 }
301 return $socket;
302 }
303
304 /**
305 * Try to fetch db credentials from a .env file and see if connect works
306 *
307 * @return array Empty array if no file is found or connect is not successful, else working credentials
308 */
309 protected function getConfigurationFromEnvironment(): array
310 {
311 $envCredentials = [];
312 foreach (['driver', 'host', 'user', 'password', 'port', 'dbname', 'unix_socket'] as $value) {
313 $envVar = 'TYPO3_INSTALL_DB_' . strtoupper($value);
314 if (getenv($envVar) !== false) {
315 $envCredentials[$value] = getenv($envVar);
316 }
317 }
318 if (!empty($envCredentials)) {
319 $connectionParams = $envCredentials;
320 $connectionParams['wrapperClass'] = Connection::class;
321 $connectionParams['charset'] = 'utf-8';
322 try {
323 DriverManager::getConnection($connectionParams)->ping();
324 return $envCredentials;
325 } catch (DBALException $e) {
326 return [];
327 }
328 }
329 return [];
330 }
331 }