diff options
Diffstat (limited to 'apps/settings/lib/SetupChecks')
54 files changed, 3419 insertions, 304 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/AppDirsWithDifferentOwner.php b/apps/settings/lib/SetupChecks/AppDirsWithDifferentOwner.php new file mode 100644 index 00000000000..0d18037c3b5 --- /dev/null +++ b/apps/settings/lib/SetupChecks/AppDirsWithDifferentOwner.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class AppDirsWithDifferentOwner implements ISetupCheck { + public function __construct( + private IL10N $l10n, + ) { + } + + public function getName(): string { + return $this->l10n->t('App directories owner'); + } + + public function getCategory(): string { + return 'security'; + } + + /** + * Iterates through the configured app roots and + * tests if the subdirectories are owned by the same user than the current user. + * + * @return string[] + */ + private function getAppDirsWithDifferentOwner(int $currentUser): array { + $appDirsWithDifferentOwner = [[]]; + + foreach (\OC::$APPSROOTS as $appRoot) { + if ($appRoot['writable'] === true) { + $appDirsWithDifferentOwner[] = $this->getAppDirsWithDifferentOwnerForAppRoot($currentUser, $appRoot); + } + } + + $appDirsWithDifferentOwner = array_merge(...$appDirsWithDifferentOwner); + sort($appDirsWithDifferentOwner); + + return $appDirsWithDifferentOwner; + } + + /** + * Tests if the directories for one apps directory are writable by the current user. + * + * @param int $currentUser The current user + * @param array $appRoot The app root config + * @return string[] The none writable directory paths inside the app root + */ + private function getAppDirsWithDifferentOwnerForAppRoot(int $currentUser, array $appRoot): array { + $appDirsWithDifferentOwner = []; + $appsPath = $appRoot['path']; + $appsDir = new \DirectoryIterator($appRoot['path']); + + foreach ($appsDir as $fileInfo) { + if ($fileInfo->isDir() && !$fileInfo->isDot()) { + $absAppPath = $appsPath . DIRECTORY_SEPARATOR . $fileInfo->getFilename(); + $appDirUser = fileowner($absAppPath); + if ($appDirUser !== $currentUser) { + $appDirsWithDifferentOwner[] = $absAppPath; + } + } + } + + return $appDirsWithDifferentOwner; + } + + public function run(): SetupResult { + $currentUser = posix_getuid(); + $currentUserInfos = posix_getpwuid($currentUser) ?: []; + $appDirsWithDifferentOwner = $this->getAppDirsWithDifferentOwner($currentUser); + if (count($appDirsWithDifferentOwner) > 0) { + return SetupResult::warning( + $this->l10n->t("Some app directories are owned by a different user than the web server one. This may be the case if apps have been installed manually. Check the permissions of the following app directories:\n%s", implode("\n", $appDirsWithDifferentOwner)) + ); + } else { + return SetupResult::success($this->l10n->t('App directories have the correct owner "%s"', [$currentUserInfos['name'] ?? ''])); + } + } +} diff --git a/apps/settings/lib/SetupChecks/BruteForceThrottler.php b/apps/settings/lib/SetupChecks/BruteForceThrottler.php new file mode 100644 index 00000000000..e97e363944f --- /dev/null +++ b/apps/settings/lib/SetupChecks/BruteForceThrottler.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\Security\Bruteforce\IThrottler; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class BruteForceThrottler implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IRequest $request, + private IThrottler $throttler, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Brute-force Throttle'); + } + + public function run(): SetupResult { + $address = $this->request->getRemoteAddress(); + if ($address === '') { + if (\OC::$CLI) { + /* We were called from CLI */ + return SetupResult::info($this->l10n->t('Your remote address could not be determined.')); + } else { + /* Should never happen */ + return SetupResult::error($this->l10n->t('Your remote address could not be determined.')); + } + } elseif ($this->throttler->showBruteforceWarning($address)) { + return SetupResult::error( + $this->l10n->t('Your remote address was identified as "%s" and is brute-force throttled at the moment slowing down the performance of various requests. If the remote address is not your address this can be an indication that a proxy is not configured correctly.', [$address]), + $this->urlGenerator->linkToDocs('admin-reverse-proxy') + ); + } else { + return SetupResult::success( + $this->l10n->t('Your remote address "%s" is not brute-force throttled.', [$address]) + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/CheckUserCertificates.php b/apps/settings/lib/SetupChecks/CheckUserCertificates.php index 52fea7b6551..d1e3551c085 100644 --- a/apps/settings/lib/SetupChecks/CheckUserCertificates.php +++ b/apps/settings/lib/SetupChecks/CheckUserCertificates.php @@ -3,74 +3,42 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Morris Jobke <hey@morrisjobke.de> - * - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 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\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; -class CheckUserCertificates { - /** @var IL10N */ - private $l10n; - /** @var string */ - private $configValue; - /** @var IURLGenerator */ - private $urlGenerator; +class CheckUserCertificates implements ISetupCheck { + private string $configValue; - public function __construct(IL10N $l10n, IConfig $config, IURLGenerator $urlGenerator) { - $this->l10n = $l10n; - $configValue = $config->getAppValue('files_external', 'user_certificate_scan', ''); - $this->configValue = $configValue; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l10n, + IConfig $config, + ) { + $this->configValue = $config->getAppValue('files_external', 'user_certificate_scan', ''); } - public function description(): string { - if ($this->configValue === '') { - return ''; - } - if ($this->configValue === 'not-run-yet') { - return $this->l10n->t('A background job is pending that checks for user imported SSL certificates. Please check back later.'); - } - return $this->l10n->t('There are some user imported SSL certificates present, that are not used anymore with Nextcloud 21. They can be imported on the command line via "occ security:certificates:import" command. Their paths inside the data directory are shown below.'); + public function getCategory(): string { + return 'security'; } - public function severity(): string { - return 'warning'; + public function getName(): string { + return $this->l10n->t('Old administration imported certificates'); } - public function run(): bool { + public function run(): SetupResult { // all fine if neither "not-run-yet" nor a result - return $this->configValue === ''; - } - - public function elements(): array { - if ($this->configValue === '' || $this->configValue === 'not-run-yet') { - return []; + if ($this->configValue === '') { + return SetupResult::success(); } - $data = json_decode($this->configValue); - if (!is_array($data)) { - return []; + if ($this->configValue === 'not-run-yet') { + return SetupResult::info($this->l10n->t('A background job is pending that checks for administration imported SSL certificates. Please check back later.')); } - return $data; + return SetupResult::error($this->l10n->t('There are some administration imported SSL certificates present, that are not used anymore with Nextcloud 21. They can be imported on the command line via "occ security:certificates:import" command. Their paths inside the data directory are shown below.')); } } diff --git a/apps/settings/lib/SetupChecks/CodeIntegrity.php b/apps/settings/lib/SetupChecks/CodeIntegrity.php new file mode 100644 index 00000000000..2b4271fae9c --- /dev/null +++ b/apps/settings/lib/SetupChecks/CodeIntegrity.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\IntegrityCheck\Checker; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class CodeIntegrity implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private Checker $checker, + ) { + } + + public function getName(): string { + return $this->l10n->t('Code integrity'); + } + + public function getCategory(): string { + return 'security'; + } + + public function run(): SetupResult { + if (!$this->checker->isCodeCheckEnforced()) { + return SetupResult::info($this->l10n->t('Integrity checker has been disabled. Integrity cannot be verified.')); + } + + // If there are no results we need to run the verification + if ($this->checker->getResults() === null) { + $this->checker->runInstanceVerification(); + } + + if ($this->checker->hasPassedCheck()) { + return SetupResult::success($this->l10n->t('No altered files')); + } else { + return SetupResult::error( + $this->l10n->t('Some files have not passed the integrity check. {link1} {link2}'), + $this->urlGenerator->linkToDocs('admin-code-integrity'), + [ + 'link1' => [ + 'type' => 'highlight', + 'id' => 'getFailedIntegrityCheckFiles', + 'name' => 'List of invalid files…', + 'link' => $this->urlGenerator->linkToRoute('settings.CheckSetup.getFailedIntegrityCheckFiles'), + ], + 'link2' => [ + 'type' => 'highlight', + 'id' => 'rescanFailedIntegrityCheck', + 'name' => 'Rescan…', + 'link' => $this->urlGenerator->linkToRoute('settings.CheckSetup.rescanFailedIntegrityCheck'), + ], + ], + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/CronErrors.php b/apps/settings/lib/SetupChecks/CronErrors.php new file mode 100644 index 00000000000..dc625b04477 --- /dev/null +++ b/apps/settings/lib/SetupChecks/CronErrors.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class CronErrors implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Cron errors'); + } + + public function run(): SetupResult { + $errors = json_decode($this->config->getAppValue('core', 'cronErrors', ''), true); + if (is_array($errors) && count($errors) > 0) { + 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)) + ) + ); + } else { + return SetupResult::success($this->l10n->t('The last cron job ran without errors.')); + } + } +} diff --git a/apps/settings/lib/SetupChecks/CronInfo.php b/apps/settings/lib/SetupChecks/CronInfo.php new file mode 100644 index 00000000000..f18148c9d14 --- /dev/null +++ b/apps/settings/lib/SetupChecks/CronInfo.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IDateTimeFormatter; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class CronInfo implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IAppConfig $appConfig, + private IURLGenerator $urlGenerator, + private IDateTimeFormatter $dateTimeFormatter, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Cron last run'); + } + + public function run(): SetupResult { + $lastCronRun = $this->appConfig->getValueInt('core', 'lastcron', 0); + $relativeTime = $this->dateTimeFormatter->formatTimeSpan($lastCronRun); + + if ((time() - $lastCronRun) > 3600) { + return SetupResult::error( + $this->l10n->t( + 'Last background job execution ran %s. Something seems wrong. {link}.', + [$relativeTime] + ), + descriptionParameters:[ + 'link' => [ + 'type' => 'highlight', + 'id' => 'backgroundjobs', + 'name' => 'Check the background job settings', + 'link' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'server']) . '#backgroundjobs', + ], + ], + ); + } else { + return SetupResult::success( + $this->l10n->t( + 'Last background job execution ran %s.', + [$relativeTime] + ) + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/DataDirectoryProtected.php b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php new file mode 100644 index 00000000000..e572c345079 --- /dev/null +++ b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php @@ -0,0 +1,71 @@ +<?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 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; + +/** + * Checks if the data directory can not be accessed from outside + */ +class DataDirectoryProtected implements ISetupCheck { + use CheckServerResponseTrait; + + public function __construct( + protected IL10N $l10n, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IClientService $clientService, + protected LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'network'; + } + + public function getName(): string { + return $this->l10n->t('Data directory protected'); + } + + public function run(): SetupResult { + $dataDir = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValueString('datadirectory', '')); + $dataUrl = $this->urlGenerator->linkTo('', $dataDir . '/.ncdata'); + + $noResponse = true; + foreach ($this->runRequest('GET', $dataUrl, [ 'httpErrors' => false ]) as $response) { + $noResponse = false; + 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]); + } + } + + if ($noResponse) { + 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 new file mode 100644 index 00000000000..ec004f73021 --- /dev/null +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\DB\Connection; +use OC\DB\MissingColumnInformation; +use OC\DB\SchemaWrapper; +use OCP\DB\Events\AddMissingColumnsEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class DatabaseHasMissingColumns implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { + } + + public function getCategory(): string { + return 'database'; + } + + public function getName(): string { + return $this->l10n->t('Database missing columns'); + } + + private function getMissingColumns(): array { + $columnInfo = new MissingColumnInformation(); + // Dispatch event so apps can also hint for pending column updates if needed + $event = new AddMissingColumnsEvent(); + $this->dispatcher->dispatchTyped($event); + $missingColumns = $event->getMissingColumns(); + + if (!empty($missingColumns)) { + $schema = new SchemaWrapper($this->connection); + foreach ($missingColumns as $missingColumn) { + if ($schema->hasTable($missingColumn['tableName'])) { + $table = $schema->getTable($missingColumn['tableName']); + if (!$table->hasColumn($missingColumn['columnName'])) { + $columnInfo->addHintForMissingColumn($missingColumn['tableName'], $missingColumn['columnName']); + } + } + } + } + + return $columnInfo->getListOfMissingColumns(); + } + + public function run(): SetupResult { + $missingColumns = $this->getMissingColumns(); + if (empty($missingColumns)) { + return SetupResult::success('None'); + } else { + $list = ''; + foreach ($missingColumns as $missingColumn) { + $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 + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php b/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php new file mode 100644 index 00000000000..97e80c2aaa9 --- /dev/null +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\DB\Connection; +use OC\DB\MissingIndexInformation; +use OC\DB\SchemaWrapper; +use OCP\DB\Events\AddMissingIndicesEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class DatabaseHasMissingIndices implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private Connection $connection, + private IEventDispatcher $dispatcher, + private IURLGenerator $urlGenerator, + ) { + } + + public function getCategory(): string { + return 'database'; + } + + public function getName(): string { + return $this->l10n->t('Database missing indices'); + } + + private function getMissingIndices(): array { + $indexInfo = new MissingIndexInformation(); + // Dispatch event so apps can also hint for pending index updates if needed + $event = new AddMissingIndicesEvent(); + $this->dispatcher->dispatchTyped($event); + $missingIndices = $event->getMissingIndices(); + $indicesToReplace = $event->getIndicesToReplace(); + + if (!empty($missingIndices)) { + $schema = new SchemaWrapper($this->connection); + foreach ($missingIndices as $missingIndex) { + if ($schema->hasTable($missingIndex['tableName'])) { + $table = $schema->getTable($missingIndex['tableName']); + if (!$table->hasIndex($missingIndex['indexName'])) { + $indexInfo->addHintForMissingIndex($missingIndex['tableName'], $missingIndex['indexName']); + } + } + } + } + + if (!empty($indicesToReplace)) { + $schema = new SchemaWrapper($this->connection); + foreach ($indicesToReplace as $indexToReplace) { + if ($schema->hasTable($indexToReplace['tableName'])) { + $table = $schema->getTable($indexToReplace['tableName']); + if (!$table->hasIndex($indexToReplace['newIndexName'])) { + $indexInfo->addHintForMissingIndex($indexToReplace['tableName'], $indexToReplace['newIndexName']); + } + } + } + } + + return $indexInfo->getListOfMissingIndices(); + } + + public function run(): SetupResult { + $missingIndices = $this->getMissingIndices(); + if (empty($missingIndices)) { + return SetupResult::success('None'); + } else { + $processed = 0; + $list = $this->l10n->t('Missing indices:'); + foreach ($missingIndices as $missingIndex) { + $processed++; + $list .= "\n " . $this->l10n->t('"%s" in table "%s"', [$missingIndex['indexName'], $missingIndex['tableName']]); + if (count($missingIndices) > $processed) { + $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.') . "\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 new file mode 100644 index 00000000000..03810ca8faf --- /dev/null +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\DB\Connection; +use OC\DB\MissingPrimaryKeyInformation; +use OC\DB\SchemaWrapper; +use OCP\DB\Events\AddMissingPrimaryKeyEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class DatabaseHasMissingPrimaryKeys implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { + } + + public function getCategory(): string { + return 'database'; + } + + public function getName(): string { + return $this->l10n->t('Database missing primary keys'); + } + + private function getMissingPrimaryKeys(): array { + $primaryKeyInfo = new MissingPrimaryKeyInformation(); + // Dispatch event so apps can also hint for pending primary key updates if needed + $event = new AddMissingPrimaryKeyEvent(); + $this->dispatcher->dispatchTyped($event); + $missingPrimaryKeys = $event->getMissingPrimaryKeys(); + + if (!empty($missingPrimaryKeys)) { + $schema = new SchemaWrapper($this->connection); + foreach ($missingPrimaryKeys as $missingPrimaryKey) { + if ($schema->hasTable($missingPrimaryKey['tableName'])) { + $table = $schema->getTable($missingPrimaryKey['tableName']); + if ($table->getPrimaryKey() === null) { + $primaryKeyInfo->addHintForMissingPrimaryKey($missingPrimaryKey['tableName']); + } + } + } + } + + return $primaryKeyInfo->getListOfMissingPrimaryKeys(); + } + + public function run(): SetupResult { + $missingPrimaryKeys = $this->getMissingPrimaryKeys(); + if (empty($missingPrimaryKeys)) { + return SetupResult::success('None'); + } else { + $list = ''; + foreach ($missingPrimaryKeys as $missingPrimaryKey) { + $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 + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php b/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php new file mode 100644 index 00000000000..bb9794c1e03 --- /dev/null +++ b/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use Doctrine\DBAL\Types\BigIntType; +use OC\Core\Command\Db\ConvertFilecacheBigInt; +use OC\DB\Connection; +use OC\DB\SchemaWrapper; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class DatabasePendingBigIntConversions implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private Connection $db, + private IEventDispatcher $dispatcher, + private IDBConnection $connection, + ) { + } + + public function getCategory(): string { + return 'database'; + } + + public function getName(): string { + return $this->l10n->t('Database pending bigint migrations'); + } + + protected function getBigIntConversionPendingColumns(): array { + $tables = ConvertFilecacheBigInt::getColumnsByTable(); + + $schema = new SchemaWrapper($this->db); + $isSqlite = $this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_SQLITE; + $pendingColumns = []; + + foreach ($tables as $tableName => $columns) { + if (!$schema->hasTable($tableName)) { + continue; + } + + $table = $schema->getTable($tableName); + foreach ($columns as $columnName) { + $column = $table->getColumn($columnName); + $isAutoIncrement = $column->getAutoincrement(); + $isAutoIncrementOnSqlite = $isSqlite && $isAutoIncrement; + if (!($column->getType() instanceof BigIntType) && !$isAutoIncrementOnSqlite) { + $pendingColumns[] = $tableName . '.' . $columnName; + } + } + } + + return $pendingColumns; + } + + public function run(): SetupResult { + $pendingColumns = $this->getBigIntConversionPendingColumns(); + if (empty($pendingColumns)) { + return SetupResult::success('None'); + } else { + $list = ''; + foreach ($pendingColumns as $pendingColumn) { + $list .= "\n$pendingColumn"; + } + $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->urlGenerator->linkToDocs('admin-bigint-conversion') + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/DebugMode.php b/apps/settings/lib/SetupChecks/DebugMode.php new file mode 100644 index 00000000000..8841ecc607d --- /dev/null +++ b/apps/settings/lib/SetupChecks/DebugMode.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class DebugMode implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + ) { + } + + public function getName(): string { + return $this->l10n->t('Debug mode'); + } + + public function getCategory(): string { + return 'system'; + } + + public function run(): SetupResult { + if ($this->config->getSystemValueBool('debug', false)) { + return SetupResult::warning($this->l10n->t('This instance is running in debug mode. Only enable this for local development and not in production environments.')); + } else { + return SetupResult::success($this->l10n->t('Debug mode is disabled.')); + } + } +} diff --git a/apps/settings/lib/SetupChecks/DefaultPhoneRegionSet.php b/apps/settings/lib/SetupChecks/DefaultPhoneRegionSet.php new file mode 100644 index 00000000000..fa94cd9d059 --- /dev/null +++ b/apps/settings/lib/SetupChecks/DefaultPhoneRegionSet.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class DefaultPhoneRegionSet implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + ) { + } + + public function getName(): string { + return $this->l10n->t('Default phone region'); + } + + public function getCategory(): string { + return 'config'; + } + + public function run(): SetupResult { + if ($this->config->getSystemValueString('default_phone_region', '') !== '') { + return SetupResult::success($this->config->getSystemValueString('default_phone_region', '')); + } else { + return SetupResult::info( + $this->l10n->t('Your installation has no default phone region set. This is required to validate phone numbers in the profile settings without a country code. To allow numbers without a country code, please add "default_phone_region" with the respective ISO 3166-1 code of the region to your config file.'), + 'https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements' + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/EmailTestSuccessful.php b/apps/settings/lib/SetupChecks/EmailTestSuccessful.php new file mode 100644 index 00000000000..8cad8e82156 --- /dev/null +++ b/apps/settings/lib/SetupChecks/EmailTestSuccessful.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class EmailTestSuccessful implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('Email test'); + } + + public function getCategory(): string { + return 'config'; + } + + protected function wasEmailTestSuccessful(): bool { + // 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 + if ($this->config->getAppValue('core', 'emailTestSuccessful', '') === '' && $this->config->getSystemValue('mail_domain', '') === '') { + return false; + } + + // The mail test was unsuccessful or the config was changed using the UI without verifying with a testmail, hence return false + if ($this->config->getAppValue('core', 'emailTestSuccessful', '') === '0') { + return false; + } + + return true; + } + + public function run(): SetupResult { + 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') + return SetupResult::info( + $this->l10n->t('You have not set or verified your email server configuration, yet. Please head over to the "Basic settings" in order to set them. Afterwards, use the "Send email" button below the form to verify your settings.'), + $this->urlGenerator->linkToDocs('admin-email') + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/FileLocking.php b/apps/settings/lib/SetupChecks/FileLocking.php new file mode 100644 index 00000000000..f683ee05f03 --- /dev/null +++ b/apps/settings/lib/SetupChecks/FileLocking.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OC\Lock\DBLockingProvider; +use OC\Lock\NoopLockingProvider; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Lock\ILockingProvider; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class FileLocking implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private ILockingProvider $lockingProvider, + ) { + } + + public function getName(): string { + return $this->l10n->t('Transactional File Locking'); + } + + public function getCategory(): string { + return 'system'; + } + + protected function hasWorkingFileLocking(): bool { + return !($this->lockingProvider instanceof NoopLockingProvider); + } + + protected function hasDBFileLocking(): bool { + return ($this->lockingProvider instanceof DBLockingProvider); + } + + public function run(): SetupResult { + if (!$this->hasWorkingFileLocking()) { + 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') + ); + } + + if ($this->hasDBFileLocking()) { + return SetupResult::info( + $this->l10n->t('The database is used for transactional file locking. To enhance performance, please configure memcache, if available.'), + $this->urlGenerator->linkToDocs('admin-transactional-locking') + ); + } + + return SetupResult::success(); + } +} diff --git a/apps/settings/lib/SetupChecks/ForwardedForHeaders.php b/apps/settings/lib/SetupChecks/ForwardedForHeaders.php new file mode 100644 index 00000000000..8238ce07554 --- /dev/null +++ b/apps/settings/lib/SetupChecks/ForwardedForHeaders.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\IRequest; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class ForwardedForHeaders implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + private IRequest $request, + ) { + } + + public function getCategory(): string { + return 'security'; + } + + public function getName(): string { + return $this->l10n->t('Forwarded for headers'); + } + + public function run(): SetupResult { + $trustedProxies = $this->config->getSystemValue('trusted_proxies', []); + $remoteAddress = $this->request->getHeader('REMOTE_ADDR'); + $detectedRemoteAddress = $this->request->getRemoteAddress(); + + if (!\is_array($trustedProxies)) { + return SetupResult::error($this->l10n->t('Your "trusted_proxies" setting is not correctly set, it should be an array.')); + } + + foreach ($trustedProxies as $proxy) { + $addressParts = explode('/', $proxy, 2); + if (filter_var($addressParts[0], FILTER_VALIDATE_IP) === false || !ctype_digit($addressParts[1] ?? '24')) { + return SetupResult::error( + $this->l10n->t('Your "trusted_proxies" setting is not correctly set, it should be an array of IP addresses - optionally with range in CIDR notation.'), + $this->urlGenerator->linkToDocs('admin-reverse-proxy'), + ); + } + } + + if (($remoteAddress === '') && ($detectedRemoteAddress === '')) { + if (\OC::$CLI) { + /* We were called from CLI */ + return SetupResult::info($this->l10n->t('Your remote address could not be determined.')); + } else { + /* Should never happen */ + return SetupResult::error($this->l10n->t('Your remote address could not be determined.')); + } + } + + if (empty($trustedProxies) && $this->request->getHeader('X-Forwarded-Host') !== '') { + return SetupResult::error( + $this->l10n->t('The reverse proxy header configuration is incorrect. This is a security issue and can allow an attacker to spoof their IP address as visible to the Nextcloud.'), + $this->urlGenerator->linkToDocs('admin-reverse-proxy') + ); + } + + if (\in_array($remoteAddress, $trustedProxies, true) && ($remoteAddress !== '127.0.0.1')) { + if ($remoteAddress !== $detectedRemoteAddress) { + /* Remote address was successfuly fixed */ + return SetupResult::success($this->l10n->t('Your IP address was resolved as %s', [$detectedRemoteAddress])); + } else { + return SetupResult::warning( + $this->l10n->t('The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. If not, this is a security issue and can allow an attacker to spoof their IP address as visible to the Nextcloud.'), + $this->urlGenerator->linkToDocs('admin-reverse-proxy') + ); + } + } + + /* Either not enabled or working correctly */ + return SetupResult::success(); + } +} diff --git a/apps/settings/lib/SetupChecks/HttpsUrlGeneration.php b/apps/settings/lib/SetupChecks/HttpsUrlGeneration.php new file mode 100644 index 00000000000..7f76297748b --- /dev/null +++ b/apps/settings/lib/SetupChecks/HttpsUrlGeneration.php @@ -0,0 +1,65 @@ +<?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 OCP\IL10N; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class HttpsUrlGeneration implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IRequest $request, + ) { + } + + public function getCategory(): string { + return 'security'; + } + + public function getName(): string { + return $this->l10n->t('HTTPS access and URLs'); + } + + public function run(): SetupResult { + if (!\OC::$CLI && $this->request->getServerProtocol() !== 'https') { + if (!preg_match('/(?:^(?:localhost|127\.0\.0\.1|::1)|\.onion)$/', $this->request->getInsecureServerHost())) { + return SetupResult::error( + $this->l10n->t('Accessing site insecurely via HTTP. You are strongly advised to set up your server to require HTTPS instead. Without it some important web functionality like "copy to clipboard" or "service workers" will not work!'), + $this->urlGenerator->linkToDocs('admin-security') + ); + } else { + return SetupResult::info( + $this->l10n->t('Accessing site insecurely via HTTP.'), + $this->urlGenerator->linkToDocs('admin-security') + ); + } + } + $generatedUrl = $this->urlGenerator->getAbsoluteURL('index.php'); + if (!str_starts_with($generatedUrl, 'https://')) { + if (!\OC::$CLI) { + return SetupResult::warning( + $this->l10n->t('You are accessing your instance over a secure connection, however your instance is generating insecure URLs. This likely means that your instance is behind a reverse proxy and the Nextcloud `overwrite*` config values are not set correctly.'), + $this->urlGenerator->linkToDocs('admin-reverse-proxy') + ); + /* We were called from CLI so we can't be 100% sure which scenario is applicable */ + } else { + return SetupResult::info( + $this->l10n->t('Your instance is generating insecure URLs. If you access your instance over HTTPS, this likely means that your instance is behind a reverse proxy and the Nextcloud `overwrite*` config values are not set correctly.'), + $this->urlGenerator->linkToDocs('admin-reverse-proxy') + ); + } + } + return SetupResult::success($this->l10n->t('You are accessing your instance over a secure connection, and your instance is generating secure URLs.')); + } +} diff --git a/apps/settings/lib/SetupChecks/InternetConnectivity.php b/apps/settings/lib/SetupChecks/InternetConnectivity.php new file mode 100644 index 00000000000..18f2af63b8d --- /dev/null +++ b/apps/settings/lib/SetupChecks/InternetConnectivity.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; +use Psr\Log\LoggerInterface; + +/** + * Checks if the server can connect to the internet using HTTPS and HTTP + */ +class InternetConnectivity implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IClientService $clientService, + private LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'network'; + } + + public function getName(): string { + return $this->l10n->t('Internet connectivity'); + } + + public function run(): SetupResult { + if ($this->config->getSystemValue('has_internet_connection', true) === false) { + return SetupResult::success($this->l10n->t('Internet connectivity is disabled in configuration file.')); + } + + $siteArray = $this->config->getSystemValue('connectivity_check_domains', [ + '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(); + } + } + return SetupResult::warning($this->l10n->t('This server has no working internet connection: Multiple endpoints could not be reached. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. Establish a connection from this server to the internet to enjoy all features.')); + } + + /** + * 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(); + $client->get($site); + } catch (\Exception $e) { + $this->logger->error('Cannot connect to: ' . $site, [ + 'app' => 'internet_connection_check', + 'exception' => $e, + ]); + return false; + } + return true; + } +} diff --git a/apps/settings/lib/SetupChecks/JavaScriptModules.php b/apps/settings/lib/SetupChecks/JavaScriptModules.php new file mode 100644 index 00000000000..72f58405811 --- /dev/null +++ b/apps/settings/lib/SetupChecks/JavaScriptModules.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +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; + +/** + * Checks if the webserver serves '.mjs' files using the correct MIME type + */ +class JavaScriptModules implements ISetupCheck { + use CheckServerResponseTrait; + + public function __construct( + protected IL10N $l10n, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IClientService $clientService, + protected LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'network'; + } + + public function getName(): string { + return $this->l10n->t('JavaScript modules support'); + } + + public function run(): SetupResult { + $testFile = $this->urlGenerator->linkTo('settings', 'js/esm-test.mjs'); + + $noResponse = true; + foreach ($this->runRequest('HEAD', $testFile) as $response) { + $noResponse = false; + if (preg_match('/(text|application)\/javascript/i', $response->getHeader('Content-Type'))) { + return SetupResult::success(); + } + } + + if ($noResponse) { + 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 new file mode 100644 index 00000000000..dcfc40192b9 --- /dev/null +++ b/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php @@ -0,0 +1,52 @@ +<?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 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; + +/** + * Checks if the webserver serves '.map' files using the correct MIME type + */ +class JavaScriptSourceMaps implements ISetupCheck { + use CheckServerResponseTrait; + + public function __construct( + protected IL10N $l10n, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IClientService $clientService, + protected LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'network'; + } + + public function getName(): string { + return $this->l10n->t('JavaScript source map support'); + } + + public function run(): SetupResult { + $testFile = $this->urlGenerator->linkTo('settings', 'js/map-test.js.map'); + + foreach ($this->runRequest('HEAD', $testFile) as $response) { + return SetupResult::success(); + } + + return SetupResult::warning($this->l10n->t('Your webserver is not set up to serve `.js.map` files. Without these files, JavaScript Source Maps won\'t function properly, making it more challenging to troubleshoot and debug any issues that may arise.')); + } +} diff --git a/apps/settings/lib/SetupChecks/LdapInvalidUuids.php b/apps/settings/lib/SetupChecks/LdapInvalidUuids.php deleted file mode 100644 index 11b0105cada..00000000000 --- a/apps/settings/lib/SetupChecks/LdapInvalidUuids.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - */ - -namespace OCA\Settings\SetupChecks; - -use OCA\User_LDAP\Mapping\GroupMapping; -use OCA\User_LDAP\Mapping\UserMapping; -use OCP\App\IAppManager; -use OCP\IL10N; -use OCP\IServerContainer; - -class LdapInvalidUuids { - - /** @var IAppManager */ - private $appManager; - /** @var IL10N */ - private $l10n; - /** @var IServerContainer */ - private $server; - - public function __construct(IAppManager $appManager, IL10N $l10n, IServerContainer $server) { - $this->appManager = $appManager; - $this->l10n = $l10n; - $this->server = $server; - } - - public function description(): string { - return $this->l10n->t('Invalid UUIDs of LDAP users or groups have been found. Please review your "Override UUID detection" settings in the Expert part of the LDAP configuration and use "occ ldap:update-uuid" to update them.'); - } - - public function severity(): string { - return 'warning'; - } - - public function run(): bool { - if (!$this->appManager->isEnabledForUser('user_ldap')) { - return true; - } - /** @var UserMapping $userMapping */ - $userMapping = $this->server->get(UserMapping::class); - /** @var GroupMapping $groupMapping */ - $groupMapping = $this->server->get(GroupMapping::class); - return count($userMapping->getList(0, 1, true)) === 0 - && count($groupMapping->getList(0, 1, true)) === 0; - } -} diff --git a/apps/settings/lib/SetupChecks/LegacySSEKeyFormat.php b/apps/settings/lib/SetupChecks/LegacySSEKeyFormat.php index 4814d3fba7c..47594e201cb 100644 --- a/apps/settings/lib/SetupChecks/LegacySSEKeyFormat.php +++ b/apps/settings/lib/SetupChecks/LegacySSEKeyFormat.php @@ -3,59 +3,37 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Morris Jobke <hey@morrisjobke.de> - * - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 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\IURLGenerator; - -class LegacySSEKeyFormat { - /** @var IL10N */ - private $l10n; - /** @var IConfig */ - private $config; - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l10n, IConfig $config, IURLGenerator $urlGenerator) { - $this->l10n = $l10n; - $this->config = $config; - $this->urlGenerator = $urlGenerator; - } - - public function description(): string { - return $this->l10n->t('The old server-side-encryption format is enabled. We recommend disabling this.'); +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class LegacySSEKeyFormat implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + ) { } - public function severity(): string { - return 'warning'; + public function getCategory(): string { + return 'security'; } - public function run(): bool { - return $this->config->getSystemValueBool('encryption.legacy_format_support', false) === false; + public function getName(): string { + return $this->l10n->t('Old server-side-encryption'); } - public function linkToDocumentation(): string { - return $this->urlGenerator->linkToDocs('admin-sse-legacy-format'); + public function run(): SetupResult { + if ($this->config->getSystemValueBool('encryption.legacy_format_support', false) === false) { + return SetupResult::success($this->l10n->t('Disabled')); + } + return SetupResult::warning($this->l10n->t('The old server-side-encryption format is enabled. We recommend disabling this.'), $this->urlGenerator->linkToDocs('admin-sse-legacy-format')); } } 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 new file mode 100644 index 00000000000..ca8df039b1e --- /dev/null +++ b/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class MaintenanceWindowStart implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IConfig $config, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Maintenance window start'); + } + + public function run(): SetupResult { + $configValue = $this->config->getSystemValue('maintenance_window_start', null); + if ($configValue === null) { + return SetupResult::warning( + $this->l10n->t('Server has no maintenance window start time configured. This means resource intensive daily background jobs will also be executed during your main usage time. We recommend to set it to a time of low usage, so users are less impacted by the load caused from these heavy tasks.'), + $this->urlGenerator->linkToDocs('admin-background-jobs') + ); + } + + $startValue = (int)$configValue; + $endValue = ($startValue + 6) % 24; + return SetupResult::success( + str_replace( + ['{start}', '{end}'], + [$startValue, $endValue], + $this->l10n->t('Maintenance window to execute heavy background jobs is between {start}:00 UTC and {end}:00 UTC') + ) + ); + } +} diff --git a/apps/settings/lib/SetupChecks/MemcacheConfigured.php b/apps/settings/lib/SetupChecks/MemcacheConfigured.php new file mode 100644 index 00000000000..e3601d428bb --- /dev/null +++ b/apps/settings/lib/SetupChecks/MemcacheConfigured.php @@ -0,0 +1,98 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\Memcache\Memcached; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class MemcacheConfigured implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + private ICacheFactory $cacheFactory, + ) { + } + + public function getName(): string { + return $this->l10n->t('Memcache'); + } + + public function getCategory(): string { + return 'system'; + } + + public function run(): SetupResult { + $memcacheDistributedClass = $this->config->getSystemValue('memcache.distributed', null); + $memcacheLockingClass = $this->config->getSystemValue('memcache.locking', null); + $memcacheLocalClass = $this->config->getSystemValue('memcache.local', null); + $caches = array_filter([$memcacheDistributedClass,$memcacheLockingClass,$memcacheLocalClass]); + 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( + $this->l10n->t('Memcached is configured as distributed cache, but the wrong PHP module ("memcache") is installed. Please install the PHP module "memcached".') + ); + } + // required PHP module is missing + if (!extension_loaded('memcached')) { + return SetupResult::warning( + $this->l10n->t('Memcached is configured as distributed cache, but the PHP module "memcached" is not installed. Please install the PHP module "memcached".') + ); + } + } + if ($memcacheLocalClass === null) { + return SetupResult::info( + $this->l10n->t('No memory cache has been configured. To enhance performance, please configure a memcache, if available.'), + $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 new file mode 100644 index 00000000000..cf237f68670 --- /dev/null +++ b/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php @@ -0,0 +1,41 @@ +<?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\Repair\RepairMimeTypes; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class MimeTypeMigrationAvailable implements ISetupCheck { + + public function __construct( + private RepairMimeTypes $repairMimeTypes, + private IL10N $l10n, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Mimetype migrations available'); + } + + public function run(): SetupResult { + if ($this->repairMimeTypes->migrationsAvailable()) { + return SetupResult::warning( + $this->l10n->t('One or more mimetype migrations are available. Occasionally new mimetypes are added to better handle certain file types. Migrating the mimetypes take a long time on larger instances so this is not done automatically during upgrades. Use the command `occ maintenance:repair --include-expensive` to perform the migrations.'), + ); + } else { + return SetupResult::success('None'); + } + } +} 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/MysqlUnicodeSupport.php b/apps/settings/lib/SetupChecks/MysqlUnicodeSupport.php new file mode 100644 index 00000000000..ba2fa93e094 --- /dev/null +++ b/apps/settings/lib/SetupChecks/MysqlUnicodeSupport.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class MysqlUnicodeSupport implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('MySQL Unicode support'); + } + + public function getCategory(): string { + return 'database'; + } + + public function run(): SetupResult { + if ($this->config->getSystemValueString('dbtype') !== 'mysql') { + return SetupResult::success($this->l10n->t('You are not using MySQL')); + } + if ($this->config->getSystemValueBool('mysql.utf8mb4', false)) { + return SetupResult::success($this->l10n->t('MySQL is used as database and does support 4-byte characters')); + } else { + return SetupResult::warning( + $this->l10n->t('MySQL is used as database but does not support 4-byte characters. To be able to handle 4-byte characters (like emojis) without issues in filenames or comments for example it is recommended to enable the 4-byte support in MySQL.'), + $this->urlGenerator->linkToDocs('admin-mysql-utf8mb4'), + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/OcxProviders.php b/apps/settings/lib/SetupChecks/OcxProviders.php new file mode 100644 index 00000000000..c53e8087bd9 --- /dev/null +++ b/apps/settings/lib/SetupChecks/OcxProviders.php @@ -0,0 +1,83 @@ +<?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 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; + +/** + * Checks if the webserver serves the OCM and OCS providers + */ +class OcxProviders implements ISetupCheck { + use CheckServerResponseTrait; + + public function __construct( + protected IL10N $l10n, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IClientService $clientService, + protected LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'network'; + } + + public function getName(): string { + return $this->l10n->t('OCS provider resolving'); + } + + public function run(): SetupResult { + // List of providers that work + $workingProviders = []; + // List of providers we tested (in case one or multiple do not yield any response) + $testedProviders = []; + // All providers that we need to test + $providers = [ + '/ocm-provider/', + '/ocs-provider/', + ]; + + foreach ($providers as $provider) { + foreach ($this->runRequest('HEAD', $provider, ['httpErrors' => false]) as $response) { + $testedProviders[$provider] = true; + if ($response->getStatusCode() === 200) { + $workingProviders[] = $provider; + break; + } + } + } + + if (count($testedProviders) < count($providers)) { + return SetupResult::warning( + $this->l10n->t('Could not check if your web server properly resolves the OCM and OCS provider URLs.', ) . "\n" . $this->serverConfigHelp(), + ); + } + + $missingProviders = array_diff($providers, $workingProviders); + if (empty($missingProviders)) { + return SetupResult::success(); + } + + return SetupResult::warning( + $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))]), + $this->urlGenerator->linkToDocs('admin-nginx'), + ); + } +} diff --git a/apps/settings/lib/SetupChecks/OverwriteCliUrl.php b/apps/settings/lib/SetupChecks/OverwriteCliUrl.php new file mode 100644 index 00000000000..6fe0a1260cc --- /dev/null +++ b/apps/settings/lib/SetupChecks/OverwriteCliUrl.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\IRequest; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class OverwriteCliUrl implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IRequest $request, + ) { + } + + public function getCategory(): string { + return 'config'; + } + + public function getName(): string { + return $this->l10n->t('Overwrite CLI URL'); + } + + public function run(): SetupResult { + $currentOverwriteCliUrl = $this->config->getSystemValue('overwrite.cli.url', ''); + $suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT; + + // Check correctness by checking if it is a valid URL + if (filter_var($currentOverwriteCliUrl, FILTER_VALIDATE_URL)) { + if ($currentOverwriteCliUrl == $suggestedOverwriteCliUrl) { + return SetupResult::success( + $this->l10n->t( + 'The "overwrite.cli.url" option in your config.php is correctly set to "%s".', + [$currentOverwriteCliUrl] + ) + ); + } else { + return SetupResult::success( + $this->l10n->t( + 'The "overwrite.cli.url" option in your config.php is set to "%s" which is a correct URL. Suggested URL is "%s".', + [$currentOverwriteCliUrl, $suggestedOverwriteCliUrl] + ) + ); + } + } else { + return SetupResult::warning( + $this->l10n->t( + 'Please make sure to set the "overwrite.cli.url" option in your config.php file to the URL that your users mainly use to access this Nextcloud. Suggestion: "%s". Otherwise there might be problems with the URL generation via cron. (It is possible though that the suggested URL is not the URL that your users mainly use to access this Nextcloud. Best is to double check this in any case.)', + [$suggestedOverwriteCliUrl] + ) + ); + } + } +} 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/PhpDefaultCharset.php b/apps/settings/lib/SetupChecks/PhpDefaultCharset.php index 0ad5e2f56ea..580db8cbd02 100644 --- a/apps/settings/lib/SetupChecks/PhpDefaultCharset.php +++ b/apps/settings/lib/SetupChecks/PhpDefaultCharset.php @@ -3,38 +3,34 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Daniel Kesselberg <mail@danielkesselberg.de> - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\SetupChecks; -class PhpDefaultCharset { - public function description(): string { - return 'PHP configuration option default_charset should be UTF-8'; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpDefaultCharset implements ISetupCheck { + public function __construct( + private IL10N $l10n, + ) { + } + + public function getName(): string { + return $this->l10n->t('PHP default charset'); } - public function severity(): string { - return 'warning'; + public function getCategory(): string { + return 'php'; } - public function run(): bool { - return strtoupper(trim(ini_get('default_charset'))) === 'UTF-8'; + public function run(): SetupResult { + if (strtoupper(trim(ini_get('default_charset'))) === 'UTF-8') { + return SetupResult::success('UTF-8'); + } else { + return SetupResult::warning($this->l10n->t('PHP configuration option "default_charset" should be UTF-8')); + } } } diff --git a/apps/settings/lib/SetupChecks/PhpDisabledFunctions.php b/apps/settings/lib/SetupChecks/PhpDisabledFunctions.php new file mode 100644 index 00000000000..b82b5921989 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpDisabledFunctions.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpDisabledFunctions implements ISetupCheck { + + public function __construct( + private IL10N $l10n, + ) { + } + + public function getName(): string { + return $this->l10n->t('PHP set_time_limit'); + } + + public function getCategory(): string { + return 'php'; + } + + public function run(): SetupResult { + if (function_exists('set_time_limit') && !str_contains(ini_get('disable_functions'), 'set_time_limit')) { + return SetupResult::success($this->l10n->t('The function is available.')); + } else { + return SetupResult::warning( + $this->l10n->t('The PHP function "set_time_limit" is not available. This could result in scripts being halted mid-execution, breaking your installation. Enabling this function is strongly recommended.'), + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/PhpFreetypeSupport.php b/apps/settings/lib/SetupChecks/PhpFreetypeSupport.php new file mode 100644 index 00000000000..ec5d6c7e146 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpFreetypeSupport.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpFreetypeSupport implements ISetupCheck { + public function __construct( + private IL10N $l10n, + ) { + } + + public function getName(): string { + return $this->l10n->t('Freetype'); + } + + public function getCategory(): string { + return 'php'; + } + + /** + * Check if the required FreeType functions are present + */ + protected function hasFreeTypeSupport(): bool { + return function_exists('imagettfbbox') && function_exists('imagettftext'); + } + + public function run(): SetupResult { + if ($this->hasFreeTypeSupport()) { + return SetupResult::success($this->l10n->t('Supported')); + } else { + return SetupResult::info( + $this->l10n->t('Your PHP does not have FreeType support, resulting in breakage of profile pictures and the settings interface.'), + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/PhpGetEnv.php b/apps/settings/lib/SetupChecks/PhpGetEnv.php new file mode 100644 index 00000000000..279a23a9691 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpGetEnv.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpGetEnv implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('PHP getenv'); + } + + public function getCategory(): string { + return 'php'; + } + + public function run(): SetupResult { + if (!empty(getenv('PATH'))) { + return SetupResult::success(); + } else { + return SetupResult::warning($this->l10n->t('PHP does not seem to be setup properly to query system environment variables. The test with getenv("PATH") only returns an empty response.'), $this->urlGenerator->linkToDocs('admin-php-fpm')); + } + } +} 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 new file mode 100644 index 00000000000..7b693169f10 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpMemoryLimit.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OC\MemoryInfo; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; +use OCP\Util; + +class PhpMemoryLimit implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private MemoryInfo $memoryInfo, + ) { + } + + public function getCategory(): string { + return 'php'; + } + + public function getName(): string { + return $this->l10n->t('PHP memory limit'); + } + + public function run(): SetupResult { + 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. 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 new file mode 100644 index 00000000000..b0b4f106f4a --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpModules.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpModules implements ISetupCheck { + protected const REQUIRED_MODULES = [ + 'ctype', + 'curl', + 'dom', + 'fileinfo', + 'gd', + 'json', + 'mbstring', + 'openssl', + 'posix', + 'session', + 'xml', + 'xmlreader', + 'xmlwriter', + 'zip', + 'zlib', + ]; + protected const RECOMMENDED_MODULES = [ + 'exif', + 'gmp', + 'intl', + 'sodium', + 'sysvsem', + ]; + + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('PHP modules'); + } + + public function getCategory(): string { + return 'php'; + } + + protected function getRecommendedModuleDescription(string $module): string { + 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'), + '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 => '', + }; + } + + public function run(): SetupResult { + $missingRecommendedModules = $this->getMissingModules(self::RECOMMENDED_MODULES); + $missingRequiredModules = $this->getMissingModules(self::REQUIRED_MODULES); + if (!empty($missingRequiredModules)) { + return SetupResult::error( + $this->l10n->t('This instance is missing some required PHP modules. It is required to install them: %s.', implode(', ', $missingRequiredModules)), + $this->urlGenerator->linkToDocs('admin-php-modules') + ); + } elseif (!empty($missingRecommendedModules)) { + $moduleList = implode( + "\n", + array_map( + fn (string $module) => '- ' . $module . ' ' . $this->getRecommendedModuleDescription($module), + $missingRecommendedModules + ) + ); + return SetupResult::info( + $this->l10n->t("This instance is missing some recommended PHP modules. For improved performance and better compatibility it is highly recommended to install them:\n%s", $moduleList), + $this->urlGenerator->linkToDocs('admin-php-modules') + ); + } else { + return SetupResult::success(); + } + } + + /** + * Checks for potential PHP modules that would improve the instance + * + * @param string[] $modules modules to test + * @return string[] A list of PHP modules which are missing + */ + protected function getMissingModules(array $modules): array { + return array_values(array_filter( + $modules, + fn (string $module) => !extension_loaded($module), + )); + } +} diff --git a/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php b/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php new file mode 100644 index 00000000000..83b7be1c390 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php @@ -0,0 +1,136 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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; + +class PhpOpcacheSetup implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IniGetWrapper $iniGetWrapper, + ) { + } + + public function getName(): string { + return $this->l10n->t('PHP opcache'); + } + + public function getCategory(): string { + return 'php'; + } + + /** + * Checks whether a PHP OPcache is properly set up + * @return array{'warning'|'error',list<string>} The level and the list of OPcache setup recommendations + */ + protected function getOpcacheSetupRecommendations(): array { + $level = 'warning'; + + // If the module is not loaded, return directly to skip inapplicable checks + if (!extension_loaded('Zend OPcache')) { + return ['error',[$this->l10n->t('The PHP OPcache module is not loaded. For better performance it is recommended to load it into your PHP installation.')]]; + } + + $recommendations = []; + + // Check whether Nextcloud is allowed to use the OPcache API + $isPermitted = true; + $permittedPath = (string)$this->iniGetWrapper->getString('opcache.restrict_api'); + if ($permittedPath !== '' && !str_starts_with(\OC::$SERVERROOT, rtrim($permittedPath, '/'))) { + $isPermitted = false; + } + + if (!$this->iniGetWrapper->getBool('opcache.enable')) { + $recommendations[] = $this->l10n->t('OPcache is disabled. For better performance, it is recommended to apply "opcache.enable=1" to your PHP configuration.'); + $level = 'error'; + } 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 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]; + } + + $status = opcache_get_status(false); + + if ($status === false) { + $recommendations[] = $this->l10n->t('OPcache is not working as it should, opcache_get_status() returns false, please check configuration.'); + $level = 'error'; + } + + // 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) + ) + ) { + $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')]); + } + } + + // 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. + if (!$this->iniGetWrapper->getBool('opcache.save_comments')) { + $recommendations[] = $this->l10n->t('OPcache is configured to remove code comments. With OPcache enabled, "opcache.save_comments=1" must be set for Nextcloud to function.'); + $level = 'error'; + } + + if (!$isPermitted) { + $recommendations[] = $this->l10n->t('Nextcloud is not allowed to use the OPcache API. With OPcache enabled, it is highly recommended to include all Nextcloud directories with "opcache.restrict_api" or unset this setting to disable OPcache API restrictions, to prevent errors during Nextcloud core or app upgrades.'); + $level = 'error'; + } + + return [$level, $recommendations]; + } + + 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) { + 'error' => SetupResult::error( + $this->l10n->t('The PHP OPcache module is not properly configured. %s.', implode("\n", $recommendations)), + $this->urlGenerator->linkToDocs('admin-php-opcache') + ), + default => SetupResult::warning( + $this->l10n->t('The PHP OPcache module is not properly configured. %s.', implode("\n", $recommendations)), + $this->urlGenerator->linkToDocs('admin-php-opcache') + ), + }; + } else { + return SetupResult::success($this->l10n->t('Correctly configured')); + } + } +} diff --git a/apps/settings/lib/SetupChecks/PhpOutdated.php b/apps/settings/lib/SetupChecks/PhpOutdated.php new file mode 100644 index 00000000000..d0d8e03c705 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpOutdated.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +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, + ) { + } + + public function getCategory(): string { + return 'security'; + } + + public function getName(): string { + return $this->l10n->t('PHP version'); + } + + public function run(): SetupResult { + if (PHP_VERSION_ID < 80200) { + 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/PhpOutputBuffering.php b/apps/settings/lib/SetupChecks/PhpOutputBuffering.php index 3bf52695301..be8154fbb1b 100644 --- a/apps/settings/lib/SetupChecks/PhpOutputBuffering.php +++ b/apps/settings/lib/SetupChecks/PhpOutputBuffering.php @@ -3,39 +3,35 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Daniel Kesselberg <mail@danielkesselberg.de> - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\SetupChecks; -class PhpOutputBuffering { - public function description(): string { - return 'PHP configuration option output_buffering must be disabled'; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpOutputBuffering implements ISetupCheck { + public function __construct( + private IL10N $l10n, + ) { + } + + public function getCategory(): string { + return 'php'; } - public function severity(): string { - return 'error'; + public function getName(): string { + return $this->l10n->t('PHP "output_buffering" option'); } - public function run(): bool { + public function run(): SetupResult { $value = trim(ini_get('output_buffering')); - return $value === '' || $value === '0'; + if ($value === '' || $value === '0') { + return SetupResult::success($this->l10n->t('Disabled')); + } else { + return SetupResult::error($this->l10n->t('PHP configuration option "output_buffering" must be disabled')); + } } } diff --git a/apps/settings/lib/SetupChecks/PushService.php b/apps/settings/lib/SetupChecks/PushService.php new file mode 100644 index 00000000000..1f03404d80e --- /dev/null +++ b/apps/settings/lib/SetupChecks/PushService.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IL10N; +use OCP\Notification\IManager; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; +use OCP\Support\Subscription\IRegistry; + +class PushService implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IManager $notificationsManager, + private IRegistry $subscriptionRegistry, + private ITimeFactory $timeFactory, + ) { + } + + public function getName(): string { + return $this->l10n->t('Push service'); + } + + public function getCategory(): string { + return 'system'; + } + + /** + * Check if is fair use of free push service + */ + private function isFairUseOfFreePushService(): bool { + $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; + } + return $this->notificationsManager->isFairUseOfFreePushService(); + } + + public function run(): SetupResult { + if ($this->subscriptionRegistry->delegateHasValidSubscription()) { + return SetupResult::success($this->l10n->t('Valid enterprise license')); + } + + if ($this->isFairUseOfFreePushService()) { + return SetupResult::success($this->l10n->t('Free push service')); + } + + return SetupResult::error( + $this->l10n->t('This is the unsupported community build of Nextcloud. Given the size of this instance, performance, reliability and scalability cannot be guaranteed. Push notifications are limited to avoid overloading our free service. Learn more about the benefits of Nextcloud Enterprise at {link}.'), + descriptionParameters:[ + 'link' => [ + 'type' => 'highlight', + 'id' => 'link', + 'name' => 'https://nextcloud.com/enterprise', + 'link' => 'https://nextcloud.com/enterprise', + ], + ], + ); + } +} diff --git a/apps/settings/lib/SetupChecks/RandomnessSecure.php b/apps/settings/lib/SetupChecks/RandomnessSecure.php new file mode 100644 index 00000000000..045ddde0b9d --- /dev/null +++ b/apps/settings/lib/SetupChecks/RandomnessSecure.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\IURLGenerator; +use OCP\Security\ISecureRandom; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class RandomnessSecure implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + private ISecureRandom $secureRandom, + ) { + } + + public function getName(): string { + return $this->l10n->t('Random generator'); + } + + public function getCategory(): string { + return 'security'; + } + + public function run(): SetupResult { + try { + $this->secureRandom->generate(1); + } catch (\Exception $ex) { + return SetupResult::error( + $this->l10n->t('No suitable source for randomness found by PHP which is highly discouraged for security reasons.'), + $this->urlGenerator->linkToDocs('admin-security') + ); + } + return SetupResult::success($this->l10n->t('Secure')); + } +} diff --git a/apps/settings/lib/SetupChecks/ReadOnlyConfig.php b/apps/settings/lib/SetupChecks/ReadOnlyConfig.php new file mode 100644 index 00000000000..b616f8a7155 --- /dev/null +++ b/apps/settings/lib/SetupChecks/ReadOnlyConfig.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class ReadOnlyConfig implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + ) { + } + + public function getName(): string { + return $this->l10n->t('Configuration file access rights'); + } + + public function getCategory(): string { + return 'config'; + } + + public function run(): SetupResult { + if ($this->config->getSystemValueBool('config_is_read_only', false)) { + return SetupResult::info($this->l10n->t('The read-only config has been enabled. This prevents setting some configurations via the web-interface. Furthermore, the file needs to be made writable manually for every update.')); + } else { + return SetupResult::success($this->l10n->t('Nextcloud configuration file is writable')); + } + } +} diff --git a/apps/settings/lib/SetupChecks/SchedulingTableSize.php b/apps/settings/lib/SetupChecks/SchedulingTableSize.php new file mode 100644 index 00000000000..b23972ca7dc --- /dev/null +++ b/apps/settings/lib/SetupChecks/SchedulingTableSize.php @@ -0,0 +1,52 @@ +<?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 OCP\IDBConnection; +use OCP\IL10N; +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, + ) { + } + + public function getName(): string { + return $this->l10n->t('Scheduling objects table size'); + } + + public function getCategory(): string { + return 'database'; + } + + public function run(): SetupResult { + $qb = $this->connection->getQueryBuilder(); + $qb->select($qb->func()->count('id')) + ->from('schedulingobjects'); + $query = $qb->executeQuery(); + $count = $query->fetchOne(); + $query->closeCursor(); + + if ($count > self::MAX_SCHEDULING_ENTRIES) { + return SetupResult::warning( + $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( + $this->l10n->t('Scheduling objects table size is within acceptable range.') + ); + } +} diff --git a/apps/settings/lib/SetupChecks/SecurityHeaders.php b/apps/settings/lib/SetupChecks/SecurityHeaders.php new file mode 100644 index 00000000000..9cc6856a170 --- /dev/null +++ b/apps/settings/lib/SetupChecks/SecurityHeaders.php @@ -0,0 +1,139 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +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; + +class SecurityHeaders implements ISetupCheck { + + use CheckServerResponseTrait; + + public function __construct( + protected IL10N $l10n, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IClientService $clientService, + protected LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'security'; + } + + public function getName(): string { + return $this->l10n->t('HTTP headers'); + } + + public function run(): SetupResult { + $urls = [ + ['get', $this->urlGenerator->linkToRoute('heartbeat'), [200]], + ]; + $securityHeaders = [ + 'X-Content-Type-Options' => ['nosniff', null], + 'X-Robots-Tag' => ['noindex,nofollow', null], + 'X-Frame-Options' => ['sameorigin', 'deny'], + 'X-Permitted-Cross-Domain-Policies' => ['none', null], + ]; + + foreach ($urls as [$verb,$url,$validStatuses]) { + $works = null; + foreach ($this->runRequest($verb, $url, ['httpErrors' => false]) as $response) { + // Check that the response status matches + if (!in_array($response->getStatusCode(), $validStatuses)) { + $works = false; + continue; + } + $msg = ''; + $msgParameters = []; + foreach ($securityHeaders as $header => [$expected, $accepted]) { + /* Convert to lowercase and remove spaces after comas */ + $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"; + } 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"; + } + } + } + + $referrerPolicy = $response->getHeader('Referrer-Policy'); + if (!preg_match('/(no-referrer(-when-downgrade)?|strict-origin(-when-cross-origin)?|same-origin)(,|$)/', $referrerPolicy)) { + $msg .= $this->l10n->t( + '- The `%1$s` HTTP header is not set to `%2$s`, `%3$s`, `%4$s`, `%5$s` or `%6$s`. This can leak referer information. See the {w3c-recommendation}.', + [ + 'Referrer-Policy', + 'no-referrer', + 'no-referrer-when-downgrade', + 'strict-origin', + 'strict-origin-when-cross-origin', + 'same-origin', + ] + ) . "\n"; + $msgParameters['w3c-recommendation'] = [ + 'type' => 'highlight', + 'id' => 'w3c-recommendation', + 'name' => 'W3C Recommendation', + 'link' => 'https://www.w3.org/TR/referrer-policy/', + ]; + } + + $transportSecurityValidity = $response->getHeader('Strict-Transport-Security'); + $minimumSeconds = 15552000; + 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"; + } + } 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"; + } 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"; + } + + if (!empty($msg)) { + return SetupResult::warning( + $this->l10n->t('Some headers are not set correctly on your instance') . "\n" . $msg, + $this->urlGenerator->linkToDocs('admin-security'), + $msgParameters, + ); + } + // Skip the other requests if one works + $works = true; + break; + } + // If 'works' is null then we could not connect to the server + if ($works === null) { + return SetupResult::info( + $this->l10n->t('Could not check that your web server serves security headers correctly. Please check manually.'), + $this->urlGenerator->linkToDocs('admin-security'), + ); + } + // 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->urlGenerator->linkToDocs('admin-security'), + ); + } + } + return SetupResult::success( + $this->l10n->t('Your server is correctly configured to send security headers.') + ); + } +} diff --git a/apps/settings/lib/SetupChecks/SupportedDatabase.php b/apps/settings/lib/SetupChecks/SupportedDatabase.php index 089fb69bbc9..d083958d16e 100644 --- a/apps/settings/lib/SetupChecks/SupportedDatabase.php +++ b/apps/settings/lib/SetupChecks/SupportedDatabase.php @@ -3,111 +3,124 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Morris Jobke <hey@morrisjobke.de> - * - * @author Claas Augner <github@caugner.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\SetupChecks; -use Doctrine\DBAL\Platforms\MariaDb1027Platform; -use Doctrine\DBAL\Platforms\MySQL57Platform; -use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; -use Doctrine\DBAL\Platforms\PostgreSQL100Platform; -use Doctrine\DBAL\Platforms\PostgreSQL94Platform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\SqlitePlatform; use OCP\IDBConnection; use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; -class SupportedDatabase { - /** @var IL10N */ - private $l10n; - /** @var IDBConnection */ - private $connection; +class SupportedDatabase implements ISetupCheck { - private $checked = false; - private $description = ''; + private const MIN_MARIADB = '10.6'; + 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(IL10N $l10n, IDBConnection $connection) { - $this->l10n = $l10n; - $this->connection = $connection; + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IDBConnection $connection, + ) { } - public function check() { - if ($this->checked === true) { - return; - } - $this->checked = true; + public function getCategory(): string { + return 'database'; + } - switch (get_class($this->connection->getDatabasePlatform())) { - case MySQL80Platform::class: # extends MySQL57Platform - case MySQL57Platform::class: # extends MySQLPlatform - case MariaDb1027Platform::class: # extends MySQLPlatform - case MySQLPlatform::class: - $result = $this->connection->prepare("SHOW VARIABLES LIKE 'version';"); - $result->execute(); - $row = $result->fetch(); - $version = strtolower($row['Value']); + public function getName(): string { + return $this->l10n->t('Database version'); + } - if (strpos($version, 'mariadb') !== false) { - if (version_compare($version, '10.2', '<')) { - $this->description = $this->l10n->t('MariaDB version "%s" is used. Nextcloud 21 will no longer support this version and requires MariaDB 10.2 or higher.', $row['Value']); - return; - } - } else { - if (version_compare($version, '8', '<')) { - $this->description = $this->l10n->t('MySQL version "%s" is used. Nextcloud 21 will no longer support this version and requires MySQL 8.0 or MariaDB 10.2 or higher.', $row['Value']); - return; - } + public function run(): SetupResult { + $version = null; + $databasePlatform = $this->connection->getDatabasePlatform(); + if ($databasePlatform instanceof MySQLPlatform) { + $statement = $this->connection->prepare("SHOW VARIABLES LIKE 'version';"); + $result = $statement->execute(); + $row = $result->fetch(); + $version = $row['Value']; + $versionlc = strtolower($version); + // we only care about X.Y not X.Y.Z differences + [$major, $minor, ] = explode('.', $versionlc); + $versionConcern = $major . '.' . $minor; + if (str_contains($versionlc, 'mariadb')) { + if (version_compare($versionConcern, '10.3', '=')) { + return SetupResult::info( + $this->l10n->t( + 'MariaDB version 10.3 detected, this version is end-of-life and only supported as part of Ubuntu 20.04. MariaDB >=%1$s and <=%2$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + self::MIN_MARIADB, + self::MAX_MARIADB, + ] + ), + ); + } elseif (version_compare($versionConcern, self::MIN_MARIADB, '<') || version_compare($versionConcern, self::MAX_MARIADB, '>')) { + return SetupResult::warning( + $this->l10n->t( + 'MariaDB version "%1$s" detected. MariaDB >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + $version, + self::MIN_MARIADB, + self::MAX_MARIADB, + ], + ), + ); } - break; - case SqlitePlatform::class: - break; - case PostgreSQL100Platform::class: # extends PostgreSQL94Platform - case PostgreSQL94Platform::class: - $result = $this->connection->prepare('SHOW server_version;'); - $result->execute(); - $row = $result->fetch(); - if (version_compare($row['server_version'], '9.6', '<')) { - $this->description = $this->l10n->t('PostgreSQL version "%s" is used. Nextcloud 21 will no longer support this version and requires PostgreSQL 9.6 or higher.', $row['server_version']); - return; + } else { + if (version_compare($versionConcern, self::MIN_MYSQL, '<') || version_compare($versionConcern, self::MAX_MYSQL, '>')) { + return SetupResult::warning( + $this->l10n->t( + 'MySQL version "%1$s" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + $version, + self::MIN_MYSQL, + self::MAX_MYSQL, + ], + ), + ); } - break; - case OraclePlatform::class: - break; + } + } elseif ($databasePlatform instanceof PostgreSQLPlatform) { + $statement = $this->connection->prepare('SHOW server_version;'); + $result = $statement->execute(); + $row = $result->fetch(); + $version = $row['server_version']; + $versionlc = strtolower($version); + // we only care about X not X.Y or X.Y.Z differences + [$major, ] = explode('.', $versionlc); + $versionConcern = $major; + 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'; + } elseif ($databasePlatform instanceof SqlitePlatform) { + return SetupResult::warning( + $this->l10n->t('SQLite is currently being used as the backend database. For larger installations we recommend that you switch to a different database backend. This is particularly recommended when using the desktop client for file synchronisation. To migrate to another database use the command line tool: "occ db:convert-type".'), + $this->urlGenerator->linkToDocs('admin-db-conversion') + ); + } else { + return SetupResult::error($this->l10n->t('Unknown database platform')); } - } - - public function description(): string { - $this->check(); - return $this->description; - } - - public function severity(): string { - return 'info'; - } - - public function run(): bool { - $this->check(); - return $this->description === ''; + return SetupResult::success($version); } } diff --git a/apps/settings/lib/SetupChecks/SystemIs64bit.php b/apps/settings/lib/SetupChecks/SystemIs64bit.php new file mode 100644 index 00000000000..308011c218e --- /dev/null +++ b/apps/settings/lib/SetupChecks/SystemIs64bit.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class SystemIs64bit implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('Architecture'); + } + + public function getCategory(): string { + return 'system'; + } + + protected function is64bit(): bool { + if (PHP_INT_SIZE < 8) { + return false; + } else { + return true; + } + } + + public function run(): SetupResult { + if ($this->is64bit()) { + return SetupResult::success($this->l10n->t('64-bit')); + } else { + return SetupResult::warning( + $this->l10n->t('It seems like you are running a 32-bit PHP version. Nextcloud needs 64-bit to run well. Please upgrade your OS and PHP to 64-bit!'), + $this->urlGenerator->linkToDocs('admin-system-requirements') + ); + } + } +} 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 new file mode 100644 index 00000000000..49dc0d377e7 --- /dev/null +++ b/apps/settings/lib/SetupChecks/TempSpaceAvailable.php @@ -0,0 +1,113 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\ITempManager; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class TempSpaceAvailable implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + private ITempManager $tempManager, + ) { + } + + public function getName(): string { + return $this->l10n->t('Temporary space available'); + } + + public function getCategory(): string { + return 'system'; + } + + private function isPrimaryStorageS3(): bool { + $objectStore = $this->config->getSystemValue('objectstore', null); + $objectStoreMultibucket = $this->config->getSystemValue('objectstore_multibucket', null); + + if (!isset($objectStoreMultibucket) && !isset($objectStore)) { + return false; + } + + if (isset($objectStoreMultibucket['class']) && $objectStoreMultibucket['class'] !== 'OC\\Files\\ObjectStore\\S3') { + return false; + } + + if (isset($objectStore['class']) && $objectStore['class'] !== 'OC\\Files\\ObjectStore\\S3') { + return false; + } + + return true; + } + + public function run(): SetupResult { + $phpTempPath = sys_get_temp_dir(); + $nextcloudTempPath = ''; + try { + $nextcloudTempPath = $this->tempManager->getTempBaseDir(); + } catch (\Exception $e) { + } + + if (empty($nextcloudTempPath)) { + return SetupResult::error('The temporary directory of this instance points to an either non-existing or non-writable directory.'); + } + + if (!is_dir($phpTempPath)) { + return SetupResult::error($this->l10n->t('Error while checking the temporary PHP path - it was not properly set to a directory. Returned value: %s', [$phpTempPath])); + } + + if (!function_exists('disk_free_space')) { + return SetupResult::info($this->l10n->t('The PHP function "disk_free_space" is disabled, which prevents the check for enough space in the temporary directories.')); + } + + $freeSpaceInTemp = disk_free_space($phpTempPath); + if ($freeSpaceInTemp === false) { + 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', [$phpTempPath])); + } + + /** Build details data about temporary directory, either one or two of them */ + $freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024; + $spaceDetail = $this->l10n->t('- %.1f GiB available in %s (PHP temporary directory)', [round($freeSpaceInTempInGB, 1),$phpTempPath]); + if ($nextcloudTempPath !== $phpTempPath) { + $freeSpaceInNextcloudTemp = disk_free_space($nextcloudTempPath); + if ($freeSpaceInNextcloudTemp === false) { + 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]); + } + + if (!$this->isPrimaryStorageS3()) { + return SetupResult::success( + $this->l10n->t("Temporary directory is correctly configured:\n%s", [$spaceDetail]) + ); + } + + if ($freeSpaceInTempInGB > 50) { + return SetupResult::success( + $this->l10n->t( + "This instance uses an S3 based object store as primary storage, and has enough space in the temporary directory.\n%s", + [$spaceDetail] + ) + ); + } + + return SetupResult::warning( + $this->l10n->t( + "This instance uses an S3 based object store as primary storage. The uploaded files are stored temporarily on the server and thus it is recommended to have 50 GiB of free space available in the temp directory of PHP. To improve this please change the temporary directory in the php.ini or make more space available in that path. \nChecking the available space in the temporary path resulted in %.1f GiB instead of the recommended 50 GiB. Path: %s", + [round($freeSpaceInTempInGB, 1),$phpTempPath] + ) + ); + } +} diff --git a/apps/settings/lib/SetupChecks/TransactionIsolation.php b/apps/settings/lib/SetupChecks/TransactionIsolation.php new file mode 100644 index 00000000000..892c0ecbda6 --- /dev/null +++ b/apps/settings/lib/SetupChecks/TransactionIsolation.php @@ -0,0 +1,58 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use Doctrine\DBAL\Exception; +use Doctrine\DBAL\TransactionIsolationLevel; +use OC\DB\Connection; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class TransactionIsolation implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IDBConnection $connection, + private Connection $db, + ) { + } + + public function getName(): string { + return $this->l10n->t('Database transaction isolation level'); + } + + public function getCategory(): string { + return 'database'; + } + + public function run(): SetupResult { + try { + if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_SQLITE) { + return SetupResult::success(); + } + + if ($this->db->getTransactionIsolation() === TransactionIsolationLevel::READ_COMMITTED) { + return SetupResult::success('Read committed'); + } else { + return SetupResult::error( + $this->l10n->t('Your database does not run with "READ COMMITTED" transaction isolation level. This can cause problems when multiple actions are executed in parallel.'), + $this->urlGenerator->linkToDocs('admin-db-transaction') + ); + } + } catch (Exception $e) { + return SetupResult::warning( + $this->l10n->t('Was not able to get transaction isolation level: %s', $e->getMessage()) + ); + } + } +} diff --git a/apps/settings/lib/SetupChecks/WellKnownUrls.php b/apps/settings/lib/SetupChecks/WellKnownUrls.php new file mode 100644 index 00000000000..4eeaff8f3c4 --- /dev/null +++ b/apps/settings/lib/SetupChecks/WellKnownUrls.php @@ -0,0 +1,95 @@ +<?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 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; + +class WellKnownUrls implements ISetupCheck { + + use CheckServerResponseTrait; + + public function __construct( + protected IL10N $l10n, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IClientService $clientService, + protected LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'network'; + } + + public function getName(): string { + return $this->l10n->t('.well-known URLs'); + } + + public function run(): SetupResult { + if (!$this->config->getSystemValueBool('check_for_working_wellknown_setup', true)) { + return SetupResult::info($this->l10n->t('`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped.')); + } + + $urls = [ + ['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, $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 + if ($checkCustomHeader) { + $works = $works && !empty($response->getHeader('X-NEXTCLOUD-WELL-KNOWN')); + } else { + // For default DAV endpoints we lack authorization, but we still can check that the redirect works as expected + if (!$works && $response->getStatusCode() === 401) { + $redirectHops = explode(',', $response->getHeader('X-Guzzle-Redirect-History')); + $effectiveUri = end($redirectHops); + $works = str_ends_with(rtrim($effectiveUri, '/'), '/remote.php/dav'); + } + } + // Skip the other requests if one works + if ($works === true) { + break; + } + } + // If 'works' is null then we could not connect to the server + if ($works === null) { + return SetupResult::info( + $this->l10n->t('Could not check that your web server serves `.well-known` correctly. Please check manually.') . "\n" . $this->serverConfigHelp(), + $this->urlGenerator->linkToDocs('admin-setup-well-known-URL'), + ); + } + // Otherwise if we fail we can abort here + if ($works === false) { + return SetupResult::warning( + $this->l10n->t("Your web server is not properly set up to resolve `.well-known` URLs, failed on:\n`%s`", [$url]), + $this->urlGenerator->linkToDocs('admin-setup-well-known-URL'), + ); + } + } + return SetupResult::success( + $this->l10n->t('Your server is correctly configured to serve `.well-known` URLs.') + ); + } +} diff --git a/apps/settings/lib/SetupChecks/Woff2Loading.php b/apps/settings/lib/SetupChecks/Woff2Loading.php new file mode 100644 index 00000000000..27aff4ea999 --- /dev/null +++ b/apps/settings/lib/SetupChecks/Woff2Loading.php @@ -0,0 +1,81 @@ +<?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 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 OTF and WOFF2 URLs works + */ +class Woff2Loading implements ISetupCheck { + use CheckServerResponseTrait; + + public function __construct( + protected IL10N $l10n, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IClientService $clientService, + protected LoggerInterface $logger, + ) { + } + + public function getCategory(): string { + return 'network'; + } + + public function getName(): string { + return $this->l10n->t('Font file loading'); + } + + public function run(): SetupResult { + $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->runRequest('HEAD', $url); + foreach ($responses as $response) { + $noResponse = false; + if ($response->getStatusCode() === 200) { + return SetupResult::success(); + } + } + + if ($noResponse) { + return SetupResult::info( + 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( + 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'), + ); + + } +} |