diff options
Diffstat (limited to 'apps/settings/lib/Controller')
17 files changed, 957 insertions, 1756 deletions
diff --git a/apps/settings/lib/Controller/AISettingsController.php b/apps/settings/lib/Controller/AISettingsController.php new file mode 100644 index 00000000000..114cbf61514 --- /dev/null +++ b/apps/settings/lib/Controller/AISettingsController.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Settings\Controller; + +use OCA\Settings\Settings\Admin\ArtificialIntelligence; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\DataResponse; +use OCP\IAppConfig; +use OCP\IRequest; + +class AISettingsController extends Controller { + + public function __construct( + $appName, + IRequest $request, + private IAppConfig $appConfig, + ) { + parent::__construct($appName, $request); + } + + /** + * Sets the email settings + * + * @param array $settings + * @return DataResponse + */ + #[AuthorizedAdminSetting(settings: ArtificialIntelligence::class)] + public function update($settings) { + $keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.taskprocessing_provider_preferences','ai.taskprocessing_type_preferences', 'ai.translation_provider_preferences', 'ai.text2image_provider', 'ai.taskprocessing_guests']; + foreach ($keys as $key) { + if (!isset($settings[$key])) { + continue; + } + $this->appConfig->setValueString('core', $key, json_encode($settings[$key]), lazy: in_array($key, \OC\TaskProcessing\Manager::LAZY_CONFIG_KEYS, true)); + } + + return new DataResponse(); + } +} diff --git a/apps/settings/lib/Controller/AdminSettingsController.php b/apps/settings/lib/Controller/AdminSettingsController.php index dfaa26ff695..15e2c392148 100644 --- a/apps/settings/lib/Controller/AdminSettingsController.php +++ b/apps/settings/lib/Controller/AdminSettingsController.php @@ -1,42 +1,26 @@ <?php + /** - * @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * - * @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: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\Controller; -use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Group\ISubAdmin; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IRequest; -use OCP\IUser; use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; use OCP\Settings\IManager as ISettingsManager; -use OCP\Template; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class AdminSettingsController extends Controller { use CommonSettingsTrait; @@ -47,7 +31,9 @@ class AdminSettingsController extends Controller { ISettingsManager $settingsManager, IUserSession $userSession, IGroupManager $groupManager, - ISubAdmin $subAdmin + ISubAdmin $subAdmin, + IDeclarativeManager $declarativeSettingsManager, + IInitialState $initialState, ) { parent::__construct($appName, $request); $this->navigationManager = $navigationManager; @@ -55,66 +41,21 @@ class AdminSettingsController extends Controller { $this->userSession = $userSession; $this->groupManager = $groupManager; $this->subAdmin = $subAdmin; + $this->declarativeSettingsManager = $declarativeSettingsManager; + $this->initialState = $initialState; } /** - * @NoCSRFRequired - * @NoAdminRequired * @NoSubAdminRequired * We are checking the permissions in the getSettings method. If there is no allowed - * settings for the given section. The user will be gretted by an error message. + * settings for the given section. The user will be greeted by an error message. */ + #[NoAdminRequired] + #[NoCSRFRequired] public function index(string $section): TemplateResponse { - return $this->getIndexResponse('admin', $section); - } - - /** - * @param string $section - * @return array - */ - protected function getSettings($section) { - /** @var IUser $user */ - $user = $this->userSession->getUser(); - $isSubAdmin = !$this->groupManager->isAdmin($user->getUID()) && $this->subAdmin->isSubAdmin($user); - $settings = $this->settingsManager->getAllowedAdminSettings($section, $user); - if (empty($settings)) { - throw new NotAdminException("Logged in user doesn't have permission to access these settings."); - } - $formatted = $this->formatSettings($settings); - // Do not show legacy forms for sub admins - if ($section === 'additional' && !$isSubAdmin) { - $formatted['content'] .= $this->getLegacyForms(); - } - return $formatted; - } - - /** - * @return bool|string - */ - private function getLegacyForms() { - $forms = \OC_App::getForms('admin'); - - $forms = array_map(function ($form) { - if (preg_match('%(<h2(?P<class>[^>]*)>.*?</h2>)%i', $form, $regs)) { - $sectionName = str_replace('<h2' . $regs['class'] . '>', '', $regs[0]); - $sectionName = str_replace('</h2>', '', $sectionName); - $anchor = strtolower($sectionName); - $anchor = str_replace(' ', '-', $anchor); - - return [ - 'anchor' => $anchor, - 'section-name' => $sectionName, - 'form' => $form - ]; - } - return [ - 'form' => $form - ]; - }, $forms); - - $out = new Template('settings', 'settings/additional'); - $out->assign('forms', $forms); - - return $out->fetchPage(); + return $this->getIndexResponse( + 'admin', + $section, + ); } } diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php index a4addfc5b35..a85ee8cc20a 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -1,149 +1,251 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Settings\Controller; +use OC\App\AppManager; use OC\App\AppStore\Bundles\BundleFetcher; +use OC\App\AppStore\Fetcher\AppDiscoverFetcher; use OC\App\AppStore\Fetcher\AppFetcher; use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\App\AppStore\Version\VersionParser; use OC\App\DependencyAnalyzer; use OC\App\Platform; use OC\Installer; -use OC_App; -use OCP\App\IAppManager; +use OCA\AppAPI\Service\ExAppsPageService; +use OCP\App\AppPathNotFoundException; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\Http\Client\IClientService; use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; use OCP\IL10N; use OCP\INavigationManager; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\Security\RateLimiting\ILimiter; +use OCP\Server; +use OCP\Util; use Psr\Log\LoggerInterface; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class AppSettingsController extends Controller { - /** @var \OCP\IL10N */ - private $l10n; - /** @var IConfig */ - private $config; - /** @var INavigationManager */ - private $navigationManager; - /** @var IAppManager */ - private $appManager; - /** @var CategoryFetcher */ - private $categoryFetcher; - /** @var AppFetcher */ - private $appFetcher; - /** @var IFactory */ - private $l10nFactory; - /** @var BundleFetcher */ - private $bundleFetcher; - /** @var Installer */ - private $installer; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var LoggerInterface */ - private $logger; - /** @var array */ private $allApps = []; - /** - * @param string $appName - * @param IRequest $request - * @param IL10N $l10n - * @param IConfig $config - * @param INavigationManager $navigationManager - * @param IAppManager $appManager - * @param CategoryFetcher $categoryFetcher - * @param AppFetcher $appFetcher - * @param IFactory $l10nFactory - * @param BundleFetcher $bundleFetcher - * @param Installer $installer - * @param IURLGenerator $urlGenerator - * @param LoggerInterface $logger - */ - public function __construct(string $appName, - IRequest $request, - IL10N $l10n, - IConfig $config, - INavigationManager $navigationManager, - IAppManager $appManager, - CategoryFetcher $categoryFetcher, - AppFetcher $appFetcher, - IFactory $l10nFactory, - BundleFetcher $bundleFetcher, - Installer $installer, - IURLGenerator $urlGenerator, - LoggerInterface $logger) { + private IAppData $appData; + + public function __construct( + string $appName, + IRequest $request, + IAppDataFactory $appDataFactory, + private IL10N $l10n, + private IConfig $config, + private INavigationManager $navigationManager, + private AppManager $appManager, + private CategoryFetcher $categoryFetcher, + private AppFetcher $appFetcher, + private IFactory $l10nFactory, + private BundleFetcher $bundleFetcher, + private Installer $installer, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + private IInitialState $initialState, + private AppDiscoverFetcher $discoverFetcher, + private IClientService $clientService, + ) { parent::__construct($appName, $request); - $this->l10n = $l10n; - $this->config = $config; - $this->navigationManager = $navigationManager; - $this->appManager = $appManager; - $this->categoryFetcher = $categoryFetcher; - $this->appFetcher = $appFetcher; - $this->l10nFactory = $l10nFactory; - $this->bundleFetcher = $bundleFetcher; - $this->installer = $installer; - $this->urlGenerator = $urlGenerator; - $this->logger = $logger; + $this->appData = $appDataFactory->get('appstore'); } /** - * @NoCSRFRequired + * @psalm-suppress UndefinedClass AppAPI is shipped since 30.0.1 * * @return TemplateResponse */ + #[NoCSRFRequired] public function viewApps(): TemplateResponse { - $params = []; - $params['appstoreEnabled'] = $this->config->getSystemValueBool('appstoreenabled', true); - $params['updateCount'] = count($this->getAppsWithUpdates()); - $params['developerDocumentation'] = $this->urlGenerator->linkToDocs('developer-manual'); - $params['bundles'] = $this->getBundles(); $this->navigationManager->setActiveEntry('core_apps'); - $templateResponse = new TemplateResponse('settings', 'settings-vue', ['serverData' => $params]); + $this->initialState->provideInitialState('appstoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true)); + $this->initialState->provideInitialState('appstoreBundles', $this->getBundles()); + $this->initialState->provideInitialState('appstoreDeveloperDocs', $this->urlGenerator->linkToDocs('developer-manual')); + $this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates())); + + if ($this->appManager->isEnabledForAnyone('app_api')) { + try { + Server::get(ExAppsPageService::class)->provideAppApiState($this->initialState); + } catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) { + } + } + $policy = new ContentSecurityPolicy(); $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); + + $templateResponse = new TemplateResponse('settings', 'settings/empty', ['pageTitle' => $this->l10n->t('Settings')]); $templateResponse->setContentSecurityPolicy($policy); + Util::addStyle('settings', 'settings'); + Util::addScript('settings', 'vue-settings-apps-users-management'); + return $templateResponse; } + /** + * Get all active entries for the app discover section + */ + #[NoCSRFRequired] + public function getAppDiscoverJSON(): JSONResponse { + $data = $this->discoverFetcher->get(true); + return new JSONResponse(array_values($data)); + } + + /** + * Get a image for the app discover section - this is proxied for privacy and CSP reasons + * + * @param string $image + * @throws \Exception + */ + #[NoCSRFRequired] + public function getAppDiscoverMedia(string $fileName, ILimiter $limiter, IUserSession $session): Response { + $getEtag = $this->discoverFetcher->getETag() ?? date('Y-m'); + $etag = trim($getEtag, '"'); + + $folder = null; + try { + $folder = $this->appData->getFolder('app-discover-cache'); + $this->cleanUpImageCache($folder, $etag); + } catch (\Throwable $e) { + $folder = $this->appData->newFolder('app-discover-cache'); + } + + // Get the current cache folder + try { + $folder = $folder->getFolder($etag); + } catch (NotFoundException $e) { + $folder = $folder->newFolder($etag); + } + + $info = pathinfo($fileName); + $hashName = md5($fileName); + $allFiles = $folder->getDirectoryListing(); + // Try to find the file + $file = array_filter($allFiles, function (ISimpleFile $file) use ($hashName) { + return str_starts_with($file->getName(), $hashName); + }); + // Get the first entry + $file = reset($file); + // If not found request from Web + if ($file === false) { + $user = $session->getUser(); + // this route is not public thus we can assume a user is logged-in + assert($user !== null); + // Register a user request to throttle fetching external data + // this will prevent using the server for DoS of other systems. + $limiter->registerUserRequest( + 'settings-discover-media', + // allow up to 24 media requests per hour + // this should be a sane default when a completely new section is loaded + // keep in mind browsers request all files from a source-set + 24, + 60 * 60, + $user, + ); + + if (!$this->checkCanDownloadMedia($fileName)) { + $this->logger->warning('Tried to load media files for app discover section from untrusted source'); + return new NotFoundResponse(Http::STATUS_BAD_REQUEST); + } + + try { + $client = $this->clientService->newClient(); + $fileResponse = $client->get($fileName); + $contentType = $fileResponse->getHeader('Content-Type'); + $extension = $info['extension'] ?? ''; + $file = $folder->newFile($hashName . '.' . base64_encode($contentType) . '.' . $extension, $fileResponse->getBody()); + } catch (\Throwable $e) { + $this->logger->warning('Could not load media file for app discover section', ['media_src' => $fileName, 'exception' => $e]); + return new NotFoundResponse(); + } + } else { + // File was found so we can get the content type from the file name + $contentType = base64_decode(explode('.', $file->getName())[1] ?? ''); + } + + $response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $contentType]); + // cache for 7 days + $response->cacheFor(604800, false, true); + return $response; + } + + private function checkCanDownloadMedia(string $filename): bool { + $urlInfo = parse_url($filename); + if (!isset($urlInfo['host']) || !isset($urlInfo['path'])) { + return false; + } + + // Always allowed hosts + if ($urlInfo['host'] === 'nextcloud.com') { + return true; + } + + // Hosts that need further verification + // Github is only allowed if from our organization + $ALLOWED_HOSTS = ['github.com', 'raw.githubusercontent.com']; + if (!in_array($urlInfo['host'], $ALLOWED_HOSTS)) { + return false; + } + + if (str_starts_with($urlInfo['path'], '/nextcloud/') || str_starts_with($urlInfo['path'], '/nextcloud-gmbh/')) { + return true; + } + + return false; + } + + /** + * Remove orphaned folders from the image cache that do not match the current etag + * @param ISimpleFolder $folder The folder to clear + * @param string $etag The etag (directory name) to keep + */ + private function cleanUpImageCache(ISimpleFolder $folder, string $etag): void { + // Cleanup old cache folders + $allFiles = $folder->getDirectoryListing(); + foreach ($allFiles as $dir) { + try { + if ($dir->getName() !== $etag) { + $dir->delete(); + } + } catch (NotPermittedException $e) { + // ignore folder for now + } + } + } + private function getAppsWithUpdates() { $appClass = new \OC_App(); $apps = $appClass->listAllApps(); @@ -181,17 +283,21 @@ class AppSettingsController extends Controller { private function getAllCategories() { $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); - $formattedCategories = []; $categories = $this->categoryFetcher->get(); - foreach ($categories as $category) { - $formattedCategories[] = [ - 'id' => $category['id'], - 'ident' => $category['id'], - 'displayName' => isset($category['translations'][$currentLanguage]['name']) ? $category['translations'][$currentLanguage]['name'] : $category['translations']['en']['name'], - ]; - } + return array_map(fn ($category) => [ + 'id' => $category['id'], + 'displayName' => $category['translations'][$currentLanguage]['name'] ?? $category['translations']['en']['name'], + ], $categories); + } - return $formattedCategories; + /** + * Convert URL to proxied URL so CSP is no problem + */ + private function createProxyPreviewUrl(string $url): string { + if ($url === '') { + return ''; + } + return 'https://usercontent.apps.nextcloud.com/' . base64_encode($url); } private function fetchApps() { @@ -199,6 +305,16 @@ class AppSettingsController extends Controller { $apps = $appClass->listAllApps(); foreach ($apps as $app) { $app['installed'] = true; + + if (isset($app['screenshot'][0])) { + $appScreenshot = $app['screenshot'][0] ?? null; + if (is_array($appScreenshot)) { + // Screenshot with thumbnail + $appScreenshot = $appScreenshot['@value']; + } + + $app['screenshot'] = $this->createProxyPreviewUrl($appScreenshot); + } $this->allApps[$app['id']] = $app; } @@ -234,6 +350,7 @@ class AppSettingsController extends Controller { private function getAllApps() { return $this->allApps; } + /** * Get all available apps in a category * @@ -256,7 +373,7 @@ class AppSettingsController extends Controller { $apps = array_map(function (array $appData) use ($dependencyAnalyzer, $ignoreMaxApps) { if (isset($appData['appstoreData'])) { $appstoreData = $appData['appstoreData']; - $appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($appstoreData['screenshots'][0]['url']) : ''; + $appData['screenshot'] = $this->createProxyPreviewUrl($appstoreData['screenshots'][0]['url'] ?? ''); $appData['category'] = $appstoreData['categories']; $appData['releases'] = $appstoreData['releases']; } @@ -270,6 +387,10 @@ class AppSettingsController extends Controller { $groups = []; if (is_string($appData['groups'])) { $groups = json_decode($appData['groups']); + // ensure 'groups' is an array + if (!is_array($groups)) { + $groups = [$groups]; + } } $appData['groups'] = $groups; $appData['canUnInstall'] = !$appData['active'] && $appData['removable']; @@ -335,7 +456,14 @@ class AppSettingsController extends Controller { $nextCloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion(); } $phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']); - $existsLocally = \OC_App::getAppPath($app['id']) !== false; + + try { + $this->appManager->getAppPath($app['id']); + $existsLocally = true; + } catch (AppPathNotFoundException) { + $existsLocally = false; + } + $phpDependencies = []; if ($phpVersion->getMinimumVersion() !== '') { $phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion(); @@ -354,7 +482,7 @@ class AppSettingsController extends Controller { } } - $currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2); + $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); $enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no'); $groups = null; if ($enabledValue !== 'no' && $enabledValue !== 'yes') { @@ -362,20 +490,21 @@ class AppSettingsController extends Controller { } $currentVersion = ''; - if ($this->appManager->isInstalled($app['id'])) { + if ($this->appManager->isEnabledForAnyone($app['id'])) { $currentVersion = $this->appManager->getAppVersion($app['id']); } else { - $currentLanguage = $app['releases'][0]['version']; + $currentVersion = $app['releases'][0]['version']; } $formattedApps[] = [ 'id' => $app['id'], - 'name' => isset($app['translations'][$currentLanguage]['name']) ? $app['translations'][$currentLanguage]['name'] : $app['translations']['en']['name'], - 'description' => isset($app['translations'][$currentLanguage]['description']) ? $app['translations'][$currentLanguage]['description'] : $app['translations']['en']['description'], - 'summary' => isset($app['translations'][$currentLanguage]['summary']) ? $app['translations'][$currentLanguage]['summary'] : $app['translations']['en']['summary'], + 'app_api' => false, + 'name' => $app['translations'][$currentLanguage]['name'] ?? $app['translations']['en']['name'], + 'description' => $app['translations'][$currentLanguage]['description'] ?? $app['translations']['en']['description'], + 'summary' => $app['translations'][$currentLanguage]['summary'] ?? $app['translations']['en']['summary'], 'license' => $app['releases'][0]['licenses'], 'author' => $authors, - 'shipped' => false, + 'shipped' => $this->appManager->isShipped($app['id']), 'version' => $currentVersion, 'default_enable' => '', 'types' => [], @@ -395,7 +524,7 @@ class AppSettingsController extends Controller { 'missingMaxOwnCloudVersion' => false, 'missingMinOwnCloudVersion' => false, 'canInstall' => true, - 'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '', + 'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($app['screenshots'][0]['url']) : '', 'score' => $app['ratingOverall'], 'ratingNumOverall' => $app['ratingNumOverall'], 'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5, @@ -412,12 +541,11 @@ class AppSettingsController extends Controller { } /** - * @PasswordConfirmationRequired - * * @param string $appId * @param array $groups * @return JSONResponse */ + #[PasswordConfirmationRequired] public function enableApp(string $appId, array $groups = []): JSONResponse { return $this->enableApps([$appId], $groups); } @@ -427,21 +555,21 @@ class AppSettingsController extends Controller { * * apps will be enabled for specific groups only if $groups is defined * - * @PasswordConfirmationRequired * @param array $appIds * @param array $groups * @return JSONResponse */ + #[PasswordConfirmationRequired] public function enableApps(array $appIds, array $groups = []): JSONResponse { try { $updateRequired = false; foreach ($appIds as $appId) { - $appId = OC_App::cleanAppId($appId); + $appId = $this->appManager->cleanAppId($appId); // Check if app is already downloaded /** @var Installer $installer */ - $installer = \OC::$server->query(Installer::class); + $installer = Server::get(Installer::class); $isDownloaded = $installer->isDownloaded($appId); if (!$isDownloaded) { @@ -460,18 +588,18 @@ class AppSettingsController extends Controller { } } return new JSONResponse(['data' => ['update_required' => $updateRequired]]); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->logger->error('could not enable apps', ['exception' => $e]); return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR); } } private function getGroupList(array $groups) { - $groupManager = \OC::$server->getGroupManager(); + $groupManager = Server::get(IGroupManager::class); $groupsList = []; foreach ($groups as $group) { $groupItem = $groupManager->get($group); - if ($groupItem instanceof \OCP\IGroup) { + if ($groupItem instanceof IGroup) { $groupsList[] = $groupManager->get($group); } } @@ -479,25 +607,23 @@ class AppSettingsController extends Controller { } /** - * @PasswordConfirmationRequired - * * @param string $appId * @return JSONResponse */ + #[PasswordConfirmationRequired] public function disableApp(string $appId): JSONResponse { return $this->disableApps([$appId]); } /** - * @PasswordConfirmationRequired - * * @param array $appIds * @return JSONResponse */ + #[PasswordConfirmationRequired] public function disableApps(array $appIds): JSONResponse { try { foreach ($appIds as $appId) { - $appId = OC_App::cleanAppId($appId); + $appId = $this->appManager->cleanAppId($appId); $this->appManager->disableApp($appId); } return new JSONResponse([]); @@ -508,15 +634,16 @@ class AppSettingsController extends Controller { } /** - * @PasswordConfirmationRequired - * * @param string $appId * @return JSONResponse */ + #[PasswordConfirmationRequired] public function uninstallApp(string $appId): JSONResponse { - $appId = OC_App::cleanAppId($appId); + $appId = $this->appManager->cleanAppId($appId); $result = $this->installer->removeApp($appId); if ($result !== false) { + // If this app was force enabled, remove the force-enabled-state + $this->appManager->removeOverwriteNextcloudRequirement($appId); $this->appManager->clearAppsCache(); return new JSONResponse(['data' => ['appid' => $appId]]); } @@ -528,7 +655,7 @@ class AppSettingsController extends Controller { * @return JSONResponse */ public function updateApp(string $appId): JSONResponse { - $appId = OC_App::cleanAppId($appId); + $appId = $this->appManager->cleanAppId($appId); $this->config->setSystemValue('maintenance', true); try { @@ -555,8 +682,8 @@ class AppSettingsController extends Controller { } public function force(string $appId): JSONResponse { - $appId = OC_App::cleanAppId($appId); - $this->appManager->ignoreNextcloudRequirementForApp($appId); + $appId = $this->appManager->cleanAppId($appId); + $this->appManager->overwriteNextcloudRequirement($appId); return new JSONResponse(); } } diff --git a/apps/settings/lib/Controller/AuthSettingsController.php b/apps/settings/lib/Controller/AuthSettingsController.php index 38db7be1e91..8652a49fb1d 100644 --- a/apps/settings/lib/Controller/AuthSettingsController.php +++ b/apps/settings/lib/Controller/AuthSettingsController.php @@ -1,50 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Fabrizio Steiner <fabrizio.steiner@gmail.com> - * @author Greta Doci <gretadoci@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Marcel Waldvogel <marcel.waldvogel@uni-konstanz.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergej Nikolaev <kinolaev@gmail.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Settings\Controller; use BadMethodCallException; -use OC\Authentication\Exceptions\ExpiredTokenException; -use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\InvalidTokenException as OcInvalidTokenException; use OC\Authentication\Exceptions\PasswordlessTokenException; -use OC\Authentication\Exceptions\WipeTokenException; use OC\Authentication\Token\INamedToken; use OC\Authentication\Token\IProvider; -use OC\Authentication\Token\IToken; use OC\Authentication\Token\RemoteWipe; use OCA\Settings\Activity\Provider; use OCP\Activity\IManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\JSONResponse; +use OCP\Authentication\Exceptions\ExpiredTokenException; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Token\IToken; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; @@ -53,31 +32,12 @@ use OCP\Session\Exceptions\SessionNotAvailableException; use Psr\Log\LoggerInterface; class AuthSettingsController extends Controller { - /** @var IProvider */ private $tokenProvider; - /** @var ISession */ - private $session; - - /** IUserSession */ - private $userSession; - - /** @var string */ - private $uid; - - /** @var ISecureRandom */ - private $random; - - /** @var IManager */ - private $activityManager; - /** @var RemoteWipe */ private $remoteWipe; - /** @var LoggerInterface */ - private $logger; - /** * @param string $appName * @param IRequest $request @@ -90,35 +50,31 @@ class AuthSettingsController extends Controller { * @param RemoteWipe $remoteWipe * @param LoggerInterface $logger */ - public function __construct(string $appName, - IRequest $request, - IProvider $tokenProvider, - ISession $session, - ISecureRandom $random, - ?string $userId, - IUserSession $userSession, - IManager $activityManager, - RemoteWipe $remoteWipe, - LoggerInterface $logger) { + public function __construct( + string $appName, + IRequest $request, + IProvider $tokenProvider, + private ISession $session, + private ISecureRandom $random, + private ?string $userId, + private IUserSession $userSession, + private IManager $activityManager, + RemoteWipe $remoteWipe, + private LoggerInterface $logger, + ) { parent::__construct($appName, $request); $this->tokenProvider = $tokenProvider; - $this->uid = $userId; - $this->userSession = $userSession; - $this->session = $session; - $this->random = $random; - $this->activityManager = $activityManager; $this->remoteWipe = $remoteWipe; - $this->logger = $logger; } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired * * @param string $name * @return JSONResponse */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] public function create($name) { if ($this->checkAppToken()) { return $this->getServiceNotAvailableResponse(); @@ -150,7 +106,7 @@ class AuthSettingsController extends Controller { } $token = $this->generateRandomDeviceToken(); - $deviceToken = $this->tokenProvider->generateToken($token, $this->uid, $loginName, $password, $name, IToken::PERMANENT_TOKEN); + $deviceToken = $this->tokenProvider->generateToken($token, $this->userId, $loginName, $password, $name, IToken::PERMANENT_TOKEN); $tokenData = $deviceToken->jsonSerialize(); $tokenData['canDelete'] = true; $tokenData['canRename'] = true; @@ -193,12 +149,12 @@ class AuthSettingsController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired * * @param int $id * @return array|JSONResponse */ + #[NoAdminRequired] public function destroy($id) { if ($this->checkAppToken()) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); @@ -213,13 +169,12 @@ class AuthSettingsController extends Controller { return new JSONResponse([], Http::STATUS_NOT_FOUND); } - $this->tokenProvider->invalidateTokenById($this->uid, $token->getId()); + $this->tokenProvider->invalidateTokenById($this->userId, $token->getId()); $this->publishActivity(Provider::APP_TOKEN_DELETED, $token->getId(), ['name' => $token->getName()]); return []; } /** - * @NoAdminRequired * @NoSubAdminRequired * * @param int $id @@ -227,6 +182,7 @@ class AuthSettingsController extends Controller { * @param string $name * @return array|JSONResponse */ + #[NoAdminRequired] public function update($id, array $scope, string $name) { if ($this->checkAppToken()) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); @@ -241,8 +197,8 @@ class AuthSettingsController extends Controller { $currentName = $token->getName(); if ($scope !== $token->getScopeAsArray()) { - $token->setScope(['filesystem' => $scope['filesystem']]); - $this->publishActivity($scope['filesystem'] ? Provider::APP_TOKEN_FILESYSTEM_GRANTED : Provider::APP_TOKEN_FILESYSTEM_REVOKED, $token->getId(), ['name' => $currentName]); + $token->setScope([IToken::SCOPE_FILESYSTEM => $scope[IToken::SCOPE_FILESYSTEM]]); + $this->publishActivity($scope[IToken::SCOPE_FILESYSTEM] ? Provider::APP_TOKEN_FILESYSTEM_GRANTED : Provider::APP_TOKEN_FILESYSTEM_REVOKED, $token->getId(), ['name' => $currentName]); } if (mb_strlen($name) > 128) { @@ -267,8 +223,8 @@ class AuthSettingsController extends Controller { $event = $this->activityManager->generateEvent(); $event->setApp('settings') ->setType('security') - ->setAffectedUser($this->uid) - ->setAuthor($this->uid) + ->setAffectedUser($this->userId) + ->setAuthor($this->userId) ->setSubject($subject, $parameters) ->setObject('app_token', $id, 'App Password'); @@ -292,22 +248,23 @@ class AuthSettingsController extends Controller { } catch (ExpiredTokenException $e) { $token = $e->getToken(); } - if ($token->getUID() !== $this->uid) { - throw new InvalidTokenException('This token does not belong to you!'); + if ($token->getUID() !== $this->userId) { + /** @psalm-suppress DeprecatedClass We have to throw the OC version so both OC and OCP catches catch it */ + throw new OcInvalidTokenException('This token does not belong to you!'); } return $token; } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired * * @param int $id * @return JSONResponse * @throws InvalidTokenException - * @throws \OC\Authentication\Exceptions\ExpiredTokenException + * @throws ExpiredTokenException */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] public function wipe(int $id): JSONResponse { if ($this->checkAppToken()) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); diff --git a/apps/settings/lib/Controller/AuthorizedGroupController.php b/apps/settings/lib/Controller/AuthorizedGroupController.php index dcae5b31332..82a1ca4703e 100644 --- a/apps/settings/lib/Controller/AuthorizedGroupController.php +++ b/apps/settings/lib/Controller/AuthorizedGroupController.php @@ -1,43 +1,26 @@ <?php /** - * @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu> - * - * @author Carl Schwan <carl@carlschwan.eu> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\Controller; use OC\Settings\AuthorizedGroup; use OCA\Settings\Service\AuthorizedGroupService; use OCA\Settings\Service\NotFoundException; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\DataResponse; use OCP\DB\Exception; use OCP\IRequest; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Controller; class AuthorizedGroupController extends Controller { - /** @var AuthorizedGroupService $authorizedGroupService */ - private $authorizedGroupService; - - public function __construct(string $AppName, IRequest $request, AuthorizedGroupService $authorizedGroupService) { + public function __construct( + string $AppName, + IRequest $request, + private AuthorizedGroupService $authorizedGroupService, + ) { parent::__construct($AppName, $request); - $this->authorizedGroupService = $authorizedGroupService; } /** @@ -74,7 +57,7 @@ class AuthorizedGroupController extends Controller { $this->authorizedGroupService->create($groupData['gid'], $class); } } - + return new DataResponse(['valid' => true]); } } diff --git a/apps/settings/lib/Controller/ChangePasswordController.php b/apps/settings/lib/Controller/ChangePasswordController.php index 20ec28220a5..a874a47c16a 100644 --- a/apps/settings/lib/Controller/ChangePasswordController.php +++ b/apps/settings/lib/Controller/ChangePasswordController.php @@ -1,33 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Matthew Setter <matthew@matthewsetter.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ // FIXME: disabled for now to be able to inject IGroupManager and also use // getSubAdmin() @@ -37,49 +12,45 @@ namespace OCA\Settings\Controller; use OC\Group\Manager as GroupManager; use OC\User\Session; +use OCA\Encryption\KeyManager; +use OCA\Encryption\Recovery; use OCP\App\IAppManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\JSONResponse; use OCP\HintException; -use OCP\IGroupManager; use OCP\IL10N; use OCP\IRequest; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Server; class ChangePasswordController extends Controller { - private ?string $userId; - private IUserManager $userManager; - private IL10N $l; - private GroupManager $groupManager; private Session $userSession; - private IAppManager $appManager; - public function __construct(string $appName, - IRequest $request, - ?string $userId, - IUserManager $userManager, - IUserSession $userSession, - IGroupManager $groupManager, - IAppManager $appManager, - IL10N $l) { + public function __construct( + string $appName, + IRequest $request, + private ?string $userId, + private IUserManager $userManager, + IUserSession $userSession, + private GroupManager $groupManager, + private IAppManager $appManager, + private IL10N $l, + ) { parent::__construct($appName, $request); - - $this->userId = $userId; - $this->userManager = $userManager; $this->userSession = $userSession; - $this->groupManager = $groupManager; - $this->appManager = $appManager; - $this->l = $l; } /** - * @NoAdminRequired * @NoSubAdminRequired - * @BruteForceProtection(action=changePersonalPassword) */ - public function changePersonalPassword(string $oldpassword = '', string $newpassword = null): JSONResponse { + #[NoAdminRequired] + #[BruteForceProtection(action: 'changePersonalPassword')] + public function changePersonalPassword(string $oldpassword = '', ?string $newpassword = null): JSONResponse { $loginName = $this->userSession->getLoginName(); /** @var IUser $user */ $user = $this->userManager->checkPassword($loginName, $oldpassword); @@ -123,16 +94,14 @@ class ChangePasswordController extends Controller { ]); } - /** - * @NoAdminRequired - * @PasswordConfirmationRequired - */ - public function changeUserPassword(string $username = null, string $password = null, string $recoveryPassword = null): JSONResponse { + #[NoAdminRequired] + #[PasswordConfirmationRequired] + public function changeUserPassword(?string $username = null, ?string $password = null, ?string $recoveryPassword = null): JSONResponse { if ($username === null) { return new JSONResponse([ 'status' => 'error', 'data' => [ - 'message' => $this->l->t('No user supplied'), + 'message' => $this->l->t('No Login supplied'), ], ]); } @@ -157,9 +126,9 @@ class ChangePasswordController extends Controller { $currentUser = $this->userSession->getUser(); $targetUser = $this->userManager->get($username); - if ($currentUser === null || $targetUser === null || - !($this->groupManager->isAdmin($this->userId) || - $this->groupManager->getSubAdmin()->isUserAccessible($currentUser, $targetUser)) + if ($currentUser === null || $targetUser === null + || !($this->groupManager->isAdmin($this->userId) + || $this->groupManager->getSubAdmin()->isUserAccessible($currentUser, $targetUser)) ) { return new JSONResponse([ 'status' => 'error', @@ -171,36 +140,8 @@ class ChangePasswordController extends Controller { if ($this->appManager->isEnabledForUser('encryption')) { //handle the recovery case - $crypt = new \OCA\Encryption\Crypto\Crypt( - \OC::$server->getLogger(), - \OC::$server->getUserSession(), - \OC::$server->getConfig(), - \OC::$server->getL10N('encryption')); - $keyStorage = \OC::$server->getEncryptionKeyStorage(); - $util = new \OCA\Encryption\Util( - new \OC\Files\View(), - $crypt, - \OC::$server->getLogger(), - \OC::$server->getUserSession(), - \OC::$server->getConfig(), - \OC::$server->getUserManager()); - $keyManager = new \OCA\Encryption\KeyManager( - $keyStorage, - $crypt, - \OC::$server->getConfig(), - \OC::$server->getUserSession(), - new \OCA\Encryption\Session(\OC::$server->getSession()), - \OC::$server->getLogger(), - $util, - \OC::$server->getLockingProvider() - ); - $recovery = new \OCA\Encryption\Recovery( - \OC::$server->getUserSession(), - $crypt, - $keyManager, - \OC::$server->getConfig(), - \OC::$server->getEncryptionFilesHelper(), - new \OC\Files\View()); + $keyManager = Server::get(KeyManager::class); + $recovery = Server::get(Recovery::class); $recoveryAdminEnabled = $recovery->isRecoveryKeyEnabled(); $validRecoveryPassword = false; @@ -214,7 +155,7 @@ class ChangePasswordController extends Controller { return new JSONResponse([ 'status' => 'error', 'data' => [ - 'message' => $this->l->t('Please provide an admin recovery password; otherwise, all user data will be lost.'), + 'message' => $this->l->t('Please provide an admin recovery password; otherwise, all account data will be lost.'), ] ]); } elseif ($recoveryEnabledForUser && ! $validRecoveryPassword) { @@ -240,7 +181,7 @@ class ChangePasswordController extends Controller { return new JSONResponse([ 'status' => 'error', 'data' => [ - 'message' => $this->l->t('Backend does not support password change, but the user\'s encryption key was updated.'), + 'message' => $this->l->t('Backend does not support password change, but the encryption of the account key was updated.'), ] ]); } elseif (!$result && !$recoveryEnabledForUser) { diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index a5c158d2602..2a189a37ce6 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -1,408 +1,63 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Cthulhux <git@tuxproject.de> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Derek <derek.kelly27@gmail.com> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Ko- <k.stoffelen@cs.ru.nl> - * @author Lauris Binde <laurisb@users.noreply.github.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Michael Weimann <mail@michael-weimann.eu> - * @author Morris Jobke <hey@morrisjobke.de> - * @author nhirokinet <nhirokinet@nhiroki.net> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sven Strickroth <email@cs-ware.de> - * @author Sylvia van Os <sylvia@hackerchick.me> - * @author timm2k <timm2k@gmx.de> - * @author Timo Förster <tfoerster@webfoersterei.de> - * @author Valdnet <47037905+Valdnet@users.noreply.github.com> - * @author MichaIng <micha@dietpi.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Settings\Controller; -use bantu\IniGetWrapper\IniGetWrapper; -use DirectoryIterator; -use Doctrine\DBAL\Exception; -use Doctrine\DBAL\Platforms\SqlitePlatform; -use Doctrine\DBAL\TransactionIsolationLevel; -use GuzzleHttp\Exception\ClientException; -use OC; use OC\AppFramework\Http; -use OC\DB\Connection; -use OC\DB\MissingColumnInformation; -use OC\DB\MissingIndexInformation; -use OC\DB\MissingPrimaryKeyInformation; -use OC\DB\SchemaWrapper; use OC\IntegrityCheck\Checker; -use OC\Lock\NoopLockingProvider; -use OC\MemoryInfo; -use OCA\Settings\SetupChecks\CheckUserCertificates; -use OCA\Settings\SetupChecks\LdapInvalidUuids; -use OCA\Settings\SetupChecks\LegacySSEKeyFormat; -use OCA\Settings\SetupChecks\PhpDefaultCharset; -use OCA\Settings\SetupChecks\PhpOutputBuffering; -use OCA\Settings\SetupChecks\SupportedDatabase; -use OCP\App\IAppManager; +use OCA\Settings\Settings\Admin\Overview; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\RedirectResponse; -use OCP\DB\Types; -use OCP\Http\Client\IClientService; use OCP\IConfig; -use OCP\IDateTimeFormatter; -use OCP\IDBConnection; use OCP\IL10N; use OCP\IRequest; -use OCP\IServerContainer; -use OCP\ITempManager; use OCP\IURLGenerator; -use OCP\Lock\ILockingProvider; -use OCP\Notification\IManager; -use OCP\Security\ISecureRandom; +use OCP\SetupCheck\ISetupCheckManager; use Psr\Log\LoggerInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class CheckSetupController extends Controller { - /** @var IConfig */ - private $config; - /** @var IClientService */ - private $clientService; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IL10N */ - private $l10n; /** @var Checker */ private $checker; - /** @var LoggerInterface */ - private $logger; - /** @var EventDispatcherInterface */ - private $dispatcher; - /** @var Connection */ - private $db; - /** @var ILockingProvider */ - private $lockingProvider; - /** @var IDateTimeFormatter */ - private $dateTimeFormatter; - /** @var MemoryInfo */ - private $memoryInfo; - /** @var ISecureRandom */ - private $secureRandom; - /** @var IniGetWrapper */ - private $iniGetWrapper; - /** @var IDBConnection */ - private $connection; - /** @var ITempManager */ - private $tempManager; - /** @var IManager */ - private $manager; - /** @var IAppManager */ - private $appManager; - /** @var IServerContainer */ - private $serverContainer; - public function __construct($AppName, - IRequest $request, - IConfig $config, - IClientService $clientService, - IURLGenerator $urlGenerator, - IL10N $l10n, - Checker $checker, - LoggerInterface $logger, - EventDispatcherInterface $dispatcher, - Connection $db, - ILockingProvider $lockingProvider, - IDateTimeFormatter $dateTimeFormatter, - MemoryInfo $memoryInfo, - ISecureRandom $secureRandom, - IniGetWrapper $iniGetWrapper, - IDBConnection $connection, - ITempManager $tempManager, - IManager $manager, - IAppManager $appManager, - IServerContainer $serverContainer + public function __construct( + $AppName, + IRequest $request, + private IConfig $config, + private IURLGenerator $urlGenerator, + private IL10N $l10n, + Checker $checker, + private LoggerInterface $logger, + private ISetupCheckManager $setupCheckManager, ) { parent::__construct($AppName, $request); - $this->config = $config; - $this->clientService = $clientService; - $this->urlGenerator = $urlGenerator; - $this->l10n = $l10n; $this->checker = $checker; - $this->logger = $logger; - $this->dispatcher = $dispatcher; - $this->db = $db; - $this->lockingProvider = $lockingProvider; - $this->dateTimeFormatter = $dateTimeFormatter; - $this->memoryInfo = $memoryInfo; - $this->secureRandom = $secureRandom; - $this->iniGetWrapper = $iniGetWrapper; - $this->connection = $connection; - $this->tempManager = $tempManager; - $this->manager = $manager; - $this->appManager = $appManager; - $this->serverContainer = $serverContainer; - } - - /** - * Check if is fair use of free push service - * @return bool - */ - private function isFairUseOfFreePushService(): bool { - return $this->manager->isFairUseOfFreePushService(); - } - - /** - * Checks if the server can connect to the internet using HTTPS and HTTP - * @return bool - */ - private function hasInternetConnectivityProblems(): bool { - if ($this->config->getSystemValue('has_internet_connection', true) === false) { - return false; - } - - $siteArray = $this->config->getSystemValue('connectivity_check_domains', [ - 'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org' - ]); - - foreach ($siteArray as $site) { - if ($this->isSiteReachable($site)) { - return false; - } - } - return true; - } - - /** - * 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 - */ - private function isSiteReachable(string $site): bool { - try { - $client = $this->clientService->newClient(); - // if there is no protocol, test http:// AND https:// - if (preg_match('/^https?:\/\//', $site) !== 1) { - $httpSite = 'http://' . $site . '/'; - $client->get($httpSite); - $httpsSite = 'https://' . $site . '/'; - $client->get($httpsSite); - } else { - $client->get($site); - } - } catch (\Exception $e) { - $this->logger->error('Cannot connect to: ' . $site, [ - 'app' => 'internet_connection_check', - 'exception' => $e, - ]); - return false; - } - return true; - } - - /** - * Checks whether a local memcache is installed or not - * @return bool - */ - private function isMemcacheConfigured() { - return $this->config->getSystemValue('memcache.local', null) !== null; - } - - /** - * Whether PHP can generate "secure" pseudorandom integers - * - * @return bool - */ - private function isRandomnessSecure() { - try { - $this->secureRandom->generate(1); - } catch (\Exception $ex) { - return false; - } - return true; - } - - /** - * Public for the sake of unit-testing - * - * @return array - */ - protected function getCurlVersion() { - return curl_version(); - } - - /** - * Check if the used SSL lib is outdated. Older OpenSSL and NSS versions do - * have multiple bugs which likely lead to problems in combination with - * functionality required by ownCloud such as SNI. - * - * @link https://github.com/owncloud/core/issues/17446#issuecomment-122877546 - * @link https://bugzilla.redhat.com/show_bug.cgi?id=1241172 - * @return string - */ - private function isUsedTlsLibOutdated() { - // Don't run check when: - // 1. Server has `has_internet_connection` set to false - // 2. AppStore AND S2S is disabled - if (!$this->config->getSystemValue('has_internet_connection', true)) { - return ''; - } - if (!$this->config->getSystemValue('appstoreenabled', true) - && $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'no' - && $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'no') { - return ''; - } - - $versionString = $this->getCurlVersion(); - if (isset($versionString['ssl_version'])) { - $versionString = $versionString['ssl_version']; - } else { - return ''; - } - - $features = $this->l10n->t('installing and updating apps via the App Store or Federated Cloud Sharing'); - if (!$this->config->getSystemValue('appstoreenabled', true)) { - $features = $this->l10n->t('Federated Cloud Sharing'); - } - - // Check if at least OpenSSL after 1.01d or 1.0.2b - if (strpos($versionString, 'OpenSSL/') === 0) { - $majorVersion = substr($versionString, 8, 5); - $patchRelease = substr($versionString, 13, 6); - - if (($majorVersion === '1.0.1' && ord($patchRelease) < ord('d')) || - ($majorVersion === '1.0.2' && ord($patchRelease) < ord('b'))) { - return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['OpenSSL', $versionString, $features]); - } - } - - // Check if NSS and perform heuristic check - if (strpos($versionString, 'NSS/') === 0) { - try { - $firstClient = $this->clientService->newClient(); - $firstClient->get('https://nextcloud.com/'); - - $secondClient = $this->clientService->newClient(); - $secondClient->get('https://nextcloud.com/'); - } catch (ClientException $e) { - if ($e->getResponse()->getStatusCode() === 400) { - return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['NSS', $versionString, $features]); - } - } catch (\Exception $e) { - $this->logger->warning('error checking curl', [ - 'app' => 'settings', - 'exception' => $e, - ]); - return $this->l10n->t('Could not determine if TLS version of cURL is outdated or not because an error happened during the HTTPS request against https://nextcloud.com. Please check the Nextcloud log file for more details.'); - } - } - - return ''; - } - - /** - * Whether the version is outdated - * - * @return bool - */ - protected function isPhpOutdated(): bool { - return PHP_VERSION_ID < 80000; } /** - * Whether the php version is still supported (at time of release) - * according to: https://www.php.net/supported-versions.php - * - * @return array - */ - private function isPhpSupported(): array { - return ['eol' => $this->isPhpOutdated(), 'version' => PHP_VERSION]; - } - - /** - * Check if the reverse proxy configuration is working as expected - * - * @return bool - */ - private function forwardedForHeadersWorking(): bool { - $trustedProxies = $this->config->getSystemValue('trusted_proxies', []); - $remoteAddress = $this->request->getHeader('REMOTE_ADDR'); - - if (empty($trustedProxies) && $this->request->getHeader('X-Forwarded-Host') !== '') { - return false; - } - - if (\is_array($trustedProxies)) { - if (\in_array($remoteAddress, $trustedProxies, true) && $remoteAddress !== '127.0.0.1') { - return $remoteAddress !== $this->request->getRemoteAddress(); - } - } else { - return false; - } - - // either not enabled or working correctly - return true; - } - - /** - * Checks if the correct memcache module for PHP is installed. Only - * fails if memcached is configured and the working module is not installed. - * - * @return bool - */ - private function isCorrectMemcachedPHPModuleInstalled() { - if ($this->config->getSystemValue('memcache.distributed', null) !== '\OC\Memcache\Memcached') { - return true; - } - - // there are two different memcache modules for PHP - // we only support memcached and not memcache - // https://code.google.com/p/memcached/wiki/PHPClientComparison - return !(!extension_loaded('memcached') && extension_loaded('memcache')); - } - - /** - * Checks if set_time_limit is not disabled. - * - * @return bool + * @return DataResponse */ - private function isSettimelimitAvailable() { - if (function_exists('set_time_limit') - && strpos(ini_get('disable_functions'), 'set_time_limit') === false) { - return true; - } - - return false; + #[NoCSRFRequired] + #[NoAdminRequired] + public function setupCheckManager(): DataResponse { + return new DataResponse($this->setupCheckManager->runAll()); } /** * @return RedirectResponse - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) */ + #[NoCSRFRequired] + #[AuthorizedAdminSetting(settings: Overview::class)] public function rescanFailedIntegrityCheck(): RedirectResponse { $this->checker->runInstanceVerification(); return new RedirectResponse( @@ -410,10 +65,8 @@ class CheckSetupController extends Controller { ); } - /** - * @NoCSRFRequired - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) - */ + #[NoCSRFRequired] + #[AuthorizedAdminSetting(settings: Overview::class)] public function getFailedIntegrityCheckFiles(): DataDisplayResponse { if (!$this->checker->isCodeCheckEnforced()) { return new DataDisplayResponse('Integrity checker has been disabled. Integrity cannot be verified.'); @@ -421,6 +74,10 @@ class CheckSetupController extends Controller { $completeResults = $this->checker->getResults(); + if ($completeResults === null) { + return new DataDisplayResponse('Integrity checker has not been run. Integrity information not available.'); + } + if (!empty($completeResults)) { $formattedTextResponse = 'Technical information ===================== @@ -468,448 +125,13 @@ Raw output } /** - * Checks whether a PHP OPcache is properly set up - * @return string[] The list of OPcache setup recommendations - */ - protected function getOpcacheSetupRecommendations(): array { - // If the module is not loaded, return directly to skip inapplicable checks - if (!extension_loaded('Zend OPcache')) { - return [$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 = $this->iniGetWrapper->getString('opcache.restrict_api'); - if (isset($permittedPath) && $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 <code>opcache.enable=1</code> to your PHP configuration.'); - - // 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, <code>opcache.save_comments=1</code> must be set for Nextcloud to function.'); - } - - 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 <code>opcache.restrict_api</code> or unset this setting to disable OPcache API restrictions, to prevent errors during Nextcloud core or app upgrades.'); - } - } elseif (!$isPermitted) { - $recommendations[] = $this->l10n->t('Nextcloud is not allowed to use the OPcache API. It is highly recommended to include all Nextcloud directories with <code>opcache.restrict_api</code> or unset this setting to disable OPcache API restrictions, to prevent errors during Nextcloud core or app upgrades.'); - } else { - // Check whether opcache_get_status has been explicitly disabled an in case skip usage based checks - $disabledFunctions = $this->iniGetWrapper->getString('disable_functions'); - if (isset($disabledFunctions) && str_contains($disabledFunctions, 'opcache_get_status')) { - return []; - } - - $status = opcache_get_status(false); - - // Recommend to raise value, if more than 90% of max value is reached - if ( - empty($status['opcache_statistics']['max_cached_keys']) || - ($status['opcache_statistics']['num_cached_keys'] / $status['opcache_statistics']['max_cached_keys'] > 0.9) - ) { - $recommendations[] = $this->l10n->t('The maximum number of OPcache keys is nearly exceeded. To assure that all scripts can be kept in the cache, it is recommended to apply <code>opcache.max_accelerated_files</code> to your PHP configuration with a value higher than <code>%s</code>.', [($this->iniGetWrapper->getNumeric('opcache.max_accelerated_files') ?: 'currently')]); - } - - if ( - empty($status['memory_usage']['free_memory']) || - ($status['memory_usage']['used_memory'] / $status['memory_usage']['free_memory'] > 9) - ) { - $recommendations[] = $this->l10n->t('The OPcache buffer is nearly full. To assure that all scripts can be hold in cache, it is recommended to apply <code>opcache.memory_consumption</code> to your PHP configuration with a value higher than <code>%s</code>.', [($this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?: 'currently')]); - } - - if ( - // Do not recommend to raise the interned strings buffer size above a quarter of the total OPcache size - ($this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') < $this->iniGetWrapper->getNumeric('opcache.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 <code>opcache.interned_strings_buffer</code> to your PHP configuration with a value higher than <code>%s</code>.', [($this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') ?: 'currently')]); - } - } - - return $recommendations; - } - - /** - * Check if the required FreeType functions are present - * @return bool - */ - protected function hasFreeTypeSupport() { - return function_exists('imagettfbbox') && function_exists('imagettftext'); - } - - protected function hasMissingIndexes(): array { - $indexInfo = new MissingIndexInformation(); - // Dispatch event so apps can also hint for pending index updates if needed - $event = new GenericEvent($indexInfo); - $this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event); - - return $indexInfo->getListOfMissingIndexes(); - } - - protected function hasMissingPrimaryKeys(): array { - $info = new MissingPrimaryKeyInformation(); - // Dispatch event so apps can also hint for pending index updates if needed - $event = new GenericEvent($info); - $this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_PRIMARY_KEYS_EVENT, $event); - - return $info->getListOfMissingPrimaryKeys(); - } - - protected function hasMissingColumns(): array { - $indexInfo = new MissingColumnInformation(); - // Dispatch event so apps can also hint for pending index updates if needed - $event = new GenericEvent($indexInfo); - $this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_COLUMNS_EVENT, $event); - - return $indexInfo->getListOfMissingColumns(); - } - - protected function isSqliteUsed() { - return strpos($this->config->getSystemValue('dbtype'), 'sqlite') !== false; - } - - protected function isReadOnlyConfig(): bool { - return \OC_Helper::isReadOnlyConfigEnabled(); - } - - 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; - } - - protected function hasValidTransactionIsolationLevel(): bool { - try { - if ($this->db->getDatabasePlatform() instanceof SqlitePlatform) { - return true; - } - - return $this->db->getTransactionIsolation() === TransactionIsolationLevel::READ_COMMITTED; - } catch (Exception $e) { - // ignore - } - - return true; - } - - protected function hasFileinfoInstalled(): bool { - return \OC_Util::fileInfoLoaded(); - } - - protected function hasWorkingFileLocking(): bool { - return !($this->lockingProvider instanceof NoopLockingProvider); - } - - protected function getSuggestedOverwriteCliURL(): string { - $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)) { - $suggestedOverwriteCliUrl = ''; - } - - return $suggestedOverwriteCliUrl; - } - - protected function getLastCronInfo(): array { - $lastCronRun = $this->config->getAppValue('core', 'lastcron', 0); - return [ - 'diffInSeconds' => time() - $lastCronRun, - 'relativeTime' => $this->dateTimeFormatter->formatTimeSpan($lastCronRun), - 'backgroundJobsUrl' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'server']) . '#backgroundjobs', - ]; - } - - protected function getCronErrors() { - $errors = json_decode($this->config->getAppValue('core', 'cronErrors', ''), true); - - if (is_array($errors)) { - return $errors; - } - - return []; - } - - private function isTemporaryDirectoryWritable(): bool { - try { - if (!empty($this->tempManager->getTempBaseDir())) { - return true; - } - } catch (\Exception $e) { - } - return false; - } - - /** - * Iterates through the configured app roots and - * tests if the subdirectories are owned by the same user than the current user. - * - * @return array - */ - protected function getAppDirsWithDifferentOwner(): array { - $currentUser = posix_getuid(); - $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; - } - - /** - * Checks for potential PHP modules that would improve the instance - * - * @return string[] A list of PHP modules that is recommended - */ - protected function hasRecommendedPHPModules(): array { - $recommendedPHPModules = []; - - if (!extension_loaded('intl')) { - $recommendedPHPModules[] = 'intl'; - } - - if (!extension_loaded('sysvsem')) { - // used to limit the usage of resources by preview generator - $recommendedPHPModules[] = 'sysvsem'; - } - - if (!defined('PASSWORD_ARGON2I')) { - // Installing php-sodium on >=php7.4 will provide PASSWORD_ARGON2I - // on previous version argon2 wasn't part of the "standard" extension - // and RedHat disabled it so even installing php-sodium won't provide argon2i - // support in password_hash/password_verify. - $recommendedPHPModules[] = 'sodium'; - } - - return $recommendedPHPModules; - } - - protected function isImagickEnabled(): bool { - if ($this->config->getAppValue('theming', 'enabled', 'no') === 'yes') { - if (!extension_loaded('imagick')) { - return false; - } - } - return true; - } - - protected function areWebauthnExtensionsEnabled(): bool { - if (!extension_loaded('bcmath')) { - return false; - } - if (!extension_loaded('gmp')) { - return false; - } - return true; - } - - protected function is64bit(): bool { - if (PHP_INT_SIZE < 8) { - return false; - } else { - return true; - } - } - - protected function isMysqlUsedWithoutUTF8MB4(): bool { - return ($this->config->getSystemValue('dbtype', 'sqlite') === 'mysql') && ($this->config->getSystemValue('mysql.utf8mb4', false) === false); - } - - protected function hasBigIntConversionPendingColumns(): array { - // copy of ConvertFilecacheBigInt::getColumnsByTable() - $tables = [ - 'activity' => ['activity_id', 'object_id'], - 'activity_mq' => ['mail_id'], - 'authtoken' => ['id'], - 'bruteforce_attempts' => ['id'], - 'federated_reshares' => ['share_id'], - 'filecache' => ['fileid', 'storage', 'parent', 'mimetype', 'mimepart', 'mtime', 'storage_mtime'], - 'filecache_extended' => ['fileid'], - 'files_trash' => ['auto_id'], - 'file_locks' => ['id'], - 'file_metadata' => ['id'], - 'jobs' => ['id'], - 'mimetypes' => ['id'], - 'mounts' => ['id', 'storage_id', 'root_id', 'mount_id'], - 'share_external' => ['id', 'parent'], - 'storages' => ['numeric_id'], - ]; - - $schema = new SchemaWrapper($this->db); - $isSqlite = $this->db->getDatabasePlatform() instanceof SqlitePlatform; - $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()->getName() !== Types::BIGINT && !$isAutoIncrementOnSqlite) { - $pendingColumns[] = $tableName . '.' . $columnName; - } - } - } - - return $pendingColumns; - } - - protected function isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(): bool { - $objectStore = $this->config->getSystemValue('objectstore', null); - $objectStoreMultibucket = $this->config->getSystemValue('objectstore_multibucket', null); - - if (!isset($objectStoreMultibucket) && !isset($objectStore)) { - return true; - } - - if (isset($objectStoreMultibucket['class']) && $objectStoreMultibucket['class'] !== 'OC\\Files\\ObjectStore\\S3') { - return true; - } - - if (isset($objectStore['class']) && $objectStore['class'] !== 'OC\\Files\\ObjectStore\\S3') { - return true; - } - - $tempPath = sys_get_temp_dir(); - if (!is_dir($tempPath)) { - $this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. Returned value: ' . $tempPath); - return false; - } - $freeSpaceInTemp = function_exists('disk_free_space') ? disk_free_space($tempPath) : false; - if ($freeSpaceInTemp === false) { - $this->logger->error('Error while checking the available disk space of temporary PHP path or no free disk space returned. Temporary path: ' . $tempPath); - return false; - } - - $freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024; - if ($freeSpaceInTempInGB > 50) { - return true; - } - - $this->logger->warning('Checking the available space in the temporary path resulted in ' . round($freeSpaceInTempInGB, 1) . ' GB instead of the recommended 50GB. Path: ' . $tempPath); - return false; - } - - protected function imageMagickLacksSVGSupport(): bool { - return extension_loaded('imagick') && count(\Imagick::queryFormats('SVG')) === 0; - } - - /** * @return DataResponse - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) */ + #[AuthorizedAdminSetting(settings: Overview::class)] public function check() { - $phpDefaultCharset = new PhpDefaultCharset(); - $phpOutputBuffering = new PhpOutputBuffering(); - $legacySSEKeyFormat = new LegacySSEKeyFormat($this->l10n, $this->config, $this->urlGenerator); - $checkUserCertificates = new CheckUserCertificates($this->l10n, $this->config, $this->urlGenerator); - $supportedDatabases = new SupportedDatabase($this->l10n, $this->connection); - $ldapInvalidUuids = new LdapInvalidUuids($this->appManager, $this->l10n, $this->serverContainer); - return new DataResponse( [ - 'isGetenvServerWorking' => !empty(getenv('PATH')), - 'isReadOnlyConfig' => $this->isReadOnlyConfig(), - 'hasValidTransactionIsolationLevel' => $this->hasValidTransactionIsolationLevel(), - 'wasEmailTestSuccessful' => $this->wasEmailTestSuccessful(), - 'hasFileinfoInstalled' => $this->hasFileinfoInstalled(), - 'hasWorkingFileLocking' => $this->hasWorkingFileLocking(), - 'suggestedOverwriteCliURL' => $this->getSuggestedOverwriteCliURL(), - 'cronInfo' => $this->getLastCronInfo(), - 'cronErrors' => $this->getCronErrors(), - 'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(), - 'serverHasInternetConnectionProblems' => $this->hasInternetConnectivityProblems(), - 'isMemcacheConfigured' => $this->isMemcacheConfigured(), - 'memcacheDocs' => $this->urlGenerator->linkToDocs('admin-performance'), - 'isRandomnessSecure' => $this->isRandomnessSecure(), - 'securityDocs' => $this->urlGenerator->linkToDocs('admin-security'), - 'isUsedTlsLibOutdated' => $this->isUsedTlsLibOutdated(), - 'phpSupported' => $this->isPhpSupported(), - 'forwardedForHeadersWorking' => $this->forwardedForHeadersWorking(), - 'reverseProxyDocs' => $this->urlGenerator->linkToDocs('admin-reverse-proxy'), - 'isCorrectMemcachedPHPModuleInstalled' => $this->isCorrectMemcachedPHPModuleInstalled(), - 'hasPassedCodeIntegrityCheck' => $this->checker->hasPassedCheck(), - 'codeIntegrityCheckerDocumentation' => $this->urlGenerator->linkToDocs('admin-code-integrity'), - 'OpcacheSetupRecommendations' => $this->getOpcacheSetupRecommendations(), - 'isSettimelimitAvailable' => $this->isSettimelimitAvailable(), - 'hasFreeTypeSupport' => $this->hasFreeTypeSupport(), - 'missingPrimaryKeys' => $this->hasMissingPrimaryKeys(), - 'missingIndexes' => $this->hasMissingIndexes(), - 'missingColumns' => $this->hasMissingColumns(), - 'isSqliteUsed' => $this->isSqliteUsed(), - 'databaseConversionDocumentation' => $this->urlGenerator->linkToDocs('admin-db-conversion'), - 'isMemoryLimitSufficient' => $this->memoryInfo->isMemoryLimitSufficient(), - 'appDirsWithDifferentOwner' => $this->getAppDirsWithDifferentOwner(), - 'isImagickEnabled' => $this->isImagickEnabled(), - 'areWebauthnExtensionsEnabled' => $this->areWebauthnExtensionsEnabled(), - 'is64bit' => $this->is64bit(), - 'recommendedPHPModules' => $this->hasRecommendedPHPModules(), - 'pendingBigIntConversionColumns' => $this->hasBigIntConversionPendingColumns(), - 'isMysqlUsedWithoutUTF8MB4' => $this->isMysqlUsedWithoutUTF8MB4(), - 'isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed' => $this->isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(), - 'reverseProxyGeneratedURL' => $this->urlGenerator->getAbsoluteURL('index.php'), - 'imageMagickLacksSVGSupport' => $this->imageMagickLacksSVGSupport(), - PhpDefaultCharset::class => ['pass' => $phpDefaultCharset->run(), 'description' => $phpDefaultCharset->description(), 'severity' => $phpDefaultCharset->severity()], - PhpOutputBuffering::class => ['pass' => $phpOutputBuffering->run(), 'description' => $phpOutputBuffering->description(), 'severity' => $phpOutputBuffering->severity()], - LegacySSEKeyFormat::class => ['pass' => $legacySSEKeyFormat->run(), 'description' => $legacySSEKeyFormat->description(), 'severity' => $legacySSEKeyFormat->severity(), 'linkToDocumentation' => $legacySSEKeyFormat->linkToDocumentation()], - CheckUserCertificates::class => ['pass' => $checkUserCertificates->run(), 'description' => $checkUserCertificates->description(), 'severity' => $checkUserCertificates->severity(), 'elements' => $checkUserCertificates->elements()], - 'isDefaultPhoneRegionSet' => $this->config->getSystemValueString('default_phone_region', '') !== '', - SupportedDatabase::class => ['pass' => $supportedDatabases->run(), 'description' => $supportedDatabases->description(), 'severity' => $supportedDatabases->severity()], - 'temporaryDirectoryWritable' => $this->isTemporaryDirectoryWritable(), - LdapInvalidUuids::class => ['pass' => $ldapInvalidUuids->run(), 'description' => $ldapInvalidUuids->description(), 'severity' => $ldapInvalidUuids->severity()], + 'generic' => $this->setupCheckManager->runAll(), ] ); } diff --git a/apps/settings/lib/Controller/CommonSettingsTrait.php b/apps/settings/lib/Controller/CommonSettingsTrait.php index 8ca62b9d1b3..75d2b1f2f9e 100644 --- a/apps/settings/lib/Controller/CommonSettingsTrait.php +++ b/apps/settings/lib/Controller/CommonSettingsTrait.php @@ -1,42 +1,32 @@ <?php + /** - * @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\Settings\Controller; +use InvalidArgumentException; +use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; +use OCA\Settings\AppInfo\Application; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Group\ISubAdmin; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; +use OCP\Settings\IDeclarativeSettingsForm; use OCP\Settings\IIconSection; use OCP\Settings\IManager as ISettingsManager; use OCP\Settings\ISettings; +use OCP\Util; +/** + * @psalm-import-type DeclarativeSettingsFormSchemaWithValues from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm + */ trait CommonSettingsTrait { /** @var ISettingsManager */ @@ -54,28 +44,26 @@ trait CommonSettingsTrait { /** @var ISubAdmin */ private $subAdmin; + private IDeclarativeManager $declarativeSettingsManager; + + /** @var IInitialState */ + private $initialState; + /** * @return array{forms: array{personal: array, admin: array}} */ private function getNavigationParameters(string $currentType, string $currentSection): array { - $templateParameters = [ - 'personal' => $this->formatPersonalSections($currentType, $currentSection), - 'admin' => [] - ]; - - $templateParameters['admin'] = $this->formatAdminSections( - $currentType, - $currentSection - ); - return [ - 'forms' => $templateParameters + 'forms' => [ + 'personal' => $this->formatPersonalSections($currentType, $currentSection), + 'admin' => $this->formatAdminSections($currentType, $currentSection), + ], ]; } /** * @param IIconSection[][] $sections - * @psam-param 'admin'|'personal' $type + * @psalm-param 'admin'|'personal' $type * @return list<array{anchor: string, section-name: string, active: bool, icon: string}> */ protected function formatSections(array $sections, string $currentSection, string $type, string $currentType): array { @@ -87,7 +75,11 @@ trait CommonSettingsTrait { } elseif ($type === 'personal') { $settings = $this->settingsManager->getPersonalSettings($section->getID()); } - if (empty($settings) && !($section->getID() === 'additional' && count(\OC_App::getForms('admin')) > 0)) { + + /** @psalm-suppress PossiblyNullArgument */ + $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($this->userSession->getUser(), $type, $section->getID()); + + if (empty($settings) && empty($declarativeFormIDs)) { continue; } @@ -107,54 +99,93 @@ trait CommonSettingsTrait { return $templateParameters; } - protected function formatPersonalSections(string $currentType, string $currentSections): array { + protected function formatPersonalSections(string $currentType, string $currentSection): array { $sections = $this->settingsManager->getPersonalSections(); - return $this->formatSections($sections, $currentSections, 'personal', $currentType); + return $this->formatSections($sections, $currentSection, 'personal', $currentType); } - protected function formatAdminSections(string $currentType, string $currentSections): array { + protected function formatAdminSections(string $currentType, string $currentSection): array { $sections = $this->settingsManager->getAdminSections(); - return $this->formatSections($sections, $currentSections, 'admin', $currentType); + return $this->formatSections($sections, $currentSection, 'admin', $currentType); } /** - * @param array<int, list<\OCP\Settings\ISettings>> $settings + * @param list<ISettings> $settings + * @param list<DeclarativeSettingsFormSchemaWithValues> $declarativeSettings * @return array{content: string} */ - private function formatSettings(array $settings): array { + private function formatSettings(array $settings, array $declarativeSettings): array { + $settings = array_merge($settings, $declarativeSettings); + + usort($settings, function ($first, $second) { + $priorityOne = $first instanceof ISettings ? $first->getPriority() : $first['priority']; + $priorityTwo = $second instanceof ISettings ? $second->getPriority() : $second['priority']; + return $priorityOne - $priorityTwo; + }); + $html = ''; - foreach ($settings as $prioritizedSettings) { - foreach ($prioritizedSettings as $setting) { - /** @var ISettings $setting */ + foreach ($settings as $setting) { + if ($setting instanceof ISettings) { $form = $setting->getForm(); $html .= $form->renderAs('')->render(); + } else { + $html .= '<div id="' . $setting['app'] . '_' . $setting['id'] . '"></div>'; } } return ['content' => $html]; } + /** + * @psalm-param 'admin'|'personal' $type + */ private function getIndexResponse(string $type, string $section): TemplateResponse { + $user = $this->userSession->getUser(); + assert($user !== null, 'No user logged in for settings'); + + $this->declarativeSettingsManager->loadSchemas(); + $declarativeSettings = $this->declarativeSettingsManager->getFormsWithValues($user, $type, $section); + + foreach ($declarativeSettings as &$form) { + foreach ($form['fields'] as &$field) { + if (isset($field['sensitive']) && $field['sensitive'] === true && !empty($field['value'])) { + $field['value'] = 'dummySecret'; + } + } + } + if ($type === 'personal') { + $settings = array_values($this->settingsManager->getPersonalSettings($section)); if ($section === 'theming') { $this->navigationManager->setActiveEntry('accessibility_settings'); } else { $this->navigationManager->setActiveEntry('settings'); } } elseif ($type === 'admin') { + $settings = array_values($this->settingsManager->getAllowedAdminSettings($section, $user)); + if (empty($settings) && empty($declarativeSettings)) { + throw new NotAdminException('Logged in user does not have permission to access these settings.'); + } $this->navigationManager->setActiveEntry('admin_settings'); + } else { + throw new InvalidArgumentException('$type must be either "admin" or "personal"'); + } + + if (!empty($declarativeSettings)) { + Util::addScript(Application::APP_ID, 'declarative-settings-forms'); + $this->initialState->provideInitialState('declarative-settings-forms', $declarativeSettings); } - $templateParams = []; + $settings = array_merge(...$settings); + $templateParams = $this->formatSettings($settings, $declarativeSettings); $templateParams = array_merge($templateParams, $this->getNavigationParameters($type, $section)); - $templateParams = array_merge($templateParams, $this->getSettings($section)); + $activeSection = $this->settingsManager->getSection($type, $section); if ($activeSection) { $templateParams['pageTitle'] = $activeSection->getName(); $templateParams['activeSectionId'] = $activeSection->getID(); + $templateParams['activeSectionType'] = $type; } return new TemplateResponse('settings', 'settings/frame', $templateParams); } - - abstract protected function getSettings($section); } diff --git a/apps/settings/lib/Controller/DeclarativeSettingsController.php b/apps/settings/lib/Controller/DeclarativeSettingsController.php new file mode 100644 index 00000000000..4e4bee4043c --- /dev/null +++ b/apps/settings/lib/Controller/DeclarativeSettingsController.php @@ -0,0 +1,131 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\Controller; + +use Exception; +use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; +use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; +use OCA\Settings\ResponseDefinitions; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; +use Psr\Log\LoggerInterface; + +/** + * @psalm-import-type SettingsDeclarativeForm from ResponseDefinitions + */ +class DeclarativeSettingsController extends OCSController { + public function __construct( + string $appName, + IRequest $request, + private IUserSession $userSession, + private IDeclarativeManager $declarativeManager, + private LoggerInterface $logger, + ) { + parent::__construct($appName, $request); + } + + /** + * Sets a declarative settings value + * + * @param string $app ID of the app + * @param string $formId ID of the form + * @param string $fieldId ID of the field + * @param mixed $value Value to be saved + * @return DataResponse<Http::STATUS_OK, null, array{}> + * @throws NotLoggedInException Not logged in or not an admin user + * @throws NotAdminException Not logged in or not an admin user + * @throws OCSBadRequestException Invalid arguments to save value + * + * 200: Value set successfully + */ + #[NoAdminRequired] + public function setValue(string $app, string $formId, string $fieldId, mixed $value): DataResponse { + return $this->saveValue($app, $formId, $fieldId, $value); + } + + /** + * Sets a declarative settings value. + * Password confirmation is required for sensitive values. + * + * @param string $app ID of the app + * @param string $formId ID of the form + * @param string $fieldId ID of the field + * @param mixed $value Value to be saved + * @return DataResponse<Http::STATUS_OK, null, array{}> + * @throws NotLoggedInException Not logged in or not an admin user + * @throws NotAdminException Not logged in or not an admin user + * @throws OCSBadRequestException Invalid arguments to save value + * + * 200: Value set successfully + */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] + public function setSensitiveValue(string $app, string $formId, string $fieldId, mixed $value): DataResponse { + return $this->saveValue($app, $formId, $fieldId, $value); + } + + /** + * Sets a declarative settings value. + * + * @param string $app ID of the app + * @param string $formId ID of the form + * @param string $fieldId ID of the field + * @param mixed $value Value to be saved + * @return DataResponse<Http::STATUS_OK, null, array{}> + * @throws NotLoggedInException Not logged in or not an admin user + * @throws NotAdminException Not logged in or not an admin user + * @throws OCSBadRequestException Invalid arguments to save value + * + * 200: Value set successfully + */ + private function saveValue(string $app, string $formId, string $fieldId, mixed $value): DataResponse { + $user = $this->userSession->getUser(); + if ($user === null) { + throw new NotLoggedInException(); + } + + try { + $this->declarativeManager->loadSchemas(); + $this->declarativeManager->setValue($user, $app, $formId, $fieldId, $value); + return new DataResponse(null); + } catch (NotAdminException $e) { + throw $e; + } catch (Exception $e) { + $this->logger->error('Failed to set declarative settings value: ' . $e->getMessage()); + throw new OCSBadRequestException(); + } + } + + /** + * Gets all declarative forms with the values prefilled. + * + * @return DataResponse<Http::STATUS_OK, list<SettingsDeclarativeForm>, array{}> + * @throws NotLoggedInException + * @NoSubAdminRequired + * + * 200: Forms returned + */ + #[NoAdminRequired] + public function getForms(): DataResponse { + $user = $this->userSession->getUser(); + if ($user === null) { + throw new NotLoggedInException(); + } + $this->declarativeManager->loadSchemas(); + return new DataResponse($this->declarativeManager->getFormsWithValues($user, null, null)); + } +} diff --git a/apps/settings/lib/Controller/HelpController.php b/apps/settings/lib/Controller/HelpController.php index 38ce84843ed..05bff158ee6 100644 --- a/apps/settings/lib/Controller/HelpController.php +++ b/apps/settings/lib/Controller/HelpController.php @@ -3,79 +3,50 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\Controller; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\TemplateResponse; +use OCP\IAppConfig; +use OCP\IConfig; use OCP\IGroupManager; use OCP\IL10N; use OCP\INavigationManager; use OCP\IRequest; use OCP\IURLGenerator; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class HelpController extends Controller { - /** @var INavigationManager */ - private $navigationManager; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IGroupManager */ - private $groupManager; - /** @var IL10N */ - private $l10n; - - /** @var string */ - private $userId; - public function __construct( string $appName, IRequest $request, - INavigationManager $navigationManager, - IURLGenerator $urlGenerator, - ?string $userId, - IGroupManager $groupManager, - IL10N $l10n + private INavigationManager $navigationManager, + private IURLGenerator $urlGenerator, + /** @var string */ + private ?string $userId, + private IGroupManager $groupManager, + private IL10N $l10n, + private IConfig $config, + private IAppConfig $appConfig, ) { parent::__construct($appName, $request); - $this->navigationManager = $navigationManager; - $this->urlGenerator = $urlGenerator; - $this->userId = $userId; - $this->groupManager = $groupManager; - $this->l10n = $l10n; } /** * @return TemplateResponse * - * @NoCSRFRequired - * @NoAdminRequired * @NoSubAdminRequired */ + #[NoCSRFRequired] + #[NoAdminRequired] public function help(string $mode = 'user'): TemplateResponse { $this->navigationManager->setActiveEntry('help'); $pageTitle = $this->l10n->t('Administrator documentation'); @@ -91,6 +62,16 @@ class HelpController extends Controller { $urlUserDocs = $this->urlGenerator->linkToRoute('settings.Help.help', ['mode' => 'user']); $urlAdminDocs = $this->urlGenerator->linkToRoute('settings.Help.help', ['mode' => 'admin']); + $knowledgebaseEmbedded = $this->config->getSystemValueBool('knowledgebase.embedded', false); + if (!$knowledgebaseEmbedded) { + $pageTitle = $this->l10n->t('Nextcloud help overview'); + $urlUserDocs = $this->urlGenerator->linkToDocs('user'); + $urlAdminDocs = $this->urlGenerator->linkToDocs('admin'); + } + + $legalNoticeUrl = $this->appConfig->getValueString('theming', 'imprintUrl'); + $privacyUrl = $this->appConfig->getValueString('theming', 'privacyUrl'); + $response = new TemplateResponse('settings', 'help', [ 'admin' => $this->groupManager->isAdmin($this->userId), 'url' => $documentationUrl, @@ -98,6 +79,9 @@ class HelpController extends Controller { 'urlAdminDocs' => $urlAdminDocs, 'mode' => $mode, 'pageTitle' => $pageTitle, + 'knowledgebaseEmbedded' => $knowledgebaseEmbedded, + 'legalNoticeUrl' => $legalNoticeUrl, + 'privacyUrl' => $privacyUrl, ]); $policy = new ContentSecurityPolicy(); $policy->addAllowedFrameDomain('\'self\''); diff --git a/apps/settings/lib/Controller/LogSettingsController.php b/apps/settings/lib/Controller/LogSettingsController.php index 08c18189d03..90cf4549d2f 100644 --- a/apps/settings/lib/Controller/LogSettingsController.php +++ b/apps/settings/lib/Controller/LogSettingsController.php @@ -1,33 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Settings\Controller; use OC\Log; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\StreamResponse; use OCP\IRequest; @@ -44,17 +28,23 @@ class LogSettingsController extends Controller { /** * download logfile * - * @NoCSRFRequired + * @return StreamResponse<Http::STATUS_OK, array{Content-Type: 'application/octet-stream', 'Content-Disposition': 'attachment; filename="nextcloud.log"'}> * - * @return StreamResponse + * 200: Logfile returned */ + #[NoCSRFRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION)] public function download() { if (!$this->log instanceof Log) { throw new \UnexpectedValueException('Log file not available'); } - $resp = new StreamResponse($this->log->getLogPath()); - $resp->addHeader('Content-Type', 'application/octet-stream'); - $resp->addHeader('Content-Disposition', 'attachment; filename="nextcloud.log"'); - return $resp; + return new StreamResponse( + $this->log->getLogPath(), + Http::STATUS_OK, + [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="nextcloud.log"', + ], + ); } } diff --git a/apps/settings/lib/Controller/MailSettingsController.php b/apps/settings/lib/Controller/MailSettingsController.php index 2df79b67731..f1e3b8032dc 100644 --- a/apps/settings/lib/Controller/MailSettingsController.php +++ b/apps/settings/lib/Controller/MailSettingsController.php @@ -1,34 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Settings\Controller; +use OCA\Settings\Settings\Admin\Overview; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\DataResponse; use OCP\IConfig; use OCP\IL10N; @@ -39,17 +22,6 @@ use OCP\Mail\IMailer; class MailSettingsController extends Controller { - /** @var IL10N */ - private $l10n; - /** @var IConfig */ - private $config; - /** @var IUserSession */ - private $userSession; - /** @var IMailer */ - private $mailer; - /** @var IURLGenerator */ - private $urlGenerator; - /** * @param string $appName * @param IRequest $request @@ -59,52 +31,51 @@ class MailSettingsController extends Controller { * @param IURLGenerator $urlGenerator, * @param IMailer $mailer */ - public function __construct($appName, - IRequest $request, - IL10N $l10n, - IConfig $config, - IUserSession $userSession, - IURLGenerator $urlGenerator, - IMailer $mailer) { + public function __construct( + $appName, + IRequest $request, + private IL10N $l10n, + private IConfig $config, + private IUserSession $userSession, + private IURLGenerator $urlGenerator, + private IMailer $mailer, + ) { parent::__construct($appName, $request); - $this->l10n = $l10n; - $this->config = $config; - $this->userSession = $userSession; - $this->urlGenerator = $urlGenerator; - $this->mailer = $mailer; } /** * Sets the email settings - * - * @PasswordConfirmationRequired - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) - * - * @param string $mail_domain - * @param string $mail_from_address - * @param string $mail_smtpmode - * @param string $mail_smtpsecure - * @param string $mail_smtphost - * @param int $mail_smtpauth - * @param string $mail_smtpport - * @return DataResponse */ - public function setMailSettings($mail_domain, - $mail_from_address, - $mail_smtpmode, - $mail_smtpsecure, - $mail_smtphost, - $mail_smtpauth, - $mail_smtpport, - $mail_sendmailmode) { - $params = get_defined_vars(); - $configs = []; - foreach ($params as $key => $value) { + #[AuthorizedAdminSetting(settings: Overview::class)] + #[PasswordConfirmationRequired] + public function setMailSettings( + string $mail_domain, + string $mail_from_address, + string $mail_smtpmode, + string $mail_smtpsecure, + string $mail_smtphost, + ?string $mail_smtpauth, + string $mail_smtpport, + string $mail_sendmailmode, + ): DataResponse { + $mail_smtpauth = $mail_smtpauth == '1'; + + $configs = [ + 'mail_domain' => $mail_domain, + 'mail_from_address' => $mail_from_address, + 'mail_smtpmode' => $mail_smtpmode, + 'mail_smtpsecure' => $mail_smtpsecure, + 'mail_smtphost' => $mail_smtphost, + 'mail_smtpauth' => $mail_smtpauth, + 'mail_smtpport' => $mail_smtpport, + 'mail_sendmailmode' => $mail_sendmailmode, + ]; + foreach ($configs as $key => $value) { $configs[$key] = empty($value) ? null : $value; } // Delete passwords from config in case no auth is specified - if ($params['mail_smtpauth'] !== 1) { + if (!$mail_smtpauth) { $configs['mail_smtpname'] = null; $configs['mail_smtppassword'] = null; } @@ -119,13 +90,12 @@ class MailSettingsController extends Controller { /** * Store the credentials used for SMTP in the config * - * @PasswordConfirmationRequired - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) - * * @param string $mail_smtpname * @param string $mail_smtppassword * @return DataResponse */ + #[AuthorizedAdminSetting(settings: Overview::class)] + #[PasswordConfirmationRequired] public function storeCredentials($mail_smtpname, $mail_smtppassword) { if ($mail_smtppassword === '********') { return new DataResponse($this->l10n->t('Invalid SMTP password.'), Http::STATUS_BAD_REQUEST); @@ -143,9 +113,9 @@ class MailSettingsController extends Controller { /** * Send a mail to test the settings - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) * @return DataResponse */ + #[AuthorizedAdminSetting(settings: Overview::class)] public function sendTestMail() { $email = $this->config->getUserValue($this->userSession->getUser()->getUID(), $this->appName, 'email', ''); if (!empty($email)) { @@ -180,6 +150,6 @@ class MailSettingsController extends Controller { } $this->config->setAppValue('core', 'emailTestSuccessful', '0'); - return new DataResponse($this->l10n->t('You need to set your user email before being able to send test emails. Go to %s for that.', [$this->urlGenerator->linkToRouteAbsolute('settings.PersonalSettings.index')]), Http::STATUS_BAD_REQUEST); + return new DataResponse($this->l10n->t('You need to set your account email before being able to send test emails. Go to %s for that.', [$this->urlGenerator->linkToRouteAbsolute('settings.PersonalSettings.index')]), Http::STATUS_BAD_REQUEST); } } diff --git a/apps/settings/lib/Controller/PersonalSettingsController.php b/apps/settings/lib/Controller/PersonalSettingsController.php index 8781ecd214e..340ca3f93eb 100644 --- a/apps/settings/lib/Controller/PersonalSettingsController.php +++ b/apps/settings/lib/Controller/PersonalSettingsController.php @@ -1,40 +1,26 @@ <?php + /** - * @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\Controller; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Group\ISubAdmin; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IRequest; use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; use OCP\Settings\IManager as ISettingsManager; -use OCP\Template; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class PersonalSettingsController extends Controller { use CommonSettingsTrait; @@ -45,7 +31,9 @@ class PersonalSettingsController extends Controller { ISettingsManager $settingsManager, IUserSession $userSession, IGroupManager $groupManager, - ISubAdmin $subAdmin + ISubAdmin $subAdmin, + IDeclarativeManager $declarativeSettingsManager, + IInitialState $initialState, ) { parent::__construct($appName, $request); $this->navigationManager = $navigationManager; @@ -53,57 +41,19 @@ class PersonalSettingsController extends Controller { $this->userSession = $userSession; $this->subAdmin = $subAdmin; $this->groupManager = $groupManager; + $this->declarativeSettingsManager = $declarativeSettingsManager; + $this->initialState = $initialState; } /** - * @NoCSRFRequired - * @NoAdminRequired * @NoSubAdminRequired */ + #[NoAdminRequired] + #[NoCSRFRequired] public function index(string $section): TemplateResponse { - return $this->getIndexResponse('personal', $section); - } - - /** - * @param string $section - * @return array - */ - protected function getSettings($section) { - $settings = $this->settingsManager->getPersonalSettings($section); - $formatted = $this->formatSettings($settings); - if ($section === 'additional') { - $formatted['content'] .= $this->getLegacyForms(); - } - return $formatted; - } - - /** - * @return bool|string - */ - private function getLegacyForms() { - $forms = \OC_App::getForms('personal'); - - $forms = array_map(function ($form) { - if (preg_match('%(<h2(?P<class>[^>]*)>.*?</h2>)%i', $form, $regs)) { - $sectionName = str_replace('<h2' . $regs['class'] . '>', '', $regs[0]); - $sectionName = str_replace('</h2>', '', $sectionName); - $anchor = strtolower($sectionName); - $anchor = str_replace(' ', '-', $anchor); - - return [ - 'anchor' => $anchor, - 'section-name' => $sectionName, - 'form' => $form - ]; - } - return [ - 'form' => $form - ]; - }, $forms); - - $out = new Template('settings', 'settings/additional'); - $out->assign('forms', $forms); - - return $out->fetchPage(); + return $this->getIndexResponse( + 'personal', + $section, + ); } } diff --git a/apps/settings/lib/Controller/ReasonsController.php b/apps/settings/lib/Controller/ReasonsController.php index 2ceb7005407..91d0a8640d1 100644 --- a/apps/settings/lib/Controller/ReasonsController.php +++ b/apps/settings/lib/Controller/ReasonsController.php @@ -3,38 +3,25 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Jan C. Borchardt <hey@jancborchardt.net> - * - * @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\Controller; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\DataDisplayResponse; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class ReasonsController extends Controller { /** - * @NoCSRFRequired - * @NoAdminRequired * @NoSubAdminRequired */ + #[NoAdminRequired] + #[NoCSRFRequired] public function getPdf() { $data = file_get_contents(__DIR__ . '/../../data/Reasons to use Nextcloud.pdf'); diff --git a/apps/settings/lib/Controller/TwoFactorSettingsController.php b/apps/settings/lib/Controller/TwoFactorSettingsController.php index 45722767cda..e08fca8ec6c 100644 --- a/apps/settings/lib/Controller/TwoFactorSettingsController.php +++ b/apps/settings/lib/Controller/TwoFactorSettingsController.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @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: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\Controller; @@ -37,8 +20,8 @@ class TwoFactorSettingsController extends Controller { private $mandatoryTwoFactor; public function __construct(string $appName, - IRequest $request, - MandatoryTwoFactor $mandatoryTwoFactor) { + IRequest $request, + MandatoryTwoFactor $mandatoryTwoFactor) { parent::__construct($appName, $request); $this->mandatoryTwoFactor = $mandatoryTwoFactor; diff --git a/apps/settings/lib/Controller/UsersController.php b/apps/settings/lib/Controller/UsersController.php index 217abf0e156..8efd3eeb8ca 100644 --- a/apps/settings/lib/Controller/UsersController.php +++ b/apps/settings/lib/Controller/UsersController.php @@ -3,37 +3,10 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author GretaD <gretadoci@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ -// FIXME: disabled for now to be able to inject IGroupManager and also use -// getSubAdmin() namespace OCA\Settings\Controller; @@ -41,145 +14,112 @@ use InvalidArgumentException; use OC\AppFramework\Http; use OC\Encryption\Exceptions\ModuleDoesNotExistsException; use OC\ForbiddenException; -use OC\Group\Manager as GroupManager; +use OC\Group\MetaData; use OC\KnownUser\KnownUserService; -use OC\L10N\Factory; use OC\Security\IdentityProof\Manager; use OC\User\Manager as UserManager; use OCA\Settings\BackgroundJobs\VerifyUserData; use OCA\Settings\Events\BeforeTemplateRenderedEvent; +use OCA\Settings\Settings\Admin\Users; use OCA\User_LDAP\User_Proxy; use OCP\Accounts\IAccount; use OCP\Accounts\IAccountManager; use OCP\Accounts\PropertyDoesNotExistException; use OCP\App\IAppManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\BackgroundJob\IJobList; use OCP\Encryption\IManager; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Group\ISubAdmin; use OCP\IConfig; +use OCP\IGroup; use OCP\IGroupManager; use OCP\IL10N; +use OCP\INavigationManager; use OCP\IRequest; use OCP\IUser; -use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; use OCP\Mail\IMailer; +use OCP\Util; use function in_array; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class UsersController extends Controller { - /** @var UserManager */ - private $userManager; - /** @var GroupManager */ - private $groupManager; - /** @var IUserSession */ - private $userSession; - /** @var IConfig */ - private $config; - /** @var bool */ - private $isAdmin; - /** @var IL10N */ - private $l10n; - /** @var IMailer */ - private $mailer; - /** @var Factory */ - private $l10nFactory; - /** @var IAppManager */ - private $appManager; - /** @var IAccountManager */ - private $accountManager; - /** @var Manager */ - private $keyManager; - /** @var IJobList */ - private $jobList; - /** @var IManager */ - private $encryptionManager; - /** @var KnownUserService */ - private $knownUserService; - /** @var IEventDispatcher */ - private $dispatcher; - + /** Limit for counting users for subadmins, to avoid spending too much time */ + private const COUNT_LIMIT_FOR_SUBADMINS = 999; public function __construct( string $appName, IRequest $request, - IUserManager $userManager, - IGroupManager $groupManager, - IUserSession $userSession, - IConfig $config, - bool $isAdmin, - IL10N $l10n, - IMailer $mailer, - IFactory $l10nFactory, - IAppManager $appManager, - IAccountManager $accountManager, - Manager $keyManager, - IJobList $jobList, - IManager $encryptionManager, - KnownUserService $knownUserService, - IEventDispatcher $dispatcher + private UserManager $userManager, + private IGroupManager $groupManager, + private IUserSession $userSession, + private IConfig $config, + private IL10N $l10n, + private IMailer $mailer, + private IFactory $l10nFactory, + private IAppManager $appManager, + private IAccountManager $accountManager, + private Manager $keyManager, + private IJobList $jobList, + private IManager $encryptionManager, + private KnownUserService $knownUserService, + private IEventDispatcher $dispatcher, + private IInitialState $initialState, ) { parent::__construct($appName, $request); - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->userSession = $userSession; - $this->config = $config; - $this->isAdmin = $isAdmin; - $this->l10n = $l10n; - $this->mailer = $mailer; - $this->l10nFactory = $l10nFactory; - $this->appManager = $appManager; - $this->accountManager = $accountManager; - $this->keyManager = $keyManager; - $this->jobList = $jobList; - $this->encryptionManager = $encryptionManager; - $this->knownUserService = $knownUserService; - $this->dispatcher = $dispatcher; } /** - * @NoCSRFRequired - * @NoAdminRequired - * * Display users list template * * @return TemplateResponse */ - public function usersListByGroup(): TemplateResponse { - return $this->usersList(); + #[NoAdminRequired] + #[NoCSRFRequired] + public function usersListByGroup(INavigationManager $navigationManager, ISubAdmin $subAdmin): TemplateResponse { + return $this->usersList($navigationManager, $subAdmin); } /** - * @NoCSRFRequired - * @NoAdminRequired - * * Display users list template * * @return TemplateResponse */ - public function usersList(): TemplateResponse { + #[NoAdminRequired] + #[NoCSRFRequired] + public function usersList(INavigationManager $navigationManager, ISubAdmin $subAdmin): TemplateResponse { $user = $this->userSession->getUser(); $uid = $user->getUID(); + $isAdmin = $this->groupManager->isAdmin($uid); + $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid); - \OC::$server->getNavigationManager()->setActiveEntry('core_users'); + $navigationManager->setActiveEntry('core_users'); /* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */ - $sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT; + $sortGroupsBy = MetaData::SORT_USERCOUNT; $isLDAPUsed = false; - if ($this->config->getSystemValue('sort_groups_by_name', false)) { - $sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME; + if ($this->config->getSystemValueBool('sort_groups_by_name', false)) { + $sortGroupsBy = MetaData::SORT_GROUPNAME; } else { if ($this->appManager->isEnabledForUser('user_ldap')) { - $isLDAPUsed = - $this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy'); + $isLDAPUsed + = $this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy'); if ($isLDAPUsed) { // LDAP user count can be slow, so we sort by group name here - $sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME; + $sortGroupsBy = MetaData::SORT_GROUPNAME; } } } @@ -187,15 +127,23 @@ class UsersController extends Controller { $canChangePassword = $this->canAdminChangeUserPasswords(); /* GROUPS */ - $groupsInfo = new \OC\Group\MetaData( + $groupsInfo = new MetaData( $uid, - $this->isAdmin, + $isAdmin, + $isDelegatedAdmin, $this->groupManager, $this->userSession ); - $groupsInfo->setSorting($sortGroupsBy); - [$adminGroup, $groups] = $groupsInfo->get(); + $adminGroup = $this->groupManager->get('admin'); + $adminGroupData = [ + 'id' => $adminGroup->getGID(), + 'name' => $adminGroup->getDisplayName(), + 'usercount' => $sortGroupsBy === MetaData::SORT_USERCOUNT ? $adminGroup->count() : 0, + 'disabled' => $adminGroup->countDisabled(), + 'canAdd' => $adminGroup->canAddUser(), + 'canRemove' => $adminGroup->canRemoveUser(), + ]; if (!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) { $isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) { @@ -207,40 +155,41 @@ class UsersController extends Controller { $userCount = 0; if (!$isLDAPUsed) { - if ($this->isAdmin) { + if ($isAdmin || $isDelegatedAdmin) { $disabledUsers = $this->userManager->countDisabledUsers(); $userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) { return $v + (int)$w; }, 0); } else { // User is subadmin ! - // Map group list to names to retrieve the countDisabledUsersOfGroups - $userGroups = $this->groupManager->getUserGroups($user); - $groupsNames = []; - - foreach ($groups as $key => $group) { - // $userCount += (int)$group['usercount']; - array_push($groupsNames, $group['name']); - // we prevent subadmins from looking up themselves - // so we lower the count of the groups he belongs to - if (array_key_exists($group['id'], $userGroups)) { - $groups[$key]['usercount']--; - $userCount -= 1; // we also lower from one the total count - } - } - $userCount += $this->userManager->countUsersOfGroups($groupsInfo->getGroups()); - $disabledUsers = $this->userManager->countDisabledUsersOfGroups($groupsNames); + [$userCount,$disabledUsers] = $this->userManager->countUsersAndDisabledUsersOfGroups($groupsInfo->getGroups(), self::COUNT_LIMIT_FOR_SUBADMINS); } - $userCount -= $disabledUsers; + if ($disabledUsers > 0) { + $userCount -= $disabledUsers; + } } + $recentUsersGroup = [ + 'id' => '__nc_internal_recent', + 'name' => $this->l10n->t('Recently active'), + 'usercount' => $this->userManager->countSeenUsers(), + ]; + $disabledUsersGroup = [ 'id' => 'disabled', - 'name' => 'Disabled users', + 'name' => $this->l10n->t('Disabled accounts'), 'usercount' => $disabledUsers ]; + if (!$isAdmin && !$isDelegatedAdmin) { + $subAdminGroups = array_map( + fn (IGroup $group) => ['id' => $group->getGID(), 'name' => $group->getDisplayName()], + $subAdmin->getSubAdminsGroups($user), + ); + $subAdminGroups = array_values($subAdminGroups); + } + /* QUOTAS PRESETS */ $quotaPreset = $this->parseQuotaPreset($this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB')); $allowUnlimitedQuota = $this->config->getAppValue('files', 'allow_unlimited_quota', '1') === '1'; @@ -257,13 +206,21 @@ class UsersController extends Controller { /* LANGUAGES */ $languages = $this->l10nFactory->getLanguages(); + /** Using LDAP or admins (system config) can enfore sorting by group name, in this case the frontend setting is overwritten */ + $forceSortGroupByName = $sortGroupsBy === MetaData::SORT_GROUPNAME; + /* FINAL DATA */ $serverData = []; // groups - $serverData['groups'] = array_merge_recursive($adminGroup, [$disabledUsersGroup], $groups); + $serverData['systemGroups'] = [$adminGroupData, $recentUsersGroup, $disabledUsersGroup]; + $serverData['subAdminGroups'] = $subAdminGroups ?? []; // Various data - $serverData['isAdmin'] = $this->isAdmin; - $serverData['sortGroups'] = $sortGroupsBy; + $serverData['isAdmin'] = $isAdmin; + $serverData['isDelegatedAdmin'] = $isDelegatedAdmin; + $serverData['sortGroups'] = $forceSortGroupByName + ? MetaData::SORT_GROUPNAME + : (int)$this->config->getAppValue('core', 'group.sortBy', (string)MetaData::SORT_USERCOUNT); + $serverData['forceSortGroupByName'] = $forceSortGroupByName; $serverData['quotaPreset'] = $quotaPreset; $serverData['allowUnlimitedQuota'] = $allowUnlimitedQuota; $serverData['userCount'] = $userCount; @@ -277,7 +234,12 @@ class UsersController extends Controller { $serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes'; $serverData['newUserSendEmail'] = $this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes'; - return new TemplateResponse('settings', 'settings-vue', ['serverData' => $serverData]); + $this->initialState->provideInitialState('usersSettings', $serverData); + + Util::addStyle('settings', 'settings'); + Util::addScript('settings', 'vue-settings-apps-users-management'); + + return new TemplateResponse('settings', 'settings/empty', ['pageTitle' => $this->l10n->t('Settings')]); } /** @@ -286,8 +248,9 @@ class UsersController extends Controller { * * @return JSONResponse */ + #[AuthorizedAdminSetting(settings:Users::class)] public function setPreference(string $key, string $value): JSONResponse { - $allowed = ['newUser.sendEmail']; + $allowed = ['newUser.sendEmail', 'group.sortBy']; if (!in_array($key, $allowed, true)) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -341,9 +304,7 @@ class UsersController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired * * @param string|null $avatarScope * @param string|null $displayname @@ -358,26 +319,39 @@ class UsersController extends Controller { * @param string|null $addressScope * @param string|null $twitter * @param string|null $twitterScope + * @param string|null $bluesky + * @param string|null $blueskyScope * @param string|null $fediverse * @param string|null $fediverseScope + * @param string|null $birthdate + * @param string|null $birthdateScope * * @return DataResponse */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] + #[UserRateLimit(limit: 5, period: 60)] public function setUserSettings(?string $avatarScope = null, - ?string $displayname = null, - ?string $displaynameScope = null, - ?string $phone = null, - ?string $phoneScope = null, - ?string $email = null, - ?string $emailScope = null, - ?string $website = null, - ?string $websiteScope = null, - ?string $address = null, - ?string $addressScope = null, - ?string $twitter = null, - ?string $twitterScope = null, - ?string $fediverse = null, - ?string $fediverseScope = null + ?string $displayname = null, + ?string $displaynameScope = null, + ?string $phone = null, + ?string $phoneScope = null, + ?string $email = null, + ?string $emailScope = null, + ?string $website = null, + ?string $websiteScope = null, + ?string $address = null, + ?string $addressScope = null, + ?string $twitter = null, + ?string $twitterScope = null, + ?string $bluesky = null, + ?string $blueskyScope = null, + ?string $fediverse = null, + ?string $fediverseScope = null, + ?string $birthdate = null, + ?string $birthdateScope = null, + ?string $pronouns = null, + ?string $pronounsScope = null, ) { $user = $this->userSession->getUser(); if (!$user instanceof IUser) { @@ -385,7 +359,7 @@ class UsersController extends Controller { [ 'status' => 'error', 'data' => [ - 'message' => $this->l10n->t('Invalid user') + 'message' => $this->l10n->t('Invalid account') ] ], Http::STATUS_UNAUTHORIZED @@ -416,7 +390,10 @@ class UsersController extends Controller { IAccountManager::PROPERTY_ADDRESS => ['value' => $address, 'scope' => $addressScope], IAccountManager::PROPERTY_PHONE => ['value' => $phone, 'scope' => $phoneScope], IAccountManager::PROPERTY_TWITTER => ['value' => $twitter, 'scope' => $twitterScope], + IAccountManager::PROPERTY_BLUESKY => ['value' => $bluesky, 'scope' => $blueskyScope], IAccountManager::PROPERTY_FEDIVERSE => ['value' => $fediverse, 'scope' => $fediverseScope], + IAccountManager::PROPERTY_BIRTHDATE => ['value' => $birthdate, 'scope' => $birthdateScope], + IAccountManager::PROPERTY_PRONOUNS => ['value' => $pronouns, 'scope' => $pronounsScope], ]; $allowUserToChangeDisplayName = $this->config->getSystemValueBool('allow_user_to_change_display_name', true); foreach ($updatable as $property => $data) { @@ -425,10 +402,10 @@ class UsersController extends Controller { continue; } $property = $userAccount->getProperty($property); - if (null !== $data['value']) { + if ($data['value'] !== null) { $property->setValue($data['value']); } - if (null !== $data['scope']) { + if ($data['scope'] !== null) { $property->setScope($data['scope']); } } @@ -456,14 +433,20 @@ class UsersController extends Controller { 'addressScope' => $userAccount->getProperty(IAccountManager::PROPERTY_ADDRESS)->getScope(), 'twitter' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getValue(), 'twitterScope' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getScope(), + 'bluesky' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getValue(), + 'blueskyScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BLUESKY)->getScope(), 'fediverse' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue(), 'fediverseScope' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getScope(), + 'birthdate' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getValue(), + 'birthdateScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getScope(), + 'pronouns' => $userAccount->getProperty(IAccountManager::PROPERTY_PRONOUNS)->getValue(), + 'pronounsScope' => $userAccount->getProperty(IAccountManager::PROPERTY_PRONOUNS)->getScope(), 'message' => $this->l10n->t('Settings saved'), ], ], Http::STATUS_OK ); - } catch (ForbiddenException | InvalidArgumentException | PropertyDoesNotExistException $e) { + } catch (ForbiddenException|InvalidArgumentException|PropertyDoesNotExistException $e) { return new DataResponse([ 'status' => 'error', 'data' => [ @@ -516,14 +499,14 @@ class UsersController extends Controller { /** * Set the mail address of a user * - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired * * @param string $account * @param bool $onlyVerificationCode only return verification code without updating the data * @return DataResponse */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] public function getVerificationCode(string $account, bool $onlyVerificationCode): DataResponse { $user = $this->userSession->getUser(); diff --git a/apps/settings/lib/Controller/WebAuthnController.php b/apps/settings/lib/Controller/WebAuthnController.php index 2692882301d..495b58e6a4b 100644 --- a/apps/settings/lib/Controller/WebAuthnController.php +++ b/apps/settings/lib/Controller/WebAuthnController.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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\Controller; @@ -30,46 +12,39 @@ use OC\Authentication\WebAuthn\Manager; use OCA\Settings\AppInfo\Application; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\JSONResponse; -use OCP\ILogger; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; +use Psr\Log\LoggerInterface; use Webauthn\PublicKeyCredentialCreationOptions; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class WebAuthnController extends Controller { private const WEBAUTHN_REGISTRATION = 'webauthn_registration'; - /** @var Manager */ - private $manager; - - /** @var IUserSession */ - private $userSession; - /** - * @var ISession - */ - private $session; - /** - * @var ILogger - */ - private $logger; - - public function __construct(IRequest $request, ILogger $logger, Manager $webAuthnManager, IUserSession $userSession, ISession $session) { + public function __construct( + IRequest $request, + private LoggerInterface $logger, + private Manager $manager, + private IUserSession $userSession, + private ISession $session, + ) { parent::__construct(Application::APP_ID, $request); - - $this->manager = $webAuthnManager; - $this->userSession = $userSession; - $this->session = $session; - $this->logger = $logger; } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired - * @UseSession - * @NoCSRFRequired */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] + #[UseSession] + #[NoCSRFRequired] public function startRegistration(): JSONResponse { $this->logger->debug('Starting WebAuthn registration'); @@ -82,11 +57,11 @@ class WebAuthnController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired - * @UseSession */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] + #[UseSession] public function finishRegistration(string $name, string $data): JSONResponse { $this->logger->debug('Finishing WebAuthn registration'); @@ -104,10 +79,10 @@ class WebAuthnController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] public function deleteRegistration(int $id): JSONResponse { $this->logger->debug('Finishing WebAuthn registration'); |