Passed
Push — master ( 697e1d...23e8ae )
by John
16:38 queued 12s
created

areWebauthnExtensionsEnabled()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 8
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 J0WI <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Julius Härtl <[email protected]>
14
 * @author Ko- <[email protected]>
15
 * @author Lauris Binde <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Michael Weimann <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author nhirokinet <[email protected]>
20
 * @author Robin Appelman <[email protected]>
21
 * @author Robin McCorkell <[email protected]>
22
 * @author Roeland Jago Douma <[email protected]>
23
 * @author Sven Strickroth <[email protected]>
24
 * @author Sylvia van Os <[email protected]>
25
 * @author timm2k <[email protected]>
26
 * @author Timo Förster <[email protected]>
27
 * @author Valdnet <[email protected]>
28
 * @author MichaIng <[email protected]>
29
 *
30
 * @license AGPL-3.0
31
 *
32
 * This code is free software: you can redistribute it and/or modify
33
 * it under the terms of the GNU Affero General Public License, version 3,
34
 * as published by the Free Software Foundation.
35
 *
36
 * This program is distributed in the hope that it will be useful,
37
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
38
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39
 * GNU Affero General Public License for more details.
40
 *
41
 * You should have received a copy of the GNU Affero General Public License, version 3,
42
 * along with this program. If not, see <http://www.gnu.org/licenses/>
43
 *
44
 */
45
namespace OCA\Settings\Controller;
46
47
use bantu\IniGetWrapper\IniGetWrapper;
48
use DirectoryIterator;
49
use Doctrine\DBAL\Exception;
50
use Doctrine\DBAL\Platforms\SqlitePlatform;
51
use Doctrine\DBAL\TransactionIsolationLevel;
52
use GuzzleHttp\Exception\ClientException;
53
use OC;
54
use OC\AppFramework\Http;
55
use OC\DB\Connection;
56
use OC\DB\MissingColumnInformation;
57
use OC\DB\MissingIndexInformation;
58
use OC\DB\MissingPrimaryKeyInformation;
59
use OC\DB\SchemaWrapper;
60
use OC\IntegrityCheck\Checker;
61
use OC\Lock\NoopLockingProvider;
62
use OC\MemoryInfo;
63
use OCA\Settings\SetupChecks\CheckUserCertificates;
64
use OCA\Settings\SetupChecks\LdapInvalidUuids;
65
use OCA\Settings\SetupChecks\LegacySSEKeyFormat;
66
use OCA\Settings\SetupChecks\PhpDefaultCharset;
67
use OCA\Settings\SetupChecks\PhpOutputBuffering;
68
use OCA\Settings\SetupChecks\SupportedDatabase;
69
use OCP\App\IAppManager;
70
use OCP\AppFramework\Controller;
71
use OCP\AppFramework\Http\DataDisplayResponse;
72
use OCP\AppFramework\Http\DataResponse;
73
use OCP\AppFramework\Http\RedirectResponse;
74
use OCP\DB\Types;
75
use OCP\Http\Client\IClientService;
76
use OCP\IConfig;
77
use OCP\IDateTimeFormatter;
78
use OCP\IDBConnection;
79
use OCP\IL10N;
80
use OCP\IRequest;
81
use OCP\IServerContainer;
82
use OCP\ITempManager;
83
use OCP\IURLGenerator;
84
use OCP\Lock\ILockingProvider;
85
use OCP\Notification\IManager;
86
use OCP\Security\ISecureRandom;
87
use Psr\Log\LoggerInterface;
88
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
89
use Symfony\Component\EventDispatcher\GenericEvent;
90
91
class CheckSetupController extends Controller {
92
	/** @var IConfig */
93
	private $config;
94
	/** @var IClientService */
95
	private $clientService;
96
	/** @var IURLGenerator */
97
	private $urlGenerator;
98
	/** @var IL10N */
99
	private $l10n;
100
	/** @var Checker */
101
	private $checker;
102
	/** @var LoggerInterface */
103
	private $logger;
104
	/** @var EventDispatcherInterface */
105
	private $dispatcher;
106
	/** @var Connection */
107
	private $db;
108
	/** @var ILockingProvider */
109
	private $lockingProvider;
110
	/** @var IDateTimeFormatter */
111
	private $dateTimeFormatter;
112
	/** @var MemoryInfo */
113
	private $memoryInfo;
114
	/** @var ISecureRandom */
115
	private $secureRandom;
116
	/** @var IniGetWrapper */
117
	private $iniGetWrapper;
118
	/** @var IDBConnection */
119
	private $connection;
120
	/** @var ITempManager */
121
	private $tempManager;
122
	/** @var IManager */
123
	private $manager;
124
	/** @var IAppManager */
125
	private $appManager;
126
	/** @var IServerContainer */
127
	private $serverContainer;
128
129
	public function __construct($AppName,
130
								IRequest $request,
131
								IConfig $config,
132
								IClientService $clientService,
133
								IURLGenerator $urlGenerator,
134
								IL10N $l10n,
135
								Checker $checker,
136
								LoggerInterface $logger,
137
								EventDispatcherInterface $dispatcher,
138
								Connection $db,
139
								ILockingProvider $lockingProvider,
140
								IDateTimeFormatter $dateTimeFormatter,
141
								MemoryInfo $memoryInfo,
142
								ISecureRandom $secureRandom,
143
								IniGetWrapper $iniGetWrapper,
144
								IDBConnection $connection,
145
								ITempManager $tempManager,
146
								IManager $manager,
147
								IAppManager $appManager,
148
								IServerContainer $serverContainer
149
	) {
150
		parent::__construct($AppName, $request);
151
		$this->config = $config;
152
		$this->clientService = $clientService;
153
		$this->urlGenerator = $urlGenerator;
154
		$this->l10n = $l10n;
155
		$this->checker = $checker;
156
		$this->logger = $logger;
157
		$this->dispatcher = $dispatcher;
158
		$this->db = $db;
159
		$this->lockingProvider = $lockingProvider;
160
		$this->dateTimeFormatter = $dateTimeFormatter;
161
		$this->memoryInfo = $memoryInfo;
162
		$this->secureRandom = $secureRandom;
163
		$this->iniGetWrapper = $iniGetWrapper;
164
		$this->connection = $connection;
165
		$this->tempManager = $tempManager;
166
		$this->manager = $manager;
167
		$this->appManager = $appManager;
168
		$this->serverContainer = $serverContainer;
169
	}
170
171
	/**
172
	 * Check if is fair use of free push service
173
	 * @return bool
174
	 */
175
	private function isFairUseOfFreePushService(): bool {
176
		return $this->manager->isFairUseOfFreePushService();
177
	}
178
179
	/**
180
	 * Checks if the server can connect to the internet using HTTPS and HTTP
181
	 * @return bool
182
	 */
183
	private function hasInternetConnectivityProblems(): bool {
184
		if ($this->config->getSystemValue('has_internet_connection', true) === false) {
185
			return false;
186
		}
187
188
		$siteArray = $this->config->getSystemValue('connectivity_check_domains', [
189
			'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org'
190
		]);
191
192
		foreach ($siteArray as $site) {
193
			if ($this->isSiteReachable($site)) {
194
				return false;
195
			}
196
		}
197
		return true;
198
	}
199
200
	/**
201
	 * Checks if the Nextcloud server can connect to a specific URL
202
	 * @param string $site site domain or full URL with http/https protocol
203
	 * @return bool
204
	 */
205
	private function isSiteReachable(string $site): bool {
206
		try {
207
			$client = $this->clientService->newClient();
208
			// if there is no protocol, test http:// AND https://
209
			if (preg_match('/^https?:\/\//', $site) !== 1) {
210
				$httpSite = 'http://' . $site . '/';
211
				$client->get($httpSite);
212
				$httpsSite = 'https://' . $site . '/';
213
				$client->get($httpsSite);
214
			} else {
215
				$client->get($site);
216
			}
217
		} catch (\Exception $e) {
218
			$this->logger->error('Cannot connect to: ' . $site, [
219
				'app' => 'internet_connection_check',
220
				'exception' => $e,
221
			]);
222
			return false;
223
		}
224
		return true;
225
	}
226
227
	/**
228
	 * Checks whether a local memcache is installed or not
229
	 * @return bool
230
	 */
231
	private function isMemcacheConfigured() {
232
		return $this->config->getSystemValue('memcache.local', null) !== null;
233
	}
234
235
	/**
236
	 * Whether PHP can generate "secure" pseudorandom integers
237
	 *
238
	 * @return bool
239
	 */
240
	private function isRandomnessSecure() {
241
		try {
242
			$this->secureRandom->generate(1);
243
		} catch (\Exception $ex) {
244
			return false;
245
		}
246
		return true;
247
	}
248
249
	/**
250
	 * Public for the sake of unit-testing
251
	 *
252
	 * @return array
253
	 */
254
	protected function getCurlVersion() {
255
		return curl_version();
256
	}
257
258
	/**
259
	 * Check if the used SSL lib is outdated. Older OpenSSL and NSS versions do
260
	 * have multiple bugs which likely lead to problems in combination with
261
	 * functionality required by ownCloud such as SNI.
262
	 *
263
	 * @link https://github.com/owncloud/core/issues/17446#issuecomment-122877546
264
	 * @link https://bugzilla.redhat.com/show_bug.cgi?id=1241172
265
	 * @return string
266
	 */
267
	private function isUsedTlsLibOutdated() {
268
		// Don't run check when:
269
		// 1. Server has `has_internet_connection` set to false
270
		// 2. AppStore AND S2S is disabled
271
		if (!$this->config->getSystemValue('has_internet_connection', true)) {
272
			return '';
273
		}
274
		if (!$this->config->getSystemValue('appstoreenabled', true)
275
			&& $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'no'
276
			&& $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'no') {
277
			return '';
278
		}
279
280
		$versionString = $this->getCurlVersion();
281
		if (isset($versionString['ssl_version'])) {
282
			$versionString = $versionString['ssl_version'];
283
		} else {
284
			return '';
285
		}
286
287
		$features = $this->l10n->t('installing and updating apps via the App Store or Federated Cloud Sharing');
288
		if (!$this->config->getSystemValue('appstoreenabled', true)) {
289
			$features = $this->l10n->t('Federated Cloud Sharing');
290
		}
291
292
		// Check if at least OpenSSL after 1.01d or 1.0.2b
293
		if (strpos($versionString, 'OpenSSL/') === 0) {
294
			$majorVersion = substr($versionString, 8, 5);
295
			$patchRelease = substr($versionString, 13, 6);
296
297
			if (($majorVersion === '1.0.1' && ord($patchRelease) < ord('d')) ||
298
				($majorVersion === '1.0.2' && ord($patchRelease) < ord('b'))) {
299
				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]);
300
			}
301
		}
302
303
		// Check if NSS and perform heuristic check
304
		if (strpos($versionString, 'NSS/') === 0) {
305
			try {
306
				$firstClient = $this->clientService->newClient();
307
				$firstClient->get('https://nextcloud.com/');
308
309
				$secondClient = $this->clientService->newClient();
310
				$secondClient->get('https://nextcloud.com/');
311
			} catch (ClientException $e) {
312
				if ($e->getResponse()->getStatusCode() === 400) {
313
					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]);
314
				}
315
			} catch (\Exception $e) {
316
				$this->logger->warning('error checking curl', [
317
					'app' => 'settings',
318
					'exception' => $e,
319
				]);
320
				return $this->l10n->t('Could not determine if TLS version of cURL is outdated or not because an error happened during the HTTPS request against https://nextcloud.com. Please check the nextcloud log file for more details.');
321
			}
322
		}
323
324
		return '';
325
	}
326
327
	/**
328
	 * Whether the version is outdated
329
	 *
330
	 * @return bool
331
	 */
332
	protected function isPhpOutdated(): bool {
333
		return PHP_VERSION_ID < 70400;
334
	}
335
336
	/**
337
	 * Whether the php version is still supported (at time of release)
338
	 * according to: https://www.php.net/supported-versions.php
339
	 *
340
	 * @return array
341
	 */
342
	private function isPhpSupported(): array {
343
		return ['eol' => $this->isPhpOutdated(), 'version' => PHP_VERSION];
344
	}
345
346
	/**
347
	 * Check if the reverse proxy configuration is working as expected
348
	 *
349
	 * @return bool
350
	 */
351
	private function forwardedForHeadersWorking(): bool {
352
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
353
		$remoteAddress = $this->request->getHeader('REMOTE_ADDR');
354
355
		if (empty($trustedProxies) && $this->request->getHeader('X-Forwarded-Host') !== '') {
356
			return false;
357
		}
358
359
		if (\is_array($trustedProxies)) {
360
			if (\in_array($remoteAddress, $trustedProxies, true) && $remoteAddress !== '127.0.0.1') {
361
				return $remoteAddress !== $this->request->getRemoteAddress();
362
			}
363
		} else {
364
			return false;
365
		}
366
367
		// either not enabled or working correctly
368
		return true;
369
	}
370
371
	/**
372
	 * Checks if the correct memcache module for PHP is installed. Only
373
	 * fails if memcached is configured and the working module is not installed.
374
	 *
375
	 * @return bool
376
	 */
377
	private function isCorrectMemcachedPHPModuleInstalled() {
378
		if ($this->config->getSystemValue('memcache.distributed', null) !== '\OC\Memcache\Memcached') {
379
			return true;
380
		}
381
382
		// there are two different memcached modules for PHP
383
		// we only support memcached and not memcache
384
		// https://code.google.com/p/memcached/wiki/PHPClientComparison
385
		return !(!extension_loaded('memcached') && extension_loaded('memcache'));
386
	}
387
388
	/**
389
	 * Checks if set_time_limit is not disabled.
390
	 *
391
	 * @return bool
392
	 */
393
	private function isSettimelimitAvailable() {
394
		if (function_exists('set_time_limit')
395
			&& strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
396
			return true;
397
		}
398
399
		return false;
400
	}
401
402
	/**
403
	 * @return RedirectResponse
404
	 * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
405
	 */
406
	public function rescanFailedIntegrityCheck(): RedirectResponse {
407
		$this->checker->runInstanceVerification();
408
		return new RedirectResponse(
409
			$this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview'])
410
		);
411
	}
412
413
	/**
414
	 * @NoCSRFRequired
415
	 * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
416
	 */
417
	public function getFailedIntegrityCheckFiles(): DataDisplayResponse {
418
		if (!$this->checker->isCodeCheckEnforced()) {
419
			return new DataDisplayResponse('Integrity checker has been disabled. Integrity cannot be verified.');
420
		}
421
422
		$completeResults = $this->checker->getResults();
423
424
		if (!empty($completeResults)) {
425
			$formattedTextResponse = 'Technical information
426
=====================
427
The following list covers which files have failed the integrity check. Please read
428
the previous linked documentation to learn more about the errors and how to fix
429
them.
430
431
Results
432
=======
433
';
434
			foreach ($completeResults as $context => $contextResult) {
435
				$formattedTextResponse .= "- $context\n";
436
437
				foreach ($contextResult as $category => $result) {
438
					$formattedTextResponse .= "\t- $category\n";
439
					if ($category !== 'EXCEPTION') {
440
						foreach ($result as $key => $results) {
441
							$formattedTextResponse .= "\t\t- $key\n";
442
						}
443
					} else {
444
						foreach ($result as $key => $results) {
445
							$formattedTextResponse .= "\t\t- $results\n";
446
						}
447
					}
448
				}
449
			}
450
451
			$formattedTextResponse .= '
452
Raw output
453
==========
454
';
455
			$formattedTextResponse .= print_r($completeResults, true);
456
		} else {
457
			$formattedTextResponse = 'No errors have been found.';
458
		}
459
460
461
		return new DataDisplayResponse(
462
			$formattedTextResponse,
463
			Http::STATUS_OK,
464
			[
465
				'Content-Type' => 'text/plain',
466
			]
467
		);
468
	}
469
470
	/**
471
	 * Checks whether a PHP OPcache is properly set up
472
	 * @return string[] The list of OPcache setup recommendations
473
	 */
474
	protected function getOpcacheSetupRecommendations(): array {
475
		// If the module is not loaded, return directly to skip inapplicable checks
476
		if (!extension_loaded('Zend OPcache')) {
477
			return ['The PHP OPcache module is not loaded. For better performance it is recommended to load it into your PHP installation.'];
478
		}
479
480
		$recommendations = [];
481
482
		// Check whether Nextcloud is allowed to use the OPcache API
483
		$isPermitted = true;
484
		$permittedPath = $this->iniGetWrapper->getString('opcache.restrict_api');
485
		if (isset($permittedPath) && $permittedPath !== '' && !str_starts_with(\OC::$SERVERROOT, rtrim($permittedPath, '/'))) {
486
			$isPermitted = false;
487
		}
488
489
		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...
490
			$recommendations[] = 'OPcache is disabled. For better performance, it is recommended to apply <code>opcache.enable=1</code> to your PHP configuration.';
491
492
			// Check for saved comments only when OPcache is currently disabled. If it was enabled, opcache.save_comments=0 would break Nextcloud in the first place.
493
			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...
494
				$recommendations[] = 'OPcache is configured to remove code comments. With OPcache enabled, <code>opcache.save_comments=1</code> must be set for Nextcloud to function.';
495
			}
496
497
			if (!$isPermitted) {
498
				$recommendations[] = 'Nextcloud is not allowed to use the OPcache API. With OPcache enabled, it is highly recommended to include all Nextcloud directories with <code>opcache.restrict_api</code> or unset this setting to disable OPcache API restrictions, to prevent errors during Nextcloud core or app upgrades.';
499
			}
500
		} elseif (!$isPermitted) {
501
			$recommendations[] = 'Nextcloud is not allowed to use the OPcache API. It is highly recommended to include all Nextcloud directories with <code>opcache.restrict_api</code> or unset this setting to disable OPcache API restrictions, to prevent errors during Nextcloud core or app upgrades.';
502
		} else {
503
			// Check whether opcache_get_status has been explicitly disabled an in case skip usage based checks
504
			$disabledFunctions = $this->iniGetWrapper->getString('disable_functions');
505
			if (isset($disabledFunctions) && str_contains($disabledFunctions, 'opcache_get_status')) {
506
				return [];
507
			}
508
509
			$status = opcache_get_status(false);
510
511
			// Recommend to raise value, if more than 90% of max value is reached
512
			if (
513
				empty($status['opcache_statistics']['max_cached_keys']) ||
514
				($status['opcache_statistics']['num_cached_keys'] / $status['opcache_statistics']['max_cached_keys'] > 0.9)
515
			) {
516
				$recommendations[] = 'The maximum number of OPcache keys is nearly exceeded. To assure that all scripts can be hold in cache, it is recommended to apply <code>opcache.max_accelerated_files</code> to your PHP configuration with a value higher than <code>' . ($this->iniGetWrapper->getNumeric('opcache.max_accelerated_files') ?: 'currently') . '</code>.';
517
			}
518
519
			if (
520
				empty($status['memory_usage']['free_memory']) ||
521
				($status['memory_usage']['used_memory'] / $status['memory_usage']['free_memory'] > 9)
522
			) {
523
				$recommendations[] = 'The OPcache buffer is nearly full. To assure that all scripts can be hold in cache, it is recommended to apply <code>opcache.memory_consumption</code> to your PHP configuration with a value higher than <code>' . ($this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?: 'currently') . '</code>.';
524
			}
525
526
			if (
527
				empty($status['interned_strings_usage']['free_memory']) ||
528
				($status['interned_strings_usage']['used_memory'] / $status['interned_strings_usage']['free_memory'] > 9)
529
			) {
530
				$recommendations[] = 'The OPcache interned strings buffer is nearly full. To assure that repeating strings can be effectively cached, it is recommended to apply <code>opcache.interned_strings_buffer</code> to your PHP configuration with a value higher than <code>' . ($this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') ?: 'currently') . '</code>.';
531
			}
532
		}
533
534
		return $recommendations;
535
	}
536
537
	/**
538
	 * Check if the required FreeType functions are present
539
	 * @return bool
540
	 */
541
	protected function hasFreeTypeSupport() {
542
		return function_exists('imagettfbbox') && function_exists('imagettftext');
543
	}
544
545
	protected function hasMissingIndexes(): array {
546
		$indexInfo = new MissingIndexInformation();
547
		// Dispatch event so apps can also hint for pending index updates if needed
548
		$event = new GenericEvent($indexInfo);
549
		$this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\IDBConnection::CHECK_MISSING_INDEXES_EVENT has been deprecated: 22.0.0 this is an internal event ( Ignorable by Annotation )

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

549
		$this->dispatcher->dispatch(/** @scrutinizer ignore-deprecated */ IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event);

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...
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

549
		$this->dispatcher->/** @scrutinizer ignore-call */ 
550
                     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...
550
551
		return $indexInfo->getListOfMissingIndexes();
552
	}
553
554
	protected function hasMissingPrimaryKeys(): array {
555
		$info = new MissingPrimaryKeyInformation();
556
		// Dispatch event so apps can also hint for pending index updates if needed
557
		$event = new GenericEvent($info);
558
		$this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_PRIMARY_KEYS_EVENT, $event);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\IDBConnection::CHECK...SING_PRIMARY_KEYS_EVENT has been deprecated: 22.0.0 this is an internal event ( Ignorable by Annotation )

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

558
		$this->dispatcher->dispatch(/** @scrutinizer ignore-deprecated */ IDBConnection::CHECK_MISSING_PRIMARY_KEYS_EVENT, $event);

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...
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

558
		$this->dispatcher->/** @scrutinizer ignore-call */ 
559
                     dispatch(IDBConnection::CHECK_MISSING_PRIMARY_KEYS_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...
559
560
		return $info->getListOfMissingPrimaryKeys();
561
	}
562
563
	protected function hasMissingColumns(): array {
564
		$indexInfo = new MissingColumnInformation();
565
		// Dispatch event so apps can also hint for pending index updates if needed
566
		$event = new GenericEvent($indexInfo);
567
		$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

567
		$this->dispatcher->/** @scrutinizer ignore-call */ 
568
                     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...
Deprecated Code introduced by
The constant OCP\IDBConnection::CHECK_MISSING_COLUMNS_EVENT has been deprecated: 22.0.0 this is an internal event ( Ignorable by Annotation )

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

567
		$this->dispatcher->dispatch(/** @scrutinizer ignore-deprecated */ IDBConnection::CHECK_MISSING_COLUMNS_EVENT, $event);

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...
568
569
		return $indexInfo->getListOfMissingColumns();
570
	}
571
572
	protected function isSqliteUsed() {
573
		return strpos($this->config->getSystemValue('dbtype'), 'sqlite') !== false;
574
	}
575
576
	protected function isReadOnlyConfig(): bool {
577
		return \OC_Helper::isReadOnlyConfigEnabled();
578
	}
579
580
	protected function wasEmailTestSuccessful(): bool {
581
		// Handle the case that the configuration was set before the check was introduced or it was only set via command line and not from the UI
582
		if ($this->config->getAppValue('core', 'emailTestSuccessful', '') === '' && $this->config->getSystemValue('mail_domain', '') === '') {
583
			return false;
584
		}
585
586
		// The mail test was unsuccessful or the config was changed using the UI without verifying with a testmail, hence return false
587
		if ($this->config->getAppValue('core', 'emailTestSuccessful', '') === '0') {
588
			return false;
589
		}
590
591
		return true;
592
	}
593
594
	protected function hasValidTransactionIsolationLevel(): bool {
595
		try {
596
			if ($this->db->getDatabasePlatform() instanceof SqlitePlatform) {
597
				return true;
598
			}
599
600
			return $this->db->getTransactionIsolation() === TransactionIsolationLevel::READ_COMMITTED;
601
		} catch (Exception $e) {
602
			// ignore
603
		}
604
605
		return true;
606
	}
607
608
	protected function hasFileinfoInstalled(): bool {
609
		return \OC_Util::fileInfoLoaded();
610
	}
611
612
	protected function hasWorkingFileLocking(): bool {
613
		return !($this->lockingProvider instanceof NoopLockingProvider);
614
	}
615
616
	protected function getSuggestedOverwriteCliURL(): string {
617
		$suggestedOverwriteCliUrl = '';
618
		if ($this->config->getSystemValue('overwrite.cli.url', '') === '') {
619
			$suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT;
620
			if (!$this->config->getSystemValue('config_is_read_only', false)) {
621
				// Set the overwrite URL when it was not set yet.
622
				$this->config->setSystemValue('overwrite.cli.url', $suggestedOverwriteCliUrl);
623
				$suggestedOverwriteCliUrl = '';
624
			}
625
		}
626
		return $suggestedOverwriteCliUrl;
627
	}
628
629
	protected function getLastCronInfo(): array {
630
		$lastCronRun = $this->config->getAppValue('core', 'lastcron', 0);
631
		return [
632
			'diffInSeconds' => time() - $lastCronRun,
633
			'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

633
			'relativeTime' => $this->dateTimeFormatter->formatTimeSpan(/** @scrutinizer ignore-type */ $lastCronRun),
Loading history...
634
			'backgroundJobsUrl' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'server']) . '#backgroundjobs',
635
		];
636
	}
637
638
	protected function getCronErrors() {
639
		$errors = json_decode($this->config->getAppValue('core', 'cronErrors', ''), true);
640
641
		if (is_array($errors)) {
642
			return $errors;
643
		}
644
645
		return [];
646
	}
647
648
	private function isTemporaryDirectoryWritable(): bool {
649
		try {
650
			if (!empty($this->tempManager->getTempBaseDir())) {
651
				return true;
652
			}
653
		} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
654
		}
655
		return false;
656
	}
657
658
	/**
659
	 * Iterates through the configured app roots and
660
	 * tests if the subdirectories are owned by the same user than the current user.
661
	 *
662
	 * @return array
663
	 */
664
	protected function getAppDirsWithDifferentOwner(): array {
665
		$currentUser = posix_getuid();
666
		$appDirsWithDifferentOwner = [[]];
667
668
		foreach (OC::$APPSROOTS as $appRoot) {
669
			if ($appRoot['writable'] === true) {
670
				$appDirsWithDifferentOwner[] = $this->getAppDirsWithDifferentOwnerForAppRoot($currentUser, $appRoot);
671
			}
672
		}
673
674
		$appDirsWithDifferentOwner = array_merge(...$appDirsWithDifferentOwner);
675
		sort($appDirsWithDifferentOwner);
676
677
		return $appDirsWithDifferentOwner;
678
	}
679
680
	/**
681
	 * Tests if the directories for one apps directory are writable by the current user.
682
	 *
683
	 * @param int $currentUser The current user
684
	 * @param array $appRoot The app root config
685
	 * @return string[] The none writable directory paths inside the app root
686
	 */
687
	private function getAppDirsWithDifferentOwnerForAppRoot(int $currentUser, array $appRoot): array {
688
		$appDirsWithDifferentOwner = [];
689
		$appsPath = $appRoot['path'];
690
		$appsDir = new DirectoryIterator($appRoot['path']);
691
692
		foreach ($appsDir as $fileInfo) {
693
			if ($fileInfo->isDir() && !$fileInfo->isDot()) {
694
				$absAppPath = $appsPath . DIRECTORY_SEPARATOR . $fileInfo->getFilename();
695
				$appDirUser = fileowner($absAppPath);
696
				if ($appDirUser !== $currentUser) {
697
					$appDirsWithDifferentOwner[] = $absAppPath;
698
				}
699
			}
700
		}
701
702
		return $appDirsWithDifferentOwner;
703
	}
704
705
	/**
706
	 * Checks for potential PHP modules that would improve the instance
707
	 *
708
	 * @return string[] A list of PHP modules that is recommended
709
	 */
710
	protected function hasRecommendedPHPModules(): array {
711
		$recommendedPHPModules = [];
712
713
		if (!extension_loaded('intl')) {
714
			$recommendedPHPModules[] = 'intl';
715
		}
716
717
		if (!defined('PASSWORD_ARGON2I') && PHP_VERSION_ID >= 70400) {
718
			// Installing php-sodium on >=php7.4 will provide PASSWORD_ARGON2I
719
			// on previous version argon2 wasn't part of the "standard" extension
720
			// and RedHat disabled it so even installing php-sodium won't provide argon2i
721
			// support in password_hash/password_verify.
722
			$recommendedPHPModules[] = 'sodium';
723
		}
724
725
		return $recommendedPHPModules;
726
	}
727
728
	protected function isImagickEnabled(): bool {
729
		if ($this->config->getAppValue('theming', 'enabled', 'no') === 'yes') {
730
			if (!extension_loaded('imagick')) {
731
				return false;
732
			}
733
		}
734
		return true;
735
	}
736
737
	protected function areWebauthnExtensionsEnabled(): bool {
738
		if (!extension_loaded('bcmath')) {
739
			return false;
740
		}
741
		if (!extension_loaded('gmp')) {
742
			return false;
743
		}
744
		return true;
745
	}
746
747
	protected function isMysqlUsedWithoutUTF8MB4(): bool {
748
		return ($this->config->getSystemValue('dbtype', 'sqlite') === 'mysql') && ($this->config->getSystemValue('mysql.utf8mb4', false) === false);
749
	}
750
751
	protected function hasBigIntConversionPendingColumns(): array {
752
		// copy of ConvertFilecacheBigInt::getColumnsByTable()
753
		$tables = [
754
			'activity' => ['activity_id', 'object_id'],
755
			'activity_mq' => ['mail_id'],
756
			'authtoken' => ['id'],
757
			'bruteforce_attempts' => ['id'],
758
			'federated_reshares' => ['share_id'],
759
			'filecache' => ['fileid', 'storage', 'parent', 'mimetype', 'mimepart', 'mtime', 'storage_mtime'],
760
			'filecache_extended' => ['fileid'],
761
			'file_locks' => ['id'],
762
			'jobs' => ['id'],
763
			'mimetypes' => ['id'],
764
			'mounts' => ['id', 'storage_id', 'root_id', 'mount_id'],
765
			'share_external' => ['id', 'parent'],
766
			'storages' => ['numeric_id'],
767
		];
768
769
		$schema = new SchemaWrapper($this->db);
770
		$isSqlite = $this->db->getDatabasePlatform() instanceof SqlitePlatform;
771
		$pendingColumns = [];
772
773
		foreach ($tables as $tableName => $columns) {
774
			if (!$schema->hasTable($tableName)) {
775
				continue;
776
			}
777
778
			$table = $schema->getTable($tableName);
779
			foreach ($columns as $columnName) {
780
				$column = $table->getColumn($columnName);
781
				$isAutoIncrement = $column->getAutoincrement();
782
				$isAutoIncrementOnSqlite = $isSqlite && $isAutoIncrement;
783
				if ($column->getType()->getName() !== Types::BIGINT && !$isAutoIncrementOnSqlite) {
784
					$pendingColumns[] = $tableName . '.' . $columnName;
785
				}
786
			}
787
		}
788
789
		return $pendingColumns;
790
	}
791
792
	protected function isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(): bool {
793
		$objectStore = $this->config->getSystemValue('objectstore', null);
794
		$objectStoreMultibucket = $this->config->getSystemValue('objectstore_multibucket', null);
795
796
		if (!isset($objectStoreMultibucket) && !isset($objectStore)) {
797
			return true;
798
		}
799
800
		if (isset($objectStoreMultibucket['class']) && $objectStoreMultibucket['class'] !== 'OC\\Files\\ObjectStore\\S3') {
801
			return true;
802
		}
803
804
		if (isset($objectStore['class']) && $objectStore['class'] !== 'OC\\Files\\ObjectStore\\S3') {
805
			return true;
806
		}
807
808
		$tempPath = sys_get_temp_dir();
809
		if (!is_dir($tempPath)) {
810
			$this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. value: ' . $tempPath);
811
			return false;
812
		}
813
		$freeSpaceInTemp = disk_free_space($tempPath);
814
		if ($freeSpaceInTemp === false) {
815
			$this->logger->error('Error while checking the available disk space of temporary PHP path - no free disk space returned. temporary path: ' . $tempPath);
816
			return false;
817
		}
818
819
		$freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024;
820
		if ($freeSpaceInTempInGB > 50) {
821
			return true;
822
		}
823
824
		$this->logger->warning('Checking the available space in the temporary path resulted in ' . round($freeSpaceInTempInGB, 1) . ' GB instead of the recommended 50GB. Path: ' . $tempPath);
825
		return false;
826
	}
827
828
	protected function imageMagickLacksSVGSupport(): bool {
829
		return extension_loaded('imagick') && count(\Imagick::queryFormats('SVG')) === 0;
830
	}
831
832
	/**
833
	 * @return DataResponse
834
	 * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
835
	 */
836
	public function check() {
837
		$phpDefaultCharset = new PhpDefaultCharset();
838
		$phpOutputBuffering = new PhpOutputBuffering();
839
		$legacySSEKeyFormat = new LegacySSEKeyFormat($this->l10n, $this->config, $this->urlGenerator);
840
		$checkUserCertificates = new CheckUserCertificates($this->l10n, $this->config, $this->urlGenerator);
841
		$supportedDatabases = new SupportedDatabase($this->l10n, $this->connection);
842
		$ldapInvalidUuids = new LdapInvalidUuids($this->appManager, $this->l10n, $this->serverContainer);
843
844
		return new DataResponse(
845
			[
846
				'isGetenvServerWorking' => !empty(getenv('PATH')),
847
				'isReadOnlyConfig' => $this->isReadOnlyConfig(),
848
				'hasValidTransactionIsolationLevel' => $this->hasValidTransactionIsolationLevel(),
849
				'wasEmailTestSuccessful' => $this->wasEmailTestSuccessful(),
850
				'hasFileinfoInstalled' => $this->hasFileinfoInstalled(),
851
				'hasWorkingFileLocking' => $this->hasWorkingFileLocking(),
852
				'suggestedOverwriteCliURL' => $this->getSuggestedOverwriteCliURL(),
853
				'cronInfo' => $this->getLastCronInfo(),
854
				'cronErrors' => $this->getCronErrors(),
855
				'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(),
856
				'serverHasInternetConnectionProblems' => $this->hasInternetConnectivityProblems(),
857
				'isMemcacheConfigured' => $this->isMemcacheConfigured(),
858
				'memcacheDocs' => $this->urlGenerator->linkToDocs('admin-performance'),
859
				'isRandomnessSecure' => $this->isRandomnessSecure(),
860
				'securityDocs' => $this->urlGenerator->linkToDocs('admin-security'),
861
				'isUsedTlsLibOutdated' => $this->isUsedTlsLibOutdated(),
862
				'phpSupported' => $this->isPhpSupported(),
863
				'forwardedForHeadersWorking' => $this->forwardedForHeadersWorking(),
864
				'reverseProxyDocs' => $this->urlGenerator->linkToDocs('admin-reverse-proxy'),
865
				'isCorrectMemcachedPHPModuleInstalled' => $this->isCorrectMemcachedPHPModuleInstalled(),
866
				'hasPassedCodeIntegrityCheck' => $this->checker->hasPassedCheck(),
867
				'codeIntegrityCheckerDocumentation' => $this->urlGenerator->linkToDocs('admin-code-integrity'),
868
				'OpcacheSetupRecommendations' => $this->getOpcacheSetupRecommendations(),
869
				'isSettimelimitAvailable' => $this->isSettimelimitAvailable(),
870
				'hasFreeTypeSupport' => $this->hasFreeTypeSupport(),
871
				'missingPrimaryKeys' => $this->hasMissingPrimaryKeys(),
872
				'missingIndexes' => $this->hasMissingIndexes(),
873
				'missingColumns' => $this->hasMissingColumns(),
874
				'isSqliteUsed' => $this->isSqliteUsed(),
875
				'databaseConversionDocumentation' => $this->urlGenerator->linkToDocs('admin-db-conversion'),
876
				'isMemoryLimitSufficient' => $this->memoryInfo->isMemoryLimitSufficient(),
877
				'appDirsWithDifferentOwner' => $this->getAppDirsWithDifferentOwner(),
878
				'isImagickEnabled' => $this->isImagickEnabled(),
879
				'areWebauthnExtensionsEnabled' => $this->areWebauthnExtensionsEnabled(),
880
				'recommendedPHPModules' => $this->hasRecommendedPHPModules(),
881
				'pendingBigIntConversionColumns' => $this->hasBigIntConversionPendingColumns(),
882
				'isMysqlUsedWithoutUTF8MB4' => $this->isMysqlUsedWithoutUTF8MB4(),
883
				'isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed' => $this->isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(),
884
				'reverseProxyGeneratedURL' => $this->urlGenerator->getAbsoluteURL('index.php'),
885
				'imageMagickLacksSVGSupport' => $this->imageMagickLacksSVGSupport(),
886
				PhpDefaultCharset::class => ['pass' => $phpDefaultCharset->run(), 'description' => $phpDefaultCharset->description(), 'severity' => $phpDefaultCharset->severity()],
887
				PhpOutputBuffering::class => ['pass' => $phpOutputBuffering->run(), 'description' => $phpOutputBuffering->description(), 'severity' => $phpOutputBuffering->severity()],
888
				LegacySSEKeyFormat::class => ['pass' => $legacySSEKeyFormat->run(), 'description' => $legacySSEKeyFormat->description(), 'severity' => $legacySSEKeyFormat->severity(), 'linkToDocumentation' => $legacySSEKeyFormat->linkToDocumentation()],
889
				CheckUserCertificates::class => ['pass' => $checkUserCertificates->run(), 'description' => $checkUserCertificates->description(), 'severity' => $checkUserCertificates->severity(), 'elements' => $checkUserCertificates->elements()],
890
				'isDefaultPhoneRegionSet' => $this->config->getSystemValueString('default_phone_region', '') !== '',
891
				SupportedDatabase::class => ['pass' => $supportedDatabases->run(), 'description' => $supportedDatabases->description(), 'severity' => $supportedDatabases->severity()],
892
				'temporaryDirectoryWritable' => $this->isTemporaryDirectoryWritable(),
893
				LdapInvalidUuids::class => ['pass' => $ldapInvalidUuids->run(), 'description' => $ldapInvalidUuids->description(), 'severity' => $ldapInvalidUuids->severity()],
894
			]
895
		);
896
	}
897
}
898