Passed
Push — master ( a3e409...9b015b )
by John
13:59 queued 11s
created

CheckSetupController::isPhpSupported()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Cthulhux <[email protected]>
8
 * @author Daniel Kesselberg <[email protected]>
9
 * @author Derek <[email protected]>
10
 * @author Georg Ehrke <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Ko- <[email protected]>
13
 * @author Lauris Binde <[email protected]>
14
 * @author Lukas Reschke <[email protected]>
15
 * @author Michael Weimann <[email protected]>
16
 * @author Morris Jobke <[email protected]>
17
 * @author Robin McCorkell <[email protected]>
18
 * @author Roeland Jago Douma <[email protected]>
19
 * @author Sven Strickroth <[email protected]>
20
 * @author Sylvia van Os <[email protected]>
21
 * @author Timo Förster <[email protected]>
22
 *
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 OCA\Settings\Controller;
40
41
use bantu\IniGetWrapper\IniGetWrapper;
42
use DirectoryIterator;
43
use Doctrine\DBAL\DBALException;
44
use Doctrine\DBAL\Platforms\SqlitePlatform;
45
use Doctrine\DBAL\Types\Types;
46
use GuzzleHttp\Exception\ClientException;
47
use OC;
48
use OC\AppFramework\Http;
49
use OC\DB\Connection;
50
use OC\DB\MissingColumnInformation;
51
use OC\DB\MissingIndexInformation;
52
use OC\DB\SchemaWrapper;
53
use OC\IntegrityCheck\Checker;
54
use OC\Lock\NoopLockingProvider;
55
use OC\MemoryInfo;
56
use OCA\Settings\SetupChecks\LegacySSEKeyFormat;
57
use OCA\Settings\SetupChecks\PhpDefaultCharset;
58
use OCA\Settings\SetupChecks\PhpOutputBuffering;
59
use OCP\AppFramework\Controller;
60
use OCP\AppFramework\Http\DataDisplayResponse;
61
use OCP\AppFramework\Http\DataResponse;
62
use OCP\AppFramework\Http\RedirectResponse;
63
use OCP\Http\Client\IClientService;
64
use OCP\IConfig;
65
use OCP\IDateTimeFormatter;
66
use OCP\IDBConnection;
67
use OCP\IL10N;
68
use OCP\ILogger;
69
use OCP\IRequest;
70
use OCP\IURLGenerator;
71
use OCP\Lock\ILockingProvider;
72
use OCP\Security\ISecureRandom;
73
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
74
use Symfony\Component\EventDispatcher\GenericEvent;
75
76
class CheckSetupController extends Controller {
77
	/** @var IConfig */
78
	private $config;
79
	/** @var IClientService */
80
	private $clientService;
81
	/** @var IURLGenerator */
82
	private $urlGenerator;
83
	/** @var IL10N */
84
	private $l10n;
85
	/** @var Checker */
86
	private $checker;
87
	/** @var ILogger */
88
	private $logger;
89
	/** @var EventDispatcherInterface */
90
	private $dispatcher;
91
	/** @var IDBConnection|Connection */
92
	private $db;
93
	/** @var ILockingProvider */
94
	private $lockingProvider;
95
	/** @var IDateTimeFormatter */
96
	private $dateTimeFormatter;
97
	/** @var MemoryInfo */
98
	private $memoryInfo;
99
	/** @var ISecureRandom */
100
	private $secureRandom;
101
	/** @var IniGetWrapper */
102
	private $iniGetWrapper;
103
104
	public function __construct($AppName,
105
								IRequest $request,
106
								IConfig $config,
107
								IClientService $clientService,
108
								IURLGenerator $urlGenerator,
109
								IL10N $l10n,
110
								Checker $checker,
111
								ILogger $logger,
112
								EventDispatcherInterface $dispatcher,
113
								IDBConnection $db,
114
								ILockingProvider $lockingProvider,
115
								IDateTimeFormatter $dateTimeFormatter,
116
								MemoryInfo $memoryInfo,
117
								ISecureRandom $secureRandom,
118
								IniGetWrapper $iniGetWrapper) {
119
		parent::__construct($AppName, $request);
120
		$this->config = $config;
121
		$this->clientService = $clientService;
122
		$this->urlGenerator = $urlGenerator;
123
		$this->l10n = $l10n;
124
		$this->checker = $checker;
125
		$this->logger = $logger;
126
		$this->dispatcher = $dispatcher;
127
		$this->db = $db;
128
		$this->lockingProvider = $lockingProvider;
129
		$this->dateTimeFormatter = $dateTimeFormatter;
130
		$this->memoryInfo = $memoryInfo;
131
		$this->secureRandom = $secureRandom;
132
		$this->iniGetWrapper = $iniGetWrapper;
133
	}
134
135
	/**
136
	 * Checks if the server can connect to the internet using HTTPS and HTTP
137
	 * @return bool
138
	 */
139
	private function hasInternetConnectivityProblems(): bool {
140
		if ($this->config->getSystemValue('has_internet_connection', true) === false) {
141
			return false;
142
		}
143
144
		$siteArray = $this->config->getSystemValue('connectivity_check_domains', [
145
			'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org'
146
		]);
147
148
		foreach ($siteArray as $site) {
149
			if ($this->isSiteReachable($site)) {
150
				return false;
151
			}
152
		}
153
		return true;
154
	}
155
156
	/**
157
	 * Checks if the Nextcloud server can connect to a specific URL using both HTTPS and HTTP
158
	 * @return bool
159
	 */
160
	private function isSiteReachable($sitename) {
161
		$httpSiteName = 'http://' . $sitename . '/';
162
		$httpsSiteName = 'https://' . $sitename . '/';
163
164
		try {
165
			$client = $this->clientService->newClient();
166
			$client->get($httpSiteName);
167
			$client->get($httpsSiteName);
168
		} catch (\Exception $e) {
169
			$this->logger->logException($e, ['app' => 'internet_connection_check']);
170
			return false;
171
		}
172
		return true;
173
	}
174
175
	/**
176
	 * Checks whether a local memcache is installed or not
177
	 * @return bool
178
	 */
179
	private function isMemcacheConfigured() {
180
		return $this->config->getSystemValue('memcache.local', null) !== null;
181
	}
182
183
	/**
184
	 * Whether PHP can generate "secure" pseudorandom integers
185
	 *
186
	 * @return bool
187
	 */
188
	private function isRandomnessSecure() {
189
		try {
190
			$this->secureRandom->generate(1);
191
		} catch (\Exception $ex) {
192
			return false;
193
		}
194
		return true;
195
	}
196
197
	/**
198
	 * Public for the sake of unit-testing
199
	 *
200
	 * @return array
201
	 */
202
	protected function getCurlVersion() {
203
		return curl_version();
204
	}
205
206
	/**
207
	 * Check if the used  SSL lib is outdated. Older OpenSSL and NSS versions do
208
	 * have multiple bugs which likely lead to problems in combination with
209
	 * functionality required by ownCloud such as SNI.
210
	 *
211
	 * @link https://github.com/owncloud/core/issues/17446#issuecomment-122877546
212
	 * @link https://bugzilla.redhat.com/show_bug.cgi?id=1241172
213
	 * @return string
214
	 */
215
	private function isUsedTlsLibOutdated() {
216
		// Don't run check when:
217
		// 1. Server has `has_internet_connection` set to false
218
		// 2. AppStore AND S2S is disabled
219
		if (!$this->config->getSystemValue('has_internet_connection', true)) {
220
			return '';
221
		}
222
		if (!$this->config->getSystemValue('appstoreenabled', true)
223
			&& $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'no'
224
			&& $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'no') {
225
			return '';
226
		}
227
228
		$versionString = $this->getCurlVersion();
229
		if (isset($versionString['ssl_version'])) {
230
			$versionString = $versionString['ssl_version'];
231
		} else {
232
			return '';
233
		}
234
235
		$features = (string)$this->l10n->t('installing and updating apps via the app store or Federated Cloud Sharing');
236
		if (!$this->config->getSystemValue('appstoreenabled', true)) {
237
			$features = (string)$this->l10n->t('Federated Cloud Sharing');
238
		}
239
240
		// Check if at least OpenSSL after 1.01d or 1.0.2b
241
		if (strpos($versionString, 'OpenSSL/') === 0) {
242
			$majorVersion = substr($versionString, 8, 5);
243
			$patchRelease = substr($versionString, 13, 6);
244
245
			if (($majorVersion === '1.0.1' && ord($patchRelease) < ord('d')) ||
246
				($majorVersion === '1.0.2' && ord($patchRelease) < ord('b'))) {
247
				return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['OpenSSL', $versionString, $features]);
248
			}
249
		}
250
251
		// Check if NSS and perform heuristic check
252
		if (strpos($versionString, 'NSS/') === 0) {
253
			try {
254
				$firstClient = $this->clientService->newClient();
255
				$firstClient->get('https://nextcloud.com/');
256
257
				$secondClient = $this->clientService->newClient();
258
				$secondClient->get('https://nextcloud.com/');
259
			} catch (ClientException $e) {
260
				if ($e->getResponse()->getStatusCode() === 400) {
261
					return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['NSS', $versionString, $features]);
262
				}
263
			}
264
		}
265
266
		return '';
267
	}
268
269
	/**
270
	 * Whether the version is outdated
271
	 *
272
	 * @return bool
273
	 */
274
	protected function isPhpOutdated(): bool {
275
		return PHP_VERSION_ID < 70300;
276
	}
277
278
	/**
279
	 * Whether the php version is still supported (at time of release)
280
	 * according to: https://secure.php.net/supported-versions.php
281
	 *
282
	 * @return array
283
	 */
284
	private function isPhpSupported(): array {
285
		return ['eol' => $this->isPhpOutdated(), 'version' => PHP_VERSION];
286
	}
287
288
	/**
289
	 * Check if the reverse proxy configuration is working as expected
290
	 *
291
	 * @return bool
292
	 */
293
	private function forwardedForHeadersWorking() {
294
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
295
		$remoteAddress = $this->request->getHeader('REMOTE_ADDR');
296
297
		if (empty($trustedProxies) && $this->request->getHeader('X-Forwarded-Host') !== '') {
298
			return false;
299
		}
300
301
		if (\is_array($trustedProxies) && \in_array($remoteAddress, $trustedProxies, true)) {
302
			return $remoteAddress !== $this->request->getRemoteAddress();
303
		}
304
305
		// either not enabled or working correctly
306
		return true;
307
	}
308
309
	/**
310
	 * Checks if the correct memcache module for PHP is installed. Only
311
	 * fails if memcached is configured and the working module is not installed.
312
	 *
313
	 * @return bool
314
	 */
315
	private function isCorrectMemcachedPHPModuleInstalled() {
316
		if ($this->config->getSystemValue('memcache.distributed', null) !== '\OC\Memcache\Memcached') {
317
			return true;
318
		}
319
320
		// there are two different memcached modules for PHP
321
		// we only support memcached and not memcache
322
		// https://code.google.com/p/memcached/wiki/PHPClientComparison
323
		return !(!extension_loaded('memcached') && extension_loaded('memcache'));
324
	}
325
326
	/**
327
	 * Checks if set_time_limit is not disabled.
328
	 *
329
	 * @return bool
330
	 */
331
	private function isSettimelimitAvailable() {
332
		if (function_exists('set_time_limit')
333
			&& strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
334
			return true;
335
		}
336
337
		return false;
338
	}
339
340
	/**
341
	 * @return RedirectResponse
342
	 */
343
	public function rescanFailedIntegrityCheck() {
344
		$this->checker->runInstanceVerification();
345
		return new RedirectResponse(
346
			$this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview'])
347
		);
348
	}
349
350
	/**
351
	 * @NoCSRFRequired
352
	 * @return DataResponse
353
	 */
354
	public function getFailedIntegrityCheckFiles() {
355
		if (!$this->checker->isCodeCheckEnforced()) {
356
			return new DataDisplayResponse('Integrity checker has been disabled. Integrity cannot be verified.');
0 ignored issues
show
Bug Best Practice introduced by
The expression return new OCP\AppFramew...y cannot be verified.') returns the type OCP\AppFramework\Http\DataDisplayResponse which is incompatible with the documented return type OCP\AppFramework\Http\DataResponse.
Loading history...
357
		}
358
359
		$completeResults = $this->checker->getResults();
360
361
		if (!empty($completeResults)) {
362
			$formattedTextResponse = 'Technical information
363
=====================
364
The following list covers which files have failed the integrity check. Please read
365
the previous linked documentation to learn more about the errors and how to fix
366
them.
367
368
Results
369
=======
370
';
371
			foreach ($completeResults as $context => $contextResult) {
372
				$formattedTextResponse .= "- $context\n";
373
374
				foreach ($contextResult as $category => $result) {
375
					$formattedTextResponse .= "\t- $category\n";
376
					if ($category !== 'EXCEPTION') {
377
						foreach ($result as $key => $results) {
378
							$formattedTextResponse .= "\t\t- $key\n";
379
						}
380
					} else {
381
						foreach ($result as $key => $results) {
382
							$formattedTextResponse .= "\t\t- $results\n";
383
						}
384
					}
385
				}
386
			}
387
388
			$formattedTextResponse .= '
389
Raw output
390
==========
391
';
392
			$formattedTextResponse .= print_r($completeResults, true);
393
		} else {
394
			$formattedTextResponse = 'No errors have been found.';
395
		}
396
397
398
		$response = new DataDisplayResponse(
399
			$formattedTextResponse,
400
			Http::STATUS_OK,
401
			[
402
				'Content-Type' => 'text/plain',
403
			]
404
		);
405
406
		return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type OCP\AppFramework\Http\DataDisplayResponse which is incompatible with the documented return type OCP\AppFramework\Http\DataResponse.
Loading history...
407
	}
408
409
	/**
410
	 * Checks whether a PHP opcache is properly set up
411
	 * @return bool
412
	 */
413
	protected function isOpcacheProperlySetup() {
414
		if (!$this->iniGetWrapper->getBool('opcache.enable')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->iniGetWrapper->getBool('opcache.enable') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
415
			return false;
416
		}
417
418
		if (!$this->iniGetWrapper->getBool('opcache.save_comments')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->iniGetWrapper->ge...opcache.save_comments') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
419
			return false;
420
		}
421
422
		if ($this->iniGetWrapper->getNumeric('opcache.max_accelerated_files') < 10000) {
423
			return false;
424
		}
425
426
		if ($this->iniGetWrapper->getNumeric('opcache.memory_consumption') < 128) {
427
			return false;
428
		}
429
430
		if ($this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') < 8) {
431
			return false;
432
		}
433
434
		return true;
435
	}
436
437
	/**
438
	 * Check if the required FreeType functions are present
439
	 * @return bool
440
	 */
441
	protected function hasFreeTypeSupport() {
442
		return function_exists('imagettfbbox') && function_exists('imagettftext');
443
	}
444
445
	protected function hasMissingIndexes(): array {
446
		$indexInfo = new MissingIndexInformation();
447
		// Dispatch event so apps can also hint for pending index updates if needed
448
		$event = new GenericEvent($indexInfo);
449
		$this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event);
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

449
		$this->dispatcher->/** @scrutinizer ignore-call */ 
450
                     dispatch(IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
450
451
		return $indexInfo->getListOfMissingIndexes();
452
	}
453
454
	protected function hasMissingColumns(): array {
455
		$indexInfo = new MissingColumnInformation();
456
		// Dispatch event so apps can also hint for pending index updates if needed
457
		$event = new GenericEvent($indexInfo);
458
		$this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_COLUMNS_EVENT, $event);
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

458
		$this->dispatcher->/** @scrutinizer ignore-call */ 
459
                     dispatch(IDBConnection::CHECK_MISSING_COLUMNS_EVENT, $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
459
460
		return $indexInfo->getListOfMissingColumns();
461
	}
462
463
	protected function isSqliteUsed() {
464
		return strpos($this->config->getSystemValue('dbtype'), 'sqlite') !== false;
465
	}
466
467
	protected function isReadOnlyConfig(): bool {
468
		return \OC_Helper::isReadOnlyConfigEnabled();
469
	}
470
471
	protected function hasValidTransactionIsolationLevel(): bool {
472
		try {
473
			if ($this->db->getDatabasePlatform() instanceof SqlitePlatform) {
474
				return true;
475
			}
476
477
			return $this->db->getTransactionIsolation() === Connection::TRANSACTION_READ_COMMITTED;
0 ignored issues
show
Bug introduced by
The method getTransactionIsolation() does not exist on OCP\IDBConnection. Since it exists in all sub-types, consider adding an abstract or default implementation to OCP\IDBConnection. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

477
			return $this->db->/** @scrutinizer ignore-call */ getTransactionIsolation() === Connection::TRANSACTION_READ_COMMITTED;
Loading history...
Deprecated Code introduced by
The constant Doctrine\DBAL\Connection...NSACTION_READ_COMMITTED has been deprecated: Use TransactionIsolationLevel::READ_COMMITTED. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

477
			return $this->db->getTransactionIsolation() === /** @scrutinizer ignore-deprecated */ Connection::TRANSACTION_READ_COMMITTED;

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
478
		} catch (DBALException $e) {
479
			// ignore
480
		}
481
482
		return true;
483
	}
484
485
	protected function hasFileinfoInstalled(): bool {
486
		return \OC_Util::fileInfoLoaded();
487
	}
488
489
	protected function hasWorkingFileLocking(): bool {
490
		return !($this->lockingProvider instanceof NoopLockingProvider);
491
	}
492
493
	protected function getSuggestedOverwriteCliURL(): string {
494
		$suggestedOverwriteCliUrl = '';
495
		if ($this->config->getSystemValue('overwrite.cli.url', '') === '') {
496
			$suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT;
497
			if (!$this->config->getSystemValue('config_is_read_only', false)) {
498
				// Set the overwrite URL when it was not set yet.
499
				$this->config->setSystemValue('overwrite.cli.url', $suggestedOverwriteCliUrl);
500
				$suggestedOverwriteCliUrl = '';
501
			}
502
		}
503
		return $suggestedOverwriteCliUrl;
504
	}
505
506
	protected function getLastCronInfo(): array {
507
		$lastCronRun = $this->config->getAppValue('core', 'lastcron', 0);
508
		return [
509
			'diffInSeconds' => time() - $lastCronRun,
510
			'relativeTime' => $this->dateTimeFormatter->formatTimeSpan($lastCronRun),
0 ignored issues
show
Bug introduced by
$lastCronRun of type string is incompatible with the type DateTime|integer expected by parameter $timestamp of OCP\IDateTimeFormatter::formatTimeSpan(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

510
			'relativeTime' => $this->dateTimeFormatter->formatTimeSpan(/** @scrutinizer ignore-type */ $lastCronRun),
Loading history...
511
			'backgroundJobsUrl' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'server']) . '#backgroundjobs',
512
		];
513
	}
514
515
	protected function getCronErrors() {
516
		$errors = json_decode($this->config->getAppValue('core', 'cronErrors', ''), true);
517
518
		if (is_array($errors)) {
519
			return $errors;
520
		}
521
522
		return [];
523
	}
524
525
	protected function hasOpcacheLoaded(): bool {
526
		return extension_loaded('Zend OPcache');
527
	}
528
529
	/**
530
	 * Iterates through the configured app roots and
531
	 * tests if the subdirectories are owned by the same user than the current user.
532
	 *
533
	 * @return array
534
	 */
535
	protected function getAppDirsWithDifferentOwner(): array {
536
		$currentUser = posix_getuid();
537
		$appDirsWithDifferentOwner = [[]];
538
539
		foreach (OC::$APPSROOTS as $appRoot) {
540
			if ($appRoot['writable'] === true) {
541
				$appDirsWithDifferentOwner[] = $this->getAppDirsWithDifferentOwnerForAppRoot($currentUser, $appRoot);
542
			}
543
		}
544
545
		$appDirsWithDifferentOwner = array_merge(...$appDirsWithDifferentOwner);
546
		sort($appDirsWithDifferentOwner);
547
548
		return $appDirsWithDifferentOwner;
549
	}
550
551
	/**
552
	 * Tests if the directories for one apps directory are writable by the current user.
553
	 *
554
	 * @param int $currentUser The current user
555
	 * @param array $appRoot The app root config
556
	 * @return string[] The none writable directory paths inside the app root
557
	 */
558
	private function getAppDirsWithDifferentOwnerForAppRoot(int $currentUser, array $appRoot): array {
559
		$appDirsWithDifferentOwner = [];
560
		$appsPath = $appRoot['path'];
561
		$appsDir = new DirectoryIterator($appRoot['path']);
562
563
		foreach ($appsDir as $fileInfo) {
564
			if ($fileInfo->isDir() && !$fileInfo->isDot()) {
565
				$absAppPath = $appsPath . DIRECTORY_SEPARATOR . $fileInfo->getFilename();
566
				$appDirUser = fileowner($absAppPath);
567
				if ($appDirUser !== $currentUser) {
568
					$appDirsWithDifferentOwner[] = $absAppPath;
569
				}
570
			}
571
		}
572
573
		return $appDirsWithDifferentOwner;
574
	}
575
576
	/**
577
	 * Checks for potential PHP modules that would improve the instance
578
	 *
579
	 * @return string[] A list of PHP modules that is recommended
580
	 */
581
	protected function hasRecommendedPHPModules(): array {
582
		$recommendedPHPModules = [];
583
584
		if (!extension_loaded('intl')) {
585
			$recommendedPHPModules[] = 'intl';
586
		}
587
588
		if (!extension_loaded('bcmath')) {
589
			$recommendedPHPModules[] = 'bcmath';
590
		}
591
592
		if (!extension_loaded('gmp')) {
593
			$recommendedPHPModules[] = 'gmp';
594
		}
595
596
		if ($this->config->getAppValue('theming', 'enabled', 'no') === 'yes') {
597
			if (!extension_loaded('imagick')) {
598
				$recommendedPHPModules[] = 'imagick';
599
			}
600
		}
601
602
		return $recommendedPHPModules;
603
	}
604
605
	protected function isMysqlUsedWithoutUTF8MB4(): bool {
606
		return ($this->config->getSystemValue('dbtype', 'sqlite') === 'mysql') && ($this->config->getSystemValue('mysql.utf8mb4', false) === false);
607
	}
608
609
	protected function hasBigIntConversionPendingColumns(): array {
610
		// copy of ConvertFilecacheBigInt::getColumnsByTable()
611
		$tables = [
612
			'activity' => ['activity_id', 'object_id'],
613
			'activity_mq' => ['mail_id'],
614
			'authtoken' => ['id'],
615
			'bruteforce_attempts' => ['id'],
616
			'filecache' => ['fileid', 'storage', 'parent', 'mimetype', 'mimepart', 'mtime', 'storage_mtime'],
617
			'filecache_extended' => ['fileid'],
618
			'file_locks' => ['id'],
619
			'jobs' => ['id'],
620
			'mimetypes' => ['id'],
621
			'mounts' => ['id', 'storage_id', 'root_id', 'mount_id'],
622
			'storages' => ['numeric_id'],
623
		];
624
625
		$schema = new SchemaWrapper($this->db);
626
		$isSqlite = $this->db->getDatabasePlatform() instanceof SqlitePlatform;
627
		$pendingColumns = [];
628
629
		foreach ($tables as $tableName => $columns) {
630
			if (!$schema->hasTable($tableName)) {
631
				continue;
632
			}
633
634
			$table = $schema->getTable($tableName);
635
			foreach ($columns as $columnName) {
636
				$column = $table->getColumn($columnName);
637
				$isAutoIncrement = $column->getAutoincrement();
638
				$isAutoIncrementOnSqlite = $isSqlite && $isAutoIncrement;
639
				if ($column->getType()->getName() !== Types::BIGINT && !$isAutoIncrementOnSqlite) {
640
					$pendingColumns[] = $tableName . '.' . $columnName;
641
				}
642
			}
643
		}
644
645
		return $pendingColumns;
646
	}
647
648
	protected function isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(): bool {
649
		$objectStore = $this->config->getSystemValue('objectstore', null);
650
		$objectStoreMultibucket = $this->config->getSystemValue('objectstore_multibucket', null);
651
652
		if (!isset($objectStoreMultibucket) && !isset($objectStore)) {
653
			return true;
654
		}
655
656
		if (isset($objectStoreMultibucket['class']) && $objectStoreMultibucket['class'] !== 'OC\\Files\\ObjectStore\\S3') {
657
			return true;
658
		}
659
660
		if (isset($objectStore['class']) && $objectStore['class'] !== 'OC\\Files\\ObjectStore\\S3') {
661
			return true;
662
		}
663
664
		$tempPath = sys_get_temp_dir();
665
		if (!is_dir($tempPath)) {
666
			$this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. value: ' . $tempPath);
667
			return false;
668
		}
669
		$freeSpaceInTemp = disk_free_space($tempPath);
670
		if ($freeSpaceInTemp === false) {
671
			$this->logger->error('Error while checking the available disk space of temporary PHP path - no free disk space returned. temporary path: ' . $tempPath);
672
			return false;
673
		}
674
675
		$freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024;
676
		if ($freeSpaceInTempInGB > 50) {
677
			return true;
678
		}
679
680
		$this->logger->warning('Checking the available space in the temporary path resulted in ' . round($freeSpaceInTempInGB, 1) . ' GB instead of the recommended 50GB. Path: ' . $tempPath);
681
		return false;
682
	}
683
684
	protected function imageMagickLacksSVGSupport(): bool {
685
		return extension_loaded('imagick') && count(\Imagick::queryFormats('SVG')) === 0;
686
	}
687
688
	/**
689
	 * @return DataResponse
690
	 */
691
	public function check() {
692
		$phpDefaultCharset = new PhpDefaultCharset();
693
		$phpOutputBuffering = new PhpOutputBuffering();
694
		$legacySSEKeyFormat = new LegacySSEKeyFormat($this->l10n, $this->config, $this->urlGenerator);
695
		return new DataResponse(
696
			[
697
				'isGetenvServerWorking' => !empty(getenv('PATH')),
698
				'isReadOnlyConfig' => $this->isReadOnlyConfig(),
699
				'hasValidTransactionIsolationLevel' => $this->hasValidTransactionIsolationLevel(),
700
				'hasFileinfoInstalled' => $this->hasFileinfoInstalled(),
701
				'hasWorkingFileLocking' => $this->hasWorkingFileLocking(),
702
				'suggestedOverwriteCliURL' => $this->getSuggestedOverwriteCliURL(),
703
				'cronInfo' => $this->getLastCronInfo(),
704
				'cronErrors' => $this->getCronErrors(),
705
				'serverHasInternetConnectionProblems' => $this->hasInternetConnectivityProblems(),
706
				'isMemcacheConfigured' => $this->isMemcacheConfigured(),
707
				'memcacheDocs' => $this->urlGenerator->linkToDocs('admin-performance'),
708
				'isRandomnessSecure' => $this->isRandomnessSecure(),
709
				'securityDocs' => $this->urlGenerator->linkToDocs('admin-security'),
710
				'isUsedTlsLibOutdated' => $this->isUsedTlsLibOutdated(),
711
				'phpSupported' => $this->isPhpSupported(),
712
				'forwardedForHeadersWorking' => $this->forwardedForHeadersWorking(),
713
				'reverseProxyDocs' => $this->urlGenerator->linkToDocs('admin-reverse-proxy'),
714
				'isCorrectMemcachedPHPModuleInstalled' => $this->isCorrectMemcachedPHPModuleInstalled(),
715
				'hasPassedCodeIntegrityCheck' => $this->checker->hasPassedCheck(),
716
				'codeIntegrityCheckerDocumentation' => $this->urlGenerator->linkToDocs('admin-code-integrity'),
717
				'isOpcacheProperlySetup' => $this->isOpcacheProperlySetup(),
718
				'hasOpcacheLoaded' => $this->hasOpcacheLoaded(),
719
				'phpOpcacheDocumentation' => $this->urlGenerator->linkToDocs('admin-php-opcache'),
720
				'isSettimelimitAvailable' => $this->isSettimelimitAvailable(),
721
				'hasFreeTypeSupport' => $this->hasFreeTypeSupport(),
722
				'missingIndexes' => $this->hasMissingIndexes(),
723
				'missingColumns' => $this->hasMissingColumns(),
724
				'isSqliteUsed' => $this->isSqliteUsed(),
725
				'databaseConversionDocumentation' => $this->urlGenerator->linkToDocs('admin-db-conversion'),
726
				'isMemoryLimitSufficient' => $this->memoryInfo->isMemoryLimitSufficient(),
727
				'appDirsWithDifferentOwner' => $this->getAppDirsWithDifferentOwner(),
728
				'recommendedPHPModules' => $this->hasRecommendedPHPModules(),
729
				'pendingBigIntConversionColumns' => $this->hasBigIntConversionPendingColumns(),
730
				'isMysqlUsedWithoutUTF8MB4' => $this->isMysqlUsedWithoutUTF8MB4(),
731
				'isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed' => $this->isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(),
732
				'reverseProxyGeneratedURL' => $this->urlGenerator->getAbsoluteURL('index.php'),
733
				'imageMagickLacksSVGSupport' => $this->imageMagickLacksSVGSupport(),
734
				PhpDefaultCharset::class => ['pass' => $phpDefaultCharset->run(), 'description' => $phpDefaultCharset->description(), 'severity' => $phpDefaultCharset->severity()],
735
				PhpOutputBuffering::class => ['pass' => $phpOutputBuffering->run(), 'description' => $phpOutputBuffering->description(), 'severity' => $phpOutputBuffering->severity()],
736
				LegacySSEKeyFormat::class => ['pass' => $legacySSEKeyFormat->run(), 'description' => $legacySSEKeyFormat->description(), 'severity' => $legacySSEKeyFormat->severity(), 'linkToDocumentation' => $legacySSEKeyFormat->linkToDocumentation()],
737
			]
738
		);
739
	}
740
}
741