Completed
Push — master ( 3b689e...b0b1f1 )
by Thomas
09:27
created

Setup::protectDataDirectory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 17
nc 1
nop 0
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Administrator <Administrator@WINDOWS-2012>
4
 * @author Arthur Schiwon <[email protected]>
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bernhard Posselt <[email protected]>
7
 * @author Brice Maron <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author François Kubler <[email protected]>
10
 * @author Jakob Sack <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Martin Mattel <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Sean Comeau <[email protected]>
18
 * @author Serge Martin <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Vincent Petry <[email protected]>
21
 *
22
 * @copyright Copyright (c) 2016, ownCloud GmbH.
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
39
namespace OC;
40
41
use bantu\IniGetWrapper\IniGetWrapper;
42
use Exception;
43
use OCP\IConfig;
44
use OCP\IL10N;
45
use OCP\ILogger;
46
use OCP\Security\ISecureRandom;
47
48
class Setup {
49
	/** @var \OCP\IConfig */
50
	protected $config;
51
	/** @var IniGetWrapper */
52
	protected $iniWrapper;
53
	/** @var IL10N */
54
	protected $l10n;
55
	/** @var \OC_Defaults */
56
	protected $defaults;
57
	/** @var ILogger */
58
	protected $logger;
59
	/** @var ISecureRandom */
60
	protected $random;
61
62
	/**
63
	 * @param IConfig $config
64
	 * @param IniGetWrapper $iniWrapper
65
	 * @param \OC_Defaults $defaults
66
	 */
67
	function __construct(IConfig $config,
68
						 IniGetWrapper $iniWrapper,
69
						 IL10N $l10n,
70
						 \OC_Defaults $defaults,
71
						 ILogger $logger,
72
						 ISecureRandom $random
73
		) {
74
		$this->config = $config;
75
		$this->iniWrapper = $iniWrapper;
76
		$this->l10n = $l10n;
77
		$this->defaults = $defaults;
78
		$this->logger = $logger;
79
		$this->random = $random;
80
	}
81
82
	static $dbSetupClasses = [
83
		'mysql' => '\OC\Setup\MySQL',
84
		'pgsql' => '\OC\Setup\PostgreSQL',
85
		'oci'   => '\OC\Setup\OCI',
86
		'sqlite' => '\OC\Setup\Sqlite',
87
		'sqlite3' => '\OC\Setup\Sqlite',
88
	];
89
90
	/**
91
	 * Wrapper around the "class_exists" PHP function to be able to mock it
92
	 * @param string $name
93
	 * @return bool
94
	 */
95
	protected function IsClassExisting($name) {
96
		return class_exists($name);
97
	}
98
99
	/**
100
	 * Wrapper around the "is_callable" PHP function to be able to mock it
101
	 * @param string $name
102
	 * @return bool
103
	 */
104
	protected function is_callable($name) {
105
		return is_callable($name);
106
	}
107
108
	/**
109
	 * Wrapper around \PDO::getAvailableDrivers
110
	 *
111
	 * @return array
112
	 */
113
	protected function getAvailableDbDriversForPdo() {
114
		return \PDO::getAvailableDrivers();
115
	}
116
117
	/**
118
	 * Get the available and supported databases of this instance
119
	 *
120
	 * @param bool $allowAllDatabases
121
	 * @return array
122
	 * @throws Exception
123
	 */
124
	public function getSupportedDatabases($allowAllDatabases = false) {
125
		$availableDatabases = [
126
			'sqlite' =>  [
127
				'type' => 'class',
128
				'call' => 'SQLite3',
129
				'name' => 'SQLite'
130
			],
131
			'mysql' => [
132
				'type' => 'pdo',
133
				'call' => 'mysql',
134
				'name' => 'MySQL/MariaDB'
135
			],
136
			'pgsql' => [
137
				'type' => 'function',
138
				'call' => 'pg_connect',
139
				'name' => 'PostgreSQL'
140
			],
141
			'oci' => [
142
				'type' => 'function',
143
				'call' => 'oci_connect',
144
				'name' => 'Oracle'
145
			]
146
		];
147
		if ($allowAllDatabases) {
148
			$configuredDatabases = array_keys($availableDatabases);
149
		} else {
150
			$configuredDatabases = $this->config->getSystemValue('supportedDatabases',
151
				['sqlite', 'mysql', 'pgsql']);
152
		}
153
		if(!is_array($configuredDatabases)) {
154
			throw new Exception('Supported databases are not properly configured.');
155
		}
156
157
		$supportedDatabases = [];
158
159
		foreach($configuredDatabases as $database) {
160
			if(array_key_exists($database, $availableDatabases)) {
161
				$working = false;
162
				$type = $availableDatabases[$database]['type'];
163
				$call = $availableDatabases[$database]['call'];
164
165
				if($type === 'class') {
166
					$working = $this->IsClassExisting($call);
167
				} elseif ($type === 'function') {
168
					$working = $this->is_callable($call);
169
				} elseif($type === 'pdo') {
170
					$working = in_array($call, $this->getAvailableDbDriversForPdo(), TRUE);
171
				}
172
				if($working) {
173
					$supportedDatabases[$database] = $availableDatabases[$database]['name'];
174
				}
175
			}
176
		}
177
178
		return $supportedDatabases;
179
	}
180
181
	/**
182
	 * Gathers system information like database type and does
183
	 * a few system checks.
184
	 *
185
	 * @return array of system info, including an "errors" value
186
	 * in case of errors/warnings
187
	 */
188
	public function getSystemInfo($allowAllDatabases = false) {
189
		$databases = $this->getSupportedDatabases($allowAllDatabases);
190
191
		$dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data');
192
193
		$errors = [];
194
195
		// Create data directory to test whether the .htaccess works
196
		// Notice that this is not necessarily the same data directory as the one
197
		// that will effectively be used.
198
		if(!file_exists($dataDir)) {
199
			@mkdir($dataDir);
200
		}
201
		$htAccessWorking = true;
202
		if (is_dir($dataDir) && is_writable($dataDir)) {
203
			// Protect data directory here, so we can test if the protection is working
204
			\OC\Setup::protectDataDirectory();
205
206
			try {
207
				$util = new \OC_Util();
208
				$htAccessWorking = $util->isHtaccessWorking(\OC::$server->getConfig());
209
			} catch (\OC\HintException $e) {
210
				$errors[] = [
211
					'error' => $e->getMessage(),
212
					'hint' => $e->getHint()
213
				];
214
				$htAccessWorking = false;
215
			}
216
		}
217
218
		if (\OC_Util::runningOnMac()) {
219
			$errors[] = [
220
				'error' => $this->l10n->t(
221
					'Mac OS X is not supported and %s will not work properly on this platform. ' .
222
					'Use it at your own risk! ',
223
					$this->defaults->getName()
0 ignored issues
show
Documentation introduced by
$this->defaults->getName() is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
224
				),
225
				'hint' => $this->l10n->t('For the best results, please consider using a GNU/Linux server instead.')
226
			];
227
		}
228
229
		if($this->iniWrapper->getString('open_basedir') !== '' && PHP_INT_SIZE === 4) {
230
			$errors[] = [
231
				'error' => $this->l10n->t(
232
					'It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. ' .
233
					'This will lead to problems with files over 4 GB and is highly discouraged.',
234
					$this->defaults->getName()
0 ignored issues
show
Documentation introduced by
$this->defaults->getName() is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
235
				),
236
				'hint' => $this->l10n->t('Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP.')
237
			];
238
		}
239
240
		return [
241
			'hasSQLite' => isset($databases['sqlite']),
242
			'hasMySQL' => isset($databases['mysql']),
243
			'hasPostgreSQL' => isset($databases['pgsql']),
244
			'hasOracle' => isset($databases['oci']),
245
			'databases' => $databases,
246
			'directory' => $dataDir,
247
			'htaccessWorking' => $htAccessWorking,
248
			'errors' => $errors,
249
		];
250
	}
251
252
	/**
253
	 * @param $options
254
	 * @return array
255
	 */
256
	public function install($options) {
257
		$l = $this->l10n;
258
259
		$error = [];
260
		$dbType = $options['dbtype'];
261
262
		if(empty($options['adminlogin'])) {
263
			$error[] = $l->t('Set an admin username.');
264
		}
265
		if(empty($options['adminpass'])) {
266
			$error[] = $l->t('Set an admin password.');
267
		}
268
		if(empty($options['directory'])) {
269
			$options['directory'] = \OC::$SERVERROOT."/data";
270
		}
271
272
		if (!isset(self::$dbSetupClasses[$dbType])) {
273
			$dbType = 'sqlite';
274
		}
275
276
		$username = htmlspecialchars_decode($options['adminlogin']);
277
		$password = htmlspecialchars_decode($options['adminpass']);
278
		$dataDir = htmlspecialchars_decode($options['directory']);
279
280
		$class = self::$dbSetupClasses[$dbType];
281
		/** @var \OC\Setup\AbstractDatabase $dbSetup */
282
		$dbSetup = new $class($l, 'db_structure.xml', $this->config,
283
			$this->logger, $this->random);
284
		$error = array_merge($error, $dbSetup->validate($options));
285
286
		// validate the data directory
287
		if (
288
			(!is_dir($dataDir) and !mkdir($dataDir)) or
289
			!is_writable($dataDir)
290
		) {
291
			$error[] = $l->t("Can't create or write into the data directory %s", [$dataDir]);
292
		}
293
294
		if(count($error) != 0) {
295
			return $error;
296
		}
297
298
		$request = \OC::$server->getRequest();
299
300
		//no errors, good
301
		if(isset($options['trusted_domains'])
302
		    && is_array($options['trusted_domains'])) {
303
			$trustedDomains = $options['trusted_domains'];
304
		} else {
305
			$trustedDomains = [$request->getInsecureServerHost()];
306
		}
307
308
		//use sqlite3 when available, otherwise sqlite2 will be used.
309
		if($dbType=='sqlite' and $this->IsClassExisting('SQLite3')) {
310
			$dbType='sqlite3';
311
		}
312
313
		//generate a random salt that is used to salt the local user passwords
314
		$salt = $this->random->generate(30);
315
		// generate a secret
316
		$secret = $this->random->generate(48);
317
318
		//write the config file
319
		$this->config->setSystemValues([
320
			'passwordsalt'		=> $salt,
321
			'secret'			=> $secret,
322
			'trusted_domains'	=> $trustedDomains,
323
			'datadirectory'		=> $dataDir,
324
			'overwrite.cli.url'	=> $request->getServerProtocol() . '://' . $request->getInsecureServerHost() . \OC::$WEBROOT,
325
			'dbtype'			=> $dbType,
326
			'version'			=> implode('.', \OCP\Util::getVersion()),
327
		]);
328
329
		try {
330
			$dbSetup->initialize($options);
331
			$dbSetup->setupDatabase($username);
332
		} catch (\OC\DatabaseSetupException $e) {
333
			$error[] = [
334
				'error' => $e->getMessage(),
335
				'hint' => $e->getHint()
336
			];
337
			return($error);
338
		} catch (Exception $e) {
339
			$error[] = [
340
				'error' => 'Error while trying to create admin user: ' . $e->getMessage(),
341
				'hint' => ''
342
			];
343
			return($error);
344
		}
345
346
		//create the user and group
347
		$user =  null;
348
		try {
349
			$user = \OC::$server->getUserManager()->createUser($username, $password);
350
			if (!$user) {
351
				$error[] = "User <$username> could not be created.";
352
			}
353
		} catch(Exception $exception) {
354
			$error[] = $exception->getMessage();
355
		}
356
357
		if(count($error) == 0) {
358
			$config = \OC::$server->getConfig();
359
			$config->setAppValue('core', 'installedat', microtime(true));
360
			$config->setAppValue('core', 'lastupdatedat', microtime(true));
361
362
			$group =\OC::$server->getGroupManager()->createGroup('admin');
363
			$group->addUser($user);
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \OC::$server->getUserMan...r($username, $password) on line 349 can also be of type false; however, OC\Group\Group::addUser() does only seem to accept object<OC\User\User>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
364
365
			//guess what this does
366
			Installer::installShippedApps();
367
368
			// create empty file in data dir, so we can later find
369
			// out that this is indeed an ownCloud data directory
370
			file_put_contents($config->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data').'/.ocdata', '');
371
372
			// Update .htaccess files
373
			Setup::updateHtaccess();
374
			Setup::protectDataDirectory();
375
376
			//try to write logtimezone
377
			if (date_default_timezone_get()) {
378
				$config->setSystemValue('logtimezone', date_default_timezone_get());
379
			}
380
381
			self::installBackgroundJobs();
382
383
			//and we are done
384
			$config->setSystemValue('installed', true);
385
386
			// Create a session token for the newly created user
387
			// The token provider requires a working db, so it's not injected on setup
388
			/* @var $userSession User\Session */
389
			$userSession = \OC::$server->getUserSession();
390
			$defaultTokenProvider = \OC::$server->query('OC\Authentication\Token\DefaultTokenProvider');
391
			$userSession->setTokenProvider($defaultTokenProvider);
392
			$userSession->login($username, $password);
393
			$userSession->createSessionToken($request, $userSession->getUser()->getUID(), $username, $password);
394
		}
395
396
		return $error;
397
	}
398
399
	public static function installBackgroundJobs() {
400
		\OC::$server->getJobList()->add('\OC\Authentication\Token\DefaultTokenCleanupJob');
401
	}
402
403
	/**
404
	 * @return string Absolute path to htaccess
405
	 */
406
	private function pathToHtaccess() {
407
		return \OC::$SERVERROOT.'/.htaccess';
408
	}
409
410
	/**
411
	 * Append the correct ErrorDocument path for Apache hosts
412
	 */
413
	public static function updateHtaccess() {
414
		$config = \OC::$server->getConfig();
415
416
		// For CLI read the value from overwrite.cli.url
417
		if(\OC::$CLI) {
418
			$webRoot = $config->getSystemValue('overwrite.cli.url', '');
419
			if($webRoot === '') {
420
				return;
421
			}
422
			$webRoot = parse_url($webRoot, PHP_URL_PATH);
423
			$webRoot = rtrim($webRoot, '/');
424
		} else {
425
			$webRoot = !empty(\OC::$WEBROOT) ? \OC::$WEBROOT : '/';
426
		}
427
428
		$setupHelper = new \OC\Setup($config, \OC::$server->getIniWrapper(),
429
			\OC::$server->getL10N('lib'), new \OC_Defaults(), \OC::$server->getLogger(),
430
			\OC::$server->getSecureRandom());
431
432
		$htaccessContent = file_get_contents($setupHelper->pathToHtaccess());
433
		$content = "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####\n";
434
		$htaccessContent = explode($content, $htaccessContent, 2)[0];
435
436
		//custom 403 error page
437
		$content.= "\nErrorDocument 403 ".$webRoot."/core/templates/403.php";
438
439
		//custom 404 error page
440
		$content.= "\nErrorDocument 404 ".$webRoot."/core/templates/404.php";
441
442
		// Add rewrite rules if the RewriteBase is configured
443
		$rewriteBase = $config->getSystemValue('htaccess.RewriteBase', '');
444
		if($rewriteBase !== '') {
445
			$content .= "\n<IfModule mod_rewrite.c>";
446
			$content .= "\n  Options -MultiViews";
447
			$content .= "\n  RewriteRule ^core/js/oc.js$ index.php [PT,E=PATH_INFO:$1]";
448
			$content .= "\n  RewriteRule ^core/preview.png$ index.php [PT,E=PATH_INFO:$1]";
449
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !\\.(css|js|svg|gif|png|html|ttf|woff|ico|jpg|jpeg)$";
450
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !core/img/favicon.ico$";
451
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/remote.php";
452
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/public.php";
453
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/cron.php";
454
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/ajax/update.php";
455
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/status.php";
456
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/ocs/v1.php";
457
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/ocs/v2.php";
458
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/updater/";
459
			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/ocs-provider/";
460
			$content .= "\n  RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/.*";
461
			$content .= "\n  RewriteRule . index.php [PT,E=PATH_INFO:$1]";
462
			$content .= "\n  RewriteBase " . $rewriteBase;
463
			$content .= "\n  <IfModule mod_env.c>";
464
			$content .= "\n    SetEnv front_controller_active true";
465
			$content .= "\n    <IfModule mod_dir.c>";
466
			$content .= "\n      DirectorySlash off";
467
			$content .= "\n    </IfModule>";
468
			$content .= "\n  </IfModule>";
469
			$content .= "\n</IfModule>";
470
		}
471
472
		if ($content !== '') {
473
			//suppress errors in case we don't have permissions for it
474
			@file_put_contents($setupHelper->pathToHtaccess(), $htaccessContent.$content . "\n");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
475
		}
476
477
	}
478
479
	public static function protectDataDirectory() {
480
		//Require all denied
481
		$now =  date('Y-m-d H:i:s');
482
		$content = "# Generated by ownCloud on $now\n";
483
		$content.= "# line below if for Apache 2.4\n";
484
		$content.= "<ifModule mod_authz_core.c>\n";
485
		$content.= "Require all denied\n";
486
		$content.= "</ifModule>\n\n";
487
		$content.= "# line below if for Apache 2.2\n";
488
		$content.= "<ifModule !mod_authz_core.c>\n";
489
		$content.= "deny from all\n";
490
		$content.= "Satisfy All\n";
491
		$content.= "</ifModule>\n\n";
492
		$content.= "# section for Apache 2.2 and 2.4\n";
493
		$content.= "IndexIgnore *\n";
494
495
		$baseDir = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data');
496
		file_put_contents($baseDir . '/.htaccess', $content);
497
		file_put_contents($baseDir . '/index.html', '');
498
	}
499
}
500