diff options
Diffstat (limited to 'apps/settings/lib/SetupChecks')
33 files changed, 582 insertions, 205 deletions
diff --git a/apps/settings/lib/SetupChecks/AllowedAdminRanges.php b/apps/settings/lib/SetupChecks/AllowedAdminRanges.php new file mode 100644 index 00000000000..5116676dd43 --- /dev/null +++ b/apps/settings/lib/SetupChecks/AllowedAdminRanges.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\Security\Ip\Range; +use OC\Security\Ip\RemoteAddress; +use OCP\IConfig; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class AllowedAdminRanges implements ISetupCheck { + public function __construct( + private IConfig $config, + private IL10N $l10n, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Allowed admin IP ranges'); + } + + public function run(): SetupResult { + $allowedAdminRanges = $this->config->getSystemValue(RemoteAddress::SETTING_NAME, false); + if ( + $allowedAdminRanges === false + || (is_array($allowedAdminRanges) && empty($allowedAdminRanges)) + ) { + return SetupResult::success($this->l10n->t('Admin IP filtering isn\'t applied.')); + } + + if (!is_array($allowedAdminRanges)) { + return SetupResult::error( + $this->l10n->t( + 'Configuration key "%1$s" expects an array (%2$s found). Admin IP range validation will not be applied.', + [RemoteAddress::SETTING_NAME, gettype($allowedAdminRanges)], + ) + ); + } + + $invalidRanges = array_filter($allowedAdminRanges, static fn (mixed $range): bool => !is_string($range) || !Range::isValid($range)); + if (!empty($invalidRanges)) { + return SetupResult::warning( + $this->l10n->t( + 'Configuration key "%1$s" contains invalid IP range(s): "%2$s"', + [RemoteAddress::SETTING_NAME, implode('", "', $invalidRanges)], + ), + ); + } + + return SetupResult::success($this->l10n->t('Admin IP filtering is correctly configured.')); + } +} diff --git a/apps/settings/lib/SetupChecks/CheckServerResponseTrait.php b/apps/settings/lib/SetupChecks/CheckServerResponseTrait.php deleted file mode 100644 index 734ac1ff223..00000000000 --- a/apps/settings/lib/SetupChecks/CheckServerResponseTrait.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -namespace OCA\Settings\SetupChecks; - -use Generator; -use OCP\Http\Client\IClientService; -use OCP\Http\Client\IResponse; -use OCP\IConfig; -use OCP\IL10N; -use OCP\IURLGenerator; -use Psr\Log\LoggerInterface; - -/** - * Common trait for setup checks that need to use requests to the same server and check the response - */ -trait CheckServerResponseTrait { - protected IConfig $config; - protected IURLGenerator $urlGenerator; - protected IClientService $clientService; - protected IL10N $l10n; - protected LoggerInterface $logger; - - /** - * Common helper string in case a check could not fetch any results - */ - protected function serverConfigHelp(): string { - return $this->l10n->t('To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule.'); - } - - /** - * Get all possible URLs that need to be checked for a local request test. - * This takes all `trusted_domains` and the CLI overwrite URL into account. - * - * @param string $url The relative URL to test - * @return string[] List of possible absolute URLs - */ - protected function getTestUrls(string $url): array { - $hosts = $this->config->getSystemValue('trusted_domains', []); - $cliUrl = $this->config->getSystemValue('overwrite.cli.url', ''); - if ($cliUrl !== '') { - $hosts[] = $cliUrl; - } - - $testUrls = array_merge( - [$this->urlGenerator->getAbsoluteURL($url)], - array_map(fn (string $host): string => $host . $url, $hosts), - ); - - return $testUrls; - } - - /** - * Run a HTTP request to check header - * @param string $method The HTTP method to use - * @param string $url The relative URL to check - * @param array{ignoreSSL?: bool, httpErrors?: bool, options?: array} $options Additional options, like - * [ - * // Ignore invalid SSL certificates (e.g. self signed) - * 'ignoreSSL' => true, - * // Ignore requests with HTTP errors (will not yield if request has a 4xx or 5xx response) - * 'httpErrors' => true, - * ] - * - * @return Generator<int, IResponse> - */ - protected function runRequest(string $method, string $url, array $options = []): Generator { - $options = array_merge(['ignoreSSL' => true, 'httpErrors' => true], $options); - - $client = $this->clientService->newClient(); - $requestOptions = $this->getRequestOptions($options['ignoreSSL'], $options['httpErrors']); - $requestOptions = array_merge($requestOptions, $options['options'] ?? []); - - foreach ($this->getTestUrls($url) as $testURL) { - try { - yield $client->request($method, $testURL, $requestOptions); - } catch (\Throwable $e) { - $this->logger->debug('Can not connect to local server for running setup checks', ['exception' => $e, 'url' => $testURL]); - } - } - } - - /** - * Run a HEAD request to check header - * @param string $url The relative URL to check - * @param bool $ignoreSSL Ignore SSL certificates - * @param bool $httpErrors Ignore requests with HTTP errors (will not yield if request has a 4xx or 5xx response) - * @return Generator<int, IResponse> - */ - protected function runHEAD(string $url, bool $ignoreSSL = true, bool $httpErrors = true): Generator { - return $this->runRequest('HEAD', $url, ['ignoreSSL' => $ignoreSSL, 'httpErrors' => $httpErrors]); - } - - protected function getRequestOptions(bool $ignoreSSL, bool $httpErrors): array { - $requestOptions = [ - 'connect_timeout' => 10, - 'http_errors' => $httpErrors, - 'nextcloud' => [ - 'allow_local_address' => true, - ], - ]; - if ($ignoreSSL) { - $requestOptions['verify'] = false; - } - return $requestOptions; - } -} diff --git a/apps/settings/lib/SetupChecks/CronErrors.php b/apps/settings/lib/SetupChecks/CronErrors.php index 9a5c5b8af71..dc625b04477 100644 --- a/apps/settings/lib/SetupChecks/CronErrors.php +++ b/apps/settings/lib/SetupChecks/CronErrors.php @@ -35,7 +35,7 @@ class CronErrors implements ISetupCheck { return SetupResult::error( $this->l10n->t( "It was not possible to execute the cron job via CLI. The following technical errors have appeared:\n%s", - implode("\n", array_map(fn (array $error) => '- '.$error['error'].' '.$error['hint'], $errors)) + implode("\n", array_map(fn (array $error) => '- ' . $error['error'] . ' ' . $error['hint'], $errors)) ) ); } else { diff --git a/apps/settings/lib/SetupChecks/DataDirectoryProtected.php b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php index 5afdfaaddd5..e572c345079 100644 --- a/apps/settings/lib/SetupChecks/DataDirectoryProtected.php +++ b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -40,15 +41,22 @@ class DataDirectoryProtected implements ISetupCheck { } public function run(): SetupResult { - $datadir = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValue('datadirectory', '')); - - $dataUrl = $this->urlGenerator->getWebroot() . '/' . $datadir . '/.ocdata'; + $dataDir = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValueString('datadirectory', '')); + $dataUrl = $this->urlGenerator->linkTo('', $dataDir . '/.ncdata'); $noResponse = true; - foreach ($this->runHEAD($dataUrl, httpErrors:false) as $response) { + foreach ($this->runRequest('GET', $dataUrl, [ 'httpErrors' => false ]) as $response) { $noResponse = false; - if ($response->getStatusCode() === 200) { - return SetupResult::error($this->l10n->t('Your data directory and files are probably accessible from the internet. The .htaccess file is not working. It is strongly recommended that you configure your web server so that the data directory is no longer accessible, or move the data directory outside the web server document root.')); + if ($response->getStatusCode() < 400) { + // Read the response body + $body = $response->getBody(); + if (is_resource($body)) { + $body = stream_get_contents($body, 64); + } + + if (str_contains($body, '# Nextcloud data directory')) { + return SetupResult::error($this->l10n->t('Your data directory and files are probably accessible from the internet. The .htaccess file is not working. It is strongly recommended that you configure your web server so that the data directory is no longer accessible, or move the data directory outside the web server document root.')); + } } else { $this->logger->debug('[expected] Could not access data directory from outside.', ['url' => $dataUrl]); } @@ -58,6 +66,6 @@ class DataDirectoryProtected implements ISetupCheck { return SetupResult::warning($this->l10n->t('Could not check that the data directory is protected. Please check manually that your server does not allow access to the data directory.') . "\n" . $this->serverConfigHelp()); } return SetupResult::success(); - + } } diff --git a/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php b/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php index b004c5ada35..ec004f73021 100644 --- a/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php @@ -62,10 +62,10 @@ class DatabaseHasMissingColumns implements ISetupCheck { } else { $list = ''; foreach ($missingColumns as $missingColumn) { - $list .= "\n".$this->l10n->t('Missing optional column "%s" in table "%s".', [$missingColumn['columnName'], $missingColumn['tableName']]); + $list .= "\n" . $this->l10n->t('Missing optional column "%s" in table "%s".', [$missingColumn['columnName'], $missingColumn['tableName']]); } return SetupResult::warning( - $this->l10n->t('The database is missing some optional columns. Due to the fact that adding columns on big tables could take some time they were not added automatically when they can be optional. By running "occ db:add-missing-columns" those missing columns could be added manually while the instance keeps running. Once the columns are added some features might improve responsiveness or usability.').$list + $this->l10n->t('The database is missing some optional columns. Due to the fact that adding columns on big tables could take some time they were not added automatically when they can be optional. By running "occ db:add-missing-columns" those missing columns could be added manually while the instance keeps running. Once the columns are added some features might improve responsiveness or usability.') . $list ); } } diff --git a/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php b/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php index 49e3106b017..97e80c2aaa9 100644 --- a/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php @@ -81,11 +81,11 @@ class DatabaseHasMissingIndices implements ISetupCheck { $processed++; $list .= "\n " . $this->l10n->t('"%s" in table "%s"', [$missingIndex['indexName'], $missingIndex['tableName']]); if (count($missingIndices) > $processed) { - $list .= ", "; + $list .= ', '; } } return SetupResult::warning( - $this->l10n->t('Detected some missing optional indices. Occasionally new indices are added (by Nextcloud or installed applications) to improve database performance. Adding indices can sometimes take awhile and temporarily hurt performance so this is not done automatically during upgrades. Once the indices are added, queries to those tables should be faster. Use the command `occ db:add-missing-indices` to add them. ') . $list . '.', + $this->l10n->t('Detected some missing optional indices. Occasionally new indices are added (by Nextcloud or installed applications) to improve database performance. Adding indices can sometimes take awhile and temporarily hurt performance so this is not done automatically during upgrades. Once the indices are added, queries to those tables should be faster. Use the command `occ db:add-missing-indices` to add them.') . "\n" . $list, $this->urlGenerator->linkToDocs('admin-long-running-migration-steps') ); } diff --git a/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php b/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php index 8e2a0d18e41..03810ca8faf 100644 --- a/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php @@ -62,10 +62,10 @@ class DatabaseHasMissingPrimaryKeys implements ISetupCheck { } else { $list = ''; foreach ($missingPrimaryKeys as $missingPrimaryKey) { - $list .= "\n".$this->l10n->t('Missing primary key on table "%s".', [$missingPrimaryKey['tableName']]); + $list .= "\n" . $this->l10n->t('Missing primary key on table "%s".', [$missingPrimaryKey['tableName']]); } return SetupResult::warning( - $this->l10n->t('The database is missing some primary keys. Due to the fact that adding primary keys on big tables could take some time they were not added automatically. By running "occ db:add-missing-primary-keys" those missing primary keys could be added manually while the instance keeps running.').$list + $this->l10n->t('The database is missing some primary keys. Due to the fact that adding primary keys on big tables could take some time they were not added automatically. By running "occ db:add-missing-primary-keys" those missing primary keys could be added manually while the instance keeps running.') . $list ); } } diff --git a/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php b/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php index dc9f00e8448..bb9794c1e03 100644 --- a/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php +++ b/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php @@ -74,7 +74,7 @@ class DatabasePendingBigIntConversions implements ISetupCheck { } $list .= "\n"; return SetupResult::info( - $this->l10n->t('Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running "occ db:convert-filecache-bigint" those pending changes could be applied manually. This operation needs to be made while the instance is offline.').$list, + $this->l10n->t('Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running "occ db:convert-filecache-bigint" those pending changes could be applied manually. This operation needs to be made while the instance is offline.') . $list, $this->urlGenerator->linkToDocs('admin-bigint-conversion') ); } diff --git a/apps/settings/lib/SetupChecks/EmailTestSuccessful.php b/apps/settings/lib/SetupChecks/EmailTestSuccessful.php index a8f527f9dba..8cad8e82156 100644 --- a/apps/settings/lib/SetupChecks/EmailTestSuccessful.php +++ b/apps/settings/lib/SetupChecks/EmailTestSuccessful.php @@ -46,7 +46,9 @@ class EmailTestSuccessful implements ISetupCheck { } public function run(): SetupResult { - if ($this->wasEmailTestSuccessful()) { + if ($this->config->getSystemValueString('mail_smtpmode', 'smtp') === 'null') { + return SetupResult::success($this->l10n->t('Mail delivery is disabled by instance config "%s".', ['mail_smtpmode'])); + } elseif ($this->wasEmailTestSuccessful()) { return SetupResult::success($this->l10n->t('Email test was successfully sent')); } else { // If setup check could link to settings pages, this one should link to OC.generateUrl('/settings/admin') diff --git a/apps/settings/lib/SetupChecks/FileLocking.php b/apps/settings/lib/SetupChecks/FileLocking.php index ffd7714ddc1..f683ee05f03 100644 --- a/apps/settings/lib/SetupChecks/FileLocking.php +++ b/apps/settings/lib/SetupChecks/FileLocking.php @@ -26,7 +26,7 @@ class FileLocking implements ISetupCheck { } public function getName(): string { - return $this->l10n->t('File locking'); + return $this->l10n->t('Transactional File Locking'); } public function getCategory(): string { @@ -43,8 +43,8 @@ class FileLocking implements ISetupCheck { public function run(): SetupResult { if (!$this->hasWorkingFileLocking()) { - return SetupResult::warning( - $this->l10n->t('Transactional file locking is disabled, this might lead to issues with race conditions. Enable "filelocking.enabled" in config.php to avoid these problems.'), + return SetupResult::error( + $this->l10n->t('Transactional File Locking is disabled. This is not a a supported configuraton. It may lead to difficult to isolate problems including file corruption. Please remove the `\'filelocking.enabled\' => false` configuration entry from your `config.php` to avoid these problems.'), $this->urlGenerator->linkToDocs('admin-transactional-locking') ); } diff --git a/apps/settings/lib/SetupChecks/InternetConnectivity.php b/apps/settings/lib/SetupChecks/InternetConnectivity.php index 3a0af06e71b..18f2af63b8d 100644 --- a/apps/settings/lib/SetupChecks/InternetConnectivity.php +++ b/apps/settings/lib/SetupChecks/InternetConnectivity.php @@ -41,11 +41,12 @@ class InternetConnectivity implements ISetupCheck { } $siteArray = $this->config->getSystemValue('connectivity_check_domains', [ - 'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org' + 'https://www.nextcloud.com', 'https://www.startpage.com', 'https://www.eff.org', 'https://www.edri.org' ]); foreach ($siteArray as $site) { if ($this->isSiteReachable($site)) { + // successful as soon as one connection succeeds return SetupResult::success(); } } @@ -55,19 +56,18 @@ class InternetConnectivity implements ISetupCheck { /** * Checks if the Nextcloud server can connect to a specific URL * @param string $site site domain or full URL with http/https protocol + * @return bool success/failure */ private function isSiteReachable(string $site): bool { + // if there is no protocol specified, test http:// first then, if necessary, https:// + if (preg_match('/^https?:\/\//', $site) !== 1) { + $httpSite = 'http://' . $site . '/'; + $httpsSite = 'https://' . $site . '/'; + return $this->isSiteReachable($httpSite) || $this->isSiteReachable($httpsSite); + } try { $client = $this->clientService->newClient(); - // if there is no protocol, test http:// AND https:// - if (preg_match('/^https?:\/\//', $site) !== 1) { - $httpSite = 'http://' . $site . '/'; - $client->get($httpSite); - $httpsSite = 'https://' . $site . '/'; - $client->get($httpsSite); - } else { - $client->get($site); - } + $client->get($site); } catch (\Exception $e) { $this->logger->error('Cannot connect to: ' . $site, [ 'app' => 'internet_connection_check', diff --git a/apps/settings/lib/SetupChecks/JavaScriptModules.php b/apps/settings/lib/SetupChecks/JavaScriptModules.php index ae19eacec7b..72f58405811 100644 --- a/apps/settings/lib/SetupChecks/JavaScriptModules.php +++ b/apps/settings/lib/SetupChecks/JavaScriptModules.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -43,7 +44,7 @@ class JavaScriptModules implements ISetupCheck { $testFile = $this->urlGenerator->linkTo('settings', 'js/esm-test.mjs'); $noResponse = true; - foreach ($this->runHEAD($testFile) as $response) { + foreach ($this->runRequest('HEAD', $testFile) as $response) { $noResponse = false; if (preg_match('/(text|application)\/javascript/i', $response->getHeader('Content-Type'))) { return SetupResult::success(); @@ -54,6 +55,6 @@ class JavaScriptModules implements ISetupCheck { return SetupResult::warning($this->l10n->t('Unable to run check for JavaScript support. Please remedy or confirm manually if your webserver serves `.mjs` files using the JavaScript MIME type.') . "\n" . $this->serverConfigHelp()); } return SetupResult::error($this->l10n->t('Your webserver does not serve `.mjs` files using the JavaScript MIME type. This will break some apps by preventing browsers from executing the JavaScript files. You should configure your webserver to serve `.mjs` files with either the `text/javascript` or `application/javascript` MIME type.')); - + } } diff --git a/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php b/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php index 85cbe872339..dcfc40192b9 100644 --- a/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php +++ b/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -42,7 +43,7 @@ class JavaScriptSourceMaps implements ISetupCheck { public function run(): SetupResult { $testFile = $this->urlGenerator->linkTo('settings', 'js/map-test.js.map'); - foreach ($this->runHEAD($testFile) as $response) { + foreach ($this->runRequest('HEAD', $testFile) as $response) { return SetupResult::success(); } diff --git a/apps/settings/lib/SetupChecks/LoggingLevel.php b/apps/settings/lib/SetupChecks/LoggingLevel.php new file mode 100644 index 00000000000..b9e1dbe700d --- /dev/null +++ b/apps/settings/lib/SetupChecks/LoggingLevel.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class LoggingLevel implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('Logging level'); + } + + public function getCategory(): string { + return 'system'; + } + + public function run(): SetupResult { + $configLogLevel = $this->config->getSystemValue('loglevel', ILogger::WARN); + if (!is_int($configLogLevel) + || $configLogLevel < ILogger::DEBUG + || $configLogLevel > ILogger::FATAL + ) { + return SetupResult::error( + $this->l10n->t('The %1$s configuration option must be a valid integer value.', ['`loglevel`']), + $this->urlGenerator->linkToDocs('admin-logging'), + ); + } + + if ($configLogLevel === ILogger::DEBUG) { + return SetupResult::warning( + $this->l10n->t('The logging level is set to debug level. Use debug level only when you have a problem to diagnose, and then reset your log level to a less-verbose level as it outputs a lot of information, and can affect your server performance.'), + $this->urlGenerator->linkToDocs('admin-logging'), + ); + } + + return SetupResult::success($this->l10n->t('Logging level configured correctly.')); + } +} diff --git a/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php b/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php index 48e5fa2dc49..ca8df039b1e 100644 --- a/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php +++ b/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php @@ -40,7 +40,7 @@ class MaintenanceWindowStart implements ISetupCheck { ); } - $startValue = (int) $configValue; + $startValue = (int)$configValue; $endValue = ($startValue + 6) % 24; return SetupResult::success( str_replace( diff --git a/apps/settings/lib/SetupChecks/MemcacheConfigured.php b/apps/settings/lib/SetupChecks/MemcacheConfigured.php index 03cdc91cb5f..e3601d428bb 100644 --- a/apps/settings/lib/SetupChecks/MemcacheConfigured.php +++ b/apps/settings/lib/SetupChecks/MemcacheConfigured.php @@ -8,6 +8,8 @@ declare(strict_types=1); */ namespace OCA\Settings\SetupChecks; +use OC\Memcache\Memcached; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; @@ -19,6 +21,7 @@ class MemcacheConfigured implements ISetupCheck { private IL10N $l10n, private IConfig $config, private IURLGenerator $urlGenerator, + private ICacheFactory $cacheFactory, ) { } @@ -35,7 +38,7 @@ class MemcacheConfigured implements ISetupCheck { $memcacheLockingClass = $this->config->getSystemValue('memcache.locking', null); $memcacheLocalClass = $this->config->getSystemValue('memcache.local', null); $caches = array_filter([$memcacheDistributedClass,$memcacheLockingClass,$memcacheLocalClass]); - if (in_array(\OC\Memcache\Memcached::class, array_map(fn (string $class) => ltrim($class, '\\'), $caches))) { + if (in_array(Memcached::class, array_map(fn (string $class) => ltrim($class, '\\'), $caches))) { // wrong PHP module is installed if (extension_loaded('memcache') && !extension_loaded('memcached')) { return SetupResult::warning( @@ -55,6 +58,41 @@ class MemcacheConfigured implements ISetupCheck { $this->urlGenerator->linkToDocs('admin-performance') ); } + + if ($this->cacheFactory->isLocalCacheAvailable()) { + $random = bin2hex(random_bytes(64)); + $local = $this->cacheFactory->createLocal('setupcheck.local'); + try { + $local->set('test', $random); + $local2 = $this->cacheFactory->createLocal('setupcheck.local'); + $actual = $local2->get('test'); + $local->remove('test'); + } catch (\Throwable) { + $actual = null; + } + + if ($actual !== $random) { + return SetupResult::error($this->l10n->t('Failed to write and read a value from local cache.')); + } + } + + if ($this->cacheFactory->isAvailable()) { + $random = bin2hex(random_bytes(64)); + $distributed = $this->cacheFactory->createDistributed('setupcheck'); + try { + $distributed->set('test', $random); + $distributed2 = $this->cacheFactory->createDistributed('setupcheck'); + $actual = $distributed2->get('test'); + $distributed->remove('test'); + } catch (\Throwable) { + $actual = null; + } + + if ($actual !== $random) { + return SetupResult::error($this->l10n->t('Failed to write and read a value from distributed cache.')); + } + } + return SetupResult::success($this->l10n->t('Configured')); } } diff --git a/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php b/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php index 98c8eacbfda..cf237f68670 100644 --- a/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php +++ b/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php @@ -10,18 +10,15 @@ namespace OCA\Settings\SetupChecks; use OC\Repair\RepairMimeTypes; use OCP\IL10N; -use OCP\L10N\IFactory; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; class MimeTypeMigrationAvailable implements ISetupCheck { - private IL10N $l10n; public function __construct( - IFactory $l10nFactory, private RepairMimeTypes $repairMimeTypes, + private IL10N $l10n, ) { - $this->l10n = $l10nFactory->get('core'); } public function getCategory(): string { diff --git a/apps/settings/lib/SetupChecks/MysqlRowFormat.php b/apps/settings/lib/SetupChecks/MysqlRowFormat.php new file mode 100644 index 00000000000..3c27b73db89 --- /dev/null +++ b/apps/settings/lib/SetupChecks/MysqlRowFormat.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use Doctrine\DBAL\Platforms\MySQLPlatform; +use OC\DB\Connection; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class MysqlRowFormat implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private Connection $connection, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('MySQL row format'); + } + + public function getCategory(): string { + return 'database'; + } + + public function run(): SetupResult { + if (!$this->connection->getDatabasePlatform() instanceof MySQLPlatform) { + return SetupResult::success($this->l10n->t('You are not using MySQL')); + } + + $wrongRowFormatTables = $this->getRowNotDynamicTables(); + if (empty($wrongRowFormatTables)) { + return SetupResult::success($this->l10n->t('None of your tables use ROW_FORMAT=Compressed')); + } + + return SetupResult::warning( + $this->l10n->t( + 'Incorrect row format found in your database. ROW_FORMAT=Dynamic offers the best database performances for Nextcloud. Please update row format on the following list: %s.', + [implode(', ', $wrongRowFormatTables)], + ), + 'https://dev.mysql.com/doc/refman/en/innodb-row-format.html', + ); + } + + /** + * @return string[] + */ + private function getRowNotDynamicTables(): array { + $sql = "SELECT table_name + FROM information_schema.tables + WHERE table_schema = ? + AND table_name LIKE '*PREFIX*%' + AND row_format != 'Dynamic';"; + + return $this->connection->executeQuery( + $sql, + [$this->config->getSystemValueString('dbname')], + )->fetchFirstColumn(); + } +} diff --git a/apps/settings/lib/SetupChecks/OcxProviders.php b/apps/settings/lib/SetupChecks/OcxProviders.php index ecb8ecd6609..c53e8087bd9 100644 --- a/apps/settings/lib/SetupChecks/OcxProviders.php +++ b/apps/settings/lib/SetupChecks/OcxProviders.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -51,7 +52,7 @@ class OcxProviders implements ISetupCheck { ]; foreach ($providers as $provider) { - foreach ($this->runRequest('HEAD', $this->urlGenerator->getWebroot() . $provider, ['httpErrors' => false]) as $response) { + foreach ($this->runRequest('HEAD', $provider, ['httpErrors' => false]) as $response) { $testedProviders[$provider] = true; if ($response->getStatusCode() === 200) { $workingProviders[] = $provider; @@ -75,7 +76,7 @@ class OcxProviders implements ISetupCheck { $this->l10n->t('Your web server is not properly set up to resolve %1$s. This is most likely related to a web server configuration that was not updated to deliver this folder directly. Please compare your configuration against the shipped rewrite rules in ".htaccess" for Apache or the provided one in the documentation for Nginx. -On Nginx those are typically the lines starting with "location ~" that need an update.', [join(', ', array_map(fn ($s) => '"'.$s.'"', $missingProviders))]), +On Nginx those are typically the lines starting with "location ~" that need an update.', [join(', ', array_map(fn ($s) => '"' . $s . '"', $missingProviders))]), $this->urlGenerator->linkToDocs('admin-nginx'), ); } diff --git a/apps/settings/lib/SetupChecks/PhpApcuConfig.php b/apps/settings/lib/SetupChecks/PhpApcuConfig.php new file mode 100644 index 00000000000..c91a8cefec1 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpApcuConfig.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OC\Memcache\APCu; +use OCP\IConfig; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpApcuConfig implements ISetupCheck { + public const USAGE_RATE_WARNING = 90; + public const AGE_WARNING = 3600 * 8; + + public function __construct( + private IL10N $l10n, + private IConfig $config, + ) { + } + + public function getCategory(): string { + return 'php'; + } + + public function getName(): string { + return $this->l10n->t('PHP APCu configuration'); + } + + public function run(): SetupResult { + $localIsApcu = ltrim($this->config->getSystemValueString('memcache.local'), '\\') === APCu::class; + $distributedIsApcu = ltrim($this->config->getSystemValueString('memcache.distributed'), '\\') === APCu::class; + if (!$localIsApcu && !$distributedIsApcu) { + return SetupResult::success(); + } + + if (!APCu::isAvailable()) { + return SetupResult::success(); + } + + $cache = apcu_cache_info(true); + $mem = apcu_sma_info(true); + if ($cache === false || $mem === false) { + return SetupResult::success(); + } + + $expunges = $cache['expunges']; + $memSize = $mem['num_seg'] * $mem['seg_size']; + $memAvailable = $mem['avail_mem']; + $memUsed = $memSize - $memAvailable; + $usageRate = round($memUsed / $memSize * 100, 0); + $elapsed = max(time() - $cache['start_time'], 1); + + if ($expunges > 0 && $elapsed < self::AGE_WARNING) { + return SetupResult::warning($this->l10n->t('Your APCu cache has been running full, consider increasing the apc.shm_size php setting.')); + } + + if ($usageRate > self::USAGE_RATE_WARNING) { + return SetupResult::warning($this->l10n->t('Your APCu cache is almost full at %s%%, consider increasing the apc.shm_size php setting.', [$usageRate])); + } + + return SetupResult::success(); + } +} diff --git a/apps/settings/lib/SetupChecks/PhpMaxFileSize.php b/apps/settings/lib/SetupChecks/PhpMaxFileSize.php new file mode 100644 index 00000000000..d81cbe6d45c --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpMaxFileSize.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use bantu\IniGetWrapper\IniGetWrapper; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; +use OCP\Util; + +class PhpMaxFileSize implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IniGetWrapper $iniGetWrapper, + ) { + } + + public function getCategory(): string { + return 'php'; + } + + public function getName(): string { + return $this->l10n->t('PHP file size upload limit'); + } + + public function run(): SetupResult { + $upload_max_filesize = (string)$this->iniGetWrapper->getString('upload_max_filesize'); + $post_max_size = (string)$this->iniGetWrapper->getString('post_max_size'); + $max_input_time = (int)$this->iniGetWrapper->getString('max_input_time'); + $max_execution_time = (int)$this->iniGetWrapper->getString('max_execution_time'); + + $warnings = []; + $recommendedSize = 16 * 1024 * 1024 * 1024; + $recommendedTime = 3600; + + // Check if the PHP upload limit is too low + if (Util::computerFileSize($upload_max_filesize) < $recommendedSize) { + $warnings[] = $this->l10n->t('The PHP upload_max_filesize is too low. A size of at least %1$s is recommended. Current value: %2$s.', [ + Util::humanFileSize($recommendedSize), + $upload_max_filesize, + ]); + } + if (Util::computerFileSize($post_max_size) < $recommendedSize) { + $warnings[] = $this->l10n->t('The PHP post_max_size is too low. A size of at least %1$s is recommended. Current value: %2$s.', [ + Util::humanFileSize($recommendedSize), + $post_max_size, + ]); + } + + // Check if the PHP execution time is too low + if ($max_input_time < $recommendedTime && $max_input_time !== -1) { + $warnings[] = $this->l10n->t('The PHP max_input_time is too low. A time of at least %1$s is recommended. Current value: %2$s.', [ + $recommendedTime, + $max_input_time, + ]); + } + + if ($max_execution_time < $recommendedTime && $max_execution_time !== -1) { + $warnings[] = $this->l10n->t('The PHP max_execution_time is too low. A time of at least %1$s is recommended. Current value: %2$s.', [ + $recommendedTime, + $max_execution_time, + ]); + } + + if (!empty($warnings)) { + return SetupResult::warning(join(' ', $warnings), $this->urlGenerator->linkToDocs('admin-big-file-upload')); + } + + return SetupResult::success(); + } +} diff --git a/apps/settings/lib/SetupChecks/PhpMemoryLimit.php b/apps/settings/lib/SetupChecks/PhpMemoryLimit.php index 86bb6defbec..7b693169f10 100644 --- a/apps/settings/lib/SetupChecks/PhpMemoryLimit.php +++ b/apps/settings/lib/SetupChecks/PhpMemoryLimit.php @@ -34,7 +34,7 @@ class PhpMemoryLimit implements ISetupCheck { if ($this->memoryInfo->isMemoryLimitSufficient()) { return SetupResult::success(Util::humanFileSize($this->memoryInfo->getMemoryLimit())); } else { - return SetupResult::error($this->l10n->t('The PHP memory limit is below the recommended value of %s.', Util::humanFileSize(MemoryInfo::RECOMMENDED_MEMORY_LIMIT))); + return SetupResult::error($this->l10n->t('The PHP memory limit is below the recommended value of %s. Some features or apps - including the Updater - may not function properly.', Util::humanFileSize(MemoryInfo::RECOMMENDED_MEMORY_LIMIT))); } } } diff --git a/apps/settings/lib/SetupChecks/PhpModules.php b/apps/settings/lib/SetupChecks/PhpModules.php index 6d4246ffdae..b0b4f106f4a 100644 --- a/apps/settings/lib/SetupChecks/PhpModules.php +++ b/apps/settings/lib/SetupChecks/PhpModules.php @@ -32,7 +32,6 @@ class PhpModules implements ISetupCheck { 'zlib', ]; protected const RECOMMENDED_MODULES = [ - 'bcmath', 'exif', 'gmp', 'intl', @@ -58,8 +57,7 @@ class PhpModules implements ISetupCheck { return match($module) { 'intl' => $this->l10n->t('increases language translation performance and fixes sorting of non-ASCII characters'), 'sodium' => $this->l10n->t('for Argon2 for password hashing'), - 'bcmath' => $this->l10n->t('for WebAuthn passwordless login'), - 'gmp' => $this->l10n->t('for WebAuthn passwordless login, and SFTP storage'), + 'gmp' => $this->l10n->t('required for SFTP storage and recommended for WebAuthn performance'), 'exif' => $this->l10n->t('for picture rotation in server and metadata extraction in the Photos app'), default => '', }; @@ -77,7 +75,7 @@ class PhpModules implements ISetupCheck { $moduleList = implode( "\n", array_map( - fn (string $module) => '- '.$module.' '.$this->getRecommendedModuleDescription($module), + fn (string $module) => '- ' . $module . ' ' . $this->getRecommendedModuleDescription($module), $missingRecommendedModules ) ); diff --git a/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php b/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php index e3cf4659dbe..83b7be1c390 100644 --- a/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php +++ b/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php @@ -57,7 +57,7 @@ class PhpOpcacheSetup implements ISetupCheck { } elseif ($this->iniGetWrapper->getBool('opcache.file_cache_only')) { $recommendations[] = $this->l10n->t('The shared memory based OPcache is disabled. For better performance, it is recommended to apply "opcache.file_cache_only=0" to your PHP configuration and use the file cache as second level cache only.'); } else { - // Check whether opcache_get_status has been explicitly disabled an in case skip usage based checks + // Check whether opcache_get_status has been explicitly disabled and in case skip usage based checks $disabledFunctions = $this->iniGetWrapper->getString('disable_functions'); if (isset($disabledFunctions) && str_contains($disabledFunctions, 'opcache_get_status')) { return [$level, $recommendations]; @@ -70,29 +70,27 @@ class PhpOpcacheSetup implements ISetupCheck { $level = 'error'; } - // Recommend to raise value, if more than 90% of max value is reached - if ( - empty($status['opcache_statistics']['max_cached_keys']) || - ($status['opcache_statistics']['num_cached_keys'] / $status['opcache_statistics']['max_cached_keys'] > 0.9) - ) { - $recommendations[] = $this->l10n->t('The maximum number of OPcache keys is nearly exceeded. To assure that all scripts can be kept in the cache, it is recommended to apply "opcache.max_accelerated_files" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.max_accelerated_files') ?: 'currently')]); - } - - if ( - empty($status['memory_usage']['free_memory']) || - ($status['memory_usage']['used_memory'] / $status['memory_usage']['free_memory'] > 9) - ) { - $recommendations[] = $this->l10n->t('The OPcache buffer is nearly full. To assure that all scripts can be hold in cache, it is recommended to apply "opcache.memory_consumption" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?: 'currently')]); + // Check whether OPcache is full, which can be either the overall OPcache size or limit of cached keys reached. + // If the limit of cached keys has been reached, num_cached_keys equals max_cached_keys. The recommendation contains this value instead of opcache.max_accelerated_files, since the effective limit is a next higher prime number: https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.max-accelerated-files + // Else, the remaining $status['memory_usage']['free_memory'] was too low to store another script. Aside of used_memory, this can be also due to wasted_memory, remaining cache keys from scripts changed on disk. + // Wasted memory is cleared only via opcache_reset(), or if $status['memory_usage']['current_wasted_percentage'] reached opcache.max_wasted_percentage, which triggers an engine restart and hence OPcache reset. Due to this complexity, we check for $status['cache_full'] only. + if ($status['cache_full'] === true) { + if ($status['opcache_statistics']['num_cached_keys'] === $status['opcache_statistics']['max_cached_keys']) { + $recommendations[] = $this->l10n->t('The maximum number of OPcache keys is nearly exceeded. To assure that all scripts can be kept in the cache, it is recommended to apply "opcache.max_accelerated_files" to your PHP configuration with a value higher than "%s".', [($status['opcache_statistics']['max_cached_keys'] ?: 'currently')]); + } else { + $recommendations[] = $this->l10n->t('The OPcache buffer is nearly full. To assure that all scripts can be hold in cache, it is recommended to apply "opcache.memory_consumption" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?: 'currently')]); + } } + // Interned strings buffer: recommend to raise size if more than 90% is used $interned_strings_buffer = $this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') ?? 0; $memory_consumption = $this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?? 0; if ( // Do not recommend to raise the interned strings buffer size above a quarter of the total OPcache size - ($interned_strings_buffer < ($memory_consumption / 4)) && - ( - empty($status['interned_strings_usage']['free_memory']) || - ($status['interned_strings_usage']['used_memory'] / $status['interned_strings_usage']['free_memory'] > 9) + ($interned_strings_buffer < ($memory_consumption / 4)) + && ( + empty($status['interned_strings_usage']['free_memory']) + || ($status['interned_strings_usage']['used_memory'] / $status['interned_strings_usage']['free_memory'] > 9) ) ) { $recommendations[] = $this->l10n->t('The OPcache interned strings buffer is nearly full. To assure that repeating strings can be effectively cached, it is recommended to apply "opcache.interned_strings_buffer" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') ?: 'currently')]); @@ -114,6 +112,11 @@ class PhpOpcacheSetup implements ISetupCheck { } public function run(): SetupResult { + // Skip OPcache checks if running from CLI + if (\OC::$CLI && !$this->iniGetWrapper->getBool('opcache.enable_cli')) { + return SetupResult::success($this->l10n->t('Checking from CLI, OPcache checks have been skipped.')); + } + [$level,$recommendations] = $this->getOpcacheSetupRecommendations(); if (!empty($recommendations)) { return match($level) { diff --git a/apps/settings/lib/SetupChecks/PhpOutdated.php b/apps/settings/lib/SetupChecks/PhpOutdated.php index 4c7ed5096c0..d0d8e03c705 100644 --- a/apps/settings/lib/SetupChecks/PhpOutdated.php +++ b/apps/settings/lib/SetupChecks/PhpOutdated.php @@ -14,6 +14,11 @@ use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; class PhpOutdated implements ISetupCheck { + public const DEPRECATED_PHP_VERSION = '8.1'; + public const DEPRECATED_SINCE = '30'; + public const FUTURE_REQUIRED_PHP_VERSION = '8.2'; + public const FUTURE_REQUIRED_STARTING = '32'; + public function __construct( private IL10N $l10n, ) { @@ -29,7 +34,13 @@ class PhpOutdated implements ISetupCheck { public function run(): SetupResult { if (PHP_VERSION_ID < 80200) { - return SetupResult::warning($this->l10n->t('You are currently running PHP %s. PHP 8.1 is now deprecated in Nextcloud 30. Nextcloud 31 may require at least PHP 8.2. Please upgrade to one of the officially supported PHP versions provided by the PHP Group as soon as possible.', [PHP_VERSION]), 'https://secure.php.net/supported-versions.php'); + return SetupResult::warning($this->l10n->t('You are currently running PHP %1$s. PHP %2$s is deprecated since Nextcloud %3$s. Nextcloud %4$s may require at least PHP %5$s. Please upgrade to one of the officially supported PHP versions provided by the PHP Group as soon as possible.', [ + PHP_VERSION, + self::DEPRECATED_PHP_VERSION, + self::DEPRECATED_SINCE, + self::FUTURE_REQUIRED_STARTING, + self::FUTURE_REQUIRED_PHP_VERSION, + ]), 'https://secure.php.net/supported-versions.php'); } return SetupResult::success($this->l10n->t('You are currently running PHP %s.', [PHP_VERSION])); } diff --git a/apps/settings/lib/SetupChecks/PushService.php b/apps/settings/lib/SetupChecks/PushService.php index a3ac921caef..1f03404d80e 100644 --- a/apps/settings/lib/SetupChecks/PushService.php +++ b/apps/settings/lib/SetupChecks/PushService.php @@ -38,7 +38,7 @@ class PushService implements ISetupCheck { * Check if is fair use of free push service */ private function isFairUseOfFreePushService(): bool { - $rateLimitReached = (int) $this->config->getAppValue('notifications', 'rate_limit_reached', '0'); + $rateLimitReached = (int)$this->config->getAppValue('notifications', 'rate_limit_reached', '0'); if ($rateLimitReached >= ($this->timeFactory->now()->getTimestamp() - 7 * 24 * 3600)) { // Notifications app is showing a message already return true; diff --git a/apps/settings/lib/SetupChecks/SchedulingTableSize.php b/apps/settings/lib/SetupChecks/SchedulingTableSize.php index cf629f5b12c..b23972ca7dc 100644 --- a/apps/settings/lib/SetupChecks/SchedulingTableSize.php +++ b/apps/settings/lib/SetupChecks/SchedulingTableSize.php @@ -14,6 +14,8 @@ use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; class SchedulingTableSize implements ISetupCheck { + public const MAX_SCHEDULING_ENTRIES = 50000; + public function __construct( private IL10N $l10n, private IDBConnection $connection, @@ -36,9 +38,11 @@ class SchedulingTableSize implements ISetupCheck { $count = $query->fetchOne(); $query->closeCursor(); - if ($count > 500000) { + if ($count > self::MAX_SCHEDULING_ENTRIES) { return SetupResult::warning( - $this->l10n->t('You have more than 500 000 rows in the scheduling objects table. Please run the expensive repair jobs via occ maintenance:repair --include-expensive') + $this->l10n->t('You have more than %s rows in the scheduling objects table. Please run the expensive repair jobs via occ maintenance:repair --include-expensive.', [ + self::MAX_SCHEDULING_ENTRIES, + ]) ); } return SetupResult::success( diff --git a/apps/settings/lib/SetupChecks/SecurityHeaders.php b/apps/settings/lib/SetupChecks/SecurityHeaders.php index f00e92ef496..9cc6856a170 100644 --- a/apps/settings/lib/SetupChecks/SecurityHeaders.php +++ b/apps/settings/lib/SetupChecks/SecurityHeaders.php @@ -13,6 +13,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -64,18 +65,13 @@ class SecurityHeaders implements ISetupCheck { $value = preg_replace('/,\s+/', ',', strtolower($response->getHeader($header))); if ($value !== $expected) { if ($accepted !== null && $value === $accepted) { - $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. Some features might not work correctly, as it is recommended to adjust this setting accordingly.', [$header, $expected])."\n"; + $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. Some features might not work correctly, as it is recommended to adjust this setting accordingly.', [$header, $expected]) . "\n"; } else { - $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.', [$header, $expected])."\n"; + $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.', [$header, $expected]) . "\n"; } } } - $xssfields = array_map('trim', explode(';', $response->getHeader('X-XSS-Protection'))); - if (!in_array('1', $xssfields) || !in_array('mode=block', $xssfields)) { - $msg .= $this->l10n->t('- The `%1$s` HTTP header does not contain `%2$s`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.', ['X-XSS-Protection', '1; mode=block'])."\n"; - } - $referrerPolicy = $response->getHeader('Referrer-Policy'); if (!preg_match('/(no-referrer(-when-downgrade)?|strict-origin(-when-cross-origin)?|same-origin)(,|$)/', $referrerPolicy)) { $msg .= $this->l10n->t( @@ -88,7 +84,7 @@ class SecurityHeaders implements ISetupCheck { 'strict-origin-when-cross-origin', 'same-origin', ] - )."\n"; + ) . "\n"; $msgParameters['w3c-recommendation'] = [ 'type' => 'highlight', 'id' => 'w3c-recommendation', @@ -102,17 +98,17 @@ class SecurityHeaders implements ISetupCheck { if (preg_match('/^max-age=(\d+)(;.*)?$/', $transportSecurityValidity, $m)) { $transportSecurityValidity = (int)$m[1]; if ($transportSecurityValidity < $minimumSeconds) { - $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set to at least `%d` seconds (current value: `%d`). For enhanced security, it is recommended to use a long HSTS policy.', [$minimumSeconds, $transportSecurityValidity])."\n"; + $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set to at least `%d` seconds (current value: `%d`). For enhanced security, it is recommended to use a long HSTS policy.', [$minimumSeconds, $transportSecurityValidity]) . "\n"; } } elseif (!empty($transportSecurityValidity)) { - $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is malformed: `%s`. For enhanced security, it is recommended to enable HSTS.', [$transportSecurityValidity])."\n"; + $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is malformed: `%s`. For enhanced security, it is recommended to enable HSTS.', [$transportSecurityValidity]) . "\n"; } else { - $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set (should be at least `%d` seconds). For enhanced security, it is recommended to enable HSTS.', [$minimumSeconds])."\n"; + $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set (should be at least `%d` seconds). For enhanced security, it is recommended to enable HSTS.', [$minimumSeconds]) . "\n"; } if (!empty($msg)) { return SetupResult::warning( - $this->l10n->t('Some headers are not set correctly on your instance')."\n".$msg, + $this->l10n->t('Some headers are not set correctly on your instance') . "\n" . $msg, $this->urlGenerator->linkToDocs('admin-security'), $msgParameters, ); @@ -131,7 +127,7 @@ class SecurityHeaders implements ISetupCheck { // Otherwise if we fail we can abort here if ($works === false) { return SetupResult::warning( - $this->l10n->t("Could not check that your web server serves security headers correctly, unable to query `%s`", [$url]), + $this->l10n->t('Could not check that your web server serves security headers correctly, unable to query `%s`', [$url]), $this->urlGenerator->linkToDocs('admin-security'), ); } diff --git a/apps/settings/lib/SetupChecks/SupportedDatabase.php b/apps/settings/lib/SetupChecks/SupportedDatabase.php index 89f78948305..d083958d16e 100644 --- a/apps/settings/lib/SetupChecks/SupportedDatabase.php +++ b/apps/settings/lib/SetupChecks/SupportedDatabase.php @@ -21,9 +21,11 @@ use OCP\SetupCheck\SetupResult; class SupportedDatabase implements ISetupCheck { private const MIN_MARIADB = '10.6'; - private const MAX_MARIADB = '11.4'; + private const MAX_MARIADB = '11.8'; private const MIN_MYSQL = '8.0'; private const MAX_MYSQL = '8.4'; + private const MIN_POSTGRES = '13'; + private const MAX_POSTGRES = '17'; public function __construct( private IL10N $l10n, @@ -98,8 +100,16 @@ class SupportedDatabase implements ISetupCheck { // we only care about X not X.Y or X.Y.Z differences [$major, ] = explode('.', $versionlc); $versionConcern = $major; - if (version_compare($versionConcern, '12', '<') || version_compare($versionConcern, '16', '>')) { - return SetupResult::warning($this->l10n->t('PostgreSQL version "%s" detected. PostgreSQL >=12 and <=16 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version)); + if (version_compare($versionConcern, self::MIN_POSTGRES, '<') || version_compare($versionConcern, self::MAX_POSTGRES, '>')) { + return SetupResult::warning( + $this->l10n->t( + 'PostgreSQL version "%1$s" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + $version, + self::MIN_POSTGRES, + self::MAX_POSTGRES, + ]) + ); } } elseif ($databasePlatform instanceof OraclePlatform) { $version = 'Oracle'; diff --git a/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php new file mode 100644 index 00000000000..83168ac0f3e --- /dev/null +++ b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; +use OCP\TaskProcessing\IManager; + +class TaskProcessingPickupSpeed implements ISetupCheck { + public const MAX_SLOW_PERCENTAGE = 0.2; + public const TIME_SPAN = 24; + + public function __construct( + private IL10N $l10n, + private IManager $taskProcessingManager, + private ITimeFactory $timeFactory, + ) { + } + + public function getCategory(): string { + return 'ai'; + } + + public function getName(): string { + return $this->l10n->t('Task Processing pickup speed'); + } + + public function run(): SetupResult { + $tasks = $this->taskProcessingManager->getTasks(userId: '', scheduleAfter: $this->timeFactory->now()->getTimestamp() - 60 * 60 * self::TIME_SPAN); // userId: '' means no filter, whereas null would mean guest + $taskCount = count($tasks); + if ($taskCount === 0) { + return SetupResult::success($this->l10n->n('No scheduled tasks in the last %n hour.', 'No scheduled tasks in the last %n hours.', self::TIME_SPAN)); + } + $slowCount = 0; + foreach ($tasks as $task) { + if ($task->getStartedAt() === null) { + continue; // task was not picked up yet + } + if ($task->getScheduledAt() === null) { + continue; // task was not scheduled yet -- should not happen, but the API specifies null as return value + } + $pickupDelay = $task->getScheduledAt() - $task->getStartedAt(); + if ($pickupDelay > 60 * 4) { + $slowCount++; // task pickup took longer than 4 minutes + } + } + + if ($slowCount / $taskCount < self::MAX_SLOW_PERCENTAGE) { + return SetupResult::success($this->l10n->n('The task pickup speed has been ok in the last %n hour.', 'The task pickup speed has been ok in the last %n hours.', self::TIME_SPAN)); + } else { + return SetupResult::warning($this->l10n->n('The task pickup speed has been slow in the last %n hour. Many tasks took longer than 4 minutes to be picked up. Consider setting up a worker to process tasks in the background.', 'The task pickup speed has been slow in the last %n hours. Many tasks took longer than 4 minutes to be picked up. Consider setting up a worker to process tasks in the background.', self::TIME_SPAN), 'https://docs.nextcloud.com/server/latest/admin_manual/ai/overview.html#improve-ai-task-pickup-speed'); + } + } +} diff --git a/apps/settings/lib/SetupChecks/TempSpaceAvailable.php b/apps/settings/lib/SetupChecks/TempSpaceAvailable.php index ef51ffe0e07..49dc0d377e7 100644 --- a/apps/settings/lib/SetupChecks/TempSpaceAvailable.php +++ b/apps/settings/lib/SetupChecks/TempSpaceAvailable.php @@ -85,7 +85,7 @@ class TempSpaceAvailable implements ISetupCheck { return SetupResult::error($this->l10n->t('Error while checking the available disk space of temporary PHP path or no free disk space returned. Temporary path: %s', [$nextcloudTempPath])); } $freeSpaceInNextcloudTempInGB = $freeSpaceInNextcloudTemp / 1024 / 1024 / 1024; - $spaceDetail .= "\n".$this->l10n->t('- %.1f GiB available in %s (Nextcloud temporary directory)', [round($freeSpaceInNextcloudTempInGB, 1),$nextcloudTempPath]); + $spaceDetail .= "\n" . $this->l10n->t('- %.1f GiB available in %s (Nextcloud temporary directory)', [round($freeSpaceInNextcloudTempInGB, 1),$nextcloudTempPath]); } if (!$this->isPrimaryStorageS3()) { diff --git a/apps/settings/lib/SetupChecks/WellKnownUrls.php b/apps/settings/lib/SetupChecks/WellKnownUrls.php index 2b5481d16ff..4eeaff8f3c4 100644 --- a/apps/settings/lib/SetupChecks/WellKnownUrls.php +++ b/apps/settings/lib/SetupChecks/WellKnownUrls.php @@ -13,6 +13,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -44,15 +45,16 @@ class WellKnownUrls implements ISetupCheck { } $urls = [ - ['get', '/.well-known/webfinger', [200, 404], true], + ['get', '/.well-known/webfinger', [200, 400, 404], true], // 400 indicates a handler is installed but (correctly) failed because we didn't specify a resource ['get', '/.well-known/nodeinfo', [200, 404], true], ['propfind', '/.well-known/caldav', [207], false], ['propfind', '/.well-known/carddav', [207], false], ]; + $requestOptions = ['httpErrors' => false, 'options' => ['allow_redirects' => ['track_redirects' => true]]]; foreach ($urls as [$verb,$url,$validStatuses,$checkCustomHeader]) { $works = null; - foreach ($this->runRequest($verb, $url, ['httpErrors' => false, 'options' => ['allow_redirects' => ['track_redirects' => true]]]) as $response) { + foreach ($this->runRequest($verb, $url, $requestOptions, isRootRequest: true) as $response) { // Check that the response status matches $works = in_array($response->getStatusCode(), $validStatuses); // and (if needed) the custom Nextcloud header is set diff --git a/apps/settings/lib/SetupChecks/Woff2Loading.php b/apps/settings/lib/SetupChecks/Woff2Loading.php index 5d8f23338e3..27aff4ea999 100644 --- a/apps/settings/lib/SetupChecks/Woff2Loading.php +++ b/apps/settings/lib/SetupChecks/Woff2Loading.php @@ -12,12 +12,13 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; /** - * Check whether the WOFF2 URLs works + * Check whether the OTF and WOFF2 URLs works */ class Woff2Loading implements ISetupCheck { use CheckServerResponseTrait; @@ -36,13 +37,20 @@ class Woff2Loading implements ISetupCheck { } public function getName(): string { - return $this->l10n->t('WOFF2 file loading'); + return $this->l10n->t('Font file loading'); } public function run(): SetupResult { - $url = $this->urlGenerator->linkTo('', 'core/fonts/NotoSans-Regular-latin.woff2'); + $result = $this->checkFont('otf', $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Regular.otf')); + if ($result->getSeverity() !== SetupResult::SUCCESS) { + return $result; + } + return $this->checkFont('woff2', $this->urlGenerator->linkTo('', 'core/fonts/NotoSans-Regular-latin.woff2')); + } + + protected function checkFont(string $fileExtension, string $url): SetupResult { $noResponse = true; - $responses = $this->runHEAD($url); + $responses = $this->runRequest('HEAD', $url); foreach ($responses as $response) { $noResponse = false; if ($response->getStatusCode() === 200) { @@ -52,14 +60,22 @@ class Woff2Loading implements ISetupCheck { if ($noResponse) { return SetupResult::info( - $this->l10n->t('Could not check for WOFF2 loading support. Please check manually if your webserver serves `.woff2` files.') . "\n" . $this->serverConfigHelp(), + str_replace( + '{extension}', + $fileExtension, + $this->l10n->t('Could not check for {extension} loading support. Please check manually if your webserver serves `.{extension}` files.') . "\n" . $this->serverConfigHelp(), + ), $this->urlGenerator->linkToDocs('admin-nginx'), ); } return SetupResult::warning( - $this->l10n->t('Your web server is not properly set up to deliver .woff2 files. This is typically an issue with the Nginx configuration. For Nextcloud 15 it needs an adjustement to also deliver .woff2 files. Compare your Nginx configuration to the recommended configuration in our documentation.'), + str_replace( + '{extension}', + $fileExtension, + $this->l10n->t('Your web server is not properly set up to deliver .{extension} files. This is typically an issue with the Nginx configuration. For Nextcloud 15 it needs an adjustment to also deliver .{extension} files. Compare your Nginx configuration to the recommended configuration in our documentation.'), + ), $this->urlGenerator->linkToDocs('admin-nginx'), ); - + } } |