Completed
Push — master ( f3c56f...da1dd4 )
by Joas
41:30
created
lib/private/Setup.php 1 patch
Indentation   +622 added lines, -622 removed lines patch added patch discarded remove patch
@@ -41,629 +41,629 @@
 block discarded – undo
41 41
 use Psr\Log\LoggerInterface;
42 42
 
43 43
 class Setup {
44
-	protected IL10N $l10n;
45
-
46
-	public function __construct(
47
-		protected SystemConfig $config,
48
-		protected IniGetWrapper $iniWrapper,
49
-		IL10NFactory $l10nFactory,
50
-		protected Defaults $defaults,
51
-		protected LoggerInterface $logger,
52
-		protected ISecureRandom $random,
53
-		protected Installer $installer,
54
-	) {
55
-		$this->l10n = $l10nFactory->get('lib');
56
-	}
57
-
58
-	protected static array $dbSetupClasses = [
59
-		'mysql' => \OC\Setup\MySQL::class,
60
-		'pgsql' => \OC\Setup\PostgreSQL::class,
61
-		'oci' => \OC\Setup\OCI::class,
62
-		'sqlite' => \OC\Setup\Sqlite::class,
63
-		'sqlite3' => \OC\Setup\Sqlite::class,
64
-	];
65
-
66
-	/**
67
-	 * Wrapper around the "class_exists" PHP function to be able to mock it
68
-	 */
69
-	protected function class_exists(string $name): bool {
70
-		return class_exists($name);
71
-	}
72
-
73
-	/**
74
-	 * Wrapper around the "is_callable" PHP function to be able to mock it
75
-	 */
76
-	protected function is_callable(string $name): bool {
77
-		return is_callable($name);
78
-	}
79
-
80
-	/**
81
-	 * Wrapper around \PDO::getAvailableDrivers
82
-	 */
83
-	protected function getAvailableDbDriversForPdo(): array {
84
-		if (class_exists(\PDO::class)) {
85
-			return \PDO::getAvailableDrivers();
86
-		}
87
-		return [];
88
-	}
89
-
90
-	/**
91
-	 * Get the available and supported databases of this instance
92
-	 *
93
-	 * @return array
94
-	 * @throws Exception
95
-	 */
96
-	public function getSupportedDatabases(bool $allowAllDatabases = false): array {
97
-		$availableDatabases = [
98
-			'sqlite' => [
99
-				'type' => 'pdo',
100
-				'call' => 'sqlite',
101
-				'name' => 'SQLite',
102
-			],
103
-			'mysql' => [
104
-				'type' => 'pdo',
105
-				'call' => 'mysql',
106
-				'name' => 'MySQL/MariaDB',
107
-			],
108
-			'pgsql' => [
109
-				'type' => 'pdo',
110
-				'call' => 'pgsql',
111
-				'name' => 'PostgreSQL',
112
-			],
113
-			'oci' => [
114
-				'type' => 'function',
115
-				'call' => 'oci_connect',
116
-				'name' => 'Oracle',
117
-			],
118
-		];
119
-		if ($allowAllDatabases) {
120
-			$configuredDatabases = array_keys($availableDatabases);
121
-		} else {
122
-			$configuredDatabases = $this->config->getValue('supportedDatabases',
123
-				['sqlite', 'mysql', 'pgsql']);
124
-		}
125
-		if (!is_array($configuredDatabases)) {
126
-			throw new Exception('Supported databases are not properly configured.');
127
-		}
128
-
129
-		$supportedDatabases = [];
130
-
131
-		foreach ($configuredDatabases as $database) {
132
-			if (array_key_exists($database, $availableDatabases)) {
133
-				$working = false;
134
-				$type = $availableDatabases[$database]['type'];
135
-				$call = $availableDatabases[$database]['call'];
136
-
137
-				if ($type === 'function') {
138
-					$working = $this->is_callable($call);
139
-				} elseif ($type === 'pdo') {
140
-					$working = in_array($call, $this->getAvailableDbDriversForPdo(), true);
141
-				}
142
-				if ($working) {
143
-					$supportedDatabases[$database] = $availableDatabases[$database]['name'];
144
-				}
145
-			}
146
-		}
147
-
148
-		return $supportedDatabases;
149
-	}
150
-
151
-	/**
152
-	 * Gathers system information like database type and does
153
-	 * a few system checks.
154
-	 *
155
-	 * @return array of system info, including an "errors" value
156
-	 *               in case of errors/warnings
157
-	 */
158
-	public function getSystemInfo(bool $allowAllDatabases = false): array {
159
-		$databases = $this->getSupportedDatabases($allowAllDatabases);
160
-
161
-		$dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
162
-
163
-		$errors = [];
164
-
165
-		// Create data directory to test whether the .htaccess works
166
-		// Notice that this is not necessarily the same data directory as the one
167
-		// that will effectively be used.
168
-		if (!file_exists($dataDir)) {
169
-			@mkdir($dataDir);
170
-		}
171
-		$htAccessWorking = true;
172
-		if (is_dir($dataDir) && is_writable($dataDir)) {
173
-			// Protect data directory here, so we can test if the protection is working
174
-			self::protectDataDirectory();
175
-
176
-			try {
177
-				$htAccessWorking = $this->isHtaccessWorking($dataDir);
178
-			} catch (\OCP\HintException $e) {
179
-				$errors[] = [
180
-					'error' => $e->getMessage(),
181
-					'exception' => $e,
182
-					'hint' => $e->getHint(),
183
-				];
184
-				$htAccessWorking = false;
185
-			}
186
-		}
187
-
188
-		// Check if running directly on macOS (note: Linux containers on macOS will not trigger this)
189
-		if (!getenv('CI') && PHP_OS_FAMILY === 'Darwin') {
190
-			$errors[] = [
191
-				'error' => $this->l10n->t(
192
-					'macOS is not supported and %s will not work properly on this platform.',
193
-					[$this->defaults->getProductName()]
194
-				),
195
-				'hint' => $this->l10n->t('For the best results, please consider using a GNU/Linux server instead.'),
196
-			];
197
-		}
198
-
199
-		if ($this->iniWrapper->getString('open_basedir') !== '' && PHP_INT_SIZE === 4) {
200
-			$errors[] = [
201
-				'error' => $this->l10n->t(
202
-					'It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. '
203
-					. 'This will lead to problems with files over 4 GB and is highly discouraged.',
204
-					[$this->defaults->getProductName()]
205
-				),
206
-				'hint' => $this->l10n->t('Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP.'),
207
-			];
208
-		}
209
-
210
-		return [
211
-			'databases' => $databases,
212
-			'directory' => $dataDir,
213
-			'htaccessWorking' => $htAccessWorking,
214
-			'errors' => $errors,
215
-		];
216
-	}
217
-
218
-	public function createHtaccessTestFile(string $dataDir): string|false {
219
-		// php dev server does not support htaccess
220
-		if (php_sapi_name() === 'cli-server') {
221
-			return false;
222
-		}
223
-
224
-		// testdata
225
-		$fileName = '/htaccesstest.txt';
226
-		$testContent = 'This is used for testing whether htaccess is properly enabled to disallow access from the outside. This file can be safely removed.';
227
-
228
-		// creating a test file
229
-		$testFile = $dataDir . '/' . $fileName;
230
-
231
-		if (file_exists($testFile)) {// already running this test, possible recursive call
232
-			return false;
233
-		}
234
-
235
-		$fp = @fopen($testFile, 'w');
236
-		if (!$fp) {
237
-			throw new \OCP\HintException('Can\'t create test file to check for working .htaccess file.',
238
-				'Make sure it is possible for the web server to write to ' . $testFile);
239
-		}
240
-		fwrite($fp, $testContent);
241
-		fclose($fp);
242
-
243
-		return $testContent;
244
-	}
245
-
246
-	/**
247
-	 * Check if the .htaccess file is working
248
-	 *
249
-	 * @param \OCP\IConfig $config
250
-	 * @return bool
251
-	 * @throws Exception
252
-	 * @throws \OCP\HintException If the test file can't get written.
253
-	 */
254
-	public function isHtaccessWorking(string $dataDir) {
255
-		$config = Server::get(IConfig::class);
256
-
257
-		if (\OC::$CLI || !$config->getSystemValueBool('check_for_working_htaccess', true)) {
258
-			return true;
259
-		}
260
-
261
-		$testContent = $this->createHtaccessTestFile($dataDir);
262
-		if ($testContent === false) {
263
-			return false;
264
-		}
265
-
266
-		$fileName = '/htaccesstest.txt';
267
-		$testFile = $dataDir . '/' . $fileName;
268
-
269
-		// accessing the file via http
270
-		$url = Server::get(IURLGenerator::class)->getAbsoluteURL(\OC::$WEBROOT . '/data' . $fileName);
271
-		try {
272
-			$content = Server::get(IClientService::class)->newClient()->get($url)->getBody();
273
-		} catch (\Exception $e) {
274
-			$content = false;
275
-		}
276
-
277
-		if (str_starts_with($url, 'https:')) {
278
-			$url = 'http:' . substr($url, 6);
279
-		} else {
280
-			$url = 'https:' . substr($url, 5);
281
-		}
282
-
283
-		try {
284
-			$fallbackContent = Server::get(IClientService::class)->newClient()->get($url)->getBody();
285
-		} catch (\Exception $e) {
286
-			$fallbackContent = false;
287
-		}
288
-
289
-		// cleanup
290
-		@unlink($testFile);
291
-
292
-		/*
44
+    protected IL10N $l10n;
45
+
46
+    public function __construct(
47
+        protected SystemConfig $config,
48
+        protected IniGetWrapper $iniWrapper,
49
+        IL10NFactory $l10nFactory,
50
+        protected Defaults $defaults,
51
+        protected LoggerInterface $logger,
52
+        protected ISecureRandom $random,
53
+        protected Installer $installer,
54
+    ) {
55
+        $this->l10n = $l10nFactory->get('lib');
56
+    }
57
+
58
+    protected static array $dbSetupClasses = [
59
+        'mysql' => \OC\Setup\MySQL::class,
60
+        'pgsql' => \OC\Setup\PostgreSQL::class,
61
+        'oci' => \OC\Setup\OCI::class,
62
+        'sqlite' => \OC\Setup\Sqlite::class,
63
+        'sqlite3' => \OC\Setup\Sqlite::class,
64
+    ];
65
+
66
+    /**
67
+     * Wrapper around the "class_exists" PHP function to be able to mock it
68
+     */
69
+    protected function class_exists(string $name): bool {
70
+        return class_exists($name);
71
+    }
72
+
73
+    /**
74
+     * Wrapper around the "is_callable" PHP function to be able to mock it
75
+     */
76
+    protected function is_callable(string $name): bool {
77
+        return is_callable($name);
78
+    }
79
+
80
+    /**
81
+     * Wrapper around \PDO::getAvailableDrivers
82
+     */
83
+    protected function getAvailableDbDriversForPdo(): array {
84
+        if (class_exists(\PDO::class)) {
85
+            return \PDO::getAvailableDrivers();
86
+        }
87
+        return [];
88
+    }
89
+
90
+    /**
91
+     * Get the available and supported databases of this instance
92
+     *
93
+     * @return array
94
+     * @throws Exception
95
+     */
96
+    public function getSupportedDatabases(bool $allowAllDatabases = false): array {
97
+        $availableDatabases = [
98
+            'sqlite' => [
99
+                'type' => 'pdo',
100
+                'call' => 'sqlite',
101
+                'name' => 'SQLite',
102
+            ],
103
+            'mysql' => [
104
+                'type' => 'pdo',
105
+                'call' => 'mysql',
106
+                'name' => 'MySQL/MariaDB',
107
+            ],
108
+            'pgsql' => [
109
+                'type' => 'pdo',
110
+                'call' => 'pgsql',
111
+                'name' => 'PostgreSQL',
112
+            ],
113
+            'oci' => [
114
+                'type' => 'function',
115
+                'call' => 'oci_connect',
116
+                'name' => 'Oracle',
117
+            ],
118
+        ];
119
+        if ($allowAllDatabases) {
120
+            $configuredDatabases = array_keys($availableDatabases);
121
+        } else {
122
+            $configuredDatabases = $this->config->getValue('supportedDatabases',
123
+                ['sqlite', 'mysql', 'pgsql']);
124
+        }
125
+        if (!is_array($configuredDatabases)) {
126
+            throw new Exception('Supported databases are not properly configured.');
127
+        }
128
+
129
+        $supportedDatabases = [];
130
+
131
+        foreach ($configuredDatabases as $database) {
132
+            if (array_key_exists($database, $availableDatabases)) {
133
+                $working = false;
134
+                $type = $availableDatabases[$database]['type'];
135
+                $call = $availableDatabases[$database]['call'];
136
+
137
+                if ($type === 'function') {
138
+                    $working = $this->is_callable($call);
139
+                } elseif ($type === 'pdo') {
140
+                    $working = in_array($call, $this->getAvailableDbDriversForPdo(), true);
141
+                }
142
+                if ($working) {
143
+                    $supportedDatabases[$database] = $availableDatabases[$database]['name'];
144
+                }
145
+            }
146
+        }
147
+
148
+        return $supportedDatabases;
149
+    }
150
+
151
+    /**
152
+     * Gathers system information like database type and does
153
+     * a few system checks.
154
+     *
155
+     * @return array of system info, including an "errors" value
156
+     *               in case of errors/warnings
157
+     */
158
+    public function getSystemInfo(bool $allowAllDatabases = false): array {
159
+        $databases = $this->getSupportedDatabases($allowAllDatabases);
160
+
161
+        $dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
162
+
163
+        $errors = [];
164
+
165
+        // Create data directory to test whether the .htaccess works
166
+        // Notice that this is not necessarily the same data directory as the one
167
+        // that will effectively be used.
168
+        if (!file_exists($dataDir)) {
169
+            @mkdir($dataDir);
170
+        }
171
+        $htAccessWorking = true;
172
+        if (is_dir($dataDir) && is_writable($dataDir)) {
173
+            // Protect data directory here, so we can test if the protection is working
174
+            self::protectDataDirectory();
175
+
176
+            try {
177
+                $htAccessWorking = $this->isHtaccessWorking($dataDir);
178
+            } catch (\OCP\HintException $e) {
179
+                $errors[] = [
180
+                    'error' => $e->getMessage(),
181
+                    'exception' => $e,
182
+                    'hint' => $e->getHint(),
183
+                ];
184
+                $htAccessWorking = false;
185
+            }
186
+        }
187
+
188
+        // Check if running directly on macOS (note: Linux containers on macOS will not trigger this)
189
+        if (!getenv('CI') && PHP_OS_FAMILY === 'Darwin') {
190
+            $errors[] = [
191
+                'error' => $this->l10n->t(
192
+                    'macOS is not supported and %s will not work properly on this platform.',
193
+                    [$this->defaults->getProductName()]
194
+                ),
195
+                'hint' => $this->l10n->t('For the best results, please consider using a GNU/Linux server instead.'),
196
+            ];
197
+        }
198
+
199
+        if ($this->iniWrapper->getString('open_basedir') !== '' && PHP_INT_SIZE === 4) {
200
+            $errors[] = [
201
+                'error' => $this->l10n->t(
202
+                    'It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. '
203
+                    . 'This will lead to problems with files over 4 GB and is highly discouraged.',
204
+                    [$this->defaults->getProductName()]
205
+                ),
206
+                'hint' => $this->l10n->t('Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP.'),
207
+            ];
208
+        }
209
+
210
+        return [
211
+            'databases' => $databases,
212
+            'directory' => $dataDir,
213
+            'htaccessWorking' => $htAccessWorking,
214
+            'errors' => $errors,
215
+        ];
216
+    }
217
+
218
+    public function createHtaccessTestFile(string $dataDir): string|false {
219
+        // php dev server does not support htaccess
220
+        if (php_sapi_name() === 'cli-server') {
221
+            return false;
222
+        }
223
+
224
+        // testdata
225
+        $fileName = '/htaccesstest.txt';
226
+        $testContent = 'This is used for testing whether htaccess is properly enabled to disallow access from the outside. This file can be safely removed.';
227
+
228
+        // creating a test file
229
+        $testFile = $dataDir . '/' . $fileName;
230
+
231
+        if (file_exists($testFile)) {// already running this test, possible recursive call
232
+            return false;
233
+        }
234
+
235
+        $fp = @fopen($testFile, 'w');
236
+        if (!$fp) {
237
+            throw new \OCP\HintException('Can\'t create test file to check for working .htaccess file.',
238
+                'Make sure it is possible for the web server to write to ' . $testFile);
239
+        }
240
+        fwrite($fp, $testContent);
241
+        fclose($fp);
242
+
243
+        return $testContent;
244
+    }
245
+
246
+    /**
247
+     * Check if the .htaccess file is working
248
+     *
249
+     * @param \OCP\IConfig $config
250
+     * @return bool
251
+     * @throws Exception
252
+     * @throws \OCP\HintException If the test file can't get written.
253
+     */
254
+    public function isHtaccessWorking(string $dataDir) {
255
+        $config = Server::get(IConfig::class);
256
+
257
+        if (\OC::$CLI || !$config->getSystemValueBool('check_for_working_htaccess', true)) {
258
+            return true;
259
+        }
260
+
261
+        $testContent = $this->createHtaccessTestFile($dataDir);
262
+        if ($testContent === false) {
263
+            return false;
264
+        }
265
+
266
+        $fileName = '/htaccesstest.txt';
267
+        $testFile = $dataDir . '/' . $fileName;
268
+
269
+        // accessing the file via http
270
+        $url = Server::get(IURLGenerator::class)->getAbsoluteURL(\OC::$WEBROOT . '/data' . $fileName);
271
+        try {
272
+            $content = Server::get(IClientService::class)->newClient()->get($url)->getBody();
273
+        } catch (\Exception $e) {
274
+            $content = false;
275
+        }
276
+
277
+        if (str_starts_with($url, 'https:')) {
278
+            $url = 'http:' . substr($url, 6);
279
+        } else {
280
+            $url = 'https:' . substr($url, 5);
281
+        }
282
+
283
+        try {
284
+            $fallbackContent = Server::get(IClientService::class)->newClient()->get($url)->getBody();
285
+        } catch (\Exception $e) {
286
+            $fallbackContent = false;
287
+        }
288
+
289
+        // cleanup
290
+        @unlink($testFile);
291
+
292
+        /*
293 293
 		 * If the content is not equal to test content our .htaccess
294 294
 		 * is working as required
295 295
 		 */
296
-		return $content !== $testContent && $fallbackContent !== $testContent;
297
-	}
298
-
299
-	/**
300
-	 * @return array<string|array> errors
301
-	 */
302
-	public function install(array $options, ?IOutput $output = null): array {
303
-		$l = $this->l10n;
304
-
305
-		$error = [];
306
-		$dbType = $options['dbtype'];
307
-
308
-		$disableAdminUser = (bool)($options['admindisable'] ?? false);
309
-
310
-		if (!$disableAdminUser) {
311
-			if (empty($options['adminlogin'])) {
312
-				$error[] = $l->t('Set an admin Login.');
313
-			}
314
-			if (empty($options['adminpass'])) {
315
-				$error[] = $l->t('Set an admin password.');
316
-			}
317
-		}
318
-		if (empty($options['directory'])) {
319
-			$options['directory'] = \OC::$SERVERROOT . '/data';
320
-		}
321
-
322
-		if (!isset(self::$dbSetupClasses[$dbType])) {
323
-			$dbType = 'sqlite';
324
-		}
325
-
326
-		$dataDir = htmlspecialchars_decode($options['directory']);
327
-
328
-		$class = self::$dbSetupClasses[$dbType];
329
-		/** @var \OC\Setup\AbstractDatabase $dbSetup */
330
-		$dbSetup = new $class($l, $this->config, $this->logger, $this->random);
331
-		$error = array_merge($error, $dbSetup->validate($options));
332
-
333
-		// validate the data directory
334
-		if ((!is_dir($dataDir) && !mkdir($dataDir)) || !is_writable($dataDir)) {
335
-			$error[] = $l->t('Cannot create or write into the data directory %s', [$dataDir]);
336
-		}
337
-
338
-		if (!empty($error)) {
339
-			return $error;
340
-		}
341
-
342
-		$request = Server::get(IRequest::class);
343
-
344
-		//no errors, good
345
-		if (isset($options['trusted_domains'])
346
-			&& is_array($options['trusted_domains'])) {
347
-			$trustedDomains = $options['trusted_domains'];
348
-		} else {
349
-			$trustedDomains = [$request->getInsecureServerHost()];
350
-		}
351
-
352
-		//use sqlite3 when available, otherwise sqlite2 will be used.
353
-		if ($dbType === 'sqlite' && class_exists('SQLite3')) {
354
-			$dbType = 'sqlite3';
355
-		}
356
-
357
-		//generate a random salt that is used to salt the local  passwords
358
-		$salt = $this->random->generate(30);
359
-		// generate a secret
360
-		$secret = $this->random->generate(48);
361
-
362
-		//write the config file
363
-		$newConfigValues = [
364
-			'passwordsalt' => $salt,
365
-			'secret' => $secret,
366
-			'trusted_domains' => $trustedDomains,
367
-			'datadirectory' => $dataDir,
368
-			'dbtype' => $dbType,
369
-			'version' => implode('.', \OCP\Util::getVersion()),
370
-		];
371
-
372
-		if ($this->config->getValue('overwrite.cli.url', null) === null) {
373
-			$newConfigValues['overwrite.cli.url'] = $request->getServerProtocol() . '://' . $request->getInsecureServerHost() . \OC::$WEBROOT;
374
-		}
375
-
376
-		$this->config->setValues($newConfigValues);
377
-
378
-		$this->outputDebug($output, 'Configuring database');
379
-		$dbSetup->initialize($options);
380
-		try {
381
-			$dbSetup->setupDatabase();
382
-		} catch (\OC\DatabaseSetupException $e) {
383
-			$error[] = [
384
-				'error' => $e->getMessage(),
385
-				'exception' => $e,
386
-				'hint' => $e->getHint(),
387
-			];
388
-			return $error;
389
-		} catch (Exception $e) {
390
-			$error[] = [
391
-				'error' => 'Error while trying to create admin account: ' . $e->getMessage(),
392
-				'exception' => $e,
393
-				'hint' => '',
394
-			];
395
-			return $error;
396
-		}
397
-
398
-		$this->outputDebug($output, 'Run server migrations');
399
-		try {
400
-			// apply necessary migrations
401
-			$dbSetup->runMigrations($output);
402
-		} catch (Exception $e) {
403
-			$error[] = [
404
-				'error' => 'Error while trying to initialise the database: ' . $e->getMessage(),
405
-				'exception' => $e,
406
-				'hint' => '',
407
-			];
408
-			return $error;
409
-		}
410
-
411
-		$user = null;
412
-		if (!$disableAdminUser) {
413
-			$username = htmlspecialchars_decode($options['adminlogin']);
414
-			$password = htmlspecialchars_decode($options['adminpass']);
415
-			$this->outputDebug($output, 'Create admin account');
416
-
417
-			try {
418
-				$user = Server::get(IUserManager::class)->createUser($username, $password);
419
-				if (!$user) {
420
-					$error[] = "Account <$username> could not be created.";
421
-					return $error;
422
-				}
423
-			} catch (Exception $exception) {
424
-				$error[] = $exception->getMessage();
425
-				return $error;
426
-			}
427
-		}
428
-
429
-		$config = Server::get(IConfig::class);
430
-		$config->setAppValue('core', 'installedat', (string)microtime(true));
431
-		$appConfig = Server::get(IAppConfig::class);
432
-		$appConfig->setValueInt('core', 'lastupdatedat', time());
433
-
434
-		$vendorData = $this->getVendorData();
435
-		$config->setAppValue('core', 'vendor', $vendorData['vendor']);
436
-		if ($vendorData['channel'] !== 'stable') {
437
-			$config->setSystemValue('updater.release.channel', $vendorData['channel']);
438
-		}
439
-
440
-		$group = Server::get(IGroupManager::class)->createGroup('admin');
441
-		if ($user !== null && $group instanceof IGroup) {
442
-			$group->addUser($user);
443
-		}
444
-
445
-		// Install shipped apps and specified app bundles
446
-		$this->outputDebug($output, 'Install default apps');
447
-		$installer = Server::get(Installer::class);
448
-		$installer->installShippedApps(false, $output);
449
-
450
-		// create empty file in data dir, so we can later find
451
-		// out that this is indeed a Nextcloud data directory
452
-		$this->outputDebug($output, 'Setup data directory');
453
-		file_put_contents(
454
-			$config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata',
455
-			"# Nextcloud data directory\n# Do not change this file",
456
-		);
457
-
458
-		// Update .htaccess files
459
-		self::updateHtaccess();
460
-		self::protectDataDirectory();
461
-
462
-		$this->outputDebug($output, 'Install background jobs');
463
-		self::installBackgroundJobs();
464
-
465
-		//and we are done
466
-		$config->setSystemValue('installed', true);
467
-		if (self::shouldRemoveCanInstallFile()) {
468
-			unlink(\OC::$configDir . '/CAN_INSTALL');
469
-		}
470
-
471
-		$bootstrapCoordinator = Server::get(\OC\AppFramework\Bootstrap\Coordinator::class);
472
-		$bootstrapCoordinator->runInitialRegistration();
473
-
474
-		if (!$disableAdminUser) {
475
-			// Create a session token for the newly created user
476
-			// The token provider requires a working db, so it's not injected on setup
477
-			/** @var \OC\User\Session $userSession */
478
-			$userSession = Server::get(IUserSession::class);
479
-			$provider = Server::get(PublicKeyTokenProvider::class);
480
-			$userSession->setTokenProvider($provider);
481
-			$userSession->login($username, $password);
482
-			$user = $userSession->getUser();
483
-			if (!$user) {
484
-				$error[] = 'No account found in session.';
485
-				return $error;
486
-			}
487
-			$userSession->createSessionToken($request, $user->getUID(), $username, $password);
488
-
489
-			$session = $userSession->getSession();
490
-			$session->set('last-password-confirm', Server::get(ITimeFactory::class)->getTime());
491
-
492
-			// Set email for admin
493
-			if (!empty($options['adminemail'])) {
494
-				$user->setSystemEMailAddress($options['adminemail']);
495
-			}
496
-		}
497
-
498
-		return $error;
499
-	}
500
-
501
-	public static function installBackgroundJobs(): void {
502
-		$jobList = Server::get(IJobList::class);
503
-		$jobList->add(TokenCleanupJob::class);
504
-		$jobList->add(Rotate::class);
505
-		$jobList->add(BackgroundCleanupJob::class);
506
-		$jobList->add(RemoveOldTasksBackgroundJob::class);
507
-		$jobList->add(CleanupDeletedUsers::class);
508
-		$jobList->add(GenerateMetadataJob::class);
509
-		$jobList->add(MovePreviewJob::class);
510
-	}
511
-
512
-	/**
513
-	 * @return string Absolute path to htaccess
514
-	 */
515
-	private function pathToHtaccess(): string {
516
-		return \OC::$SERVERROOT . '/.htaccess';
517
-	}
518
-
519
-	/**
520
-	 * Find webroot from config
521
-	 *
522
-	 * @throws InvalidArgumentException when invalid value for overwrite.cli.url
523
-	 */
524
-	private static function findWebRoot(SystemConfig $config): string {
525
-		// For CLI read the value from overwrite.cli.url
526
-		if (\OC::$CLI) {
527
-			$webRoot = $config->getValue('overwrite.cli.url', '');
528
-			if ($webRoot === '') {
529
-				throw new InvalidArgumentException('overwrite.cli.url is empty');
530
-			}
531
-			if (!filter_var($webRoot, FILTER_VALIDATE_URL)) {
532
-				throw new InvalidArgumentException('invalid value for overwrite.cli.url');
533
-			}
534
-			$webRoot = rtrim((parse_url($webRoot, PHP_URL_PATH) ?? ''), '/');
535
-		} else {
536
-			$webRoot = !empty(\OC::$WEBROOT) ? \OC::$WEBROOT : '/';
537
-		}
538
-
539
-		return $webRoot;
540
-	}
541
-
542
-	/**
543
-	 * Append the correct ErrorDocument path for Apache hosts
544
-	 *
545
-	 * @return bool True when success, False otherwise
546
-	 * @throws \OCP\AppFramework\QueryException
547
-	 */
548
-	public static function updateHtaccess(): bool {
549
-		$config = Server::get(SystemConfig::class);
550
-
551
-		try {
552
-			$webRoot = self::findWebRoot($config);
553
-		} catch (InvalidArgumentException $e) {
554
-			return false;
555
-		}
556
-
557
-		$setupHelper = Server::get(\OC\Setup::class);
558
-
559
-		if (!is_writable($setupHelper->pathToHtaccess())) {
560
-			return false;
561
-		}
562
-
563
-		$htaccessContent = file_get_contents($setupHelper->pathToHtaccess());
564
-		$content = "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####\n";
565
-		$htaccessContent = explode($content, $htaccessContent, 2)[0];
566
-
567
-		//custom 403 error page
568
-		$content .= "\nErrorDocument 403 " . $webRoot . '/index.php/error/403';
569
-
570
-		//custom 404 error page
571
-		$content .= "\nErrorDocument 404 " . $webRoot . '/index.php/error/404';
572
-
573
-		// Add rewrite rules if the RewriteBase is configured
574
-		$rewriteBase = $config->getValue('htaccess.RewriteBase', '');
575
-		if ($rewriteBase !== '') {
576
-			$content .= "\n<IfModule mod_rewrite.c>";
577
-			$content .= "\n  Options -MultiViews";
578
-			$content .= "\n  RewriteRule ^core/js/oc.js$ index.php [PT,E=PATH_INFO:$1]";
579
-			$content .= "\n  RewriteRule ^core/preview.png$ index.php [PT,E=PATH_INFO:$1]";
580
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !\\.(css|js|mjs|svg|gif|ico|jpg|jpeg|png|webp|html|otf|ttf|woff2?|map|webm|mp4|mp3|ogg|wav|flac|wasm|tflite)$";
581
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/ajax/update\\.php";
582
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/img/(favicon\\.ico|manifest\\.json)$";
583
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(cron|public|remote|status)\\.php";
584
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/ocs/v(1|2)\\.php";
585
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/robots\\.txt";
586
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(ocs-provider|updater)/";
587
-			$content .= "\n  RewriteCond %{REQUEST_URI} !^/\\.well-known/(acme-challenge|pki-validation)/.*";
588
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/richdocumentscode(_arm64)?/proxy.php$";
589
-			$content .= "\n  RewriteRule . index.php [PT,E=PATH_INFO:$1]";
590
-			$content .= "\n  RewriteBase " . $rewriteBase;
591
-			$content .= "\n  <IfModule mod_env.c>";
592
-			$content .= "\n    SetEnv front_controller_active true";
593
-			$content .= "\n    <IfModule mod_dir.c>";
594
-			$content .= "\n      DirectorySlash off";
595
-			$content .= "\n    </IfModule>";
596
-			$content .= "\n  </IfModule>";
597
-			$content .= "\n</IfModule>";
598
-		}
599
-
600
-		// Never write file back if disk space should be too low
601
-		if (function_exists('disk_free_space')) {
602
-			$df = disk_free_space(\OC::$SERVERROOT);
603
-			$size = strlen($content) + 10240;
604
-			if ($df !== false && $df < (float)$size) {
605
-				throw new \Exception(\OC::$SERVERROOT . ' does not have enough space for writing the htaccess file! Not writing it back!');
606
-			}
607
-		}
608
-		//suppress errors in case we don't have permissions for it
609
-		return (bool)@file_put_contents($setupHelper->pathToHtaccess(), $htaccessContent . $content . "\n");
610
-	}
611
-
612
-	public static function protectDataDirectory(): void {
613
-		//Require all denied
614
-		$now = date('Y-m-d H:i:s');
615
-		$content = "# Generated by Nextcloud on $now\n";
616
-		$content .= "# Section for Apache 2.4 to 2.6\n";
617
-		$content .= "<IfModule mod_authz_core.c>\n";
618
-		$content .= "  Require all denied\n";
619
-		$content .= "</IfModule>\n";
620
-		$content .= "<IfModule mod_access_compat.c>\n";
621
-		$content .= "  Order Allow,Deny\n";
622
-		$content .= "  Deny from all\n";
623
-		$content .= "  Satisfy All\n";
624
-		$content .= "</IfModule>\n\n";
625
-		$content .= "# Section for Apache 2.2\n";
626
-		$content .= "<IfModule !mod_authz_core.c>\n";
627
-		$content .= "  <IfModule !mod_access_compat.c>\n";
628
-		$content .= "    <IfModule mod_authz_host.c>\n";
629
-		$content .= "      Order Allow,Deny\n";
630
-		$content .= "      Deny from all\n";
631
-		$content .= "    </IfModule>\n";
632
-		$content .= "    Satisfy All\n";
633
-		$content .= "  </IfModule>\n";
634
-		$content .= "</IfModule>\n\n";
635
-		$content .= "# Section for Apache 2.2 to 2.6\n";
636
-		$content .= "<IfModule mod_autoindex.c>\n";
637
-		$content .= "  IndexIgnore *\n";
638
-		$content .= '</IfModule>';
639
-
640
-		$baseDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
641
-		file_put_contents($baseDir . '/.htaccess', $content);
642
-		file_put_contents($baseDir . '/index.html', '');
643
-	}
644
-
645
-	private function getVendorData(): array {
646
-		// this should really be a JSON file
647
-		require \OC::$SERVERROOT . '/version.php';
648
-		/** @var mixed $vendor */
649
-		/** @var mixed $OC_Channel */
650
-		return [
651
-			'vendor' => (string)$vendor,
652
-			'channel' => (string)$OC_Channel,
653
-		];
654
-	}
655
-
656
-	public function shouldRemoveCanInstallFile(): bool {
657
-		return Server::get(ServerVersion::class)->getChannel() !== 'git' && is_file(\OC::$configDir . '/CAN_INSTALL');
658
-	}
659
-
660
-	public function canInstallFileExists(): bool {
661
-		return is_file(\OC::$configDir . '/CAN_INSTALL');
662
-	}
663
-
664
-	protected function outputDebug(?IOutput $output, string $message): void {
665
-		if ($output instanceof IOutput) {
666
-			$output->debug($message);
667
-		}
668
-	}
296
+        return $content !== $testContent && $fallbackContent !== $testContent;
297
+    }
298
+
299
+    /**
300
+     * @return array<string|array> errors
301
+     */
302
+    public function install(array $options, ?IOutput $output = null): array {
303
+        $l = $this->l10n;
304
+
305
+        $error = [];
306
+        $dbType = $options['dbtype'];
307
+
308
+        $disableAdminUser = (bool)($options['admindisable'] ?? false);
309
+
310
+        if (!$disableAdminUser) {
311
+            if (empty($options['adminlogin'])) {
312
+                $error[] = $l->t('Set an admin Login.');
313
+            }
314
+            if (empty($options['adminpass'])) {
315
+                $error[] = $l->t('Set an admin password.');
316
+            }
317
+        }
318
+        if (empty($options['directory'])) {
319
+            $options['directory'] = \OC::$SERVERROOT . '/data';
320
+        }
321
+
322
+        if (!isset(self::$dbSetupClasses[$dbType])) {
323
+            $dbType = 'sqlite';
324
+        }
325
+
326
+        $dataDir = htmlspecialchars_decode($options['directory']);
327
+
328
+        $class = self::$dbSetupClasses[$dbType];
329
+        /** @var \OC\Setup\AbstractDatabase $dbSetup */
330
+        $dbSetup = new $class($l, $this->config, $this->logger, $this->random);
331
+        $error = array_merge($error, $dbSetup->validate($options));
332
+
333
+        // validate the data directory
334
+        if ((!is_dir($dataDir) && !mkdir($dataDir)) || !is_writable($dataDir)) {
335
+            $error[] = $l->t('Cannot create or write into the data directory %s', [$dataDir]);
336
+        }
337
+
338
+        if (!empty($error)) {
339
+            return $error;
340
+        }
341
+
342
+        $request = Server::get(IRequest::class);
343
+
344
+        //no errors, good
345
+        if (isset($options['trusted_domains'])
346
+            && is_array($options['trusted_domains'])) {
347
+            $trustedDomains = $options['trusted_domains'];
348
+        } else {
349
+            $trustedDomains = [$request->getInsecureServerHost()];
350
+        }
351
+
352
+        //use sqlite3 when available, otherwise sqlite2 will be used.
353
+        if ($dbType === 'sqlite' && class_exists('SQLite3')) {
354
+            $dbType = 'sqlite3';
355
+        }
356
+
357
+        //generate a random salt that is used to salt the local  passwords
358
+        $salt = $this->random->generate(30);
359
+        // generate a secret
360
+        $secret = $this->random->generate(48);
361
+
362
+        //write the config file
363
+        $newConfigValues = [
364
+            'passwordsalt' => $salt,
365
+            'secret' => $secret,
366
+            'trusted_domains' => $trustedDomains,
367
+            'datadirectory' => $dataDir,
368
+            'dbtype' => $dbType,
369
+            'version' => implode('.', \OCP\Util::getVersion()),
370
+        ];
371
+
372
+        if ($this->config->getValue('overwrite.cli.url', null) === null) {
373
+            $newConfigValues['overwrite.cli.url'] = $request->getServerProtocol() . '://' . $request->getInsecureServerHost() . \OC::$WEBROOT;
374
+        }
375
+
376
+        $this->config->setValues($newConfigValues);
377
+
378
+        $this->outputDebug($output, 'Configuring database');
379
+        $dbSetup->initialize($options);
380
+        try {
381
+            $dbSetup->setupDatabase();
382
+        } catch (\OC\DatabaseSetupException $e) {
383
+            $error[] = [
384
+                'error' => $e->getMessage(),
385
+                'exception' => $e,
386
+                'hint' => $e->getHint(),
387
+            ];
388
+            return $error;
389
+        } catch (Exception $e) {
390
+            $error[] = [
391
+                'error' => 'Error while trying to create admin account: ' . $e->getMessage(),
392
+                'exception' => $e,
393
+                'hint' => '',
394
+            ];
395
+            return $error;
396
+        }
397
+
398
+        $this->outputDebug($output, 'Run server migrations');
399
+        try {
400
+            // apply necessary migrations
401
+            $dbSetup->runMigrations($output);
402
+        } catch (Exception $e) {
403
+            $error[] = [
404
+                'error' => 'Error while trying to initialise the database: ' . $e->getMessage(),
405
+                'exception' => $e,
406
+                'hint' => '',
407
+            ];
408
+            return $error;
409
+        }
410
+
411
+        $user = null;
412
+        if (!$disableAdminUser) {
413
+            $username = htmlspecialchars_decode($options['adminlogin']);
414
+            $password = htmlspecialchars_decode($options['adminpass']);
415
+            $this->outputDebug($output, 'Create admin account');
416
+
417
+            try {
418
+                $user = Server::get(IUserManager::class)->createUser($username, $password);
419
+                if (!$user) {
420
+                    $error[] = "Account <$username> could not be created.";
421
+                    return $error;
422
+                }
423
+            } catch (Exception $exception) {
424
+                $error[] = $exception->getMessage();
425
+                return $error;
426
+            }
427
+        }
428
+
429
+        $config = Server::get(IConfig::class);
430
+        $config->setAppValue('core', 'installedat', (string)microtime(true));
431
+        $appConfig = Server::get(IAppConfig::class);
432
+        $appConfig->setValueInt('core', 'lastupdatedat', time());
433
+
434
+        $vendorData = $this->getVendorData();
435
+        $config->setAppValue('core', 'vendor', $vendorData['vendor']);
436
+        if ($vendorData['channel'] !== 'stable') {
437
+            $config->setSystemValue('updater.release.channel', $vendorData['channel']);
438
+        }
439
+
440
+        $group = Server::get(IGroupManager::class)->createGroup('admin');
441
+        if ($user !== null && $group instanceof IGroup) {
442
+            $group->addUser($user);
443
+        }
444
+
445
+        // Install shipped apps and specified app bundles
446
+        $this->outputDebug($output, 'Install default apps');
447
+        $installer = Server::get(Installer::class);
448
+        $installer->installShippedApps(false, $output);
449
+
450
+        // create empty file in data dir, so we can later find
451
+        // out that this is indeed a Nextcloud data directory
452
+        $this->outputDebug($output, 'Setup data directory');
453
+        file_put_contents(
454
+            $config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata',
455
+            "# Nextcloud data directory\n# Do not change this file",
456
+        );
457
+
458
+        // Update .htaccess files
459
+        self::updateHtaccess();
460
+        self::protectDataDirectory();
461
+
462
+        $this->outputDebug($output, 'Install background jobs');
463
+        self::installBackgroundJobs();
464
+
465
+        //and we are done
466
+        $config->setSystemValue('installed', true);
467
+        if (self::shouldRemoveCanInstallFile()) {
468
+            unlink(\OC::$configDir . '/CAN_INSTALL');
469
+        }
470
+
471
+        $bootstrapCoordinator = Server::get(\OC\AppFramework\Bootstrap\Coordinator::class);
472
+        $bootstrapCoordinator->runInitialRegistration();
473
+
474
+        if (!$disableAdminUser) {
475
+            // Create a session token for the newly created user
476
+            // The token provider requires a working db, so it's not injected on setup
477
+            /** @var \OC\User\Session $userSession */
478
+            $userSession = Server::get(IUserSession::class);
479
+            $provider = Server::get(PublicKeyTokenProvider::class);
480
+            $userSession->setTokenProvider($provider);
481
+            $userSession->login($username, $password);
482
+            $user = $userSession->getUser();
483
+            if (!$user) {
484
+                $error[] = 'No account found in session.';
485
+                return $error;
486
+            }
487
+            $userSession->createSessionToken($request, $user->getUID(), $username, $password);
488
+
489
+            $session = $userSession->getSession();
490
+            $session->set('last-password-confirm', Server::get(ITimeFactory::class)->getTime());
491
+
492
+            // Set email for admin
493
+            if (!empty($options['adminemail'])) {
494
+                $user->setSystemEMailAddress($options['adminemail']);
495
+            }
496
+        }
497
+
498
+        return $error;
499
+    }
500
+
501
+    public static function installBackgroundJobs(): void {
502
+        $jobList = Server::get(IJobList::class);
503
+        $jobList->add(TokenCleanupJob::class);
504
+        $jobList->add(Rotate::class);
505
+        $jobList->add(BackgroundCleanupJob::class);
506
+        $jobList->add(RemoveOldTasksBackgroundJob::class);
507
+        $jobList->add(CleanupDeletedUsers::class);
508
+        $jobList->add(GenerateMetadataJob::class);
509
+        $jobList->add(MovePreviewJob::class);
510
+    }
511
+
512
+    /**
513
+     * @return string Absolute path to htaccess
514
+     */
515
+    private function pathToHtaccess(): string {
516
+        return \OC::$SERVERROOT . '/.htaccess';
517
+    }
518
+
519
+    /**
520
+     * Find webroot from config
521
+     *
522
+     * @throws InvalidArgumentException when invalid value for overwrite.cli.url
523
+     */
524
+    private static function findWebRoot(SystemConfig $config): string {
525
+        // For CLI read the value from overwrite.cli.url
526
+        if (\OC::$CLI) {
527
+            $webRoot = $config->getValue('overwrite.cli.url', '');
528
+            if ($webRoot === '') {
529
+                throw new InvalidArgumentException('overwrite.cli.url is empty');
530
+            }
531
+            if (!filter_var($webRoot, FILTER_VALIDATE_URL)) {
532
+                throw new InvalidArgumentException('invalid value for overwrite.cli.url');
533
+            }
534
+            $webRoot = rtrim((parse_url($webRoot, PHP_URL_PATH) ?? ''), '/');
535
+        } else {
536
+            $webRoot = !empty(\OC::$WEBROOT) ? \OC::$WEBROOT : '/';
537
+        }
538
+
539
+        return $webRoot;
540
+    }
541
+
542
+    /**
543
+     * Append the correct ErrorDocument path for Apache hosts
544
+     *
545
+     * @return bool True when success, False otherwise
546
+     * @throws \OCP\AppFramework\QueryException
547
+     */
548
+    public static function updateHtaccess(): bool {
549
+        $config = Server::get(SystemConfig::class);
550
+
551
+        try {
552
+            $webRoot = self::findWebRoot($config);
553
+        } catch (InvalidArgumentException $e) {
554
+            return false;
555
+        }
556
+
557
+        $setupHelper = Server::get(\OC\Setup::class);
558
+
559
+        if (!is_writable($setupHelper->pathToHtaccess())) {
560
+            return false;
561
+        }
562
+
563
+        $htaccessContent = file_get_contents($setupHelper->pathToHtaccess());
564
+        $content = "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####\n";
565
+        $htaccessContent = explode($content, $htaccessContent, 2)[0];
566
+
567
+        //custom 403 error page
568
+        $content .= "\nErrorDocument 403 " . $webRoot . '/index.php/error/403';
569
+
570
+        //custom 404 error page
571
+        $content .= "\nErrorDocument 404 " . $webRoot . '/index.php/error/404';
572
+
573
+        // Add rewrite rules if the RewriteBase is configured
574
+        $rewriteBase = $config->getValue('htaccess.RewriteBase', '');
575
+        if ($rewriteBase !== '') {
576
+            $content .= "\n<IfModule mod_rewrite.c>";
577
+            $content .= "\n  Options -MultiViews";
578
+            $content .= "\n  RewriteRule ^core/js/oc.js$ index.php [PT,E=PATH_INFO:$1]";
579
+            $content .= "\n  RewriteRule ^core/preview.png$ index.php [PT,E=PATH_INFO:$1]";
580
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !\\.(css|js|mjs|svg|gif|ico|jpg|jpeg|png|webp|html|otf|ttf|woff2?|map|webm|mp4|mp3|ogg|wav|flac|wasm|tflite)$";
581
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/ajax/update\\.php";
582
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/img/(favicon\\.ico|manifest\\.json)$";
583
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(cron|public|remote|status)\\.php";
584
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/ocs/v(1|2)\\.php";
585
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/robots\\.txt";
586
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(ocs-provider|updater)/";
587
+            $content .= "\n  RewriteCond %{REQUEST_URI} !^/\\.well-known/(acme-challenge|pki-validation)/.*";
588
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/richdocumentscode(_arm64)?/proxy.php$";
589
+            $content .= "\n  RewriteRule . index.php [PT,E=PATH_INFO:$1]";
590
+            $content .= "\n  RewriteBase " . $rewriteBase;
591
+            $content .= "\n  <IfModule mod_env.c>";
592
+            $content .= "\n    SetEnv front_controller_active true";
593
+            $content .= "\n    <IfModule mod_dir.c>";
594
+            $content .= "\n      DirectorySlash off";
595
+            $content .= "\n    </IfModule>";
596
+            $content .= "\n  </IfModule>";
597
+            $content .= "\n</IfModule>";
598
+        }
599
+
600
+        // Never write file back if disk space should be too low
601
+        if (function_exists('disk_free_space')) {
602
+            $df = disk_free_space(\OC::$SERVERROOT);
603
+            $size = strlen($content) + 10240;
604
+            if ($df !== false && $df < (float)$size) {
605
+                throw new \Exception(\OC::$SERVERROOT . ' does not have enough space for writing the htaccess file! Not writing it back!');
606
+            }
607
+        }
608
+        //suppress errors in case we don't have permissions for it
609
+        return (bool)@file_put_contents($setupHelper->pathToHtaccess(), $htaccessContent . $content . "\n");
610
+    }
611
+
612
+    public static function protectDataDirectory(): void {
613
+        //Require all denied
614
+        $now = date('Y-m-d H:i:s');
615
+        $content = "# Generated by Nextcloud on $now\n";
616
+        $content .= "# Section for Apache 2.4 to 2.6\n";
617
+        $content .= "<IfModule mod_authz_core.c>\n";
618
+        $content .= "  Require all denied\n";
619
+        $content .= "</IfModule>\n";
620
+        $content .= "<IfModule mod_access_compat.c>\n";
621
+        $content .= "  Order Allow,Deny\n";
622
+        $content .= "  Deny from all\n";
623
+        $content .= "  Satisfy All\n";
624
+        $content .= "</IfModule>\n\n";
625
+        $content .= "# Section for Apache 2.2\n";
626
+        $content .= "<IfModule !mod_authz_core.c>\n";
627
+        $content .= "  <IfModule !mod_access_compat.c>\n";
628
+        $content .= "    <IfModule mod_authz_host.c>\n";
629
+        $content .= "      Order Allow,Deny\n";
630
+        $content .= "      Deny from all\n";
631
+        $content .= "    </IfModule>\n";
632
+        $content .= "    Satisfy All\n";
633
+        $content .= "  </IfModule>\n";
634
+        $content .= "</IfModule>\n\n";
635
+        $content .= "# Section for Apache 2.2 to 2.6\n";
636
+        $content .= "<IfModule mod_autoindex.c>\n";
637
+        $content .= "  IndexIgnore *\n";
638
+        $content .= '</IfModule>';
639
+
640
+        $baseDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
641
+        file_put_contents($baseDir . '/.htaccess', $content);
642
+        file_put_contents($baseDir . '/index.html', '');
643
+    }
644
+
645
+    private function getVendorData(): array {
646
+        // this should really be a JSON file
647
+        require \OC::$SERVERROOT . '/version.php';
648
+        /** @var mixed $vendor */
649
+        /** @var mixed $OC_Channel */
650
+        return [
651
+            'vendor' => (string)$vendor,
652
+            'channel' => (string)$OC_Channel,
653
+        ];
654
+    }
655
+
656
+    public function shouldRemoveCanInstallFile(): bool {
657
+        return Server::get(ServerVersion::class)->getChannel() !== 'git' && is_file(\OC::$configDir . '/CAN_INSTALL');
658
+    }
659
+
660
+    public function canInstallFileExists(): bool {
661
+        return is_file(\OC::$configDir . '/CAN_INSTALL');
662
+    }
663
+
664
+    protected function outputDebug(?IOutput $output, string $message): void {
665
+        if ($output instanceof IOutput) {
666
+            $output->debug($message);
667
+        }
668
+    }
669 669
 }
Please login to merge, or discard this patch.
core/Command/Maintenance/Install.php 2 patches
Indentation   +173 added lines, -173 removed lines patch added patch discarded remove patch
@@ -26,177 +26,177 @@
 block discarded – undo
26 26
 use function get_class;
27 27
 
28 28
 class Install extends Command {
29
-	public function __construct(
30
-		private SystemConfig $config,
31
-		private IniGetWrapper $iniGetWrapper,
32
-	) {
33
-		parent::__construct();
34
-	}
35
-
36
-	protected function configure(): void {
37
-		$this
38
-			->setName('maintenance:install')
39
-			->setDescription('install Nextcloud')
40
-			->addOption('database', null, InputOption::VALUE_REQUIRED, 'Supported database type', 'sqlite')
41
-			->addOption('database-name', null, InputOption::VALUE_REQUIRED, 'Name of the database')
42
-			->addOption('database-host', null, InputOption::VALUE_REQUIRED, 'Hostname of the database', 'localhost')
43
-			->addOption('database-port', null, InputOption::VALUE_REQUIRED, 'Port the database is listening on')
44
-			->addOption('database-user', null, InputOption::VALUE_REQUIRED, 'Login to connect to the database')
45
-			->addOption('database-pass', null, InputOption::VALUE_OPTIONAL, 'Password of the database user', null)
46
-			->addOption('database-table-space', null, InputOption::VALUE_OPTIONAL, 'Table space of the database (oci only)', null)
47
-			->addOption('disable-admin-user', null, InputOption::VALUE_NONE, 'Disable the creation of an admin user')
48
-			->addOption('admin-user', null, InputOption::VALUE_REQUIRED, 'Login of the admin account', 'admin')
49
-			->addOption('admin-pass', null, InputOption::VALUE_REQUIRED, 'Password of the admin account')
50
-			->addOption('admin-email', null, InputOption::VALUE_OPTIONAL, 'E-Mail of the admin account')
51
-			->addOption('data-dir', null, InputOption::VALUE_REQUIRED, 'Path to data directory', \OC::$SERVERROOT . '/data');
52
-	}
53
-
54
-	protected function execute(InputInterface $input, OutputInterface $output): int {
55
-		// validate the environment
56
-		$setupHelper = Server::get(Setup::class);
57
-		$sysInfo = $setupHelper->getSystemInfo(true);
58
-		$errors = $sysInfo['errors'];
59
-		if (count($errors) > 0) {
60
-			$this->printErrors($output, $errors);
61
-			return 1;
62
-		}
63
-
64
-		// validate user input
65
-		$options = $this->validateInput($input, $output, array_keys($sysInfo['databases']));
66
-
67
-		if ($output->isVerbose()) {
68
-			// Prepend each line with a little timestamp
69
-			$timestampFormatter = new TimestampFormatter(null, $output->getFormatter());
70
-			$output->setFormatter($timestampFormatter);
71
-			$migrationOutput = new ConsoleOutput($output);
72
-		} else {
73
-			$migrationOutput = null;
74
-		}
75
-
76
-		// perform installation
77
-		$errors = $setupHelper->install($options, $migrationOutput);
78
-		if (count($errors) > 0) {
79
-			$this->printErrors($output, $errors);
80
-			return 1;
81
-		}
82
-		if ($setupHelper->shouldRemoveCanInstallFile()) {
83
-			$output->writeln('<warn>Could not remove CAN_INSTALL from the config folder. Please remove this file manually.</warn>');
84
-		}
85
-		$output->writeln('Nextcloud was successfully installed');
86
-		return 0;
87
-	}
88
-
89
-	/**
90
-	 * @param InputInterface $input
91
-	 * @param OutputInterface $output
92
-	 * @param string[] $supportedDatabases
93
-	 * @return array
94
-	 */
95
-	protected function validateInput(InputInterface $input, OutputInterface $output, $supportedDatabases) {
96
-		$db = strtolower($input->getOption('database'));
97
-
98
-		if (!in_array($db, $supportedDatabases)) {
99
-			throw new InvalidArgumentException("Database <$db> is not supported. " . implode(', ', $supportedDatabases) . ' are supported.');
100
-		}
101
-
102
-		$dbUser = $input->getOption('database-user');
103
-		$dbPass = $input->getOption('database-pass');
104
-		$dbName = $input->getOption('database-name');
105
-		$dbPort = $input->getOption('database-port');
106
-		if ($db === 'oci') {
107
-			// an empty hostname needs to be read from the raw parameters
108
-			$dbHost = $input->getParameterOption('--database-host', '');
109
-		} else {
110
-			$dbHost = $input->getOption('database-host');
111
-		}
112
-		if ($dbPort) {
113
-			// Append the port to the host so it is the same as in the config (there is no dbport config)
114
-			$dbHost .= ':' . $dbPort;
115
-		}
116
-		if ($input->hasParameterOption('--database-pass')) {
117
-			$dbPass = (string)$input->getOption('database-pass');
118
-		}
119
-		$disableAdminUser = (bool)$input->getOption('disable-admin-user');
120
-		$adminLogin = $input->getOption('admin-user');
121
-		$adminPassword = $input->getOption('admin-pass');
122
-		$adminEmail = $input->getOption('admin-email');
123
-		$dataDir = $input->getOption('data-dir');
124
-
125
-		if ($db !== 'sqlite') {
126
-			if (is_null($dbUser)) {
127
-				throw new InvalidArgumentException('Database account not provided.');
128
-			}
129
-			if (is_null($dbName)) {
130
-				throw new InvalidArgumentException('Database name not provided.');
131
-			}
132
-			if (is_null($dbPass)) {
133
-				/** @var QuestionHelper $helper */
134
-				$helper = $this->getHelper('question');
135
-				$question = new Question('What is the password to access the database with user <' . $dbUser . '>?');
136
-				$question->setHidden(true);
137
-				$question->setHiddenFallback(false);
138
-				$dbPass = $helper->ask($input, $output, $question);
139
-			}
140
-		}
141
-
142
-		if (!$disableAdminUser && $adminPassword === null) {
143
-			/** @var QuestionHelper $helper */
144
-			$helper = $this->getHelper('question');
145
-			$question = new Question('What is the password you like to use for the admin account <' . $adminLogin . '>?');
146
-			$question->setHidden(true);
147
-			$question->setHiddenFallback(false);
148
-			$adminPassword = $helper->ask($input, $output, $question);
149
-		}
150
-
151
-		if (!$disableAdminUser && $adminEmail !== null && !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
152
-			throw new InvalidArgumentException('Invalid e-mail-address <' . $adminEmail . '> for <' . $adminLogin . '>.');
153
-		}
154
-
155
-		$options = [
156
-			'dbtype' => $db,
157
-			'dbuser' => $dbUser,
158
-			'dbpass' => $dbPass,
159
-			'dbname' => $dbName,
160
-			'dbhost' => $dbHost,
161
-			'admindisable' => $disableAdminUser,
162
-			'adminlogin' => $adminLogin,
163
-			'adminpass' => $adminPassword,
164
-			'adminemail' => $adminEmail,
165
-			'directory' => $dataDir
166
-		];
167
-		if ($db === 'oci') {
168
-			$options['dbtablespace'] = $input->getParameterOption('--database-table-space', '');
169
-		}
170
-		return $options;
171
-	}
172
-
173
-	/**
174
-	 * @param OutputInterface $output
175
-	 * @param array<string|array> $errors
176
-	 */
177
-	protected function printErrors(OutputInterface $output, array $errors): void {
178
-		foreach ($errors as $error) {
179
-			if (is_array($error)) {
180
-				$output->writeln('<error>' . $error['error'] . '</error>');
181
-				if (isset($error['hint']) && !empty($error['hint'])) {
182
-					$output->writeln('<info> -> ' . $error['hint'] . '</info>');
183
-				}
184
-				if (isset($error['exception']) && $error['exception'] instanceof Throwable) {
185
-					$this->printThrowable($output, $error['exception']);
186
-				}
187
-			} else {
188
-				$output->writeln('<error>' . $error . '</error>');
189
-			}
190
-		}
191
-	}
192
-
193
-	private function printThrowable(OutputInterface $output, Throwable $t): void {
194
-		$output->write('<info>Trace: ' . $t->getTraceAsString() . '</info>');
195
-		$output->writeln('');
196
-		if ($t->getPrevious() !== null) {
197
-			$output->writeln('');
198
-			$output->writeln('<info>Previous: ' . get_class($t->getPrevious()) . ': ' . $t->getPrevious()->getMessage() . '</info>');
199
-			$this->printThrowable($output, $t->getPrevious());
200
-		}
201
-	}
29
+    public function __construct(
30
+        private SystemConfig $config,
31
+        private IniGetWrapper $iniGetWrapper,
32
+    ) {
33
+        parent::__construct();
34
+    }
35
+
36
+    protected function configure(): void {
37
+        $this
38
+            ->setName('maintenance:install')
39
+            ->setDescription('install Nextcloud')
40
+            ->addOption('database', null, InputOption::VALUE_REQUIRED, 'Supported database type', 'sqlite')
41
+            ->addOption('database-name', null, InputOption::VALUE_REQUIRED, 'Name of the database')
42
+            ->addOption('database-host', null, InputOption::VALUE_REQUIRED, 'Hostname of the database', 'localhost')
43
+            ->addOption('database-port', null, InputOption::VALUE_REQUIRED, 'Port the database is listening on')
44
+            ->addOption('database-user', null, InputOption::VALUE_REQUIRED, 'Login to connect to the database')
45
+            ->addOption('database-pass', null, InputOption::VALUE_OPTIONAL, 'Password of the database user', null)
46
+            ->addOption('database-table-space', null, InputOption::VALUE_OPTIONAL, 'Table space of the database (oci only)', null)
47
+            ->addOption('disable-admin-user', null, InputOption::VALUE_NONE, 'Disable the creation of an admin user')
48
+            ->addOption('admin-user', null, InputOption::VALUE_REQUIRED, 'Login of the admin account', 'admin')
49
+            ->addOption('admin-pass', null, InputOption::VALUE_REQUIRED, 'Password of the admin account')
50
+            ->addOption('admin-email', null, InputOption::VALUE_OPTIONAL, 'E-Mail of the admin account')
51
+            ->addOption('data-dir', null, InputOption::VALUE_REQUIRED, 'Path to data directory', \OC::$SERVERROOT . '/data');
52
+    }
53
+
54
+    protected function execute(InputInterface $input, OutputInterface $output): int {
55
+        // validate the environment
56
+        $setupHelper = Server::get(Setup::class);
57
+        $sysInfo = $setupHelper->getSystemInfo(true);
58
+        $errors = $sysInfo['errors'];
59
+        if (count($errors) > 0) {
60
+            $this->printErrors($output, $errors);
61
+            return 1;
62
+        }
63
+
64
+        // validate user input
65
+        $options = $this->validateInput($input, $output, array_keys($sysInfo['databases']));
66
+
67
+        if ($output->isVerbose()) {
68
+            // Prepend each line with a little timestamp
69
+            $timestampFormatter = new TimestampFormatter(null, $output->getFormatter());
70
+            $output->setFormatter($timestampFormatter);
71
+            $migrationOutput = new ConsoleOutput($output);
72
+        } else {
73
+            $migrationOutput = null;
74
+        }
75
+
76
+        // perform installation
77
+        $errors = $setupHelper->install($options, $migrationOutput);
78
+        if (count($errors) > 0) {
79
+            $this->printErrors($output, $errors);
80
+            return 1;
81
+        }
82
+        if ($setupHelper->shouldRemoveCanInstallFile()) {
83
+            $output->writeln('<warn>Could not remove CAN_INSTALL from the config folder. Please remove this file manually.</warn>');
84
+        }
85
+        $output->writeln('Nextcloud was successfully installed');
86
+        return 0;
87
+    }
88
+
89
+    /**
90
+     * @param InputInterface $input
91
+     * @param OutputInterface $output
92
+     * @param string[] $supportedDatabases
93
+     * @return array
94
+     */
95
+    protected function validateInput(InputInterface $input, OutputInterface $output, $supportedDatabases) {
96
+        $db = strtolower($input->getOption('database'));
97
+
98
+        if (!in_array($db, $supportedDatabases)) {
99
+            throw new InvalidArgumentException("Database <$db> is not supported. " . implode(', ', $supportedDatabases) . ' are supported.');
100
+        }
101
+
102
+        $dbUser = $input->getOption('database-user');
103
+        $dbPass = $input->getOption('database-pass');
104
+        $dbName = $input->getOption('database-name');
105
+        $dbPort = $input->getOption('database-port');
106
+        if ($db === 'oci') {
107
+            // an empty hostname needs to be read from the raw parameters
108
+            $dbHost = $input->getParameterOption('--database-host', '');
109
+        } else {
110
+            $dbHost = $input->getOption('database-host');
111
+        }
112
+        if ($dbPort) {
113
+            // Append the port to the host so it is the same as in the config (there is no dbport config)
114
+            $dbHost .= ':' . $dbPort;
115
+        }
116
+        if ($input->hasParameterOption('--database-pass')) {
117
+            $dbPass = (string)$input->getOption('database-pass');
118
+        }
119
+        $disableAdminUser = (bool)$input->getOption('disable-admin-user');
120
+        $adminLogin = $input->getOption('admin-user');
121
+        $adminPassword = $input->getOption('admin-pass');
122
+        $adminEmail = $input->getOption('admin-email');
123
+        $dataDir = $input->getOption('data-dir');
124
+
125
+        if ($db !== 'sqlite') {
126
+            if (is_null($dbUser)) {
127
+                throw new InvalidArgumentException('Database account not provided.');
128
+            }
129
+            if (is_null($dbName)) {
130
+                throw new InvalidArgumentException('Database name not provided.');
131
+            }
132
+            if (is_null($dbPass)) {
133
+                /** @var QuestionHelper $helper */
134
+                $helper = $this->getHelper('question');
135
+                $question = new Question('What is the password to access the database with user <' . $dbUser . '>?');
136
+                $question->setHidden(true);
137
+                $question->setHiddenFallback(false);
138
+                $dbPass = $helper->ask($input, $output, $question);
139
+            }
140
+        }
141
+
142
+        if (!$disableAdminUser && $adminPassword === null) {
143
+            /** @var QuestionHelper $helper */
144
+            $helper = $this->getHelper('question');
145
+            $question = new Question('What is the password you like to use for the admin account <' . $adminLogin . '>?');
146
+            $question->setHidden(true);
147
+            $question->setHiddenFallback(false);
148
+            $adminPassword = $helper->ask($input, $output, $question);
149
+        }
150
+
151
+        if (!$disableAdminUser && $adminEmail !== null && !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
152
+            throw new InvalidArgumentException('Invalid e-mail-address <' . $adminEmail . '> for <' . $adminLogin . '>.');
153
+        }
154
+
155
+        $options = [
156
+            'dbtype' => $db,
157
+            'dbuser' => $dbUser,
158
+            'dbpass' => $dbPass,
159
+            'dbname' => $dbName,
160
+            'dbhost' => $dbHost,
161
+            'admindisable' => $disableAdminUser,
162
+            'adminlogin' => $adminLogin,
163
+            'adminpass' => $adminPassword,
164
+            'adminemail' => $adminEmail,
165
+            'directory' => $dataDir
166
+        ];
167
+        if ($db === 'oci') {
168
+            $options['dbtablespace'] = $input->getParameterOption('--database-table-space', '');
169
+        }
170
+        return $options;
171
+    }
172
+
173
+    /**
174
+     * @param OutputInterface $output
175
+     * @param array<string|array> $errors
176
+     */
177
+    protected function printErrors(OutputInterface $output, array $errors): void {
178
+        foreach ($errors as $error) {
179
+            if (is_array($error)) {
180
+                $output->writeln('<error>' . $error['error'] . '</error>');
181
+                if (isset($error['hint']) && !empty($error['hint'])) {
182
+                    $output->writeln('<info> -> ' . $error['hint'] . '</info>');
183
+                }
184
+                if (isset($error['exception']) && $error['exception'] instanceof Throwable) {
185
+                    $this->printThrowable($output, $error['exception']);
186
+                }
187
+            } else {
188
+                $output->writeln('<error>' . $error . '</error>');
189
+            }
190
+        }
191
+    }
192
+
193
+    private function printThrowable(OutputInterface $output, Throwable $t): void {
194
+        $output->write('<info>Trace: ' . $t->getTraceAsString() . '</info>');
195
+        $output->writeln('');
196
+        if ($t->getPrevious() !== null) {
197
+            $output->writeln('');
198
+            $output->writeln('<info>Previous: ' . get_class($t->getPrevious()) . ': ' . $t->getPrevious()->getMessage() . '</info>');
199
+            $this->printThrowable($output, $t->getPrevious());
200
+        }
201
+    }
202 202
 }
Please login to merge, or discard this patch.
Spacing   +13 added lines, -13 removed lines patch added patch discarded remove patch
@@ -48,7 +48,7 @@  discard block
 block discarded – undo
48 48
 			->addOption('admin-user', null, InputOption::VALUE_REQUIRED, 'Login of the admin account', 'admin')
49 49
 			->addOption('admin-pass', null, InputOption::VALUE_REQUIRED, 'Password of the admin account')
50 50
 			->addOption('admin-email', null, InputOption::VALUE_OPTIONAL, 'E-Mail of the admin account')
51
-			->addOption('data-dir', null, InputOption::VALUE_REQUIRED, 'Path to data directory', \OC::$SERVERROOT . '/data');
51
+			->addOption('data-dir', null, InputOption::VALUE_REQUIRED, 'Path to data directory', \OC::$SERVERROOT.'/data');
52 52
 	}
53 53
 
54 54
 	protected function execute(InputInterface $input, OutputInterface $output): int {
@@ -96,7 +96,7 @@  discard block
 block discarded – undo
96 96
 		$db = strtolower($input->getOption('database'));
97 97
 
98 98
 		if (!in_array($db, $supportedDatabases)) {
99
-			throw new InvalidArgumentException("Database <$db> is not supported. " . implode(', ', $supportedDatabases) . ' are supported.');
99
+			throw new InvalidArgumentException("Database <$db> is not supported. ".implode(', ', $supportedDatabases).' are supported.');
100 100
 		}
101 101
 
102 102
 		$dbUser = $input->getOption('database-user');
@@ -111,12 +111,12 @@  discard block
 block discarded – undo
111 111
 		}
112 112
 		if ($dbPort) {
113 113
 			// Append the port to the host so it is the same as in the config (there is no dbport config)
114
-			$dbHost .= ':' . $dbPort;
114
+			$dbHost .= ':'.$dbPort;
115 115
 		}
116 116
 		if ($input->hasParameterOption('--database-pass')) {
117
-			$dbPass = (string)$input->getOption('database-pass');
117
+			$dbPass = (string) $input->getOption('database-pass');
118 118
 		}
119
-		$disableAdminUser = (bool)$input->getOption('disable-admin-user');
119
+		$disableAdminUser = (bool) $input->getOption('disable-admin-user');
120 120
 		$adminLogin = $input->getOption('admin-user');
121 121
 		$adminPassword = $input->getOption('admin-pass');
122 122
 		$adminEmail = $input->getOption('admin-email');
@@ -132,7 +132,7 @@  discard block
 block discarded – undo
132 132
 			if (is_null($dbPass)) {
133 133
 				/** @var QuestionHelper $helper */
134 134
 				$helper = $this->getHelper('question');
135
-				$question = new Question('What is the password to access the database with user <' . $dbUser . '>?');
135
+				$question = new Question('What is the password to access the database with user <'.$dbUser.'>?');
136 136
 				$question->setHidden(true);
137 137
 				$question->setHiddenFallback(false);
138 138
 				$dbPass = $helper->ask($input, $output, $question);
@@ -142,14 +142,14 @@  discard block
 block discarded – undo
142 142
 		if (!$disableAdminUser && $adminPassword === null) {
143 143
 			/** @var QuestionHelper $helper */
144 144
 			$helper = $this->getHelper('question');
145
-			$question = new Question('What is the password you like to use for the admin account <' . $adminLogin . '>?');
145
+			$question = new Question('What is the password you like to use for the admin account <'.$adminLogin.'>?');
146 146
 			$question->setHidden(true);
147 147
 			$question->setHiddenFallback(false);
148 148
 			$adminPassword = $helper->ask($input, $output, $question);
149 149
 		}
150 150
 
151 151
 		if (!$disableAdminUser && $adminEmail !== null && !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
152
-			throw new InvalidArgumentException('Invalid e-mail-address <' . $adminEmail . '> for <' . $adminLogin . '>.');
152
+			throw new InvalidArgumentException('Invalid e-mail-address <'.$adminEmail.'> for <'.$adminLogin.'>.');
153 153
 		}
154 154
 
155 155
 		$options = [
@@ -177,25 +177,25 @@  discard block
 block discarded – undo
177 177
 	protected function printErrors(OutputInterface $output, array $errors): void {
178 178
 		foreach ($errors as $error) {
179 179
 			if (is_array($error)) {
180
-				$output->writeln('<error>' . $error['error'] . '</error>');
180
+				$output->writeln('<error>'.$error['error'].'</error>');
181 181
 				if (isset($error['hint']) && !empty($error['hint'])) {
182
-					$output->writeln('<info> -> ' . $error['hint'] . '</info>');
182
+					$output->writeln('<info> -> '.$error['hint'].'</info>');
183 183
 				}
184 184
 				if (isset($error['exception']) && $error['exception'] instanceof Throwable) {
185 185
 					$this->printThrowable($output, $error['exception']);
186 186
 				}
187 187
 			} else {
188
-				$output->writeln('<error>' . $error . '</error>');
188
+				$output->writeln('<error>'.$error.'</error>');
189 189
 			}
190 190
 		}
191 191
 	}
192 192
 
193 193
 	private function printThrowable(OutputInterface $output, Throwable $t): void {
194
-		$output->write('<info>Trace: ' . $t->getTraceAsString() . '</info>');
194
+		$output->write('<info>Trace: '.$t->getTraceAsString().'</info>');
195 195
 		$output->writeln('');
196 196
 		if ($t->getPrevious() !== null) {
197 197
 			$output->writeln('');
198
-			$output->writeln('<info>Previous: ' . get_class($t->getPrevious()) . ': ' . $t->getPrevious()->getMessage() . '</info>');
198
+			$output->writeln('<info>Previous: '.get_class($t->getPrevious()).': '.$t->getPrevious()->getMessage().'</info>');
199 199
 			$this->printThrowable($output, $t->getPrevious());
200 200
 		}
201 201
 	}
Please login to merge, or discard this patch.