diff options
Diffstat (limited to 'apps/settings/lib')
107 files changed, 1770 insertions, 1319 deletions
diff --git a/apps/settings/lib/Activity/GroupProvider.php b/apps/settings/lib/Activity/GroupProvider.php index bad2c0c7a35..2d492265cf4 100644 --- a/apps/settings/lib/Activity/GroupProvider.php +++ b/apps/settings/lib/Activity/GroupProvider.php @@ -1,11 +1,12 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\Activity; -use InvalidArgumentException; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IManager; use OCP\Activity\IProvider; @@ -19,36 +20,22 @@ class GroupProvider implements IProvider { public const ADDED_TO_GROUP = 'group_added'; public const REMOVED_FROM_GROUP = 'group_removed'; - /** @var L10nFactory */ - private $l10n; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IManager */ - private $activityManager; - /** @var IUserManager */ - protected $userManager; - /** @var IGroupManager */ - protected $groupManager; - /** @var string[] */ protected $groupDisplayNames = []; - public function __construct(L10nFactory $l10n, - IURLGenerator $urlGenerator, - IManager $activityManager, - IUserManager $userManager, - IGroupManager $groupManager) { - $this->urlGenerator = $urlGenerator; - $this->l10n = $l10n; - $this->activityManager = $activityManager; - $this->userManager = $userManager; - $this->groupManager = $groupManager; + public function __construct( + private L10nFactory $l10n, + private IURLGenerator $urlGenerator, + private IManager $activityManager, + protected IUserManager $userManager, + protected IGroupManager $groupManager, + ) { } public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { if ($event->getType() !== 'group_settings') { - throw new InvalidArgumentException(); + throw new UnknownActivityException(); } $l = $this->l10n->get('settings', $language); @@ -95,7 +82,7 @@ class GroupProvider implements IProvider { } break; default: - throw new InvalidArgumentException(); + throw new UnknownActivityException(); } $this->setSubjects($event, $subject, $parsedParameters); @@ -103,9 +90,6 @@ class GroupProvider implements IProvider { return $event; } - /** - * @throws \InvalidArgumentException - */ protected function setSubjects(IEvent $event, string $subject, array $parameters): void { $event->setRichSubject($subject, $parameters); } diff --git a/apps/settings/lib/Activity/GroupSetting.php b/apps/settings/lib/Activity/GroupSetting.php index e50e59a0916..917f4a7ef26 100644 --- a/apps/settings/lib/Activity/GroupSetting.php +++ b/apps/settings/lib/Activity/GroupSetting.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,14 +11,12 @@ use OCP\IL10N; class GroupSetting implements ISetting { - /** @var IL10N */ - protected $l; - /** - * @param IL10N $l10n + * @param IL10N $l */ - public function __construct(IL10N $l10n) { - $this->l = $l10n; + public function __construct( + protected IL10N $l, + ) { } /** @@ -38,8 +37,8 @@ class GroupSetting implements ISetting { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority(): int { diff --git a/apps/settings/lib/Activity/Provider.php b/apps/settings/lib/Activity/Provider.php index ab9a8e3c85c..c31a900abd5 100644 --- a/apps/settings/lib/Activity/Provider.php +++ b/apps/settings/lib/Activity/Provider.php @@ -8,6 +8,7 @@ declare(strict_types=1); */ namespace OCA\Settings\Activity; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IManager; use OCP\Activity\IProvider; @@ -30,29 +31,15 @@ class Provider implements IProvider { public const APP_TOKEN_FILESYSTEM_GRANTED = 'app_token_filesystem_granted'; public const APP_TOKEN_FILESYSTEM_REVOKED = 'app_token_filesystem_revoked'; - /** @var IFactory */ - protected $languageFactory; - /** @var IL10N */ protected $l; - /** @var IURLGenerator */ - protected $url; - - /** @var IUserManager */ - protected $userManager; - - /** @var IManager */ - private $activityManager; - - public function __construct(IFactory $languageFactory, - IURLGenerator $url, - IUserManager $userManager, - IManager $activityManager) { - $this->languageFactory = $languageFactory; - $this->url = $url; - $this->userManager = $userManager; - $this->activityManager = $activityManager; + public function __construct( + protected IFactory $languageFactory, + protected IURLGenerator $url, + protected IUserManager $userManager, + private IManager $activityManager, + ) { } /** @@ -60,12 +47,12 @@ class Provider implements IProvider { * @param IEvent $event * @param IEvent|null $previousEvent * @return IEvent - * @throws \InvalidArgumentException + * @throws UnknownActivityException * @since 11.0.0 */ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent { if ($event->getApp() !== 'settings') { - throw new \InvalidArgumentException('Unknown app'); + throw new UnknownActivityException('Unknown app'); } $this->l = $this->languageFactory->get('settings', $language); @@ -105,7 +92,7 @@ class Provider implements IProvider { } elseif ($event->getSubject() === self::APP_TOKEN_FILESYSTEM_REVOKED) { $subject = $this->l->t('You revoked filesystem access from app password "{token}"'); } else { - throw new \InvalidArgumentException('Unknown subject'); + throw new UnknownActivityException('Unknown subject'); } $parsedParameters = $this->getParameters($event); @@ -117,7 +104,7 @@ class Provider implements IProvider { /** * @param IEvent $event * @return array - * @throws \InvalidArgumentException + * @throws UnknownActivityException */ protected function getParameters(IEvent $event): array { $subject = $event->getSubject(); @@ -142,7 +129,7 @@ class Provider implements IProvider { return [ 'token' => [ 'type' => 'highlight', - 'id' => $event->getObjectId(), + 'id' => (string)$event->getObjectId(), 'name' => $parameters['name'], ] ]; @@ -150,23 +137,20 @@ class Provider implements IProvider { return [ 'token' => [ 'type' => 'highlight', - 'id' => $event->getObjectId(), + 'id' => (string)$event->getObjectId(), 'name' => $parameters['name'], ], 'newToken' => [ 'type' => 'highlight', - 'id' => $event->getObjectId(), + 'id' => (string)$event->getObjectId(), 'name' => $parameters['newName'], ] ]; } - throw new \InvalidArgumentException('Unknown subject'); + throw new UnknownActivityException('Unknown subject'); } - /** - * @throws \InvalidArgumentException - */ protected function setSubjects(IEvent $event, string $subject, array $parameters): void { $event->setRichSubject($subject, $parameters); } diff --git a/apps/settings/lib/Activity/SecurityFilter.php b/apps/settings/lib/Activity/SecurityFilter.php index 61d657193fd..9a32e82a984 100644 --- a/apps/settings/lib/Activity/SecurityFilter.php +++ b/apps/settings/lib/Activity/SecurityFilter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,15 +12,10 @@ use OCP\IURLGenerator; class SecurityFilter implements IFilter { - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var IL10N */ - private $l10n; - - public function __construct(IURLGenerator $urlGenerator, IL10N $l10n) { - $this->urlGenerator = $urlGenerator; - $this->l10n = $l10n; + public function __construct( + private IURLGenerator $urlGenerator, + private IL10N $l10n, + ) { } public function allowedApps() { diff --git a/apps/settings/lib/Activity/SecurityProvider.php b/apps/settings/lib/Activity/SecurityProvider.php index a67b9cf365e..658e2e7b949 100644 --- a/apps/settings/lib/Activity/SecurityProvider.php +++ b/apps/settings/lib/Activity/SecurityProvider.php @@ -8,7 +8,7 @@ declare(strict_types=1); */ namespace OCA\Settings\Activity; -use InvalidArgumentException; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IManager; use OCP\Activity\IProvider; @@ -17,24 +17,16 @@ use OCP\L10N\IFactory as L10nFactory; class SecurityProvider implements IProvider { - /** @var L10nFactory */ - private $l10n; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var IManager */ - private $activityManager; - - public function __construct(L10nFactory $l10n, IURLGenerator $urlGenerator, IManager $activityManager) { - $this->urlGenerator = $urlGenerator; - $this->l10n = $l10n; - $this->activityManager = $activityManager; + public function __construct( + private L10nFactory $l10n, + private IURLGenerator $urlGenerator, + private IManager $activityManager, + ) { } public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { if ($event->getType() !== 'security') { - throw new InvalidArgumentException(); + throw new UnknownActivityException(); } $l = $this->l10n->get('settings', $language); @@ -85,7 +77,7 @@ class SecurityProvider implements IProvider { } break; default: - throw new InvalidArgumentException(); + throw new UnknownActivityException(); } return $event; } diff --git a/apps/settings/lib/Activity/SecuritySetting.php b/apps/settings/lib/Activity/SecuritySetting.php index 3253640aca3..9226b5aea5b 100644 --- a/apps/settings/lib/Activity/SecuritySetting.php +++ b/apps/settings/lib/Activity/SecuritySetting.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,11 +11,9 @@ use OCP\IL10N; class SecuritySetting implements ISetting { - /** @var IL10N */ - private $l10n; - - public function __construct(IL10N $l10n) { - $this->l10n = $l10n; + public function __construct( + private IL10N $l10n, + ) { } public function canChangeMail() { diff --git a/apps/settings/lib/Activity/Setting.php b/apps/settings/lib/Activity/Setting.php index d627a70b766..f9c659594d6 100644 --- a/apps/settings/lib/Activity/Setting.php +++ b/apps/settings/lib/Activity/Setting.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,14 +11,12 @@ use OCP\IL10N; class Setting implements ISetting { - /** @var IL10N */ - protected $l; - /** - * @param IL10N $l10n + * @param IL10N $l */ - public function __construct(IL10N $l10n) { - $this->l = $l10n; + public function __construct( + protected IL10N $l, + ) { } /** @@ -38,8 +37,8 @@ class Setting implements ISetting { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/settings/lib/AppInfo/Application.php b/apps/settings/lib/AppInfo/Application.php index 2a6b21f7f7d..6e59e56fe82 100644 --- a/apps/settings/lib/AppInfo/Application.php +++ b/apps/settings/lib/AppInfo/Application.php @@ -15,6 +15,7 @@ use OC\Server; use OCA\Settings\Hooks; use OCA\Settings\Listener\AppPasswordCreatedActivityListener; use OCA\Settings\Listener\GroupRemovedListener; +use OCA\Settings\Listener\MailProviderListener; use OCA\Settings\Listener\UserAddedToGroupActivityListener; use OCA\Settings\Listener\UserRemovedFromGroupActivityListener; use OCA\Settings\Mailer\NewUserMailHelper; @@ -22,6 +23,8 @@ use OCA\Settings\Middleware\SubadminMiddleware; use OCA\Settings\Search\AppSearch; use OCA\Settings\Search\SectionSearch; use OCA\Settings\Search\UserSearch; +use OCA\Settings\Settings\Admin\MailProvider; +use OCA\Settings\SetupChecks\AllowedAdminRanges; use OCA\Settings\SetupChecks\AppDirsWithDifferentOwner; use OCA\Settings\SetupChecks\BruteForceThrottler; use OCA\Settings\SetupChecks\CheckUserCertificates; @@ -45,9 +48,12 @@ use OCA\Settings\SetupChecks\JavaScriptSourceMaps; use OCA\Settings\SetupChecks\LegacySSEKeyFormat; use OCA\Settings\SetupChecks\MaintenanceWindowStart; use OCA\Settings\SetupChecks\MemcacheConfigured; +use OCA\Settings\SetupChecks\MimeTypeMigrationAvailable; +use OCA\Settings\SetupChecks\MysqlRowFormat; use OCA\Settings\SetupChecks\MysqlUnicodeSupport; use OCA\Settings\SetupChecks\OcxProviders; use OCA\Settings\SetupChecks\OverwriteCliUrl; +use OCA\Settings\SetupChecks\PhpApcuConfig; use OCA\Settings\SetupChecks\PhpDefaultCharset; use OCA\Settings\SetupChecks\PhpDisabledFunctions; use OCA\Settings\SetupChecks\PhpFreetypeSupport; @@ -64,6 +70,7 @@ use OCA\Settings\SetupChecks\SchedulingTableSize; use OCA\Settings\SetupChecks\SecurityHeaders; use OCA\Settings\SetupChecks\SupportedDatabase; use OCA\Settings\SetupChecks\SystemIs64bit; +use OCA\Settings\SetupChecks\TaskProcessingPickupSpeed; use OCA\Settings\SetupChecks\TempSpaceAvailable; use OCA\Settings\SetupChecks\TransactionIsolation; use OCA\Settings\SetupChecks\WellKnownUrls; @@ -76,11 +83,14 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\IAppContainer; +use OCP\AppFramework\QueryException; use OCP\Defaults; use OCP\Group\Events\GroupDeletedEvent; use OCP\Group\Events\UserAddedEvent; use OCP\Group\Events\UserRemovedEvent; use OCP\IServerContainer; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; use OCP\Settings\IManager; use OCP\Util; @@ -108,22 +118,20 @@ class Application extends App implements IBootstrap { $context->registerEventListener(UserRemovedEvent::class, UserRemovedFromGroupActivityListener::class); $context->registerEventListener(GroupDeletedEvent::class, GroupRemovedListener::class); + // Register Mail Provider listeners + $context->registerEventListener(DeclarativeSettingsGetValueEvent::class, MailProviderListener::class); + $context->registerEventListener(DeclarativeSettingsSetValueEvent::class, MailProviderListener::class); + // Register well-known handlers $context->registerWellKnownHandler(SecurityTxtHandler::class); $context->registerWellKnownHandler(ChangePasswordHandler::class); + // Register Settings Form(s) + $context->registerDeclarativeSettings(MailProvider::class); + /** * Core class wrappers */ - /** FIXME: Remove once OC_SubAdmin is non-static and mockable */ - $context->registerService('isSubAdmin', function () { - $userObject = \OC::$server->getUserSession()->getUser(); - $isSubAdmin = false; - if ($userObject !== null) { - $isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject); - } - return $isSubAdmin; - }); $context->registerService(IProvider::class, function (IAppContainer $appContainer) { /** @var IServerContainer $serverContainer */ $serverContainer = $appContainer->query(IServerContainer::class); @@ -153,6 +161,7 @@ class Application extends App implements IBootstrap { Util::getDefaultEmailAddress('no-reply') ); }); + $context->registerSetupCheck(AllowedAdminRanges::class); $context->registerSetupCheck(AppDirsWithDifferentOwner::class); $context->registerSetupCheck(BruteForceThrottler::class); $context->registerSetupCheck(CheckUserCertificates::class); @@ -176,13 +185,17 @@ class Application extends App implements IBootstrap { $context->registerSetupCheck(LegacySSEKeyFormat::class); $context->registerSetupCheck(MaintenanceWindowStart::class); $context->registerSetupCheck(MemcacheConfigured::class); + $context->registerSetupCheck(MimeTypeMigrationAvailable::class); + $context->registerSetupCheck(MysqlRowFormat::class); $context->registerSetupCheck(MysqlUnicodeSupport::class); $context->registerSetupCheck(OcxProviders::class); $context->registerSetupCheck(OverwriteCliUrl::class); $context->registerSetupCheck(PhpDefaultCharset::class); $context->registerSetupCheck(PhpDisabledFunctions::class); $context->registerSetupCheck(PhpFreetypeSupport::class); + $context->registerSetupCheck(PhpApcuConfig::class); $context->registerSetupCheck(PhpGetEnv::class); + // Temporarily disabled $context->registerSetupCheck(PhpMaxFileSize::class); $context->registerSetupCheck(PhpMemoryLimit::class); $context->registerSetupCheck(PhpModules::class); $context->registerSetupCheck(PhpOpcacheSetup::class); @@ -194,6 +207,7 @@ class Application extends App implements IBootstrap { $context->registerSetupCheck(SchedulingTableSize::class); $context->registerSetupCheck(SupportedDatabase::class); $context->registerSetupCheck(SystemIs64bit::class); + $context->registerSetupCheck(TaskProcessingPickupSpeed::class); $context->registerSetupCheck(TempSpaceAvailable::class); $context->registerSetupCheck(TransactionIsolation::class); $context->registerSetupCheck(PushService::class); @@ -213,7 +227,7 @@ class Application extends App implements IBootstrap { * @throws \InvalidArgumentException * @throws \BadMethodCallException * @throws \Exception - * @throws \OCP\AppFramework\QueryException + * @throws QueryException */ public function onChangePassword(array $parameters) { /** @var Hooks $hooks */ @@ -226,7 +240,7 @@ class Application extends App implements IBootstrap { * @throws \InvalidArgumentException * @throws \BadMethodCallException * @throws \Exception - * @throws \OCP\AppFramework\QueryException + * @throws QueryException */ public function onChangeInfo(array $parameters) { if ($parameters['feature'] !== 'eMailAddress') { diff --git a/apps/settings/lib/BackgroundJobs/VerifyUserData.php b/apps/settings/lib/BackgroundJobs/VerifyUserData.php index 728fe9febf5..eb66644ad91 100644 --- a/apps/settings/lib/BackgroundJobs/VerifyUserData.php +++ b/apps/settings/lib/BackgroundJobs/VerifyUserData.php @@ -20,7 +20,7 @@ use OCP\IUserManager; use Psr\Log\LoggerInterface; class VerifyUserData extends Job { - /** @var bool */ + /** @var bool */ private bool $retainJob = true; /** @var int max number of attempts to send the request */ @@ -120,9 +120,11 @@ class VerifyUserData extends Job { } protected function verifyViaLookupServer(array $argument, string $dataType): bool { - if (empty($this->lookupServerUrl) || - $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes') !== 'yes' || - $this->config->getSystemValue('has_internet_connection', true) === false) { + // TODO: Consider to enable for non-global-scale setups by checking 'files_sharing', 'lookupServerUploadEnabled' + if (!$this->config->getSystemValueBool('gs.enabled', false) + || empty($this->lookupServerUrl) + || $this->config->getSystemValue('has_internet_connection', true) === false + ) { return true; } diff --git a/apps/settings/lib/Command/AdminDelegation/Add.php b/apps/settings/lib/Command/AdminDelegation/Add.php index 4ec6510f205..5cbef5c5d15 100644 --- a/apps/settings/lib/Command/AdminDelegation/Add.php +++ b/apps/settings/lib/Command/AdminDelegation/Add.php @@ -39,7 +39,7 @@ class Add extends Base { protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $settingClass = $input->getArgument('settingClass'); - if (!in_array(IDelegatedSettings::class, (array) class_implements($settingClass), true)) { + if (!in_array(IDelegatedSettings::class, (array)class_implements($settingClass), true)) { $io->error('The specified class isn’t a valid delegated setting.'); return 2; } @@ -52,7 +52,7 @@ class Add extends Base { $this->authorizedGroupService->create($groupId, $settingClass); - $io->success('Administration of '.$settingClass.' delegated to '.$groupId.'.'); + $io->success('Administration of ' . $settingClass . ' delegated to ' . $groupId . '.'); return 0; } diff --git a/apps/settings/lib/Command/AdminDelegation/Remove.php b/apps/settings/lib/Command/AdminDelegation/Remove.php index 584b9201193..6b5347ce89f 100644 --- a/apps/settings/lib/Command/AdminDelegation/Remove.php +++ b/apps/settings/lib/Command/AdminDelegation/Remove.php @@ -43,12 +43,12 @@ class Remove extends Base { foreach ($groups as $group) { if ($group->getGroupId() === $groupId) { $this->authorizedGroupService->delete($group->getId()); - $io->success('Removed delegation of '.$settingClass.' to '.$groupId.'.'); + $io->success('Removed delegation of ' . $settingClass . ' to ' . $groupId . '.'); return 0; } } - $io->success('Group '.$groupId.' didn’t have delegation for '.$settingClass.'.'); + $io->success('Group ' . $groupId . ' didn’t have delegation for ' . $settingClass . '.'); return 0; } diff --git a/apps/settings/lib/Command/AdminDelegation/Show.php b/apps/settings/lib/Command/AdminDelegation/Show.php index 73f89ad0ead..9aba6bc0cb7 100644 --- a/apps/settings/lib/Command/AdminDelegation/Show.php +++ b/apps/settings/lib/Command/AdminDelegation/Show.php @@ -47,7 +47,7 @@ class Show extends Base { continue; } - $io->section('Section: '.$section->getID()); + $io->section('Section: ' . $section->getID()); $io->table($headers, array_map(function (IDelegatedSettings $setting) use ($section) { $className = get_class($setting); $groups = array_map( diff --git a/apps/settings/lib/Controller/AISettingsController.php b/apps/settings/lib/Controller/AISettingsController.php index 832a7d6d96b..114cbf61514 100644 --- a/apps/settings/lib/Controller/AISettingsController.php +++ b/apps/settings/lib/Controller/AISettingsController.php @@ -8,22 +8,19 @@ declare(strict_types=1); */ 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\IConfig; +use OCP\IAppConfig; use OCP\IRequest; class AISettingsController extends Controller { - /** - * @param string $appName - * @param IRequest $request - * @param IConfig $config - */ public function __construct( $appName, IRequest $request, - private IConfig $config, + private IAppConfig $appConfig, ) { parent::__construct($appName, $request); } @@ -31,18 +28,17 @@ class AISettingsController extends Controller { /** * Sets the email settings * - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\ArtificialIntelligence) - * * @param array $settings * @return DataResponse */ + #[AuthorizedAdminSetting(settings: ArtificialIntelligence::class)] public function update($settings) { - $keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.translation_provider_preferences', 'ai.text2image_provider']; + $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->config->setAppValue('core', $key, json_encode($settings[$key])); + $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 34a42bd5b88..15e2c392148 100644 --- a/apps/settings/lib/Controller/AdminSettingsController.php +++ b/apps/settings/lib/Controller/AdminSettingsController.php @@ -1,12 +1,14 @@ <?php + /** * 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; @@ -14,11 +16,9 @@ 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 { @@ -46,64 +46,16 @@ class AdminSettingsController extends Controller { } /** - * @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); - $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($user, 'admin', $section); - if (empty($settings) && empty($declarativeFormIDs)) { - 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 726441c9a88..a85ee8cc20a 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -6,6 +7,7 @@ */ 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; @@ -14,12 +16,13 @@ use OC\App\AppStore\Version\VersionParser; use OC\App\DependencyAnalyzer; use OC\App\Platform; use OC\Installer; -use OC_App; +use OCA\AppAPI\Service\ExAppsPageService; use OCP\App\AppPathNotFoundException; -use OCP\App\IAppManager; 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; @@ -35,11 +38,17 @@ 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)] @@ -57,7 +66,7 @@ class AppSettingsController extends Controller { private IL10N $l10n, private IConfig $config, private INavigationManager $navigationManager, - private IAppManager $appManager, + private AppManager $appManager, private CategoryFetcher $categoryFetcher, private AppFetcher $appFetcher, private IFactory $l10nFactory, @@ -74,10 +83,11 @@ class AppSettingsController extends Controller { } /** - * @NoCSRFRequired + * @psalm-suppress UndefinedClass AppAPI is shipped since 30.0.1 * * @return TemplateResponse */ + #[NoCSRFRequired] public function viewApps(): TemplateResponse { $this->navigationManager->setActiveEntry('core_apps'); @@ -86,39 +96,45 @@ class AppSettingsController extends Controller { $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); - \OCP\Util::addStyle('settings', 'settings'); - \OCP\Util::addScript('settings', 'vue-settings-apps-users-management'); + Util::addStyle('settings', 'settings'); + Util::addScript('settings', 'vue-settings-apps-users-management'); return $templateResponse; } /** * Get all active entries for the app discover section - * - * @NoCSRFRequired */ + #[NoCSRFRequired] public function getAppDiscoverJSON(): JSONResponse { $data = $this->discoverFetcher->get(true); - return new JSONResponse($data); + return new JSONResponse(array_values($data)); } /** - * @PublicPage - * @NoCSRFRequired - * * Get a image for the app discover section - this is proxied for privacy and CSP reasons * * @param string $image * @throws \Exception */ - public function getAppDiscoverMedia(string $fileName): Response { - $etag = $this->discoverFetcher->getETag() ?? date('Y-m'); + #[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'); @@ -145,6 +161,26 @@ class AppSettingsController extends Controller { $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); @@ -166,6 +202,31 @@ class AppSettingsController extends Controller { 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 @@ -229,11 +290,31 @@ class AppSettingsController extends Controller { ], $categories); } + /** + * 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() { $appClass = new \OC_App(); $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; } @@ -292,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']; } @@ -306,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']; @@ -405,7 +490,7 @@ class AppSettingsController extends Controller { } $currentVersion = ''; - if ($this->appManager->isInstalled($app['id'])) { + if ($this->appManager->isEnabledForAnyone($app['id'])) { $currentVersion = $this->appManager->getAppVersion($app['id']); } else { $currentVersion = $app['releases'][0]['version']; @@ -413,12 +498,13 @@ class AppSettingsController extends Controller { $formattedApps[] = [ 'id' => $app['id'], + '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' => [], @@ -438,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, @@ -455,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); } @@ -470,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->get(Installer::class); + $installer = Server::get(Installer::class); $isDownloaded = $installer->isDownloaded($appId); if (!$isDownloaded) { @@ -510,11 +595,11 @@ class AppSettingsController extends Controller { } 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); } } @@ -522,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([]); @@ -551,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]]); } @@ -571,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 { @@ -598,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 83db90fdc32..8652a49fb1d 100644 --- a/apps/settings/lib/Controller/AuthSettingsController.php +++ b/apps/settings/lib/Controller/AuthSettingsController.php @@ -17,6 +17,8 @@ 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; @@ -33,27 +35,9 @@ class AuthSettingsController extends Controller { /** @var IProvider */ private $tokenProvider; - /** @var ISession */ - private $session; - - /** @var 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 @@ -66,35 +50,31 @@ class AuthSettingsController extends Controller { * @param RemoteWipe $remoteWipe * @param LoggerInterface $logger */ - public function __construct(string $appName, + public function __construct( + string $appName, IRequest $request, IProvider $tokenProvider, - ISession $session, - ISecureRandom $random, - ?string $userId, - IUserSession $userSession, - IManager $activityManager, + private ISession $session, + private ISecureRandom $random, + private ?string $userId, + private IUserSession $userSession, + private IManager $activityManager, RemoteWipe $remoteWipe, - LoggerInterface $logger) { + 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(); @@ -126,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; @@ -169,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); @@ -189,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 @@ -203,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); @@ -243,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'); @@ -268,7 +248,7 @@ class AuthSettingsController extends Controller { } catch (ExpiredTokenException $e) { $token = $e->getToken(); } - if ($token->getUID() !== $this->uid) { + 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!'); } @@ -276,15 +256,15 @@ class AuthSettingsController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired * * @param int $id * @return JSONResponse * @throws InvalidTokenException * @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 f4a018b0555..82a1ca4703e 100644 --- a/apps/settings/lib/Controller/AuthorizedGroupController.php +++ b/apps/settings/lib/Controller/AuthorizedGroupController.php @@ -15,12 +15,12 @@ use OCP\DB\Exception; use OCP\IRequest; 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; } /** @@ -57,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 ff136ded981..a874a47c16a 100644 --- a/apps/settings/lib/Controller/ChangePasswordController.php +++ b/apps/settings/lib/Controller/ChangePasswordController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,48 +12,44 @@ 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, + public function __construct( + string $appName, IRequest $request, - ?string $userId, - IUserManager $userManager, + private ?string $userId, + private IUserManager $userManager, IUserSession $userSession, - IGroupManager $groupManager, - IAppManager $appManager, - IL10N $l) { + 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) */ + #[NoAdminRequired] + #[BruteForceProtection(action: 'changePersonalPassword')] public function changePersonalPassword(string $oldpassword = '', ?string $newpassword = null): JSONResponse { $loginName = $this->userSession->getLoginName(); /** @var IUser $user */ @@ -97,10 +94,8 @@ class ChangePasswordController extends Controller { ]); } - /** - * @NoAdminRequired - * @PasswordConfirmationRequired - */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] public function changeUserPassword(?string $username = null, ?string $password = null, ?string $recoveryPassword = null): JSONResponse { if ($username === null) { return new JSONResponse([ @@ -131,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', @@ -145,8 +140,8 @@ class ChangePasswordController extends Controller { if ($this->appManager->isEnabledForUser('encryption')) { //handle the recovery case - $keyManager = \OCP\Server::get(\OCA\Encryption\KeyManager::class); - $recovery = \OCP\Server::get(\OCA\Encryption\Recovery::class); + $keyManager = Server::get(KeyManager::class); + $recovery = Server::get(Recovery::class); $recoveryAdminEnabled = $recovery->isRecoveryKeyEnabled(); $validRecoveryPassword = false; diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index 5cf48d38ffb..2a189a37ce6 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -9,7 +9,11 @@ namespace OCA\Settings\Controller; use OC\AppFramework\Http; use OC\IntegrityCheck\Checker; +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; @@ -23,50 +27,37 @@ use Psr\Log\LoggerInterface; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class CheckSetupController extends Controller { - /** @var IConfig */ - private $config; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IL10N */ - private $l10n; /** @var Checker */ private $checker; - /** @var LoggerInterface */ - private $logger; - private ISetupCheckManager $setupCheckManager; - public function __construct($AppName, + public function __construct( + $AppName, IRequest $request, - IConfig $config, - IURLGenerator $urlGenerator, - IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + private IL10N $l10n, Checker $checker, - LoggerInterface $logger, - ISetupCheckManager $setupCheckManager, + private LoggerInterface $logger, + private ISetupCheckManager $setupCheckManager, ) { parent::__construct($AppName, $request); - $this->config = $config; - $this->urlGenerator = $urlGenerator; - $this->l10n = $l10n; $this->checker = $checker; - $this->logger = $logger; - $this->setupCheckManager = $setupCheckManager; } /** - * @NoAdminRequired - * @NoCSRFRequired * @return DataResponse */ + #[NoCSRFRequired] + #[NoAdminRequired] public function setupCheckManager(): DataResponse { return new DataResponse($this->setupCheckManager->runAll()); } /** - * @NoCSRFRequired * @return RedirectResponse - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) */ + #[NoCSRFRequired] + #[AuthorizedAdminSetting(settings: Overview::class)] public function rescanFailedIntegrityCheck(): RedirectResponse { $this->checker->runInstanceVerification(); return new RedirectResponse( @@ -74,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.'); @@ -85,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 ===================== @@ -133,8 +126,8 @@ Raw output /** * @return DataResponse - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview) */ + #[AuthorizedAdminSetting(settings: Overview::class)] public function check() { return new DataResponse( [ diff --git a/apps/settings/lib/Controller/CommonSettingsTrait.php b/apps/settings/lib/Controller/CommonSettingsTrait.php index eaf8e6c3774..75d2b1f2f9e 100644 --- a/apps/settings/lib/Controller/CommonSettingsTrait.php +++ b/apps/settings/lib/Controller/CommonSettingsTrait.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,6 +7,8 @@ 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; @@ -21,7 +24,8 @@ use OCP\Settings\ISettings; use OCP\Util; /** - * @psalm-import-type DeclarativeSettingsFormField from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithValues from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm */ trait CommonSettingsTrait { @@ -75,7 +79,7 @@ trait CommonSettingsTrait { /** @psalm-suppress PossiblyNullArgument */ $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($this->userSession->getUser(), $type, $section->getID()); - if (empty($settings) && empty($declarativeFormIDs) && !($section->getID() === 'additional' && count(\OC_App::getForms('admin')) > 0)) { + if (empty($settings) && empty($declarativeFormIDs)) { continue; } @@ -106,16 +110,26 @@ trait CommonSettingsTrait { } /** - * @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]; @@ -125,34 +139,46 @@ trait CommonSettingsTrait { * @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"'); } - $this->declarativeSettingsManager->loadSchemas(); - - $templateParams = []; - $templateParams = array_merge($templateParams, $this->getNavigationParameters($type, $section)); - $templateParams = array_merge($templateParams, $this->getSettings($section)); - - /** @psalm-suppress PossiblyNullArgument */ - $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($this->userSession->getUser(), $type, $section); - if (!empty($declarativeFormIDs)) { - foreach ($declarativeFormIDs as $app => $ids) { - /** @psalm-suppress PossiblyUndefinedArrayOffset */ - $templateParams['content'] .= join(array_map(fn (string $id) => '<div id="' . $app . '_' . $id . '"></div>', $ids)); - } + if (!empty($declarativeSettings)) { Util::addScript(Application::APP_ID, 'declarative-settings-forms'); - /** @psalm-suppress PossiblyNullArgument */ - $this->initialState->provideInitialState('declarative-settings-forms', $this->declarativeSettingsManager->getFormsWithValues($this->userSession->getUser(), $type, $section)); + $this->initialState->provideInitialState('declarative-settings-forms', $declarativeSettings); } + $settings = array_merge(...$settings); + $templateParams = $this->formatSettings($settings, $declarativeSettings); + $templateParams = array_merge($templateParams, $this->getNavigationParameters($type, $section)); + $activeSection = $this->settingsManager->getSection($type, $section); if ($activeSection) { $templateParams['pageTitle'] = $activeSection->getName(); @@ -162,6 +188,4 @@ trait CommonSettingsTrait { 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 index eb9d45839de..4e4bee4043c 100644 --- a/apps/settings/lib/Controller/DeclarativeSettingsController.php +++ b/apps/settings/lib/Controller/DeclarativeSettingsController.php @@ -15,6 +15,7 @@ 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; @@ -53,6 +54,45 @@ class DeclarativeSettingsController extends OCSController { */ #[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(); diff --git a/apps/settings/lib/Controller/HelpController.php b/apps/settings/lib/Controller/HelpController.php index 34e1b5cf09f..05bff158ee6 100644 --- a/apps/settings/lib/Controller/HelpController.php +++ b/apps/settings/lib/Controller/HelpController.php @@ -9,6 +9,8 @@ declare(strict_types=1); 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; @@ -23,52 +25,28 @@ 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; - - /** @var IConfig */ - private $config; - - /** @var IAppConfig */ - private $appConfig; - public function __construct( string $appName, IRequest $request, - INavigationManager $navigationManager, - IURLGenerator $urlGenerator, - ?string $userId, - IGroupManager $groupManager, - IL10N $l10n, - IConfig $config, - IAppConfig $appConfig, + 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; - $this->config = $config; - $this->appConfig = $appConfig; } /** * @return TemplateResponse * - * @NoCSRFRequired - * @NoAdminRequired * @NoSubAdminRequired */ + #[NoCSRFRequired] + #[NoAdminRequired] public function help(string $mode = 'user'): TemplateResponse { $this->navigationManager->setActiveEntry('help'); $pageTitle = $this->l10n->t('Administrator documentation'); diff --git a/apps/settings/lib/Controller/LogSettingsController.php b/apps/settings/lib/Controller/LogSettingsController.php index 7ff521d145b..90cf4549d2f 100644 --- a/apps/settings/lib/Controller/LogSettingsController.php +++ b/apps/settings/lib/Controller/LogSettingsController.php @@ -10,6 +10,8 @@ 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; @@ -26,23 +28,23 @@ class LogSettingsController extends Controller { /** * download logfile * - * @NoCSRFRequired - * - * @psalm-suppress MoreSpecificReturnType The value of Content-Disposition is not relevant - * @psalm-suppress LessSpecificReturnStatement The value of Content-Disposition is not relevant - * @return StreamResponse<Http::STATUS_OK, array{Content-Type: 'application/octet-stream', 'Content-Disposition': string}> + * @return StreamResponse<Http::STATUS_OK, array{Content-Type: 'application/octet-stream', 'Content-Disposition': 'attachment; filename="nextcloud.log"'}> * * 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->setHeaders([ - 'Content-Type' => 'application/octet-stream', - '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 b59e76f57b9..f1e3b8032dc 100644 --- a/apps/settings/lib/Controller/MailSettingsController.php +++ b/apps/settings/lib/Controller/MailSettingsController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -6,8 +7,11 @@ */ 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; @@ -18,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 @@ -38,52 +31,51 @@ class MailSettingsController extends Controller { * @param IURLGenerator $urlGenerator, * @param IMailer $mailer */ - public function __construct($appName, + public function __construct( + $appName, IRequest $request, - IL10N $l10n, - IConfig $config, - IUserSession $userSession, - IURLGenerator $urlGenerator, - IMailer $mailer) { + 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; } @@ -98,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); @@ -122,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)) { diff --git a/apps/settings/lib/Controller/PersonalSettingsController.php b/apps/settings/lib/Controller/PersonalSettingsController.php index 3c798580bc4..340ca3f93eb 100644 --- a/apps/settings/lib/Controller/PersonalSettingsController.php +++ b/apps/settings/lib/Controller/PersonalSettingsController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,6 +7,8 @@ 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; @@ -16,7 +19,6 @@ 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 { @@ -44,54 +46,14 @@ class PersonalSettingsController extends Controller { } /** - * @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 28ab7c9338c..91d0a8640d1 100644 --- a/apps/settings/lib/Controller/ReasonsController.php +++ b/apps/settings/lib/Controller/ReasonsController.php @@ -9,6 +9,8 @@ declare(strict_types=1); 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; @@ -16,10 +18,10 @@ use OCP\AppFramework\Http\DataDisplayResponse; 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/UsersController.php b/apps/settings/lib/Controller/UsersController.php index 82024340eaf..8efd3eeb8ca 100644 --- a/apps/settings/lib/Controller/UsersController.php +++ b/apps/settings/lib/Controller/UsersController.php @@ -14,18 +14,25 @@ use InvalidArgumentException; use OC\AppFramework\Http; use OC\Encryption\Exceptions\ModuleDoesNotExistsException; use OC\ForbiddenException; +use OC\Group\MetaData; use OC\KnownUser\KnownUserService; 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; @@ -33,18 +40,24 @@ 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\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 { + /** Limit for counting users for subadmins, to avoid spending too much time */ + private const COUNT_LIMIT_FOR_SUBADMINS = 999; public function __construct( string $appName, @@ -70,44 +83,43 @@ class UsersController extends Controller { /** - * @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->getSystemValueBool('sort_groups_by_name', false)) { - $sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME; + $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; } } } @@ -115,15 +127,23 @@ class UsersController extends Controller { $canChangePassword = $this->canAdminChangeUserPasswords(); /* GROUPS */ - $groupsInfo = new \OC\Group\MetaData( + $groupsInfo = new MetaData( $uid, $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) { @@ -135,41 +155,41 @@ class UsersController extends Controller { $userCount = 0; if (!$isLDAPUsed) { - if ($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']; - $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 accounts', + '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'; @@ -187,17 +207,19 @@ class UsersController extends Controller { $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 === \OC\Group\MetaData::SORT_GROUPNAME; + $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'] = $isAdmin; + $serverData['isDelegatedAdmin'] = $isDelegatedAdmin; $serverData['sortGroups'] = $forceSortGroupByName - ? \OC\Group\MetaData::SORT_GROUPNAME - : (int)$this->config->getAppValue('core', 'group.sortBy', (string)\OC\Group\MetaData::SORT_USERCOUNT); + ? MetaData::SORT_GROUPNAME + : (int)$this->config->getAppValue('core', 'group.sortBy', (string)MetaData::SORT_USERCOUNT); $serverData['forceSortGroupByName'] = $forceSortGroupByName; $serverData['quotaPreset'] = $quotaPreset; $serverData['allowUnlimitedQuota'] = $allowUnlimitedQuota; @@ -214,8 +236,8 @@ class UsersController extends Controller { $this->initialState->provideInitialState('usersSettings', $serverData); - \OCP\Util::addStyle('settings', 'settings'); - \OCP\Util::addScript('settings', 'vue-settings-apps-users-management'); + Util::addStyle('settings', 'settings'); + Util::addScript('settings', 'vue-settings-apps-users-management'); return new TemplateResponse('settings', 'settings/empty', ['pageTitle' => $this->l10n->t('Settings')]); } @@ -226,6 +248,7 @@ class UsersController extends Controller { * * @return JSONResponse */ + #[AuthorizedAdminSetting(settings:Users::class)] public function setPreference(string $key, string $value): JSONResponse { $allowed = ['newUser.sendEmail', 'group.sortBy']; if (!in_array($key, $allowed, true)) { @@ -281,9 +304,7 @@ class UsersController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired * * @param string|null $avatarScope * @param string|null $displayname @@ -298,6 +319,8 @@ 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 @@ -305,6 +328,9 @@ class UsersController extends Controller { * * @return DataResponse */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] + #[UserRateLimit(limit: 5, period: 60)] public function setUserSettings(?string $avatarScope = null, ?string $displayname = null, ?string $displaynameScope = null, @@ -318,10 +344,14 @@ class UsersController extends Controller { ?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) { @@ -360,8 +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) { @@ -401,16 +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' => [ @@ -463,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 56bc0aa4a05..495b58e6a4b 100644 --- a/apps/settings/lib/Controller/WebAuthnController.php +++ b/apps/settings/lib/Controller/WebAuthnController.php @@ -12,7 +12,11 @@ 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\IRequest; use OCP\ISession; @@ -35,12 +39,12 @@ class WebAuthnController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired - * @UseSession - * @NoCSRFRequired */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] + #[UseSession] + #[NoCSRFRequired] public function startRegistration(): JSONResponse { $this->logger->debug('Starting WebAuthn registration'); @@ -53,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'); @@ -75,10 +79,10 @@ class WebAuthnController extends Controller { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired */ + #[NoAdminRequired] + #[PasswordConfirmationRequired] public function deleteRegistration(int $id): JSONResponse { $this->logger->debug('Finishing WebAuthn registration'); diff --git a/apps/settings/lib/Hooks.php b/apps/settings/lib/Hooks.php index eb77b676914..f59013ca5e1 100644 --- a/apps/settings/lib/Hooks.php +++ b/apps/settings/lib/Hooks.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -19,43 +20,17 @@ use OCP\Mail\IMailer; class Hooks { - /** @var IActivityManager */ - protected $activityManager; - /** @var IGroupManager|\OC\Group\Manager */ - protected $groupManager; - /** @var IUserManager */ - protected $userManager; - /** @var IUserSession */ - protected $userSession; - /** @var IURLGenerator */ - protected $urlGenerator; - /** @var IMailer */ - protected $mailer; - /** @var IConfig */ - protected $config; - /** @var IFactory */ - protected $languageFactory; - /** @var Defaults */ - protected $defaults; - - public function __construct(IActivityManager $activityManager, - IGroupManager $groupManager, - IUserManager $userManager, - IUserSession $userSession, - IURLGenerator $urlGenerator, - IMailer $mailer, - IConfig $config, - IFactory $languageFactory, - Defaults $defaults) { - $this->activityManager = $activityManager; - $this->groupManager = $groupManager; - $this->userManager = $userManager; - $this->userSession = $userSession; - $this->urlGenerator = $urlGenerator; - $this->mailer = $mailer; - $this->config = $config; - $this->languageFactory = $languageFactory; - $this->defaults = $defaults; + public function __construct( + protected IActivityManager $activityManager, + protected IGroupManager $groupManager, + protected IUserManager $userManager, + protected IUserSession $userSession, + protected IURLGenerator $urlGenerator, + protected IMailer $mailer, + protected IConfig $config, + protected IFactory $languageFactory, + protected Defaults $defaults, + ) { } /** @@ -137,8 +112,8 @@ class Hooks { * @throws \BadMethodCallException */ public function onChangeEmail(IUser $user, $oldMailAddress) { - if ($oldMailAddress === $user->getEMailAddress() || - $user->getLastLogin() === 0) { + if ($oldMailAddress === $user->getEMailAddress() + || $user->getLastLogin() === 0) { // Email didn't really change or user didn't login, // so don't create activities and emails. return; @@ -149,6 +124,7 @@ class Hooks { ->setType('personal_settings') ->setAffectedUser($user->getUID()); + $instanceName = $this->defaults->getName(); $instanceUrl = $this->urlGenerator->getAbsoluteURL('/'); $language = $this->languageFactory->getUserLanguage($user); $l = $this->languageFactory->get('settings', $language); @@ -185,7 +161,7 @@ class Hooks { 'instanceUrl' => $instanceUrl, ]); - $template->setSubject($l->t('Email address for %1$s changed on %2$s', [$user->getDisplayName(), $instanceUrl])); + $template->setSubject($l->t('Email address for %1$s changed on %2$s', [$user->getDisplayName(), $instanceName])); $template->addHeader(); $template->addHeading($l->t('Email address changed for %s', [$user->getDisplayName()]), false); $template->addBodyText($text . ' ' . $l->t('If you did not request this, please contact an administrator.')); diff --git a/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php b/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php index 8bc045a38fb..a51eee1a799 100644 --- a/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php +++ b/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php @@ -21,21 +21,11 @@ use Psr\Log\LoggerInterface; * @template-implements IEventListener<\OC\Authentication\Events\AppPasswordCreatedEvent> */ class AppPasswordCreatedActivityListener implements IEventListener { - /** @var IActivityManager */ - private $activityManager; - - /** @var IUserSession */ - private $userSession; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(IActivityManager $activityManager, - IUserSession $userSession, - LoggerInterface $logger) { - $this->activityManager = $activityManager; - $this->userSession = $userSession; - $this->logger = $logger; + public function __construct( + private IActivityManager $activityManager, + private IUserSession $userSession, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { diff --git a/apps/settings/lib/Listener/GroupRemovedListener.php b/apps/settings/lib/Listener/GroupRemovedListener.php index 8375433f15a..c1b3f888ea8 100644 --- a/apps/settings/lib/Listener/GroupRemovedListener.php +++ b/apps/settings/lib/Listener/GroupRemovedListener.php @@ -14,11 +14,9 @@ use OCP\Group\Events\GroupDeletedEvent; /** @template-implements IEventListener<GroupDeletedEvent> */ class GroupRemovedListener implements IEventListener { - /** @var AuthorizedGroupService $authorizedGroupService */ - private $authorizedGroupService; - - public function __construct(AuthorizedGroupService $authorizedGroupService) { - $this->authorizedGroupService = $authorizedGroupService; + public function __construct( + private AuthorizedGroupService $authorizedGroupService, + ) { } /** diff --git a/apps/settings/lib/Listener/MailProviderListener.php b/apps/settings/lib/Listener/MailProviderListener.php new file mode 100644 index 00000000000..61446f1e6cb --- /dev/null +++ b/apps/settings/lib/Listener/MailProviderListener.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Listener; + +use OCA\Settings\AppInfo\Application; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IAppConfig; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; + +/** @template-implements IEventListener<DeclarativeSettingsGetValueEvent|DeclarativeSettingsSetValueEvent> */ +class MailProviderListener implements IEventListener { + + public function __construct( + private IAppConfig $config, + ) { + } + + public function handle(Event $event): void { + + /** @var DeclarativeSettingsGetValueEvent|DeclarativeSettingsSetValueEvent $event */ + if ($event->getApp() !== Application::APP_ID) { + return; + } + + if ($event instanceof DeclarativeSettingsGetValueEvent) { + $this->handleGetValue($event); + return; + } + + if ($event instanceof DeclarativeSettingsSetValueEvent) { + $this->handleSetValue($event); + return; + } + + } + + private function handleGetValue(DeclarativeSettingsGetValueEvent $event): void { + + if ($event->getFieldId() === 'mail_providers_enabled') { + $event->setValue((int)$this->config->getValueBool('core', 'mail_providers_enabled', true)); + } + + } + + private function handleSetValue(DeclarativeSettingsSetValueEvent $event): void { + + if ($event->getFieldId() === 'mail_providers_enabled') { + $this->config->setValueBool('core', 'mail_providers_enabled', (bool)$event->getValue()); + $event->stopPropagation(); + } + + } + +} diff --git a/apps/settings/lib/Listener/UserAddedToGroupActivityListener.php b/apps/settings/lib/Listener/UserAddedToGroupActivityListener.php index 7369f54bf37..87cd66efdbc 100644 --- a/apps/settings/lib/Listener/UserAddedToGroupActivityListener.php +++ b/apps/settings/lib/Listener/UserAddedToGroupActivityListener.php @@ -20,23 +20,11 @@ use OCP\IUserSession; /** @template-implements IEventListener<UserAddedEvent> */ class UserAddedToGroupActivityListener implements IEventListener { - /** @var Manager */ - private $groupManager; - - /** @var IManager */ - private $activityManager; - - /** @var IUserSession */ - private $userSession; - public function __construct( - Manager $groupManager, - IManager $activityManager, - IUserSession $userSession + private Manager $groupManager, + private IManager $activityManager, + private IUserSession $userSession, ) { - $this->groupManager = $groupManager; - $this->activityManager = $activityManager; - $this->userSession = $userSession; } public function handle(Event $event): void { diff --git a/apps/settings/lib/Listener/UserRemovedFromGroupActivityListener.php b/apps/settings/lib/Listener/UserRemovedFromGroupActivityListener.php index 711f78212bc..803d86d555d 100644 --- a/apps/settings/lib/Listener/UserRemovedFromGroupActivityListener.php +++ b/apps/settings/lib/Listener/UserRemovedFromGroupActivityListener.php @@ -20,23 +20,11 @@ use OCP\IUserSession; /** @template-implements IEventListener<UserRemovedEvent> */ class UserRemovedFromGroupActivityListener implements IEventListener { - /** @var Manager */ - private $groupManager; - - /** @var IManager */ - private $activityManager; - - /** @var IUserSession */ - private $userSession; - public function __construct( - Manager $groupManager, - IManager $activityManager, - IUserSession $userSession + private Manager $groupManager, + private IManager $activityManager, + private IUserSession $userSession, ) { - $this->groupManager = $groupManager; - $this->activityManager = $activityManager; - $this->userSession = $userSession; } public function handle(Event $event): void { diff --git a/apps/settings/lib/Mailer/NewUserMailHelper.php b/apps/settings/lib/Mailer/NewUserMailHelper.php index 737da8d7b9a..202495a020e 100644 --- a/apps/settings/lib/Mailer/NewUserMailHelper.php +++ b/apps/settings/lib/Mailer/NewUserMailHelper.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -18,25 +19,6 @@ use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; class NewUserMailHelper { - /** @var Defaults */ - private $themingDefaults; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IFactory */ - private $l10nFactory; - /** @var IMailer */ - private $mailer; - /** @var ISecureRandom */ - private $secureRandom; - /** @var ITimeFactory */ - private $timeFactory; - /** @var IConfig */ - private $config; - /** @var ICrypto */ - private $crypto; - /** @var string */ - private $fromAddress; - /** * @param Defaults $themingDefaults * @param IURLGenerator $urlGenerator @@ -48,24 +30,17 @@ class NewUserMailHelper { * @param ICrypto $crypto * @param string $fromAddress */ - public function __construct(Defaults $themingDefaults, - IURLGenerator $urlGenerator, - IFactory $l10nFactory, - IMailer $mailer, - ISecureRandom $secureRandom, - ITimeFactory $timeFactory, - IConfig $config, - ICrypto $crypto, - $fromAddress) { - $this->themingDefaults = $themingDefaults; - $this->urlGenerator = $urlGenerator; - $this->l10nFactory = $l10nFactory; - $this->mailer = $mailer; - $this->secureRandom = $secureRandom; - $this->timeFactory = $timeFactory; - $this->config = $config; - $this->crypto = $crypto; - $this->fromAddress = $fromAddress; + public function __construct( + private Defaults $themingDefaults, + private IURLGenerator $urlGenerator, + private IFactory $l10nFactory, + private IMailer $mailer, + private ISecureRandom $secureRandom, + private ITimeFactory $timeFactory, + private IConfig $config, + private ICrypto $crypto, + private $fromAddress, + ) { } /** diff --git a/apps/settings/lib/Middleware/SubadminMiddleware.php b/apps/settings/lib/Middleware/SubadminMiddleware.php index f985cdf278c..02d68e138da 100644 --- a/apps/settings/lib/Middleware/SubadminMiddleware.php +++ b/apps/settings/lib/Middleware/SubadminMiddleware.php @@ -1,9 +1,13 @@ <?php + +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors/** * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OCA\Settings\Middleware; use OC\AppFramework\Http; @@ -12,31 +16,29 @@ use OC\AppFramework\Utility\ControllerMethodReflector; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; +use OCP\Group\ISubAdmin; use OCP\IL10N; +use OCP\IUserSession; /** * Verifies whether an user has at least subadmin rights. * To bypass use the `@NoSubAdminRequired` annotation */ class SubadminMiddleware extends Middleware { - /** @var bool */ - protected $isSubAdmin; - /** @var ControllerMethodReflector */ - protected $reflector; - /** @var IL10N */ - private $l10n; + public function __construct( + protected ControllerMethodReflector $reflector, + protected IUserSession $userSession, + protected ISubAdmin $subAdminManager, + private IL10N $l10n, + ) { + } - /** - * @param ControllerMethodReflector $reflector - * @param bool $isSubAdmin - * @param IL10N $l10n - */ - public function __construct(ControllerMethodReflector $reflector, - $isSubAdmin, - IL10N $l10n) { - $this->reflector = $reflector; - $this->isSubAdmin = $isSubAdmin; - $this->l10n = $l10n; + private function isSubAdmin(): bool { + $userObject = $this->userSession->getUser(); + if ($userObject === null) { + return false; + } + return $this->subAdminManager->isSubAdmin($userObject); } /** @@ -47,8 +49,8 @@ class SubadminMiddleware extends Middleware { */ public function beforeController($controller, $methodName) { if (!$this->reflector->hasAnnotation('NoSubAdminRequired') && !$this->reflector->hasAnnotation('AuthorizedAdminSetting')) { - if (!$this->isSubAdmin) { - throw new NotAdminException($this->l10n->t('Logged in account must be a subadmin')); + if (!$this->isSubAdmin()) { + throw new NotAdminException($this->l10n->t('Logged in account must be a sub admin')); } } } diff --git a/apps/settings/lib/ResponseDefinitions.php b/apps/settings/lib/ResponseDefinitions.php index 0f3d6bffccf..12adefda91f 100644 --- a/apps/settings/lib/ResponseDefinitions.php +++ b/apps/settings/lib/ResponseDefinitions.php @@ -20,6 +20,7 @@ namespace OCA\Settings; * default: mixed, * options?: list<string|array{name: string, value: mixed}>, * value: string|int|float|bool|list<string>, + * sensitive?: boolean, * } * * @psalm-type SettingsDeclarativeForm = array{ diff --git a/apps/settings/lib/Search/AppSearch.php b/apps/settings/lib/Search/AppSearch.php index fbc799e6f04..19c2bce5451 100644 --- a/apps/settings/lib/Search/AppSearch.php +++ b/apps/settings/lib/Search/AppSearch.php @@ -47,8 +47,8 @@ class AppSearch implements IProvider { $result = []; foreach ($entries as $entry) { if ( - stripos($entry['name'], $term) === false && - stripos($entry['id'], $term) === false + stripos($entry['name'], $term) === false + && stripos($entry['id'], $term) === false ) { continue; } diff --git a/apps/settings/lib/Search/SectionSearch.php b/apps/settings/lib/Search/SectionSearch.php index 321534581b5..52f0c9b08db 100644 --- a/apps/settings/lib/Search/SectionSearch.php +++ b/apps/settings/lib/Search/SectionSearch.php @@ -21,26 +21,12 @@ use OCP\Settings\IManager; class SectionSearch implements IProvider { - /** @var IManager */ - protected $settingsManager; - - /** @var IGroupManager */ - protected $groupManager; - - /** @var IURLGenerator */ - protected $urlGenerator; - - /** @var IL10N */ - protected $l; - - public function __construct(IManager $settingsManager, - IGroupManager $groupManager, - IURLGenerator $urlGenerator, - IL10N $l) { - $this->settingsManager = $settingsManager; - $this->groupManager = $groupManager; - $this->urlGenerator = $urlGenerator; - $this->l = $l; + public function __construct( + protected IManager $settingsManager, + protected IGroupManager $groupManager, + protected IURLGenerator $urlGenerator, + protected IL10N $l, + ) { } /** @@ -74,9 +60,25 @@ class SectionSearch implements IProvider { public function search(IUser $user, ISearchQuery $query): SearchResult { $isAdmin = $this->groupManager->isAdmin($user->getUID()); + $personalSections = $this->settingsManager->getPersonalSections(); + foreach ($personalSections as $priority => $sections) { + $personalSections[$priority] = array_values(array_filter( + $sections, + fn (IIconSection $section) => !empty($this->settingsManager->getPersonalSettings($section->getID())), + )); + } + + $adminSections = $this->settingsManager->getAdminSections(); + foreach ($adminSections as $priority => $sections) { + $adminSections[$priority] = array_values(array_filter( + $sections, + fn (IIconSection $section) => !empty($this->settingsManager->getAllowedAdminSettings($section->getID(), $user)), + )); + } + $result = $this->searchSections( $query, - $this->settingsManager->getPersonalSections(), + $personalSections, $isAdmin ? $this->l->t('Personal') : '', 'settings.PersonalSettings.index' ); @@ -84,7 +86,7 @@ class SectionSearch implements IProvider { if ($this->groupManager->isAdmin($user->getUID())) { $result = array_merge($result, $this->searchSections( $query, - $this->settingsManager->getAdminSections(), + $adminSections, $this->l->t('Administration'), 'settings.AdminSettings.index' )); @@ -108,8 +110,8 @@ class SectionSearch implements IProvider { foreach ($sections as $priority => $sectionsByPriority) { foreach ($sectionsByPriority as $section) { if ( - stripos($section->getName(), $query->getTerm()) === false && - stripos($section->getID(), $query->getTerm()) === false + stripos($section->getName(), $query->getTerm()) === false + && stripos($section->getID(), $query->getTerm()) === false ) { continue; } diff --git a/apps/settings/lib/Search/UserSearch.php b/apps/settings/lib/Search/UserSearch.php index 316b53b7808..5326f4cefff 100644 --- a/apps/settings/lib/Search/UserSearch.php +++ b/apps/settings/lib/Search/UserSearch.php @@ -29,7 +29,7 @@ class UserSearch implements IProvider { } public function getOrder(string $route, array $routeParameters): ?int { - return $route === 'settings.Users.usersList' + return str_starts_with($route, 'settings.Users.usersList') ? 300 : null; } diff --git a/apps/settings/lib/Sections/Admin/Additional.php b/apps/settings/lib/Sections/Admin/Additional.php index 19ae2b43f95..0d83a98bbe5 100644 --- a/apps/settings/lib/Sections/Admin/Additional.php +++ b/apps/settings/lib/Sections/Admin/Additional.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class Additional implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Admin/ArtificialIntelligence.php b/apps/settings/lib/Sections/Admin/ArtificialIntelligence.php index adfcc08c4d4..2a300c260c0 100644 --- a/apps/settings/lib/Sections/Admin/ArtificialIntelligence.php +++ b/apps/settings/lib/Sections/Admin/ArtificialIntelligence.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class ArtificialIntelligence implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { @@ -34,7 +29,7 @@ class ArtificialIntelligence implements IIconSection { } public function getName(): string { - return $this->l->t('Artificial Intelligence'); + return $this->l->t('Assistant'); } public function getPriority(): int { diff --git a/apps/settings/lib/Sections/Admin/Delegation.php b/apps/settings/lib/Sections/Admin/Delegation.php index cc8aa3e3e84..0dd3b48c20b 100644 --- a/apps/settings/lib/Sections/Admin/Delegation.php +++ b/apps/settings/lib/Sections/Admin/Delegation.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,18 +12,14 @@ use OCP\IURLGenerator; use OCP\Settings\IIconSection; class Delegation implements IIconSection { - /** @var IL10N */ - private $l; - /** @var IURLGenerator */ - private $url; - /** * @param IURLGenerator $url * @param IL10N $l */ - public function __construct(IURLGenerator $url, IL10N $l) { - $this->url = $url; - $this->l = $l; + public function __construct( + private IURLGenerator $url, + private IL10N $l, + ) { } /** diff --git a/apps/settings/lib/Sections/Admin/Groupware.php b/apps/settings/lib/Sections/Admin/Groupware.php index ed790f00fe5..57d92b9cc72 100644 --- a/apps/settings/lib/Sections/Admin/Groupware.php +++ b/apps/settings/lib/Sections/Admin/Groupware.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class Groupware implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Admin/Overview.php b/apps/settings/lib/Sections/Admin/Overview.php index 518042b4380..0145a2eca93 100644 --- a/apps/settings/lib/Sections/Admin/Overview.php +++ b/apps/settings/lib/Sections/Admin/Overview.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class Overview implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Admin/Security.php b/apps/settings/lib/Sections/Admin/Security.php index 2eb1dd73715..10027be32fb 100644 --- a/apps/settings/lib/Sections/Admin/Security.php +++ b/apps/settings/lib/Sections/Admin/Security.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class Security implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Admin/Server.php b/apps/settings/lib/Sections/Admin/Server.php index 9eb3d103b74..c6a02efa4e3 100644 --- a/apps/settings/lib/Sections/Admin/Server.php +++ b/apps/settings/lib/Sections/Admin/Server.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class Server implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Admin/Sharing.php b/apps/settings/lib/Sections/Admin/Sharing.php index c15a0ff43a3..c7598bb1157 100644 --- a/apps/settings/lib/Sections/Admin/Sharing.php +++ b/apps/settings/lib/Sections/Admin/Sharing.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class Sharing implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Personal/Availability.php b/apps/settings/lib/Sections/Personal/Availability.php index a6b8878733e..e12e41ea800 100644 --- a/apps/settings/lib/Sections/Personal/Availability.php +++ b/apps/settings/lib/Sections/Personal/Availability.php @@ -15,15 +15,10 @@ use OCP\Settings\IIconSection; class Availability implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Personal/Calendar.php b/apps/settings/lib/Sections/Personal/Calendar.php index 32c99ad9c7b..602b7598f15 100644 --- a/apps/settings/lib/Sections/Personal/Calendar.php +++ b/apps/settings/lib/Sections/Personal/Calendar.php @@ -15,12 +15,10 @@ use OCP\Settings\IIconSection; class Calendar implements IIconSection { - private IL10N $l; - private IURLGenerator $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon(): string { diff --git a/apps/settings/lib/Sections/Personal/PersonalInfo.php b/apps/settings/lib/Sections/Personal/PersonalInfo.php index 9729e1ffa38..35c3e6d2926 100644 --- a/apps/settings/lib/Sections/Personal/PersonalInfo.php +++ b/apps/settings/lib/Sections/Personal/PersonalInfo.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class PersonalInfo implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon() { diff --git a/apps/settings/lib/Sections/Personal/Security.php b/apps/settings/lib/Sections/Personal/Security.php index 7b9423c5602..d7eb65724ec 100644 --- a/apps/settings/lib/Sections/Personal/Security.php +++ b/apps/settings/lib/Sections/Personal/Security.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class Security implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon() { diff --git a/apps/settings/lib/Sections/Personal/SyncClients.php b/apps/settings/lib/Sections/Personal/SyncClients.php index 5e55c6a3b6b..3f221956f0c 100644 --- a/apps/settings/lib/Sections/Personal/SyncClients.php +++ b/apps/settings/lib/Sections/Personal/SyncClients.php @@ -14,15 +14,10 @@ use OCP\Settings\IIconSection; class SyncClients implements IIconSection { - /** @var IL10N */ - private $l; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IL10N $l, + private IURLGenerator $urlGenerator, + ) { } public function getIcon() { diff --git a/apps/settings/lib/Service/AuthorizedGroupService.php b/apps/settings/lib/Service/AuthorizedGroupService.php index 9dea37b0c2e..15aca94198a 100644 --- a/apps/settings/lib/Service/AuthorizedGroupService.php +++ b/apps/settings/lib/Service/AuthorizedGroupService.php @@ -16,11 +16,9 @@ use OCP\IGroup; class AuthorizedGroupService { - /** @var AuthorizedGroupMapper $mapper */ - private $mapper; - - public function __construct(AuthorizedGroupMapper $mapper) { - $this->mapper = $mapper; + public function __construct( + private AuthorizedGroupMapper $mapper, + ) { } /** @@ -44,9 +42,9 @@ class AuthorizedGroupService { * @throws NotFoundException */ private function handleException(\Exception $e): void { - if ($e instanceof DoesNotExistException || - $e instanceof MultipleObjectsReturnedException) { - throw new NotFoundException("AuthorizedGroup not found"); + if ($e instanceof DoesNotExistException + || $e instanceof MultipleObjectsReturnedException) { + throw new NotFoundException('AuthorizedGroup not found'); } else { throw $e; } diff --git a/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php index ac4badc2434..aaec0049b20 100644 --- a/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php +++ b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php @@ -10,7 +10,7 @@ namespace OCA\Settings\Settings\Admin; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; -use OCP\IConfig; +use OCP\IAppConfig; use OCP\IL10N; use OCP\Settings\IDelegatedSettings; use OCP\SpeechToText\ISpeechToTextManager; @@ -24,10 +24,11 @@ use OCP\Translation\ITranslationProviderWithId; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; class ArtificialIntelligence implements IDelegatedSettings { public function __construct( - private IConfig $config, + private IAppConfig $appConfig, private IL10N $l, private IInitialState $initialState, private ITranslationManager $translationManager, @@ -35,6 +36,8 @@ class ArtificialIntelligence implements IDelegatedSettings { private IManager $textProcessingManager, private ContainerInterface $container, private \OCP\TextToImage\IManager $text2imageManager, + private \OCP\TaskProcessing\IManager $taskProcessingManager, + private LoggerInterface $logger, ) { } @@ -98,30 +101,80 @@ class ArtificialIntelligence implements IDelegatedSettings { ]; } + $taskProcessingProviders = []; + /** @var array<class-string<ITaskType>, string|class-string<IProvider>> $taskProcessingSettings */ + $taskProcessingSettings = []; + foreach ($this->taskProcessingManager->getProviders() as $provider) { + $taskProcessingProviders[] = [ + 'id' => $provider->getId(), + 'name' => $provider->getName(), + 'taskType' => $provider->getTaskTypeId(), + ]; + if (!isset($taskProcessingSettings[$provider->getTaskTypeId()])) { + $taskProcessingSettings[$provider->getTaskTypeId()] = $provider->getId(); + } + } + $taskProcessingTaskTypes = []; + $taskProcessingTypeSettings = []; + foreach ($this->taskProcessingManager->getAvailableTaskTypes(true) as $taskTypeId => $taskTypeDefinition) { + $taskProcessingTaskTypes[] = [ + 'id' => $taskTypeId, + 'name' => $taskTypeDefinition['name'], + 'description' => $taskTypeDefinition['description'], + ]; + $taskProcessingTypeSettings[$taskTypeId] = true; + } + + $this->initialState->provideInitialState('ai-stt-providers', $sttProviders); $this->initialState->provideInitialState('ai-translation-providers', $translationProviders); $this->initialState->provideInitialState('ai-text-processing-providers', $textProcessingProviders); $this->initialState->provideInitialState('ai-text-processing-task-types', $textProcessingTaskTypes); $this->initialState->provideInitialState('ai-text2image-providers', $text2imageProviders); + $this->initialState->provideInitialState('ai-task-processing-providers', $taskProcessingProviders); + $this->initialState->provideInitialState('ai-task-processing-task-types', $taskProcessingTaskTypes); $settings = [ 'ai.stt_provider' => count($sttProviders) > 0 ? $sttProviders[0]['class'] : null, - 'ai.textprocessing_provider_preferences' => $textProcessingSettings, 'ai.translation_provider_preferences' => $translationPreferences, + 'ai.textprocessing_provider_preferences' => $textProcessingSettings, 'ai.text2image_provider' => count($text2imageProviders) > 0 ? $text2imageProviders[0]['id'] : null, + 'ai.taskprocessing_provider_preferences' => $taskProcessingSettings, + 'ai.taskprocessing_type_preferences' => $taskProcessingTypeSettings, + 'ai.taskprocessing_guests' => false, ]; foreach ($settings as $key => $defaultValue) { $value = $defaultValue; - $json = $this->config->getAppValue('core', $key, ''); + $json = $this->appConfig->getValueString('core', $key, '', lazy: in_array($key, \OC\TaskProcessing\Manager::LAZY_CONFIG_KEYS, true)); if ($json !== '') { - $value = json_decode($json, true); - switch($key) { + try { + $value = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + $this->logger->error('Failed to get settings. JSON Error in ' . $key, ['exception' => $e]); + if ($key === 'ai.taskprocessing_type_preferences') { + $value = []; + foreach ($taskProcessingTypeSettings as $taskTypeId => $taskTypeValue) { + $value[$taskTypeId] = false; + } + $settings[$key] = $value; + } + continue; + } + + switch ($key) { + case 'ai.taskprocessing_provider_preferences': + case 'ai.taskprocessing_type_preferences': case 'ai.textprocessing_provider_preferences': // fill $value with $defaultValue values $value = array_merge($defaultValue, $value); break; case 'ai.translation_provider_preferences': - $value += array_diff($defaultValue, $value); // Add entries from $defaultValue that are not in $value to the end of $value + // Only show entries from $value (saved pref list) that are in $defaultValue (enabled providers) + // and add all providers that are enabled but not in the pref list + if (!is_array($defaultValue)) { + break; + } + $value = array_values(array_unique(array_merge(array_intersect($value, $defaultValue), $defaultValue), SORT_STRING)); break; default: break; @@ -144,8 +197,8 @@ class ArtificialIntelligence implements IDelegatedSettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ diff --git a/apps/settings/lib/Settings/Admin/Delegation.php b/apps/settings/lib/Settings/Admin/Delegation.php index e38507b4fc1..59a26d1ac04 100644 --- a/apps/settings/lib/Settings/Admin/Delegation.php +++ b/apps/settings/lib/Settings/Admin/Delegation.php @@ -17,24 +17,13 @@ use OCP\Settings\IManager; use OCP\Settings\ISettings; class Delegation implements ISettings { - private IManager $settingManager; - private IInitialState $initialStateService; - private IGroupManager $groupManager; - private AuthorizedGroupService $authorizedGroupService; - private IURLGenerator $urlGenerator; - public function __construct( - IManager $settingManager, - IInitialState $initialStateService, - IGroupManager $groupManager, - AuthorizedGroupService $authorizedGroupService, - IURLGenerator $urlGenerator + private IManager $settingManager, + private IInitialState $initialStateService, + private IGroupManager $groupManager, + private AuthorizedGroupService $authorizedGroupService, + private IURLGenerator $urlGenerator, ) { - $this->settingManager = $settingManager; - $this->initialStateService = $initialStateService; - $this->groupManager = $groupManager; - $this->authorizedGroupService = $authorizedGroupService; - $this->urlGenerator = $urlGenerator; } /** diff --git a/apps/settings/lib/Settings/Admin/Mail.php b/apps/settings/lib/Settings/Admin/Mail.php index e455f0d3bc0..8bf2342a59c 100644 --- a/apps/settings/lib/Settings/Admin/Mail.php +++ b/apps/settings/lib/Settings/Admin/Mail.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,33 +7,32 @@ namespace OCA\Settings\Settings\Admin; use OCP\AppFramework\Http\TemplateResponse; +use OCP\IBinaryFinder; use OCP\IConfig; use OCP\IL10N; +use OCP\Server; use OCP\Settings\IDelegatedSettings; class Mail implements IDelegatedSettings { - /** @var IConfig */ - private $config; - - /** @var IL10N $l */ - private $l; - /** * @param IConfig $config * @param IL10N $l */ - public function __construct(IConfig $config, IL10N $l) { - $this->config = $config; - $this->l = $l; + public function __construct( + private IConfig $config, + private IL10N $l, + ) { } /** * @return TemplateResponse */ public function getForm() { + $finder = Server::get(IBinaryFinder::class); + $parameters = [ // Mail - 'sendmail_is_available' => (bool) \OC_Helper::findBinaryPath('sendmail'), + 'sendmail_is_available' => $finder->findBinaryPath('sendmail') !== false, 'mail_domain' => $this->config->getSystemValue('mail_domain', ''), 'mail_from_address' => $this->config->getSystemValue('mail_from_address', ''), 'mail_smtpmode' => $this->config->getSystemValue('mail_smtpmode', ''), @@ -65,8 +65,8 @@ class Mail implements IDelegatedSettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ diff --git a/apps/settings/lib/Settings/Admin/MailProvider.php b/apps/settings/lib/Settings/Admin/MailProvider.php new file mode 100644 index 00000000000..c1e72378d20 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/MailProvider.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Admin; + +use OCP\IL10N; +use OCP\Settings\DeclarativeSettingsTypes; +use OCP\Settings\IDeclarativeSettingsForm; + +class MailProvider implements IDeclarativeSettingsForm { + + public function __construct( + private IL10N $l, + ) { + } + + public function getSchema(): array { + return [ + 'id' => 'mail-provider-support', + 'priority' => 10, + 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, + 'section_id' => 'server', + 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL, + 'title' => $this->l->t('Mail Providers'), + 'description' => $this->l->t('Mail provider enables sending emails directly through the user\'s personal email account. At present, this functionality is limited to calendar invitations. It requires Nextcloud Mail 4.1 and an email account in Nextcloud Mail that matches the user\'s email address in Nextcloud.'), + + 'fields' => [ + [ + 'id' => 'mail_providers_enabled', + 'title' => $this->l->t('Send emails using'), + 'type' => DeclarativeSettingsTypes::RADIO, + 'default' => 1, + 'options' => [ + [ + 'name' => $this->l->t('User\'s email account'), + 'value' => 1 + ], + [ + 'name' => $this->l->t('System email account'), + 'value' => 0 + ], + ], + ], + ], + ]; + } + +} diff --git a/apps/settings/lib/Settings/Admin/Overview.php b/apps/settings/lib/Settings/Admin/Overview.php index ac581cfb4c5..355200372f1 100644 --- a/apps/settings/lib/Settings/Admin/Overview.php +++ b/apps/settings/lib/Settings/Admin/Overview.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,18 +9,15 @@ namespace OCA\Settings\Settings\Admin; use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; use OCP\IL10N; +use OCP\ServerVersion; use OCP\Settings\IDelegatedSettings; class Overview implements IDelegatedSettings { - /** @var IConfig */ - private $config; - - /** @var IL10N $l*/ - private $l; - - public function __construct(IConfig $config, IL10N $l) { - $this->config = $config; - $this->l = $l; + public function __construct( + private ServerVersion $serverVersion, + private IConfig $config, + private IL10N $l, + ) { } /** @@ -28,6 +26,7 @@ class Overview implements IDelegatedSettings { public function getForm() { $parameters = [ 'checkForWorkingWellKnownSetup' => $this->config->getSystemValue('check_for_working_wellknown_setup', true), + 'version' => $this->serverVersion->getHumanVersion(), ]; return new TemplateResponse('settings', 'settings/admin/overview', $parameters, ''); @@ -42,8 +41,8 @@ class Overview implements IDelegatedSettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ @@ -52,7 +51,7 @@ class Overview implements IDelegatedSettings { } public function getName(): ?string { - return $this->l->t('Security & setup warnings'); + return $this->l->t('Security & setup checks'); } public function getAuthorizedAppConfig(): array { diff --git a/apps/settings/lib/Settings/Admin/Security.php b/apps/settings/lib/Settings/Admin/Security.php index 5e4366511eb..c4efdb478c7 100644 --- a/apps/settings/lib/Settings/Admin/Security.php +++ b/apps/settings/lib/Settings/Admin/Security.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -14,22 +15,16 @@ use OCP\IUserManager; use OCP\Settings\ISettings; class Security implements ISettings { - private IManager $manager; - private IUserManager $userManager; private MandatoryTwoFactor $mandatoryTwoFactor; - private IInitialState $initialState; - private IURLGenerator $urlGenerator; - public function __construct(IManager $manager, - IUserManager $userManager, + public function __construct( + private IManager $manager, + private IUserManager $userManager, MandatoryTwoFactor $mandatoryTwoFactor, - IInitialState $initialState, - IURLGenerator $urlGenerator) { - $this->manager = $manager; - $this->userManager = $userManager; + private IInitialState $initialState, + private IURLGenerator $urlGenerator, + ) { $this->mandatoryTwoFactor = $mandatoryTwoFactor; - $this->initialState = $initialState; - $this->urlGenerator = $urlGenerator; } /** @@ -67,8 +62,8 @@ class Security implements ISettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ diff --git a/apps/settings/lib/Settings/Admin/Server.php b/apps/settings/lib/Settings/Admin/Server.php index 79fcba6b92f..c0f29ce8f34 100644 --- a/apps/settings/lib/Settings/Admin/Server.php +++ b/apps/settings/lib/Settings/Admin/Server.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -41,7 +42,7 @@ class Server implements IDelegatedSettings { $cliBasedCronUser = $cliBasedCronPossible ? (posix_getpwuid($ownerConfigFile)['name'] ?? '') : ''; // Background jobs - $this->initialStateService->provideInitialState('backgroundJobsMode', $this->config->getAppValue('core', 'backgroundjobs_mode', 'ajax')); + $this->initialStateService->provideInitialState('backgroundJobsMode', $this->appConfig->getValueString('core', 'backgroundjobs_mode', 'ajax')); $this->initialStateService->provideInitialState('lastCron', $this->appConfig->getValueInt('core', 'lastcron', 0)); $this->initialStateService->provideInitialState('cronMaxAge', $this->cronMaxAge()); $this->initialStateService->provideInitialState('cronErrors', $this->config->getAppValue('core', 'cronErrors')); @@ -53,6 +54,9 @@ class Server implements IDelegatedSettings { $this->initialStateService->provideInitialState('profileEnabledGlobally', $this->profileManager->isProfileEnabled()); $this->initialStateService->provideInitialState('profileEnabledByDefault', $this->isProfileEnabledByDefault($this->config)); + // Basic settings + $this->initialStateService->provideInitialState('restrictSystemTagsCreationToAdmin', $this->appConfig->getValueBool('systemtags', 'restrict_creation_to_admin', false)); + return new TemplateResponse('settings', 'settings/admin/server', [ 'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(), ], ''); @@ -67,7 +71,7 @@ class Server implements IDelegatedSettings { $result = $query->execute(); if ($row = $result->fetch()) { - $maxAge = (int) $row['last_checked']; + $maxAge = (int)$row['last_checked']; } else { $maxAge = $this->timeFactory->getTime(); } @@ -85,8 +89,8 @@ class Server implements IDelegatedSettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ diff --git a/apps/settings/lib/Settings/Admin/Sharing.php b/apps/settings/lib/Settings/Admin/Sharing.php index df13eb9de22..ec5dcdf624d 100644 --- a/apps/settings/lib/Settings/Admin/Sharing.php +++ b/apps/settings/lib/Settings/Admin/Sharing.php @@ -1,14 +1,17 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Settings\Settings\Admin; +use OC\Core\AppInfo\ConfigLexicon; use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\Constants; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; @@ -19,6 +22,7 @@ use OCP\Util; class Sharing implements IDelegatedSettings { public function __construct( private IConfig $config, + private IAppConfig $appConfig, private IL10N $l, private IManager $shareManager, private IAppManager $appManager, @@ -46,6 +50,7 @@ class Sharing implements IDelegatedSettings { 'allowPublicUpload' => $this->getHumanBooleanConfig('core', 'shareapi_allow_public_upload', true), 'allowResharing' => $this->getHumanBooleanConfig('core', 'shareapi_allow_resharing', true), 'allowShareDialogUserEnumeration' => $this->getHumanBooleanConfig('core', 'shareapi_allow_share_dialog_user_enumeration', true), + 'allowFederationOnPublicShares' => $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES), 'restrictUserEnumerationToGroup' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_to_group'), 'restrictUserEnumerationToPhone' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_to_phone'), 'restrictUserEnumerationFullMatch' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_full_match', true), @@ -53,8 +58,8 @@ class Sharing implements IDelegatedSettings { 'restrictUserEnumerationFullMatchEmail' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_full_match_email', true), 'restrictUserEnumerationFullMatchIgnoreSecondDN' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn'), 'enforceLinksPassword' => Util::isPublicLinkPasswordRequired(false), - 'passwordExcludedGroups' => json_decode($excludedPasswordGroups) ?? [], - 'passwordExcludedGroupsFeatureEnabled' => $this->config->getSystemValueBool('sharing.allow_disabled_password_enforcement_groups', false), + 'enforceLinksPasswordExcludedGroups' => json_decode($excludedPasswordGroups) ?? [], + 'enforceLinksPasswordExcludedGroupsEnabled' => $this->config->getSystemValueBool('sharing.allow_disabled_password_enforcement_groups', false), 'onlyShareWithGroupMembers' => $this->shareManager->shareWithGroupMembersOnly(), 'onlyShareWithGroupMembersExcludeGroupList' => json_decode($onlyShareWithGroupMembersExcludeGroupList) ?? [], 'defaultExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_default_expire_date'), @@ -62,8 +67,8 @@ class Sharing implements IDelegatedSettings { 'enforceExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_enforce_expire_date'), 'excludeGroups' => $this->config->getAppValue('core', 'shareapi_exclude_groups', 'no'), 'excludeGroupsList' => json_decode($excludedGroups, true) ?? [], - 'publicShareDisclaimerText' => $this->config->getAppValue('core', 'shareapi_public_link_disclaimertext', null), - 'enableLinkPasswordByDefault' => $this->getHumanBooleanConfig('core', 'shareapi_enable_link_password_by_default'), + 'publicShareDisclaimerText' => $this->config->getAppValue('core', 'shareapi_public_link_disclaimertext'), + 'enableLinkPasswordByDefault' => $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_DEFAULT), 'defaultPermissions' => (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL), 'defaultInternalExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_default_internal_expire_date'), 'internalExpireAfterNDays' => $this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7'), @@ -71,13 +76,15 @@ class Sharing implements IDelegatedSettings { 'defaultRemoteExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_default_remote_expire_date'), 'remoteExpireAfterNDays' => $this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7'), 'enforceRemoteExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_enforce_remote_expire_date'), + 'allowCustomTokens' => $this->shareManager->allowCustomTokens(), + 'allowViewWithoutDownload' => $this->shareManager->allowViewWithoutDownload(), ]; $this->initialState->provideInitialState('sharingAppEnabled', $this->appManager->isEnabledForUser('files_sharing')); $this->initialState->provideInitialState('sharingDocumentation', $this->urlGenerator->linkToDocs('admin-sharing')); $this->initialState->provideInitialState('sharingSettings', $parameters); - \OCP\Util::addScript($this->appName, 'vue-settings-admin-sharing'); + Util::addScript($this->appName, 'vue-settings-admin-sharing'); return new TemplateResponse($this->appName, 'settings/admin/sharing', [], ''); } @@ -97,8 +104,8 @@ class Sharing implements IDelegatedSettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ diff --git a/apps/settings/lib/Settings/Admin/Users.php b/apps/settings/lib/Settings/Admin/Users.php new file mode 100644 index 00000000000..c569890a0dc --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Users.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\Settings\Admin; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IL10N; +use OCP\Settings\IDelegatedSettings; + +/** + * Empty settings class, used only for admin delegation. + */ +class Users implements IDelegatedSettings { + + public function __construct( + protected string $appName, + private IL10N $l10n, + ) { + } + + /** + * Empty template response + */ + public function getForm(): TemplateResponse { + + return new /** @template-extends TemplateResponse<\OCP\AppFramework\Http::STATUS_OK, array{}> */ class($this->appName, '') extends TemplateResponse { + public function render(): string { + return ''; + } + }; + } + + public function getSection(): ?string { + return 'admindelegation'; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority(): int { + return 0; + } + + public function getName(): string { + return $this->l10n->t('Users'); + } + + public function getAuthorizedAppConfig(): array { + return []; + } +} diff --git a/apps/settings/lib/Settings/Personal/Additional.php b/apps/settings/lib/Settings/Personal/Additional.php index 96de4f20c51..58fe08a63b7 100644 --- a/apps/settings/lib/Settings/Personal/Additional.php +++ b/apps/settings/lib/Settings/Personal/Additional.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -28,8 +29,8 @@ class Additional implements ISettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 * @since 9.1 diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php index 1c34bc52d40..9a12b18bb5e 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -11,6 +11,7 @@ namespace OCA\Settings\Settings\Personal; use OC\Profile\ProfileManager; use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\Provisioning_API\Controller\AUserDataOCSController; use OCP\Accounts\IAccount; use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountProperty; @@ -26,62 +27,28 @@ use OCP\IUser; use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Notification\IManager; +use OCP\Server; use OCP\Settings\ISettings; +use OCP\Util; class PersonalInfo implements ISettings { - /** @var IConfig */ - private $config; - - /** @var IUserManager */ - private $userManager; - - /** @var IAccountManager */ - private $accountManager; - /** @var ProfileManager */ private $profileManager; - /** @var IGroupManager */ - private $groupManager; - - /** @var IAppManager */ - private $appManager; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IL10N */ - private $l; - - /** @var IInitialState */ - private $initialStateService; - - /** @var IManager */ - private $manager; - public function __construct( - IConfig $config, - IUserManager $userManager, - IGroupManager $groupManager, - IAccountManager $accountManager, + private IConfig $config, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IAccountManager $accountManager, ProfileManager $profileManager, - IAppManager $appManager, - IFactory $l10nFactory, - IL10N $l, - IInitialState $initialStateService, - IManager $manager + private IAppManager $appManager, + private IFactory $l10nFactory, + private IL10N $l, + private IInitialState $initialStateService, + private IManager $manager, ) { - $this->config = $config; - $this->userManager = $userManager; - $this->accountManager = $accountManager; $this->profileManager = $profileManager; - $this->groupManager = $groupManager; - $this->appManager = $appManager; - $this->l10nFactory = $l10nFactory; - $this->l = $l; - $this->initialStateService = $initialStateService; - $this->manager = $manager; } public function getForm(): TemplateResponse { @@ -90,7 +57,7 @@ class PersonalInfo implements ISettings { $lookupServerUploadEnabled = false; if ($federatedFileSharingEnabled) { /** @var FederatedShareProvider $shareProvider */ - $shareProvider = \OC::$server->query(FederatedShareProvider::class); + $shareProvider = Server::get(FederatedShareProvider::class); $lookupServerUploadEnabled = $shareProvider->isLookupServerUploadEnabled(); } @@ -105,7 +72,7 @@ class PersonalInfo implements ISettings { if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) { $totalSpace = $this->l->t('Unlimited'); } else { - $totalSpace = \OC_Helper::humanFileSize($storageInfo['total']); + $totalSpace = Util::humanFileSize($storageInfo['total']); } $messageParameters = $this->getMessageParameters($account); @@ -122,7 +89,7 @@ class PersonalInfo implements ISettings { 'groups' => $this->getGroups($user), 'quota' => $storageInfo['quota'], 'totalSpace' => $totalSpace, - 'usage' => \OC_Helper::humanFileSize($storageInfo['used']), + 'usage' => Util::humanFileSize($storageInfo['used']), 'usageRelative' => round($storageInfo['relative']), 'displayName' => $this->getProperty($account, IAccountManager::PROPERTY_DISPLAYNAME), 'emailMap' => $this->getEmailMap($account), @@ -131,6 +98,7 @@ class PersonalInfo implements ISettings { 'location' => $this->getProperty($account, IAccountManager::PROPERTY_ADDRESS), 'website' => $this->getProperty($account, IAccountManager::PROPERTY_WEBSITE), 'twitter' => $this->getProperty($account, IAccountManager::PROPERTY_TWITTER), + 'bluesky' => $this->getProperty($account, IAccountManager::PROPERTY_BLUESKY), 'fediverse' => $this->getProperty($account, IAccountManager::PROPERTY_FEDIVERSE), 'languageMap' => $this->getLanguageMap($user), 'localeMap' => $this->getLocaleMap($user), @@ -141,11 +109,14 @@ class PersonalInfo implements ISettings { 'headline' => $this->getProperty($account, IAccountManager::PROPERTY_HEADLINE), 'biography' => $this->getProperty($account, IAccountManager::PROPERTY_BIOGRAPHY), 'birthdate' => $this->getProperty($account, IAccountManager::PROPERTY_BIRTHDATE), + 'firstDayOfWeek' => $this->config->getUserValue($uid, 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK), + 'pronouns' => $this->getProperty($account, IAccountManager::PROPERTY_PRONOUNS), ]; $accountParameters = [ 'avatarChangeSupported' => $user->canChangeAvatar(), 'displayNameChangeSupported' => $user->canChangeDisplayName(), + 'emailChangeSupported' => $user->canChangeEmail(), 'federationEnabled' => $federationEnabled, 'lookupServerUploadEnabled' => $lookupServerUploadEnabled, ]; @@ -195,8 +166,8 @@ class PersonalInfo implements ISettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 * @since 9.1 @@ -298,8 +269,8 @@ class PersonalInfo implements ISettings { } $uid = $user->getUID(); - $userLocaleString = $this->config->getUserValue($uid, 'core', 'locale', $this->l10nFactory->findLocale()); $userLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage()); + $userLocaleString = $this->config->getUserValue($uid, 'core', 'locale', $this->l10nFactory->findLocale($userLang)); $localeCodes = $this->l10nFactory->findAvailableLocales(); $userLocale = array_filter($localeCodes, fn ($value) => $userLocaleString === $value['code']); diff --git a/apps/settings/lib/Settings/Personal/Security/Authtokens.php b/apps/settings/lib/Settings/Personal/Security/Authtokens.php index 90f6e23cbb7..e0509b22a9c 100644 --- a/apps/settings/lib/Settings/Personal/Security/Authtokens.php +++ b/apps/settings/lib/Settings/Personal/Security/Authtokens.php @@ -22,31 +22,13 @@ use function array_map; class Authtokens implements ISettings { - /** @var IAuthTokenProvider */ - private $tokenProvider; - - /** @var ISession */ - private $session; - - /** @var IInitialState */ - private $initialState; - - /** @var string|null */ - private $uid; - - /** @var IUserSession */ - private $userSession; - - public function __construct(IAuthTokenProvider $tokenProvider, - ISession $session, - IUserSession $userSession, - IInitialState $initialState, - ?string $UserId) { - $this->tokenProvider = $tokenProvider; - $this->session = $session; - $this->initialState = $initialState; - $this->uid = $UserId; - $this->userSession = $userSession; + public function __construct( + private IAuthTokenProvider $tokenProvider, + private ISession $session, + private IUserSession $userSession, + private IInitialState $initialState, + private ?string $userId, + ) { } public function getForm(): TemplateResponse { @@ -72,7 +54,7 @@ class Authtokens implements ISettings { } private function getAppTokens(): array { - $tokens = $this->tokenProvider->getTokenByUser($this->uid); + $tokens = $this->tokenProvider->getTokenByUser($this->userId); try { $sessionId = $this->session->getId(); diff --git a/apps/settings/lib/Settings/Personal/Security/Password.php b/apps/settings/lib/Settings/Personal/Security/Password.php index 753dacf3bdb..8184dae9560 100644 --- a/apps/settings/lib/Settings/Personal/Security/Password.php +++ b/apps/settings/lib/Settings/Personal/Security/Password.php @@ -14,20 +14,14 @@ use OCP\Settings\ISettings; class Password implements ISettings { - /** @var IUserManager */ - private $userManager; - - /** @var string|null */ - private $uid; - - public function __construct(IUserManager $userManager, - ?string $UserId) { - $this->userManager = $userManager; - $this->uid = $UserId; + public function __construct( + private IUserManager $userManager, + private ?string $userId, + ) { } public function getForm(): TemplateResponse { - $user = $this->userManager->get($this->uid); + $user = $this->userManager->get($this->userId); $passwordChangeSupported = false; if ($user !== null) { $passwordChangeSupported = $user->canChangePassword(); diff --git a/apps/settings/lib/Settings/Personal/Security/TwoFactor.php b/apps/settings/lib/Settings/Personal/Security/TwoFactor.php index 63b647f7c44..0c419cb6fa7 100644 --- a/apps/settings/lib/Settings/Personal/Security/TwoFactor.php +++ b/apps/settings/lib/Settings/Personal/Security/TwoFactor.php @@ -30,25 +30,15 @@ class TwoFactor implements ISettings { /** @var MandatoryTwoFactor */ private $mandatoryTwoFactor; - /** @var IUserSession */ - private $userSession; - - /** @var string|null */ - private $uid; - - /** @var IConfig */ - private $config; - - public function __construct(ProviderLoader $providerLoader, + public function __construct( + ProviderLoader $providerLoader, MandatoryTwoFactor $mandatoryTwoFactor, - IUserSession $userSession, - IConfig $config, - ?string $UserId) { + private IUserSession $userSession, + private IConfig $config, + private ?string $userId, + ) { $this->providerLoader = $providerLoader; $this->mandatoryTwoFactor = $mandatoryTwoFactor; - $this->userSession = $userSession; - $this->uid = $UserId; - $this->config = $config; } public function getForm(): TemplateResponse { diff --git a/apps/settings/lib/Settings/Personal/Security/WebAuthn.php b/apps/settings/lib/Settings/Personal/Security/WebAuthn.php index e65004e7dd3..a6ba4e9522a 100644 --- a/apps/settings/lib/Settings/Personal/Security/WebAuthn.php +++ b/apps/settings/lib/Settings/Personal/Security/WebAuthn.php @@ -20,22 +20,16 @@ class WebAuthn implements ISettings { /** @var PublicKeyCredentialMapper */ private $mapper; - /** @var string */ - private $uid; - - /** @var IInitialStateService */ - private $initialStateService; - /** @var Manager */ private $manager; - public function __construct(PublicKeyCredentialMapper $mapper, - string $UserId, - IInitialStateService $initialStateService, - Manager $manager) { + public function __construct( + PublicKeyCredentialMapper $mapper, + private string $userId, + private IInitialStateService $initialStateService, + Manager $manager, + ) { $this->mapper = $mapper; - $this->uid = $UserId; - $this->initialStateService = $initialStateService; $this->manager = $manager; } @@ -43,11 +37,10 @@ class WebAuthn implements ISettings { $this->initialStateService->provideInitialState( Application::APP_ID, 'webauthn-devices', - $this->mapper->findAllForUid($this->uid) + $this->mapper->findAllForUid($this->userId) ); - return new TemplateResponse('settings', 'settings/personal/security/webauthn', [ - ]); + return new TemplateResponse('settings', 'settings/personal/security/webauthn'); } public function getSection(): ?string { diff --git a/apps/settings/lib/Settings/Personal/ServerDevNotice.php b/apps/settings/lib/Settings/Personal/ServerDevNotice.php index 702daf08494..c9993484abd 100644 --- a/apps/settings/lib/Settings/Personal/ServerDevNotice.php +++ b/apps/settings/lib/Settings/Personal/ServerDevNotice.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -18,36 +19,14 @@ use OCP\Util; class ServerDevNotice implements ISettings { - /** @var IRegistry */ - private $registry; - - /** @var IEventDispatcher */ - private $eventDispatcher; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IUserSession */ - private $userSession; - - /** @var IInitialState */ - private $initialState; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IRegistry $registry, - IEventDispatcher $eventDispatcher, - IRootFolder $rootFolder, - IUserSession $userSession, - IInitialState $initialState, - IURLGenerator $urlGenerator) { - $this->registry = $registry; - $this->eventDispatcher = $eventDispatcher; - $this->rootFolder = $rootFolder; - $this->userSession = $userSession; - $this->initialState = $initialState; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IRegistry $registry, + private IEventDispatcher $eventDispatcher, + private IRootFolder $rootFolder, + private IUserSession $userSession, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, + ) { } /** @@ -89,8 +68,8 @@ class ServerDevNotice implements ISettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ diff --git a/apps/settings/lib/SetupChecks/AllowedAdminRanges.php b/apps/settings/lib/SetupChecks/AllowedAdminRanges.php new file mode 100644 index 00000000000..5116676dd43 --- /dev/null +++ b/apps/settings/lib/SetupChecks/AllowedAdminRanges.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\Security\Ip\Range; +use OC\Security\Ip\RemoteAddress; +use OCP\IConfig; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class AllowedAdminRanges implements ISetupCheck { + public function __construct( + private IConfig $config, + private IL10N $l10n, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Allowed admin IP ranges'); + } + + public function run(): SetupResult { + $allowedAdminRanges = $this->config->getSystemValue(RemoteAddress::SETTING_NAME, false); + if ( + $allowedAdminRanges === false + || (is_array($allowedAdminRanges) && empty($allowedAdminRanges)) + ) { + return SetupResult::success($this->l10n->t('Admin IP filtering isn\'t applied.')); + } + + if (!is_array($allowedAdminRanges)) { + return SetupResult::error( + $this->l10n->t( + 'Configuration key "%1$s" expects an array (%2$s found). Admin IP range validation will not be applied.', + [RemoteAddress::SETTING_NAME, gettype($allowedAdminRanges)], + ) + ); + } + + $invalidRanges = array_filter($allowedAdminRanges, static fn (mixed $range): bool => !is_string($range) || !Range::isValid($range)); + if (!empty($invalidRanges)) { + return SetupResult::warning( + $this->l10n->t( + 'Configuration key "%1$s" contains invalid IP range(s): "%2$s"', + [RemoteAddress::SETTING_NAME, implode('", "', $invalidRanges)], + ), + ); + } + + return SetupResult::success($this->l10n->t('Admin IP filtering is correctly configured.')); + } +} diff --git a/apps/settings/lib/SetupChecks/CheckServerResponseTrait.php b/apps/settings/lib/SetupChecks/CheckServerResponseTrait.php deleted file mode 100644 index 734ac1ff223..00000000000 --- a/apps/settings/lib/SetupChecks/CheckServerResponseTrait.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -namespace OCA\Settings\SetupChecks; - -use Generator; -use OCP\Http\Client\IClientService; -use OCP\Http\Client\IResponse; -use OCP\IConfig; -use OCP\IL10N; -use OCP\IURLGenerator; -use Psr\Log\LoggerInterface; - -/** - * Common trait for setup checks that need to use requests to the same server and check the response - */ -trait CheckServerResponseTrait { - protected IConfig $config; - protected IURLGenerator $urlGenerator; - protected IClientService $clientService; - protected IL10N $l10n; - protected LoggerInterface $logger; - - /** - * Common helper string in case a check could not fetch any results - */ - protected function serverConfigHelp(): string { - return $this->l10n->t('To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule.'); - } - - /** - * Get all possible URLs that need to be checked for a local request test. - * This takes all `trusted_domains` and the CLI overwrite URL into account. - * - * @param string $url The relative URL to test - * @return string[] List of possible absolute URLs - */ - protected function getTestUrls(string $url): array { - $hosts = $this->config->getSystemValue('trusted_domains', []); - $cliUrl = $this->config->getSystemValue('overwrite.cli.url', ''); - if ($cliUrl !== '') { - $hosts[] = $cliUrl; - } - - $testUrls = array_merge( - [$this->urlGenerator->getAbsoluteURL($url)], - array_map(fn (string $host): string => $host . $url, $hosts), - ); - - return $testUrls; - } - - /** - * Run a HTTP request to check header - * @param string $method The HTTP method to use - * @param string $url The relative URL to check - * @param array{ignoreSSL?: bool, httpErrors?: bool, options?: array} $options Additional options, like - * [ - * // Ignore invalid SSL certificates (e.g. self signed) - * 'ignoreSSL' => true, - * // Ignore requests with HTTP errors (will not yield if request has a 4xx or 5xx response) - * 'httpErrors' => true, - * ] - * - * @return Generator<int, IResponse> - */ - protected function runRequest(string $method, string $url, array $options = []): Generator { - $options = array_merge(['ignoreSSL' => true, 'httpErrors' => true], $options); - - $client = $this->clientService->newClient(); - $requestOptions = $this->getRequestOptions($options['ignoreSSL'], $options['httpErrors']); - $requestOptions = array_merge($requestOptions, $options['options'] ?? []); - - foreach ($this->getTestUrls($url) as $testURL) { - try { - yield $client->request($method, $testURL, $requestOptions); - } catch (\Throwable $e) { - $this->logger->debug('Can not connect to local server for running setup checks', ['exception' => $e, 'url' => $testURL]); - } - } - } - - /** - * Run a HEAD request to check header - * @param string $url The relative URL to check - * @param bool $ignoreSSL Ignore SSL certificates - * @param bool $httpErrors Ignore requests with HTTP errors (will not yield if request has a 4xx or 5xx response) - * @return Generator<int, IResponse> - */ - protected function runHEAD(string $url, bool $ignoreSSL = true, bool $httpErrors = true): Generator { - return $this->runRequest('HEAD', $url, ['ignoreSSL' => $ignoreSSL, 'httpErrors' => $httpErrors]); - } - - protected function getRequestOptions(bool $ignoreSSL, bool $httpErrors): array { - $requestOptions = [ - 'connect_timeout' => 10, - 'http_errors' => $httpErrors, - 'nextcloud' => [ - 'allow_local_address' => true, - ], - ]; - if ($ignoreSSL) { - $requestOptions['verify'] = false; - } - return $requestOptions; - } -} diff --git a/apps/settings/lib/SetupChecks/CodeIntegrity.php b/apps/settings/lib/SetupChecks/CodeIntegrity.php index dc29c4da306..2b4271fae9c 100644 --- a/apps/settings/lib/SetupChecks/CodeIntegrity.php +++ b/apps/settings/lib/SetupChecks/CodeIntegrity.php @@ -33,7 +33,14 @@ class CodeIntegrity implements ISetupCheck { public function run(): SetupResult { if (!$this->checker->isCodeCheckEnforced()) { return SetupResult::info($this->l10n->t('Integrity checker has been disabled. Integrity cannot be verified.')); - } elseif ($this->checker->hasPassedCheck()) { + } + + // If there are no results we need to run the verification + if ($this->checker->getResults() === null) { + $this->checker->runInstanceVerification(); + } + + if ($this->checker->hasPassedCheck()) { return SetupResult::success($this->l10n->t('No altered files')); } else { return SetupResult::error( diff --git a/apps/settings/lib/SetupChecks/CronErrors.php b/apps/settings/lib/SetupChecks/CronErrors.php index 9a5c5b8af71..dc625b04477 100644 --- a/apps/settings/lib/SetupChecks/CronErrors.php +++ b/apps/settings/lib/SetupChecks/CronErrors.php @@ -35,7 +35,7 @@ class CronErrors implements ISetupCheck { return SetupResult::error( $this->l10n->t( "It was not possible to execute the cron job via CLI. The following technical errors have appeared:\n%s", - implode("\n", array_map(fn (array $error) => '- '.$error['error'].' '.$error['hint'], $errors)) + implode("\n", array_map(fn (array $error) => '- ' . $error['error'] . ' ' . $error['hint'], $errors)) ) ); } else { diff --git a/apps/settings/lib/SetupChecks/DataDirectoryProtected.php b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php index 5afdfaaddd5..e572c345079 100644 --- a/apps/settings/lib/SetupChecks/DataDirectoryProtected.php +++ b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -40,15 +41,22 @@ class DataDirectoryProtected implements ISetupCheck { } public function run(): SetupResult { - $datadir = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValue('datadirectory', '')); - - $dataUrl = $this->urlGenerator->getWebroot() . '/' . $datadir . '/.ocdata'; + $dataDir = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValueString('datadirectory', '')); + $dataUrl = $this->urlGenerator->linkTo('', $dataDir . '/.ncdata'); $noResponse = true; - foreach ($this->runHEAD($dataUrl, httpErrors:false) as $response) { + foreach ($this->runRequest('GET', $dataUrl, [ 'httpErrors' => false ]) as $response) { $noResponse = false; - if ($response->getStatusCode() === 200) { - return SetupResult::error($this->l10n->t('Your data directory and files are probably accessible from the internet. The .htaccess file is not working. It is strongly recommended that you configure your web server so that the data directory is no longer accessible, or move the data directory outside the web server document root.')); + if ($response->getStatusCode() < 400) { + // Read the response body + $body = $response->getBody(); + if (is_resource($body)) { + $body = stream_get_contents($body, 64); + } + + if (str_contains($body, '# Nextcloud data directory')) { + return SetupResult::error($this->l10n->t('Your data directory and files are probably accessible from the internet. The .htaccess file is not working. It is strongly recommended that you configure your web server so that the data directory is no longer accessible, or move the data directory outside the web server document root.')); + } } else { $this->logger->debug('[expected] Could not access data directory from outside.', ['url' => $dataUrl]); } @@ -58,6 +66,6 @@ class DataDirectoryProtected implements ISetupCheck { return SetupResult::warning($this->l10n->t('Could not check that the data directory is protected. Please check manually that your server does not allow access to the data directory.') . "\n" . $this->serverConfigHelp()); } return SetupResult::success(); - + } } diff --git a/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php b/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php index b004c5ada35..ec004f73021 100644 --- a/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingColumns.php @@ -62,10 +62,10 @@ class DatabaseHasMissingColumns implements ISetupCheck { } else { $list = ''; foreach ($missingColumns as $missingColumn) { - $list .= "\n".$this->l10n->t('Missing optional column "%s" in table "%s".', [$missingColumn['columnName'], $missingColumn['tableName']]); + $list .= "\n" . $this->l10n->t('Missing optional column "%s" in table "%s".', [$missingColumn['columnName'], $missingColumn['tableName']]); } return SetupResult::warning( - $this->l10n->t('The database is missing some optional columns. Due to the fact that adding columns on big tables could take some time they were not added automatically when they can be optional. By running "occ db:add-missing-columns" those missing columns could be added manually while the instance keeps running. Once the columns are added some features might improve responsiveness or usability.').$list + $this->l10n->t('The database is missing some optional columns. Due to the fact that adding columns on big tables could take some time they were not added automatically when they can be optional. By running "occ db:add-missing-columns" those missing columns could be added manually while the instance keeps running. Once the columns are added some features might improve responsiveness or usability.') . $list ); } } diff --git a/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php b/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php index 49e3106b017..97e80c2aaa9 100644 --- a/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingIndices.php @@ -81,11 +81,11 @@ class DatabaseHasMissingIndices implements ISetupCheck { $processed++; $list .= "\n " . $this->l10n->t('"%s" in table "%s"', [$missingIndex['indexName'], $missingIndex['tableName']]); if (count($missingIndices) > $processed) { - $list .= ", "; + $list .= ', '; } } return SetupResult::warning( - $this->l10n->t('Detected some missing optional indices. Occasionally new indices are added (by Nextcloud or installed applications) to improve database performance. Adding indices can sometimes take awhile and temporarily hurt performance so this is not done automatically during upgrades. Once the indices are added, queries to those tables should be faster. Use the command `occ db:add-missing-indices` to add them. ') . $list . '.', + $this->l10n->t('Detected some missing optional indices. Occasionally new indices are added (by Nextcloud or installed applications) to improve database performance. Adding indices can sometimes take awhile and temporarily hurt performance so this is not done automatically during upgrades. Once the indices are added, queries to those tables should be faster. Use the command `occ db:add-missing-indices` to add them.') . "\n" . $list, $this->urlGenerator->linkToDocs('admin-long-running-migration-steps') ); } diff --git a/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php b/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php index 8e2a0d18e41..03810ca8faf 100644 --- a/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php +++ b/apps/settings/lib/SetupChecks/DatabaseHasMissingPrimaryKeys.php @@ -62,10 +62,10 @@ class DatabaseHasMissingPrimaryKeys implements ISetupCheck { } else { $list = ''; foreach ($missingPrimaryKeys as $missingPrimaryKey) { - $list .= "\n".$this->l10n->t('Missing primary key on table "%s".', [$missingPrimaryKey['tableName']]); + $list .= "\n" . $this->l10n->t('Missing primary key on table "%s".', [$missingPrimaryKey['tableName']]); } return SetupResult::warning( - $this->l10n->t('The database is missing some primary keys. Due to the fact that adding primary keys on big tables could take some time they were not added automatically. By running "occ db:add-missing-primary-keys" those missing primary keys could be added manually while the instance keeps running.').$list + $this->l10n->t('The database is missing some primary keys. Due to the fact that adding primary keys on big tables could take some time they were not added automatically. By running "occ db:add-missing-primary-keys" those missing primary keys could be added manually while the instance keeps running.') . $list ); } } diff --git a/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php b/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php index dc9f00e8448..bb9794c1e03 100644 --- a/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php +++ b/apps/settings/lib/SetupChecks/DatabasePendingBigIntConversions.php @@ -74,7 +74,7 @@ class DatabasePendingBigIntConversions implements ISetupCheck { } $list .= "\n"; return SetupResult::info( - $this->l10n->t('Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running "occ db:convert-filecache-bigint" those pending changes could be applied manually. This operation needs to be made while the instance is offline.').$list, + $this->l10n->t('Some columns in the database are missing a conversion to big int. Due to the fact that changing column types on big tables could take some time they were not changed automatically. By running "occ db:convert-filecache-bigint" those pending changes could be applied manually. This operation needs to be made while the instance is offline.') . $list, $this->urlGenerator->linkToDocs('admin-bigint-conversion') ); } diff --git a/apps/settings/lib/SetupChecks/EmailTestSuccessful.php b/apps/settings/lib/SetupChecks/EmailTestSuccessful.php index a8f527f9dba..8cad8e82156 100644 --- a/apps/settings/lib/SetupChecks/EmailTestSuccessful.php +++ b/apps/settings/lib/SetupChecks/EmailTestSuccessful.php @@ -46,7 +46,9 @@ class EmailTestSuccessful implements ISetupCheck { } public function run(): SetupResult { - if ($this->wasEmailTestSuccessful()) { + if ($this->config->getSystemValueString('mail_smtpmode', 'smtp') === 'null') { + return SetupResult::success($this->l10n->t('Mail delivery is disabled by instance config "%s".', ['mail_smtpmode'])); + } elseif ($this->wasEmailTestSuccessful()) { return SetupResult::success($this->l10n->t('Email test was successfully sent')); } else { // If setup check could link to settings pages, this one should link to OC.generateUrl('/settings/admin') diff --git a/apps/settings/lib/SetupChecks/FileLocking.php b/apps/settings/lib/SetupChecks/FileLocking.php index ffd7714ddc1..f683ee05f03 100644 --- a/apps/settings/lib/SetupChecks/FileLocking.php +++ b/apps/settings/lib/SetupChecks/FileLocking.php @@ -26,7 +26,7 @@ class FileLocking implements ISetupCheck { } public function getName(): string { - return $this->l10n->t('File locking'); + return $this->l10n->t('Transactional File Locking'); } public function getCategory(): string { @@ -43,8 +43,8 @@ class FileLocking implements ISetupCheck { public function run(): SetupResult { if (!$this->hasWorkingFileLocking()) { - return SetupResult::warning( - $this->l10n->t('Transactional file locking is disabled, this might lead to issues with race conditions. Enable "filelocking.enabled" in config.php to avoid these problems.'), + return SetupResult::error( + $this->l10n->t('Transactional File Locking is disabled. This is not a a supported configuraton. It may lead to difficult to isolate problems including file corruption. Please remove the `\'filelocking.enabled\' => false` configuration entry from your `config.php` to avoid these problems.'), $this->urlGenerator->linkToDocs('admin-transactional-locking') ); } diff --git a/apps/settings/lib/SetupChecks/InternetConnectivity.php b/apps/settings/lib/SetupChecks/InternetConnectivity.php index 3a0af06e71b..18f2af63b8d 100644 --- a/apps/settings/lib/SetupChecks/InternetConnectivity.php +++ b/apps/settings/lib/SetupChecks/InternetConnectivity.php @@ -41,11 +41,12 @@ class InternetConnectivity implements ISetupCheck { } $siteArray = $this->config->getSystemValue('connectivity_check_domains', [ - 'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org' + 'https://www.nextcloud.com', 'https://www.startpage.com', 'https://www.eff.org', 'https://www.edri.org' ]); foreach ($siteArray as $site) { if ($this->isSiteReachable($site)) { + // successful as soon as one connection succeeds return SetupResult::success(); } } @@ -55,19 +56,18 @@ class InternetConnectivity implements ISetupCheck { /** * Checks if the Nextcloud server can connect to a specific URL * @param string $site site domain or full URL with http/https protocol + * @return bool success/failure */ private function isSiteReachable(string $site): bool { + // if there is no protocol specified, test http:// first then, if necessary, https:// + if (preg_match('/^https?:\/\//', $site) !== 1) { + $httpSite = 'http://' . $site . '/'; + $httpsSite = 'https://' . $site . '/'; + return $this->isSiteReachable($httpSite) || $this->isSiteReachable($httpsSite); + } try { $client = $this->clientService->newClient(); - // if there is no protocol, test http:// AND https:// - if (preg_match('/^https?:\/\//', $site) !== 1) { - $httpSite = 'http://' . $site . '/'; - $client->get($httpSite); - $httpsSite = 'https://' . $site . '/'; - $client->get($httpsSite); - } else { - $client->get($site); - } + $client->get($site); } catch (\Exception $e) { $this->logger->error('Cannot connect to: ' . $site, [ 'app' => 'internet_connection_check', diff --git a/apps/settings/lib/SetupChecks/JavaScriptModules.php b/apps/settings/lib/SetupChecks/JavaScriptModules.php index ae19eacec7b..72f58405811 100644 --- a/apps/settings/lib/SetupChecks/JavaScriptModules.php +++ b/apps/settings/lib/SetupChecks/JavaScriptModules.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -43,7 +44,7 @@ class JavaScriptModules implements ISetupCheck { $testFile = $this->urlGenerator->linkTo('settings', 'js/esm-test.mjs'); $noResponse = true; - foreach ($this->runHEAD($testFile) as $response) { + foreach ($this->runRequest('HEAD', $testFile) as $response) { $noResponse = false; if (preg_match('/(text|application)\/javascript/i', $response->getHeader('Content-Type'))) { return SetupResult::success(); @@ -54,6 +55,6 @@ class JavaScriptModules implements ISetupCheck { return SetupResult::warning($this->l10n->t('Unable to run check for JavaScript support. Please remedy or confirm manually if your webserver serves `.mjs` files using the JavaScript MIME type.') . "\n" . $this->serverConfigHelp()); } return SetupResult::error($this->l10n->t('Your webserver does not serve `.mjs` files using the JavaScript MIME type. This will break some apps by preventing browsers from executing the JavaScript files. You should configure your webserver to serve `.mjs` files with either the `text/javascript` or `application/javascript` MIME type.')); - + } } diff --git a/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php b/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php index 85cbe872339..dcfc40192b9 100644 --- a/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php +++ b/apps/settings/lib/SetupChecks/JavaScriptSourceMaps.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -42,7 +43,7 @@ class JavaScriptSourceMaps implements ISetupCheck { public function run(): SetupResult { $testFile = $this->urlGenerator->linkTo('settings', 'js/map-test.js.map'); - foreach ($this->runHEAD($testFile) as $response) { + foreach ($this->runRequest('HEAD', $testFile) as $response) { return SetupResult::success(); } diff --git a/apps/settings/lib/SetupChecks/LoggingLevel.php b/apps/settings/lib/SetupChecks/LoggingLevel.php new file mode 100644 index 00000000000..b9e1dbe700d --- /dev/null +++ b/apps/settings/lib/SetupChecks/LoggingLevel.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class LoggingLevel implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('Logging level'); + } + + public function getCategory(): string { + return 'system'; + } + + public function run(): SetupResult { + $configLogLevel = $this->config->getSystemValue('loglevel', ILogger::WARN); + if (!is_int($configLogLevel) + || $configLogLevel < ILogger::DEBUG + || $configLogLevel > ILogger::FATAL + ) { + return SetupResult::error( + $this->l10n->t('The %1$s configuration option must be a valid integer value.', ['`loglevel`']), + $this->urlGenerator->linkToDocs('admin-logging'), + ); + } + + if ($configLogLevel === ILogger::DEBUG) { + return SetupResult::warning( + $this->l10n->t('The logging level is set to debug level. Use debug level only when you have a problem to diagnose, and then reset your log level to a less-verbose level as it outputs a lot of information, and can affect your server performance.'), + $this->urlGenerator->linkToDocs('admin-logging'), + ); + } + + return SetupResult::success($this->l10n->t('Logging level configured correctly.')); + } +} diff --git a/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php b/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php index 48e5fa2dc49..ca8df039b1e 100644 --- a/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php +++ b/apps/settings/lib/SetupChecks/MaintenanceWindowStart.php @@ -40,7 +40,7 @@ class MaintenanceWindowStart implements ISetupCheck { ); } - $startValue = (int) $configValue; + $startValue = (int)$configValue; $endValue = ($startValue + 6) % 24; return SetupResult::success( str_replace( diff --git a/apps/settings/lib/SetupChecks/MemcacheConfigured.php b/apps/settings/lib/SetupChecks/MemcacheConfigured.php index 03cdc91cb5f..e3601d428bb 100644 --- a/apps/settings/lib/SetupChecks/MemcacheConfigured.php +++ b/apps/settings/lib/SetupChecks/MemcacheConfigured.php @@ -8,6 +8,8 @@ declare(strict_types=1); */ namespace OCA\Settings\SetupChecks; +use OC\Memcache\Memcached; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; @@ -19,6 +21,7 @@ class MemcacheConfigured implements ISetupCheck { private IL10N $l10n, private IConfig $config, private IURLGenerator $urlGenerator, + private ICacheFactory $cacheFactory, ) { } @@ -35,7 +38,7 @@ class MemcacheConfigured implements ISetupCheck { $memcacheLockingClass = $this->config->getSystemValue('memcache.locking', null); $memcacheLocalClass = $this->config->getSystemValue('memcache.local', null); $caches = array_filter([$memcacheDistributedClass,$memcacheLockingClass,$memcacheLocalClass]); - if (in_array(\OC\Memcache\Memcached::class, array_map(fn (string $class) => ltrim($class, '\\'), $caches))) { + if (in_array(Memcached::class, array_map(fn (string $class) => ltrim($class, '\\'), $caches))) { // wrong PHP module is installed if (extension_loaded('memcache') && !extension_loaded('memcached')) { return SetupResult::warning( @@ -55,6 +58,41 @@ class MemcacheConfigured implements ISetupCheck { $this->urlGenerator->linkToDocs('admin-performance') ); } + + if ($this->cacheFactory->isLocalCacheAvailable()) { + $random = bin2hex(random_bytes(64)); + $local = $this->cacheFactory->createLocal('setupcheck.local'); + try { + $local->set('test', $random); + $local2 = $this->cacheFactory->createLocal('setupcheck.local'); + $actual = $local2->get('test'); + $local->remove('test'); + } catch (\Throwable) { + $actual = null; + } + + if ($actual !== $random) { + return SetupResult::error($this->l10n->t('Failed to write and read a value from local cache.')); + } + } + + if ($this->cacheFactory->isAvailable()) { + $random = bin2hex(random_bytes(64)); + $distributed = $this->cacheFactory->createDistributed('setupcheck'); + try { + $distributed->set('test', $random); + $distributed2 = $this->cacheFactory->createDistributed('setupcheck'); + $actual = $distributed2->get('test'); + $distributed->remove('test'); + } catch (\Throwable) { + $actual = null; + } + + if ($actual !== $random) { + return SetupResult::error($this->l10n->t('Failed to write and read a value from distributed cache.')); + } + } + return SetupResult::success($this->l10n->t('Configured')); } } diff --git a/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php b/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php new file mode 100644 index 00000000000..cf237f68670 --- /dev/null +++ b/apps/settings/lib/SetupChecks/MimeTypeMigrationAvailable.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use OC\Repair\RepairMimeTypes; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class MimeTypeMigrationAvailable implements ISetupCheck { + + public function __construct( + private RepairMimeTypes $repairMimeTypes, + private IL10N $l10n, + ) { + } + + public function getCategory(): string { + return 'system'; + } + + public function getName(): string { + return $this->l10n->t('Mimetype migrations available'); + } + + public function run(): SetupResult { + if ($this->repairMimeTypes->migrationsAvailable()) { + return SetupResult::warning( + $this->l10n->t('One or more mimetype migrations are available. Occasionally new mimetypes are added to better handle certain file types. Migrating the mimetypes take a long time on larger instances so this is not done automatically during upgrades. Use the command `occ maintenance:repair --include-expensive` to perform the migrations.'), + ); + } else { + return SetupResult::success('None'); + } + } +} diff --git a/apps/settings/lib/SetupChecks/MysqlRowFormat.php b/apps/settings/lib/SetupChecks/MysqlRowFormat.php new file mode 100644 index 00000000000..3c27b73db89 --- /dev/null +++ b/apps/settings/lib/SetupChecks/MysqlRowFormat.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\SetupChecks; + +use Doctrine\DBAL\Platforms\MySQLPlatform; +use OC\DB\Connection; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class MysqlRowFormat implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IConfig $config, + private Connection $connection, + private IURLGenerator $urlGenerator, + ) { + } + + public function getName(): string { + return $this->l10n->t('MySQL row format'); + } + + public function getCategory(): string { + return 'database'; + } + + public function run(): SetupResult { + if (!$this->connection->getDatabasePlatform() instanceof MySQLPlatform) { + return SetupResult::success($this->l10n->t('You are not using MySQL')); + } + + $wrongRowFormatTables = $this->getRowNotDynamicTables(); + if (empty($wrongRowFormatTables)) { + return SetupResult::success($this->l10n->t('None of your tables use ROW_FORMAT=Compressed')); + } + + return SetupResult::warning( + $this->l10n->t( + 'Incorrect row format found in your database. ROW_FORMAT=Dynamic offers the best database performances for Nextcloud. Please update row format on the following list: %s.', + [implode(', ', $wrongRowFormatTables)], + ), + 'https://dev.mysql.com/doc/refman/en/innodb-row-format.html', + ); + } + + /** + * @return string[] + */ + private function getRowNotDynamicTables(): array { + $sql = "SELECT table_name + FROM information_schema.tables + WHERE table_schema = ? + AND table_name LIKE '*PREFIX*%' + AND row_format != 'Dynamic';"; + + return $this->connection->executeQuery( + $sql, + [$this->config->getSystemValueString('dbname')], + )->fetchFirstColumn(); + } +} diff --git a/apps/settings/lib/SetupChecks/OcxProviders.php b/apps/settings/lib/SetupChecks/OcxProviders.php index ecb8ecd6609..c53e8087bd9 100644 --- a/apps/settings/lib/SetupChecks/OcxProviders.php +++ b/apps/settings/lib/SetupChecks/OcxProviders.php @@ -12,6 +12,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -51,7 +52,7 @@ class OcxProviders implements ISetupCheck { ]; foreach ($providers as $provider) { - foreach ($this->runRequest('HEAD', $this->urlGenerator->getWebroot() . $provider, ['httpErrors' => false]) as $response) { + foreach ($this->runRequest('HEAD', $provider, ['httpErrors' => false]) as $response) { $testedProviders[$provider] = true; if ($response->getStatusCode() === 200) { $workingProviders[] = $provider; @@ -75,7 +76,7 @@ class OcxProviders implements ISetupCheck { $this->l10n->t('Your web server is not properly set up to resolve %1$s. This is most likely related to a web server configuration that was not updated to deliver this folder directly. Please compare your configuration against the shipped rewrite rules in ".htaccess" for Apache or the provided one in the documentation for Nginx. -On Nginx those are typically the lines starting with "location ~" that need an update.', [join(', ', array_map(fn ($s) => '"'.$s.'"', $missingProviders))]), +On Nginx those are typically the lines starting with "location ~" that need an update.', [join(', ', array_map(fn ($s) => '"' . $s . '"', $missingProviders))]), $this->urlGenerator->linkToDocs('admin-nginx'), ); } diff --git a/apps/settings/lib/SetupChecks/PhpApcuConfig.php b/apps/settings/lib/SetupChecks/PhpApcuConfig.php new file mode 100644 index 00000000000..c91a8cefec1 --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpApcuConfig.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OC\Memcache\APCu; +use OCP\IConfig; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; + +class PhpApcuConfig implements ISetupCheck { + public const USAGE_RATE_WARNING = 90; + public const AGE_WARNING = 3600 * 8; + + public function __construct( + private IL10N $l10n, + private IConfig $config, + ) { + } + + public function getCategory(): string { + return 'php'; + } + + public function getName(): string { + return $this->l10n->t('PHP APCu configuration'); + } + + public function run(): SetupResult { + $localIsApcu = ltrim($this->config->getSystemValueString('memcache.local'), '\\') === APCu::class; + $distributedIsApcu = ltrim($this->config->getSystemValueString('memcache.distributed'), '\\') === APCu::class; + if (!$localIsApcu && !$distributedIsApcu) { + return SetupResult::success(); + } + + if (!APCu::isAvailable()) { + return SetupResult::success(); + } + + $cache = apcu_cache_info(true); + $mem = apcu_sma_info(true); + if ($cache === false || $mem === false) { + return SetupResult::success(); + } + + $expunges = $cache['expunges']; + $memSize = $mem['num_seg'] * $mem['seg_size']; + $memAvailable = $mem['avail_mem']; + $memUsed = $memSize - $memAvailable; + $usageRate = round($memUsed / $memSize * 100, 0); + $elapsed = max(time() - $cache['start_time'], 1); + + if ($expunges > 0 && $elapsed < self::AGE_WARNING) { + return SetupResult::warning($this->l10n->t('Your APCu cache has been running full, consider increasing the apc.shm_size php setting.')); + } + + if ($usageRate > self::USAGE_RATE_WARNING) { + return SetupResult::warning($this->l10n->t('Your APCu cache is almost full at %s%%, consider increasing the apc.shm_size php setting.', [$usageRate])); + } + + return SetupResult::success(); + } +} diff --git a/apps/settings/lib/SetupChecks/PhpMaxFileSize.php b/apps/settings/lib/SetupChecks/PhpMaxFileSize.php new file mode 100644 index 00000000000..d81cbe6d45c --- /dev/null +++ b/apps/settings/lib/SetupChecks/PhpMaxFileSize.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use bantu\IniGetWrapper\IniGetWrapper; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; +use OCP\Util; + +class PhpMaxFileSize implements ISetupCheck { + public function __construct( + private IL10N $l10n, + private IURLGenerator $urlGenerator, + private IniGetWrapper $iniGetWrapper, + ) { + } + + public function getCategory(): string { + return 'php'; + } + + public function getName(): string { + return $this->l10n->t('PHP file size upload limit'); + } + + public function run(): SetupResult { + $upload_max_filesize = (string)$this->iniGetWrapper->getString('upload_max_filesize'); + $post_max_size = (string)$this->iniGetWrapper->getString('post_max_size'); + $max_input_time = (int)$this->iniGetWrapper->getString('max_input_time'); + $max_execution_time = (int)$this->iniGetWrapper->getString('max_execution_time'); + + $warnings = []; + $recommendedSize = 16 * 1024 * 1024 * 1024; + $recommendedTime = 3600; + + // Check if the PHP upload limit is too low + if (Util::computerFileSize($upload_max_filesize) < $recommendedSize) { + $warnings[] = $this->l10n->t('The PHP upload_max_filesize is too low. A size of at least %1$s is recommended. Current value: %2$s.', [ + Util::humanFileSize($recommendedSize), + $upload_max_filesize, + ]); + } + if (Util::computerFileSize($post_max_size) < $recommendedSize) { + $warnings[] = $this->l10n->t('The PHP post_max_size is too low. A size of at least %1$s is recommended. Current value: %2$s.', [ + Util::humanFileSize($recommendedSize), + $post_max_size, + ]); + } + + // Check if the PHP execution time is too low + if ($max_input_time < $recommendedTime && $max_input_time !== -1) { + $warnings[] = $this->l10n->t('The PHP max_input_time is too low. A time of at least %1$s is recommended. Current value: %2$s.', [ + $recommendedTime, + $max_input_time, + ]); + } + + if ($max_execution_time < $recommendedTime && $max_execution_time !== -1) { + $warnings[] = $this->l10n->t('The PHP max_execution_time is too low. A time of at least %1$s is recommended. Current value: %2$s.', [ + $recommendedTime, + $max_execution_time, + ]); + } + + if (!empty($warnings)) { + return SetupResult::warning(join(' ', $warnings), $this->urlGenerator->linkToDocs('admin-big-file-upload')); + } + + return SetupResult::success(); + } +} diff --git a/apps/settings/lib/SetupChecks/PhpMemoryLimit.php b/apps/settings/lib/SetupChecks/PhpMemoryLimit.php index 86bb6defbec..7b693169f10 100644 --- a/apps/settings/lib/SetupChecks/PhpMemoryLimit.php +++ b/apps/settings/lib/SetupChecks/PhpMemoryLimit.php @@ -34,7 +34,7 @@ class PhpMemoryLimit implements ISetupCheck { if ($this->memoryInfo->isMemoryLimitSufficient()) { return SetupResult::success(Util::humanFileSize($this->memoryInfo->getMemoryLimit())); } else { - return SetupResult::error($this->l10n->t('The PHP memory limit is below the recommended value of %s.', Util::humanFileSize(MemoryInfo::RECOMMENDED_MEMORY_LIMIT))); + return SetupResult::error($this->l10n->t('The PHP memory limit is below the recommended value of %s. Some features or apps - including the Updater - may not function properly.', Util::humanFileSize(MemoryInfo::RECOMMENDED_MEMORY_LIMIT))); } } } diff --git a/apps/settings/lib/SetupChecks/PhpModules.php b/apps/settings/lib/SetupChecks/PhpModules.php index 6d4246ffdae..b0b4f106f4a 100644 --- a/apps/settings/lib/SetupChecks/PhpModules.php +++ b/apps/settings/lib/SetupChecks/PhpModules.php @@ -32,7 +32,6 @@ class PhpModules implements ISetupCheck { 'zlib', ]; protected const RECOMMENDED_MODULES = [ - 'bcmath', 'exif', 'gmp', 'intl', @@ -58,8 +57,7 @@ class PhpModules implements ISetupCheck { return match($module) { 'intl' => $this->l10n->t('increases language translation performance and fixes sorting of non-ASCII characters'), 'sodium' => $this->l10n->t('for Argon2 for password hashing'), - 'bcmath' => $this->l10n->t('for WebAuthn passwordless login'), - 'gmp' => $this->l10n->t('for WebAuthn passwordless login, and SFTP storage'), + 'gmp' => $this->l10n->t('required for SFTP storage and recommended for WebAuthn performance'), 'exif' => $this->l10n->t('for picture rotation in server and metadata extraction in the Photos app'), default => '', }; @@ -77,7 +75,7 @@ class PhpModules implements ISetupCheck { $moduleList = implode( "\n", array_map( - fn (string $module) => '- '.$module.' '.$this->getRecommendedModuleDescription($module), + fn (string $module) => '- ' . $module . ' ' . $this->getRecommendedModuleDescription($module), $missingRecommendedModules ) ); diff --git a/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php b/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php index e3cf4659dbe..83b7be1c390 100644 --- a/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php +++ b/apps/settings/lib/SetupChecks/PhpOpcacheSetup.php @@ -57,7 +57,7 @@ class PhpOpcacheSetup implements ISetupCheck { } elseif ($this->iniGetWrapper->getBool('opcache.file_cache_only')) { $recommendations[] = $this->l10n->t('The shared memory based OPcache is disabled. For better performance, it is recommended to apply "opcache.file_cache_only=0" to your PHP configuration and use the file cache as second level cache only.'); } else { - // Check whether opcache_get_status has been explicitly disabled an in case skip usage based checks + // Check whether opcache_get_status has been explicitly disabled and in case skip usage based checks $disabledFunctions = $this->iniGetWrapper->getString('disable_functions'); if (isset($disabledFunctions) && str_contains($disabledFunctions, 'opcache_get_status')) { return [$level, $recommendations]; @@ -70,29 +70,27 @@ class PhpOpcacheSetup implements ISetupCheck { $level = 'error'; } - // Recommend to raise value, if more than 90% of max value is reached - if ( - empty($status['opcache_statistics']['max_cached_keys']) || - ($status['opcache_statistics']['num_cached_keys'] / $status['opcache_statistics']['max_cached_keys'] > 0.9) - ) { - $recommendations[] = $this->l10n->t('The maximum number of OPcache keys is nearly exceeded. To assure that all scripts can be kept in the cache, it is recommended to apply "opcache.max_accelerated_files" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.max_accelerated_files') ?: 'currently')]); - } - - if ( - empty($status['memory_usage']['free_memory']) || - ($status['memory_usage']['used_memory'] / $status['memory_usage']['free_memory'] > 9) - ) { - $recommendations[] = $this->l10n->t('The OPcache buffer is nearly full. To assure that all scripts can be hold in cache, it is recommended to apply "opcache.memory_consumption" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?: 'currently')]); + // Check whether OPcache is full, which can be either the overall OPcache size or limit of cached keys reached. + // If the limit of cached keys has been reached, num_cached_keys equals max_cached_keys. The recommendation contains this value instead of opcache.max_accelerated_files, since the effective limit is a next higher prime number: https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.max-accelerated-files + // Else, the remaining $status['memory_usage']['free_memory'] was too low to store another script. Aside of used_memory, this can be also due to wasted_memory, remaining cache keys from scripts changed on disk. + // Wasted memory is cleared only via opcache_reset(), or if $status['memory_usage']['current_wasted_percentage'] reached opcache.max_wasted_percentage, which triggers an engine restart and hence OPcache reset. Due to this complexity, we check for $status['cache_full'] only. + if ($status['cache_full'] === true) { + if ($status['opcache_statistics']['num_cached_keys'] === $status['opcache_statistics']['max_cached_keys']) { + $recommendations[] = $this->l10n->t('The maximum number of OPcache keys is nearly exceeded. To assure that all scripts can be kept in the cache, it is recommended to apply "opcache.max_accelerated_files" to your PHP configuration with a value higher than "%s".', [($status['opcache_statistics']['max_cached_keys'] ?: 'currently')]); + } else { + $recommendations[] = $this->l10n->t('The OPcache buffer is nearly full. To assure that all scripts can be hold in cache, it is recommended to apply "opcache.memory_consumption" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?: 'currently')]); + } } + // Interned strings buffer: recommend to raise size if more than 90% is used $interned_strings_buffer = $this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') ?? 0; $memory_consumption = $this->iniGetWrapper->getNumeric('opcache.memory_consumption') ?? 0; if ( // Do not recommend to raise the interned strings buffer size above a quarter of the total OPcache size - ($interned_strings_buffer < ($memory_consumption / 4)) && - ( - empty($status['interned_strings_usage']['free_memory']) || - ($status['interned_strings_usage']['used_memory'] / $status['interned_strings_usage']['free_memory'] > 9) + ($interned_strings_buffer < ($memory_consumption / 4)) + && ( + empty($status['interned_strings_usage']['free_memory']) + || ($status['interned_strings_usage']['used_memory'] / $status['interned_strings_usage']['free_memory'] > 9) ) ) { $recommendations[] = $this->l10n->t('The OPcache interned strings buffer is nearly full. To assure that repeating strings can be effectively cached, it is recommended to apply "opcache.interned_strings_buffer" to your PHP configuration with a value higher than "%s".', [($this->iniGetWrapper->getNumeric('opcache.interned_strings_buffer') ?: 'currently')]); @@ -114,6 +112,11 @@ class PhpOpcacheSetup implements ISetupCheck { } public function run(): SetupResult { + // Skip OPcache checks if running from CLI + if (\OC::$CLI && !$this->iniGetWrapper->getBool('opcache.enable_cli')) { + return SetupResult::success($this->l10n->t('Checking from CLI, OPcache checks have been skipped.')); + } + [$level,$recommendations] = $this->getOpcacheSetupRecommendations(); if (!empty($recommendations)) { return match($level) { diff --git a/apps/settings/lib/SetupChecks/PhpOutdated.php b/apps/settings/lib/SetupChecks/PhpOutdated.php index 4c7ed5096c0..d0d8e03c705 100644 --- a/apps/settings/lib/SetupChecks/PhpOutdated.php +++ b/apps/settings/lib/SetupChecks/PhpOutdated.php @@ -14,6 +14,11 @@ use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; class PhpOutdated implements ISetupCheck { + public const DEPRECATED_PHP_VERSION = '8.1'; + public const DEPRECATED_SINCE = '30'; + public const FUTURE_REQUIRED_PHP_VERSION = '8.2'; + public const FUTURE_REQUIRED_STARTING = '32'; + public function __construct( private IL10N $l10n, ) { @@ -29,7 +34,13 @@ class PhpOutdated implements ISetupCheck { public function run(): SetupResult { if (PHP_VERSION_ID < 80200) { - return SetupResult::warning($this->l10n->t('You are currently running PHP %s. PHP 8.1 is now deprecated in Nextcloud 30. Nextcloud 31 may require at least PHP 8.2. Please upgrade to one of the officially supported PHP versions provided by the PHP Group as soon as possible.', [PHP_VERSION]), 'https://secure.php.net/supported-versions.php'); + return SetupResult::warning($this->l10n->t('You are currently running PHP %1$s. PHP %2$s is deprecated since Nextcloud %3$s. Nextcloud %4$s may require at least PHP %5$s. Please upgrade to one of the officially supported PHP versions provided by the PHP Group as soon as possible.', [ + PHP_VERSION, + self::DEPRECATED_PHP_VERSION, + self::DEPRECATED_SINCE, + self::FUTURE_REQUIRED_STARTING, + self::FUTURE_REQUIRED_PHP_VERSION, + ]), 'https://secure.php.net/supported-versions.php'); } return SetupResult::success($this->l10n->t('You are currently running PHP %s.', [PHP_VERSION])); } diff --git a/apps/settings/lib/SetupChecks/PushService.php b/apps/settings/lib/SetupChecks/PushService.php index a3ac921caef..1f03404d80e 100644 --- a/apps/settings/lib/SetupChecks/PushService.php +++ b/apps/settings/lib/SetupChecks/PushService.php @@ -38,7 +38,7 @@ class PushService implements ISetupCheck { * Check if is fair use of free push service */ private function isFairUseOfFreePushService(): bool { - $rateLimitReached = (int) $this->config->getAppValue('notifications', 'rate_limit_reached', '0'); + $rateLimitReached = (int)$this->config->getAppValue('notifications', 'rate_limit_reached', '0'); if ($rateLimitReached >= ($this->timeFactory->now()->getTimestamp() - 7 * 24 * 3600)) { // Notifications app is showing a message already return true; diff --git a/apps/settings/lib/SetupChecks/SchedulingTableSize.php b/apps/settings/lib/SetupChecks/SchedulingTableSize.php index cf629f5b12c..b23972ca7dc 100644 --- a/apps/settings/lib/SetupChecks/SchedulingTableSize.php +++ b/apps/settings/lib/SetupChecks/SchedulingTableSize.php @@ -14,6 +14,8 @@ use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; class SchedulingTableSize implements ISetupCheck { + public const MAX_SCHEDULING_ENTRIES = 50000; + public function __construct( private IL10N $l10n, private IDBConnection $connection, @@ -36,9 +38,11 @@ class SchedulingTableSize implements ISetupCheck { $count = $query->fetchOne(); $query->closeCursor(); - if ($count > 500000) { + if ($count > self::MAX_SCHEDULING_ENTRIES) { return SetupResult::warning( - $this->l10n->t('You have more than 500 000 rows in the scheduling objects table. Please run the expensive repair jobs via occ maintenance:repair --include-expensive') + $this->l10n->t('You have more than %s rows in the scheduling objects table. Please run the expensive repair jobs via occ maintenance:repair --include-expensive.', [ + self::MAX_SCHEDULING_ENTRIES, + ]) ); } return SetupResult::success( diff --git a/apps/settings/lib/SetupChecks/SecurityHeaders.php b/apps/settings/lib/SetupChecks/SecurityHeaders.php index f00e92ef496..9cc6856a170 100644 --- a/apps/settings/lib/SetupChecks/SecurityHeaders.php +++ b/apps/settings/lib/SetupChecks/SecurityHeaders.php @@ -13,6 +13,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -64,18 +65,13 @@ class SecurityHeaders implements ISetupCheck { $value = preg_replace('/,\s+/', ',', strtolower($response->getHeader($header))); if ($value !== $expected) { if ($accepted !== null && $value === $accepted) { - $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. Some features might not work correctly, as it is recommended to adjust this setting accordingly.', [$header, $expected])."\n"; + $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. Some features might not work correctly, as it is recommended to adjust this setting accordingly.', [$header, $expected]) . "\n"; } else { - $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.', [$header, $expected])."\n"; + $msg .= $this->l10n->t('- The `%1$s` HTTP header is not set to `%2$s`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.', [$header, $expected]) . "\n"; } } } - $xssfields = array_map('trim', explode(';', $response->getHeader('X-XSS-Protection'))); - if (!in_array('1', $xssfields) || !in_array('mode=block', $xssfields)) { - $msg .= $this->l10n->t('- The `%1$s` HTTP header does not contain `%2$s`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.', ['X-XSS-Protection', '1; mode=block'])."\n"; - } - $referrerPolicy = $response->getHeader('Referrer-Policy'); if (!preg_match('/(no-referrer(-when-downgrade)?|strict-origin(-when-cross-origin)?|same-origin)(,|$)/', $referrerPolicy)) { $msg .= $this->l10n->t( @@ -88,7 +84,7 @@ class SecurityHeaders implements ISetupCheck { 'strict-origin-when-cross-origin', 'same-origin', ] - )."\n"; + ) . "\n"; $msgParameters['w3c-recommendation'] = [ 'type' => 'highlight', 'id' => 'w3c-recommendation', @@ -102,17 +98,17 @@ class SecurityHeaders implements ISetupCheck { if (preg_match('/^max-age=(\d+)(;.*)?$/', $transportSecurityValidity, $m)) { $transportSecurityValidity = (int)$m[1]; if ($transportSecurityValidity < $minimumSeconds) { - $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set to at least `%d` seconds (current value: `%d`). For enhanced security, it is recommended to use a long HSTS policy.', [$minimumSeconds, $transportSecurityValidity])."\n"; + $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set to at least `%d` seconds (current value: `%d`). For enhanced security, it is recommended to use a long HSTS policy.', [$minimumSeconds, $transportSecurityValidity]) . "\n"; } } elseif (!empty($transportSecurityValidity)) { - $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is malformed: `%s`. For enhanced security, it is recommended to enable HSTS.', [$transportSecurityValidity])."\n"; + $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is malformed: `%s`. For enhanced security, it is recommended to enable HSTS.', [$transportSecurityValidity]) . "\n"; } else { - $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set (should be at least `%d` seconds). For enhanced security, it is recommended to enable HSTS.', [$minimumSeconds])."\n"; + $msg .= $this->l10n->t('- The `Strict-Transport-Security` HTTP header is not set (should be at least `%d` seconds). For enhanced security, it is recommended to enable HSTS.', [$minimumSeconds]) . "\n"; } if (!empty($msg)) { return SetupResult::warning( - $this->l10n->t('Some headers are not set correctly on your instance')."\n".$msg, + $this->l10n->t('Some headers are not set correctly on your instance') . "\n" . $msg, $this->urlGenerator->linkToDocs('admin-security'), $msgParameters, ); @@ -131,7 +127,7 @@ class SecurityHeaders implements ISetupCheck { // Otherwise if we fail we can abort here if ($works === false) { return SetupResult::warning( - $this->l10n->t("Could not check that your web server serves security headers correctly, unable to query `%s`", [$url]), + $this->l10n->t('Could not check that your web server serves security headers correctly, unable to query `%s`', [$url]), $this->urlGenerator->linkToDocs('admin-security'), ); } diff --git a/apps/settings/lib/SetupChecks/SupportedDatabase.php b/apps/settings/lib/SetupChecks/SupportedDatabase.php index 43807a90251..d083958d16e 100644 --- a/apps/settings/lib/SetupChecks/SupportedDatabase.php +++ b/apps/settings/lib/SetupChecks/SupportedDatabase.php @@ -19,6 +19,14 @@ use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; class SupportedDatabase implements ISetupCheck { + + private const MIN_MARIADB = '10.6'; + private const MAX_MARIADB = '11.8'; + private const MIN_MYSQL = '8.0'; + private const MAX_MYSQL = '8.4'; + private const MIN_POSTGRES = '13'; + private const MAX_POSTGRES = '17'; + public function __construct( private IL10N $l10n, private IURLGenerator $urlGenerator, @@ -38,8 +46,8 @@ class SupportedDatabase implements ISetupCheck { $version = null; $databasePlatform = $this->connection->getDatabasePlatform(); if ($databasePlatform instanceof MySQLPlatform) { - $result = $this->connection->prepare("SHOW VARIABLES LIKE 'version';"); - $result->execute(); + $statement = $this->connection->prepare("SHOW VARIABLES LIKE 'version';"); + $result = $statement->execute(); $row = $result->fetch(); $version = $row['Value']; $versionlc = strtolower($version); @@ -47,25 +55,61 @@ class SupportedDatabase implements ISetupCheck { [$major, $minor, ] = explode('.', $versionlc); $versionConcern = $major . '.' . $minor; if (str_contains($versionlc, 'mariadb')) { - if (version_compare($versionConcern, '10.3', '<') || version_compare($versionConcern, '10.11', '>')) { - return SetupResult::warning($this->l10n->t('MariaDB version "%s" detected. MariaDB >=10.3 and <=10.11 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version)); + if (version_compare($versionConcern, '10.3', '=')) { + return SetupResult::info( + $this->l10n->t( + 'MariaDB version 10.3 detected, this version is end-of-life and only supported as part of Ubuntu 20.04. MariaDB >=%1$s and <=%2$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + self::MIN_MARIADB, + self::MAX_MARIADB, + ] + ), + ); + } elseif (version_compare($versionConcern, self::MIN_MARIADB, '<') || version_compare($versionConcern, self::MAX_MARIADB, '>')) { + return SetupResult::warning( + $this->l10n->t( + 'MariaDB version "%1$s" detected. MariaDB >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + $version, + self::MIN_MARIADB, + self::MAX_MARIADB, + ], + ), + ); } } else { - if (version_compare($versionConcern, '8.0', '<') || version_compare($versionConcern, '8.3', '>')) { - return SetupResult::warning($this->l10n->t('MySQL version "%s" detected. MySQL >=8.0 and <=8.3 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version)); + if (version_compare($versionConcern, self::MIN_MYSQL, '<') || version_compare($versionConcern, self::MAX_MYSQL, '>')) { + return SetupResult::warning( + $this->l10n->t( + 'MySQL version "%1$s" detected. MySQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + $version, + self::MIN_MYSQL, + self::MAX_MYSQL, + ], + ), + ); } } } elseif ($databasePlatform instanceof PostgreSQLPlatform) { - $result = $this->connection->prepare('SHOW server_version;'); - $result->execute(); + $statement = $this->connection->prepare('SHOW server_version;'); + $result = $statement->execute(); $row = $result->fetch(); $version = $row['server_version']; $versionlc = strtolower($version); // we only care about X not X.Y or X.Y.Z differences [$major, ] = explode('.', $versionlc); $versionConcern = $major; - if (version_compare($versionConcern, '12', '<') || version_compare($versionConcern, '16', '>')) { - return SetupResult::warning($this->l10n->t('PostgreSQL version "%s" detected. PostgreSQL >=12 and <=16 is suggested for best performance, stability and functionality with this version of Nextcloud.', $version)); + if (version_compare($versionConcern, self::MIN_POSTGRES, '<') || version_compare($versionConcern, self::MAX_POSTGRES, '>')) { + return SetupResult::warning( + $this->l10n->t( + 'PostgreSQL version "%1$s" detected. PostgreSQL >=%2$s and <=%3$s is suggested for best performance, stability and functionality with this version of Nextcloud.', + [ + $version, + self::MIN_POSTGRES, + self::MAX_POSTGRES, + ]) + ); } } elseif ($databasePlatform instanceof OraclePlatform) { $version = 'Oracle'; diff --git a/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php new file mode 100644 index 00000000000..83168ac0f3e --- /dev/null +++ b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings\SetupChecks; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IL10N; +use OCP\SetupCheck\ISetupCheck; +use OCP\SetupCheck\SetupResult; +use OCP\TaskProcessing\IManager; + +class TaskProcessingPickupSpeed implements ISetupCheck { + public const MAX_SLOW_PERCENTAGE = 0.2; + public const TIME_SPAN = 24; + + public function __construct( + private IL10N $l10n, + private IManager $taskProcessingManager, + private ITimeFactory $timeFactory, + ) { + } + + public function getCategory(): string { + return 'ai'; + } + + public function getName(): string { + return $this->l10n->t('Task Processing pickup speed'); + } + + public function run(): SetupResult { + $tasks = $this->taskProcessingManager->getTasks(userId: '', scheduleAfter: $this->timeFactory->now()->getTimestamp() - 60 * 60 * self::TIME_SPAN); // userId: '' means no filter, whereas null would mean guest + $taskCount = count($tasks); + if ($taskCount === 0) { + return SetupResult::success($this->l10n->n('No scheduled tasks in the last %n hour.', 'No scheduled tasks in the last %n hours.', self::TIME_SPAN)); + } + $slowCount = 0; + foreach ($tasks as $task) { + if ($task->getStartedAt() === null) { + continue; // task was not picked up yet + } + if ($task->getScheduledAt() === null) { + continue; // task was not scheduled yet -- should not happen, but the API specifies null as return value + } + $pickupDelay = $task->getScheduledAt() - $task->getStartedAt(); + if ($pickupDelay > 60 * 4) { + $slowCount++; // task pickup took longer than 4 minutes + } + } + + if ($slowCount / $taskCount < self::MAX_SLOW_PERCENTAGE) { + return SetupResult::success($this->l10n->n('The task pickup speed has been ok in the last %n hour.', 'The task pickup speed has been ok in the last %n hours.', self::TIME_SPAN)); + } else { + return SetupResult::warning($this->l10n->n('The task pickup speed has been slow in the last %n hour. Many tasks took longer than 4 minutes to be picked up. Consider setting up a worker to process tasks in the background.', 'The task pickup speed has been slow in the last %n hours. Many tasks took longer than 4 minutes to be picked up. Consider setting up a worker to process tasks in the background.', self::TIME_SPAN), 'https://docs.nextcloud.com/server/latest/admin_manual/ai/overview.html#improve-ai-task-pickup-speed'); + } + } +} diff --git a/apps/settings/lib/SetupChecks/TempSpaceAvailable.php b/apps/settings/lib/SetupChecks/TempSpaceAvailable.php index c369735bba6..49dc0d377e7 100644 --- a/apps/settings/lib/SetupChecks/TempSpaceAvailable.php +++ b/apps/settings/lib/SetupChecks/TempSpaceAvailable.php @@ -67,7 +67,11 @@ class TempSpaceAvailable implements ISetupCheck { return SetupResult::error($this->l10n->t('Error while checking the temporary PHP path - it was not properly set to a directory. Returned value: %s', [$phpTempPath])); } - $freeSpaceInTemp = function_exists('disk_free_space') ? disk_free_space($phpTempPath) : false; + if (!function_exists('disk_free_space')) { + return SetupResult::info($this->l10n->t('The PHP function "disk_free_space" is disabled, which prevents the check for enough space in the temporary directories.')); + } + + $freeSpaceInTemp = disk_free_space($phpTempPath); if ($freeSpaceInTemp === false) { return SetupResult::error($this->l10n->t('Error while checking the available disk space of temporary PHP path or no free disk space returned. Temporary path: %s', [$phpTempPath])); } @@ -76,12 +80,12 @@ class TempSpaceAvailable implements ISetupCheck { $freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024; $spaceDetail = $this->l10n->t('- %.1f GiB available in %s (PHP temporary directory)', [round($freeSpaceInTempInGB, 1),$phpTempPath]); if ($nextcloudTempPath !== $phpTempPath) { - $freeSpaceInNextcloudTemp = function_exists('disk_free_space') ? disk_free_space($nextcloudTempPath) : false; + $freeSpaceInNextcloudTemp = disk_free_space($nextcloudTempPath); if ($freeSpaceInNextcloudTemp === false) { return SetupResult::error($this->l10n->t('Error while checking the available disk space of temporary PHP path or no free disk space returned. Temporary path: %s', [$nextcloudTempPath])); } $freeSpaceInNextcloudTempInGB = $freeSpaceInNextcloudTemp / 1024 / 1024 / 1024; - $spaceDetail .= "\n".$this->l10n->t('- %.1f GiB available in %s (Nextcloud temporary directory)', [round($freeSpaceInNextcloudTempInGB, 1),$nextcloudTempPath]); + $spaceDetail .= "\n" . $this->l10n->t('- %.1f GiB available in %s (Nextcloud temporary directory)', [round($freeSpaceInNextcloudTempInGB, 1),$nextcloudTempPath]); } if (!$this->isPrimaryStorageS3()) { diff --git a/apps/settings/lib/SetupChecks/WellKnownUrls.php b/apps/settings/lib/SetupChecks/WellKnownUrls.php index d66fde3abee..4eeaff8f3c4 100644 --- a/apps/settings/lib/SetupChecks/WellKnownUrls.php +++ b/apps/settings/lib/SetupChecks/WellKnownUrls.php @@ -13,6 +13,7 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; @@ -44,15 +45,16 @@ class WellKnownUrls implements ISetupCheck { } $urls = [ - ['get', '/.well-known/webfinger', [200, 404], true], + ['get', '/.well-known/webfinger', [200, 400, 404], true], // 400 indicates a handler is installed but (correctly) failed because we didn't specify a resource ['get', '/.well-known/nodeinfo', [200, 404], true], ['propfind', '/.well-known/caldav', [207], false], ['propfind', '/.well-known/carddav', [207], false], ]; + $requestOptions = ['httpErrors' => false, 'options' => ['allow_redirects' => ['track_redirects' => true]]]; foreach ($urls as [$verb,$url,$validStatuses,$checkCustomHeader]) { $works = null; - foreach ($this->runRequest($verb, $url, ['httpErrors' => false, 'options' => ['allow_redirects' => ['track_redirects' => true]]]) as $response) { + foreach ($this->runRequest($verb, $url, $requestOptions, isRootRequest: true) as $response) { // Check that the response status matches $works = in_array($response->getStatusCode(), $validStatuses); // and (if needed) the custom Nextcloud header is set @@ -63,7 +65,7 @@ class WellKnownUrls implements ISetupCheck { if (!$works && $response->getStatusCode() === 401) { $redirectHops = explode(',', $response->getHeader('X-Guzzle-Redirect-History')); $effectiveUri = end($redirectHops); - $works = str_ends_with($effectiveUri, '/remote.php/dav/'); + $works = str_ends_with(rtrim($effectiveUri, '/'), '/remote.php/dav'); } } // Skip the other requests if one works diff --git a/apps/settings/lib/SetupChecks/Woff2Loading.php b/apps/settings/lib/SetupChecks/Woff2Loading.php index 5d8f23338e3..27aff4ea999 100644 --- a/apps/settings/lib/SetupChecks/Woff2Loading.php +++ b/apps/settings/lib/SetupChecks/Woff2Loading.php @@ -12,12 +12,13 @@ use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; /** - * Check whether the WOFF2 URLs works + * Check whether the OTF and WOFF2 URLs works */ class Woff2Loading implements ISetupCheck { use CheckServerResponseTrait; @@ -36,13 +37,20 @@ class Woff2Loading implements ISetupCheck { } public function getName(): string { - return $this->l10n->t('WOFF2 file loading'); + return $this->l10n->t('Font file loading'); } public function run(): SetupResult { - $url = $this->urlGenerator->linkTo('', 'core/fonts/NotoSans-Regular-latin.woff2'); + $result = $this->checkFont('otf', $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Regular.otf')); + if ($result->getSeverity() !== SetupResult::SUCCESS) { + return $result; + } + return $this->checkFont('woff2', $this->urlGenerator->linkTo('', 'core/fonts/NotoSans-Regular-latin.woff2')); + } + + protected function checkFont(string $fileExtension, string $url): SetupResult { $noResponse = true; - $responses = $this->runHEAD($url); + $responses = $this->runRequest('HEAD', $url); foreach ($responses as $response) { $noResponse = false; if ($response->getStatusCode() === 200) { @@ -52,14 +60,22 @@ class Woff2Loading implements ISetupCheck { if ($noResponse) { return SetupResult::info( - $this->l10n->t('Could not check for WOFF2 loading support. Please check manually if your webserver serves `.woff2` files.') . "\n" . $this->serverConfigHelp(), + str_replace( + '{extension}', + $fileExtension, + $this->l10n->t('Could not check for {extension} loading support. Please check manually if your webserver serves `.{extension}` files.') . "\n" . $this->serverConfigHelp(), + ), $this->urlGenerator->linkToDocs('admin-nginx'), ); } return SetupResult::warning( - $this->l10n->t('Your web server is not properly set up to deliver .woff2 files. This is typically an issue with the Nginx configuration. For Nextcloud 15 it needs an adjustement to also deliver .woff2 files. Compare your Nginx configuration to the recommended configuration in our documentation.'), + str_replace( + '{extension}', + $fileExtension, + $this->l10n->t('Your web server is not properly set up to deliver .{extension} files. This is typically an issue with the Nginx configuration. For Nextcloud 15 it needs an adjustment to also deliver .{extension} files. Compare your Nginx configuration to the recommended configuration in our documentation.'), + ), $this->urlGenerator->linkToDocs('admin-nginx'), ); - + } } diff --git a/apps/settings/lib/UserMigration/AccountMigrator.php b/apps/settings/lib/UserMigration/AccountMigrator.php index dc5ca2691f7..1c51aec5104 100644 --- a/apps/settings/lib/UserMigration/AccountMigrator.php +++ b/apps/settings/lib/UserMigration/AccountMigrator.php @@ -18,6 +18,7 @@ use OCA\Settings\AppInfo\Application; use OCP\Accounts\IAccountManager; use OCP\IAvatarManager; use OCP\IL10N; +use OCP\Image; use OCP\IUser; use OCP\UserMigration\IExportDestination; use OCP\UserMigration\IImportSource; @@ -32,16 +33,8 @@ class AccountMigrator implements IMigrator, ISizeEstimationMigrator { use TAccountsHelper; - private IAccountManager $accountManager; - - private IAvatarManager $avatarManager; - private ProfileManager $profileManager; - private ProfileConfigMapper $configMapper; - - private IL10N $l10n; - private const PATH_ROOT = Application::APP_ID . '/'; private const PATH_ACCOUNT_FILE = AccountMigrator::PATH_ROOT . 'account.json'; @@ -51,17 +44,13 @@ class AccountMigrator implements IMigrator, ISizeEstimationMigrator { private const PATH_CONFIG_FILE = AccountMigrator::PATH_ROOT . 'config.json'; public function __construct( - IAccountManager $accountManager, - IAvatarManager $avatarManager, + private IAccountManager $accountManager, + private IAvatarManager $avatarManager, ProfileManager $profileManager, - ProfileConfigMapper $configMapper, - IL10N $l10n + private ProfileConfigMapper $configMapper, + private IL10N $l10n, ) { - $this->accountManager = $accountManager; - $this->avatarManager = $avatarManager; $this->profileManager = $profileManager; - $this->configMapper = $configMapper; - $this->l10n = $l10n; } /** @@ -156,7 +145,7 @@ class AccountMigrator implements IMigrator, ISizeEstimationMigrator { $output->writeln('Importing avatar from ' . $importPath . '…'); $stream = $importSource->getFileAsStream($importPath); - $image = new \OCP\Image(); + $image = new Image(); $image->loadFromFileHandle($stream); try { diff --git a/apps/settings/lib/WellKnown/ChangePasswordHandler.php b/apps/settings/lib/WellKnown/ChangePasswordHandler.php index 0057a7ff330..9609579ef0a 100644 --- a/apps/settings/lib/WellKnown/ChangePasswordHandler.php +++ b/apps/settings/lib/WellKnown/ChangePasswordHandler.php @@ -18,10 +18,9 @@ use OCP\IURLGenerator; class ChangePasswordHandler implements IHandler { - private IURLGenerator $urlGenerator; - - public function __construct(IURLGenerator $urlGenerator) { - $this->urlGenerator = $urlGenerator; + public function __construct( + private IURLGenerator $urlGenerator, + ) { } public function handle(string $service, IRequestContext $context, ?IResponse $previousResponse): ?IResponse { diff --git a/apps/settings/lib/WellKnown/SecurityTxtHandler.php b/apps/settings/lib/WellKnown/SecurityTxtHandler.php index 3fb7ce83648..60ab2d57a38 100644 --- a/apps/settings/lib/WellKnown/SecurityTxtHandler.php +++ b/apps/settings/lib/WellKnown/SecurityTxtHandler.php @@ -21,13 +21,13 @@ class SecurityTxtHandler implements IHandler { return $previousResponse; } - $response = "Contact: https://hackerone.com/nextcloud -Expires: 2024-08-31T23:00:00.000Z + $response = 'Contact: https://hackerone.com/nextcloud +Expires: 2025-08-31T23:00:00.000Z Acknowledgments: https://hackerone.com/nextcloud/thanks Acknowledgments: https://github.com/nextcloud/security-advisories/security/advisories Policy: https://hackerone.com/nextcloud Preferred-Languages: en -"; +'; return new GenericResponse(new TextPlainResponse($response, 200)); } |