diff options
Diffstat (limited to 'apps/settings/lib/Settings')
16 files changed, 1634 insertions, 0 deletions
diff --git a/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php new file mode 100644 index 00000000000..aaec0049b20 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php @@ -0,0 +1,218 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 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\AppFramework\Services\IInitialState; +use OCP\IAppConfig; +use OCP\IL10N; +use OCP\Settings\IDelegatedSettings; +use OCP\SpeechToText\ISpeechToTextManager; +use OCP\SpeechToText\ISpeechToTextProviderWithId; +use OCP\TextProcessing\IManager; +use OCP\TextProcessing\IProvider; +use OCP\TextProcessing\IProviderWithId; +use OCP\TextProcessing\ITaskType; +use OCP\Translation\ITranslationManager; +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 IAppConfig $appConfig, + private IL10N $l, + private IInitialState $initialState, + private ITranslationManager $translationManager, + private ISpeechToTextManager $sttManager, + private IManager $textProcessingManager, + private ContainerInterface $container, + private \OCP\TextToImage\IManager $text2imageManager, + private \OCP\TaskProcessing\IManager $taskProcessingManager, + private LoggerInterface $logger, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm() { + $translationProviders = []; + $translationPreferences = []; + foreach ($this->translationManager->getProviders() as $provider) { + $translationProviders[] = [ + 'class' => $provider instanceof ITranslationProviderWithId ? $provider->getId() : $provider::class, + 'name' => $provider->getName(), + ]; + $translationPreferences[] = $provider instanceof ITranslationProviderWithId ? $provider->getId() : $provider::class; + } + + $sttProviders = []; + foreach ($this->sttManager->getProviders() as $provider) { + $sttProviders[] = [ + 'class' => $provider instanceof ISpeechToTextProviderWithId ? $provider->getId() : $provider::class, + 'name' => $provider->getName(), + ]; + } + + $textProcessingProviders = []; + /** @var array<class-string<ITaskType>, string|class-string<IProvider>> $textProcessingSettings */ + $textProcessingSettings = []; + foreach ($this->textProcessingManager->getProviders() as $provider) { + $textProcessingProviders[] = [ + 'class' => $provider instanceof IProviderWithId ? $provider->getId() : $provider::class, + 'name' => $provider->getName(), + 'taskType' => $provider->getTaskType(), + ]; + if (!isset($textProcessingSettings[$provider->getTaskType()])) { + $textProcessingSettings[$provider->getTaskType()] = $provider instanceof IProviderWithId ? $provider->getId() : $provider::class; + } + } + $textProcessingTaskTypes = []; + foreach ($textProcessingSettings as $taskTypeClass => $providerClass) { + /** @var ITaskType $taskType */ + try { + $taskType = $this->container->get($taskTypeClass); + } catch (NotFoundExceptionInterface $e) { + continue; + } catch (ContainerExceptionInterface $e) { + continue; + } + $textProcessingTaskTypes[] = [ + 'class' => $taskTypeClass, + 'name' => $taskType->getName(), + 'description' => $taskType->getDescription(), + ]; + } + + $text2imageProviders = []; + foreach ($this->text2imageManager->getProviders() as $provider) { + $text2imageProviders[] = [ + 'id' => $provider->getId(), + 'name' => $provider->getName(), + ]; + } + + $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.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->appConfig->getValueString('core', $key, '', lazy: in_array($key, \OC\TaskProcessing\Manager::LAZY_CONFIG_KEYS, true)); + if ($json !== '') { + 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': + // 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; + } + } + $settings[$key] = $value; + } + + $this->initialState->provideInitialState('ai-settings', $settings); + + return new TemplateResponse('settings', 'settings/admin/ai'); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'ai'; + } + + /** + * @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() { + return 10; + } + + public function getName(): ?string { + return $this->l->t('Artificial Intelligence'); + } + + public function getAuthorizedAppConfig(): array { + return [ + 'core' => ['/ai..*/'], + ]; + } +} diff --git a/apps/settings/lib/Settings/Admin/Delegation.php b/apps/settings/lib/Settings/Admin/Delegation.php new file mode 100644 index 00000000000..59a26d1ac04 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Delegation.php @@ -0,0 +1,120 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Admin; + +use OCA\Settings\AppInfo\Application; +use OCA\Settings\Service\AuthorizedGroupService; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IGroupManager; +use OCP\IURLGenerator; +use OCP\Settings\IDelegatedSettings; +use OCP\Settings\IManager; +use OCP\Settings\ISettings; + +class Delegation implements ISettings { + public function __construct( + private IManager $settingManager, + private IInitialState $initialStateService, + private IGroupManager $groupManager, + private AuthorizedGroupService $authorizedGroupService, + private IURLGenerator $urlGenerator, + ) { + } + + /** + * Filter out the ISettings that are not IDelegatedSettings from $innerSection + * and add them to $settings. + * + * @param IDelegatedSettings[] $settings + * @param ISettings[] $innerSection + * @return IDelegatedSettings[] + */ + private function getDelegatedSettings(array $settings, array $innerSection): array { + foreach ($innerSection as $setting) { + if ($setting instanceof IDelegatedSettings) { + $settings[] = $setting; + } + } + return $settings; + } + + private function initSettingState(): void { + // Available settings page initialization + $sections = $this->settingManager->getAdminSections(); + $settings = []; + foreach ($sections as $sectionPriority) { + foreach ($sectionPriority as $section) { + $sectionSettings = $this->settingManager->getAdminSettings($section->getId()); + $sectionSettings = array_reduce($sectionSettings, [$this, 'getDelegatedSettings'], []); + $settings = array_merge( + $settings, + array_map(function (IDelegatedSettings $setting) use ($section) { + $sectionName = $section->getName() . ($setting->getName() !== null ? ' - ' . $setting->getName() : ''); + return [ + 'class' => get_class($setting), + 'sectionName' => $sectionName, + 'id' => mb_strtolower(str_replace(' ', '-', $sectionName)), + 'priority' => $section->getPriority(), + ]; + }, $sectionSettings) + ); + } + } + usort($settings, function (array $a, array $b) { + if ($a['priority'] == $b['priority']) { + return 0; + } + return ($a['priority'] < $b['priority']) ? -1 : 1; + }); + $this->initialStateService->provideInitialState('available-settings', $settings); + } + + public function initAvailableGroupState(): void { + // Available groups initialization + $groups = []; + $groupsClass = $this->groupManager->search(''); + foreach ($groupsClass as $group) { + if ($group->getGID() === 'admin') { + continue; // Admin already have access to everything + } + $groups[] = [ + 'displayName' => $group->getDisplayName(), + 'gid' => $group->getGID(), + ]; + } + $this->initialStateService->provideInitialState('available-groups', $groups); + } + + public function initAuthorizedGroupState(): void { + // Already set authorized groups + $this->initialStateService->provideInitialState('authorized-groups', $this->authorizedGroupService->findAll()); + } + + public function getForm(): TemplateResponse { + $this->initSettingState(); + $this->initAvailableGroupState(); + $this->initAuthorizedGroupState(); + $this->initialStateService->provideInitialState('authorized-settings-doc-link', $this->urlGenerator->linkToDocs('admin-delegation')); + + return new TemplateResponse(Application::APP_ID, 'settings/admin/delegation', [], ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'admindelegation'; + } + + /* + * @inheritdoc + */ + public function getPriority() { + return 75; + } +} diff --git a/apps/settings/lib/Settings/Admin/Mail.php b/apps/settings/lib/Settings/Admin/Mail.php new file mode 100644 index 00000000000..8bf2342a59c --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Mail.php @@ -0,0 +1,84 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 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\IBinaryFinder; +use OCP\IConfig; +use OCP\IL10N; +use OCP\Server; +use OCP\Settings\IDelegatedSettings; + +class Mail implements IDelegatedSettings { + /** + * @param IConfig $config + * @param IL10N $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' => $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', ''), + 'mail_smtpsecure' => $this->config->getSystemValue('mail_smtpsecure', ''), + 'mail_smtphost' => $this->config->getSystemValue('mail_smtphost', ''), + 'mail_smtpport' => $this->config->getSystemValue('mail_smtpport', ''), + 'mail_smtpauth' => $this->config->getSystemValue('mail_smtpauth', false), + 'mail_smtpname' => $this->config->getSystemValue('mail_smtpname', ''), + 'mail_smtppassword' => $this->config->getSystemValue('mail_smtppassword', ''), + 'mail_sendmailmode' => $this->config->getSystemValue('mail_sendmailmode', 'smtp'), + ]; + + if ($parameters['mail_smtppassword'] !== '') { + $parameters['mail_smtppassword'] = '********'; + } + + if ($parameters['mail_smtpmode'] === '' || $parameters['mail_smtpmode'] === 'php') { + $parameters['mail_smtpmode'] = 'smtp'; + } + + return new TemplateResponse('settings', 'settings/admin/additional-mail', $parameters, ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'server'; + } + + /** + * @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() { + return 10; + } + + public function getName(): ?string { + return $this->l->t('Email server'); + } + + public function getAuthorizedAppConfig(): array { + return []; + } +} 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 new file mode 100644 index 00000000000..355200372f1 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Overview.php @@ -0,0 +1,60 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 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\IConfig; +use OCP\IL10N; +use OCP\ServerVersion; +use OCP\Settings\IDelegatedSettings; + +class Overview implements IDelegatedSettings { + public function __construct( + private ServerVersion $serverVersion, + private IConfig $config, + private IL10N $l, + ) { + } + + /** + * @return TemplateResponse + */ + 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, ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'overview'; + } + + /** + * @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() { + return 10; + } + + public function getName(): ?string { + return $this->l->t('Security & setup checks'); + } + + public function getAuthorizedAppConfig(): array { + return []; + } +} diff --git a/apps/settings/lib/Settings/Admin/Security.php b/apps/settings/lib/Settings/Admin/Security.php new file mode 100644 index 00000000000..c4efdb478c7 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Security.php @@ -0,0 +1,73 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Admin; + +use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\Encryption\IManager; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Settings\ISettings; + +class Security implements ISettings { + private MandatoryTwoFactor $mandatoryTwoFactor; + + public function __construct( + private IManager $manager, + private IUserManager $userManager, + MandatoryTwoFactor $mandatoryTwoFactor, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, + ) { + $this->mandatoryTwoFactor = $mandatoryTwoFactor; + } + + /** + * @return TemplateResponse + */ + public function getForm(): TemplateResponse { + $encryptionModules = $this->manager->getEncryptionModules(); + $defaultEncryptionModuleId = $this->manager->getDefaultEncryptionModuleId(); + $encryptionModuleList = []; + foreach ($encryptionModules as $module) { + $encryptionModuleList[$module['id']]['displayName'] = $module['displayName']; + $encryptionModuleList[$module['id']]['default'] = false; + if ($module['id'] === $defaultEncryptionModuleId) { + $encryptionModuleList[$module['id']]['default'] = true; + } + } + + $this->initialState->provideInitialState('mandatory2FAState', $this->mandatoryTwoFactor->getState()); + $this->initialState->provideInitialState('two-factor-admin-doc', $this->urlGenerator->linkToDocs('admin-2fa')); + $this->initialState->provideInitialState('encryption-enabled', $this->manager->isEnabled()); + $this->initialState->provideInitialState('encryption-ready', $this->manager->isReady()); + $this->initialState->provideInitialState('external-backends-enabled', count($this->userManager->getBackends()) > 1); + $this->initialState->provideInitialState('encryption-modules', $encryptionModuleList); + $this->initialState->provideInitialState('encryption-admin-doc', $this->urlGenerator->linkToDocs('admin-encryption')); + + return new TemplateResponse('settings', 'settings/admin/security', [], ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection(): string { + return 'security'; + } + + /** + * @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 10; + } +} diff --git a/apps/settings/lib/Settings/Admin/Server.php b/apps/settings/lib/Settings/Admin/Server.php new file mode 100644 index 00000000000..c0f29ce8f34 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Server.php @@ -0,0 +1,112 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Admin; + +use OC\Profile\ProfileManager; +use OC\Profile\TProfileHelper; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Settings\IDelegatedSettings; + +class Server implements IDelegatedSettings { + use TProfileHelper; + + public function __construct( + private IDBConnection $connection, + private IInitialState $initialStateService, + private ProfileManager $profileManager, + private ITimeFactory $timeFactory, + private IURLGenerator $urlGenerator, + private IConfig $config, + private IAppConfig $appConfig, + private IL10N $l, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm() { + $ownerConfigFile = fileowner(\OC::$configDir . 'config.php'); + $cliBasedCronPossible = function_exists('posix_getpwuid') && $ownerConfigFile !== false; + $cliBasedCronUser = $cliBasedCronPossible ? (posix_getpwuid($ownerConfigFile)['name'] ?? '') : ''; + + // Background jobs + $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')); + $this->initialStateService->provideInitialState('cliBasedCronPossible', $cliBasedCronPossible); + $this->initialStateService->provideInitialState('cliBasedCronUser', $cliBasedCronUser); + $this->initialStateService->provideInitialState('backgroundJobsDocUrl', $this->urlGenerator->linkToDocs('admin-background-jobs')); + + // Profile page + $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(), + ], ''); + } + + protected function cronMaxAge(): int { + $query = $this->connection->getQueryBuilder(); + $query->select('last_checked') + ->from('jobs') + ->orderBy('last_checked', 'ASC') + ->setMaxResults(1); + + $result = $query->execute(); + if ($row = $result->fetch()) { + $maxAge = (int)$row['last_checked']; + } else { + $maxAge = $this->timeFactory->getTime(); + } + $result->closeCursor(); + + return $maxAge; + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection(): string { + return 'server'; + } + + /** + * @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->l->t('Background jobs'); + } + + public function getAuthorizedAppConfig(): array { + return [ + 'core' => [ + '/mail_general_settings/', + ], + ]; + } +} diff --git a/apps/settings/lib/Settings/Admin/Sharing.php b/apps/settings/lib/Settings/Admin/Sharing.php new file mode 100644 index 00000000000..ec5dcdf624d --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Sharing.php @@ -0,0 +1,125 @@ +<?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; +use OCP\Settings\IDelegatedSettings; +use OCP\Share\IManager; +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, + private IURLGenerator $urlGenerator, + private IInitialState $initialState, + private string $appName, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm() { + $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); + $linksExcludedGroups = $this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', ''); + $excludedPasswordGroups = $this->config->getAppValue('core', 'shareapi_enforce_links_password_excluded_groups', ''); + $onlyShareWithGroupMembersExcludeGroupList = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''); + + $parameters = [ + // Built-In Sharing + 'enabled' => $this->getHumanBooleanConfig('core', 'shareapi_enabled', true), + 'allowGroupSharing' => $this->getHumanBooleanConfig('core', 'shareapi_allow_group_sharing', true), + 'allowLinks' => $this->getHumanBooleanConfig('core', 'shareapi_allow_links', true), + 'allowLinksExcludeGroups' => json_decode($linksExcludedGroups, true) ?? [], + '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), + 'restrictUserEnumerationFullMatchUserId' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_full_match_userid', true), + '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), + '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'), + 'expireAfterNDays' => $this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7'), + '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'), + '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'), + 'enforceInternalExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_enforce_internal_expire_date'), + '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); + + Util::addScript($this->appName, 'vue-settings-admin-sharing'); + return new TemplateResponse($this->appName, 'settings/admin/sharing', [], ''); + } + + /** + * Helper function to retrive boolean values from human readable strings ('yes' / 'no') + */ + private function getHumanBooleanConfig(string $app, string $key, bool $default = false): bool { + return $this->config->getAppValue($app, $key, $default ? 'yes' : 'no') === 'yes'; + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'sharing'; + } + + /** + * @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() { + return 0; + } + + public function getAuthorizedAppConfig(): array { + return [ + 'core' => ['/shareapi_.*/'], + ]; + } + + public function getName(): ?string { + return null; + } +} 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 new file mode 100644 index 00000000000..58fe08a63b7 --- /dev/null +++ b/apps/settings/lib/Settings/Personal/Additional.php @@ -0,0 +1,41 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Personal; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Settings\ISettings; + +class Additional implements ISettings { + + /** + * @return TemplateResponse returns the instance with all parameters set, ready to be rendered + * @since 9.1 + */ + public function getForm(): TemplateResponse { + return new TemplateResponse('settings', 'settings/empty'); + } + + /** + * @return string the section ID, e.g. 'sharing' + * @since 9.1 + */ + public function getSection(): string { + return 'additional'; + } + + /** + * @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 + * @since 9.1 + */ + public function getPriority(): int { + return 5; + } +} diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php new file mode 100644 index 00000000000..9a12b18bb5e --- /dev/null +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -0,0 +1,320 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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; +use OCP\App\IAppManager; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\Files\FileInfo; +use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IL10N; +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 ProfileManager */ + private $profileManager; + + public function __construct( + private IConfig $config, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IAccountManager $accountManager, + ProfileManager $profileManager, + private IAppManager $appManager, + private IFactory $l10nFactory, + private IL10N $l, + private IInitialState $initialStateService, + private IManager $manager, + ) { + $this->profileManager = $profileManager; + } + + public function getForm(): TemplateResponse { + $federationEnabled = $this->appManager->isEnabledForUser('federation'); + $federatedFileSharingEnabled = $this->appManager->isEnabledForUser('federatedfilesharing'); + $lookupServerUploadEnabled = false; + if ($federatedFileSharingEnabled) { + /** @var FederatedShareProvider $shareProvider */ + $shareProvider = Server::get(FederatedShareProvider::class); + $lookupServerUploadEnabled = $shareProvider->isLookupServerUploadEnabled(); + } + + $uid = \OC_User::getUser(); + $user = $this->userManager->get($uid); + $account = $this->accountManager->getAccount($user); + + // make sure FS is setup before querying storage related stuff... + \OC_Util::setupFS($user->getUID()); + + $storageInfo = \OC_Helper::getStorageInfo('/'); + if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) { + $totalSpace = $this->l->t('Unlimited'); + } else { + $totalSpace = Util::humanFileSize($storageInfo['total']); + } + + $messageParameters = $this->getMessageParameters($account); + + $parameters = [ + 'lookupServerUploadEnabled' => $lookupServerUploadEnabled, + 'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(), + 'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(), + ] + $messageParameters; + + $personalInfoParameters = [ + 'userId' => $uid, + 'avatar' => $this->getProperty($account, IAccountManager::PROPERTY_AVATAR), + 'groups' => $this->getGroups($user), + 'quota' => $storageInfo['quota'], + 'totalSpace' => $totalSpace, + 'usage' => Util::humanFileSize($storageInfo['used']), + 'usageRelative' => round($storageInfo['relative']), + 'displayName' => $this->getProperty($account, IAccountManager::PROPERTY_DISPLAYNAME), + 'emailMap' => $this->getEmailMap($account), + 'phone' => $this->getProperty($account, IAccountManager::PROPERTY_PHONE), + 'defaultPhoneRegion' => $this->config->getSystemValueString('default_phone_region'), + '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), + 'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(), + 'profileEnabled' => $this->profileManager->isProfileEnabled($user), + 'organisation' => $this->getProperty($account, IAccountManager::PROPERTY_ORGANISATION), + 'role' => $this->getProperty($account, IAccountManager::PROPERTY_ROLE), + '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, + ]; + + $profileParameters = [ + 'profileConfig' => $this->profileManager->getProfileConfigWithMetadata($user, $user), + ]; + + $this->initialStateService->provideInitialState('profileEnabledGlobally', $this->profileManager->isProfileEnabled()); + $this->initialStateService->provideInitialState('personalInfoParameters', $personalInfoParameters); + $this->initialStateService->provideInitialState('accountParameters', $accountParameters); + $this->initialStateService->provideInitialState('profileParameters', $profileParameters); + + return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, ''); + } + + /** + * Check if is fair use of free push service + * @return boolean + */ + private function isFairUseOfFreePushService(): bool { + return $this->manager->isFairUseOfFreePushService(); + } + + /** + * returns the property data in an + * associative array + */ + private function getProperty(IAccount $account, string $property): array { + $property = [ + 'name' => $account->getProperty($property)->getName(), + 'value' => $account->getProperty($property)->getValue(), + 'scope' => $account->getProperty($property)->getScope(), + 'verified' => $account->getProperty($property)->getVerified(), + ]; + + return $property; + } + + /** + * returns the section ID string, e.g. 'sharing' + * @since 9.1 + */ + public function getSection(): string { + return 'personal-info'; + } + + /** + * @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 + * @since 9.1 + */ + public function getPriority(): int { + return 10; + } + + /** + * returns a sorted list of the user's group GIDs + */ + private function getGroups(IUser $user): array { + $groups = array_map( + static function (IGroup $group) { + return $group->getDisplayName(); + }, + $this->groupManager->getUserGroups($user) + ); + sort($groups); + + return $groups; + } + + /** + * returns the primary email and additional emails in an + * associative array + */ + private function getEmailMap(IAccount $account): array { + $systemEmail = [ + 'name' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getName(), + 'value' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(), + 'scope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(), + 'verified' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(), + ]; + + $additionalEmails = array_map( + function (IAccountProperty $property) { + return [ + 'name' => $property->getName(), + 'value' => $property->getValue(), + 'scope' => $property->getScope(), + 'verified' => $property->getVerified(), + 'locallyVerified' => $property->getLocallyVerified(), + ]; + }, + $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties(), + ); + + $emailMap = [ + 'primaryEmail' => $systemEmail, + 'additionalEmails' => $additionalEmails, + 'notificationEmail' => (string)$account->getUser()->getPrimaryEMailAddress(), + ]; + + return $emailMap; + } + + /** + * returns the user's active language, common languages, and other languages in an + * associative array + */ + private function getLanguageMap(IUser $user): array { + $forceLanguage = $this->config->getSystemValue('force_language', false); + if ($forceLanguage !== false) { + return []; + } + + $uid = $user->getUID(); + + $userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage()); + $languages = $this->l10nFactory->getLanguages(); + + // associate the user language with the proper array + $userLangIndex = array_search($userConfLang, array_column($languages['commonLanguages'], 'code')); + $userLang = $languages['commonLanguages'][$userLangIndex]; + // search in the other languages + if ($userLangIndex === false) { + $userLangIndex = array_search($userConfLang, array_column($languages['otherLanguages'], 'code')); + $userLang = $languages['otherLanguages'][$userLangIndex]; + } + // if user language is not available but set somehow: show the actual code as name + if (!is_array($userLang)) { + $userLang = [ + 'code' => $userConfLang, + 'name' => $userConfLang, + ]; + } + + return array_merge( + ['activeLanguage' => $userLang], + $languages + ); + } + + private function getLocaleMap(IUser $user): array { + $forceLanguage = $this->config->getSystemValue('force_locale', false); + if ($forceLanguage !== false) { + return []; + } + + $uid = $user->getUID(); + $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']); + + if (!empty($userLocale)) { + $userLocale = reset($userLocale); + } + + $localesForLanguage = array_values(array_filter($localeCodes, fn ($localeCode) => str_starts_with($localeCode['code'], $userLang))); + $otherLocales = array_values(array_filter($localeCodes, fn ($localeCode) => !str_starts_with($localeCode['code'], $userLang))); + + if (!$userLocale) { + $userLocale = [ + 'code' => 'en', + 'name' => 'English' + ]; + } + + return [ + 'activeLocaleLang' => $userLocaleString, + 'activeLocale' => $userLocale, + 'localesForLanguage' => $localesForLanguage, + 'otherLocales' => $otherLocales, + ]; + } + + /** + * returns the message parameters + */ + private function getMessageParameters(IAccount $account): array { + $needVerifyMessage = [IAccountManager::PROPERTY_EMAIL, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER]; + $messageParameters = []; + foreach ($needVerifyMessage as $property) { + switch ($account->getProperty($property)->getVerified()) { + case IAccountManager::VERIFIED: + $message = $this->l->t('Verifying'); + break; + case IAccountManager::VERIFICATION_IN_PROGRESS: + $message = $this->l->t('Verifying …'); + break; + default: + $message = $this->l->t('Verify'); + } + $messageParameters[$property . 'Message'] = $message; + } + return $messageParameters; + } +} diff --git a/apps/settings/lib/Settings/Personal/Security/Authtokens.php b/apps/settings/lib/Settings/Personal/Security/Authtokens.php new file mode 100644 index 00000000000..e0509b22a9c --- /dev/null +++ b/apps/settings/lib/Settings/Personal/Security/Authtokens.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Personal\Security; + +use OC\Authentication\Token\INamedToken; +use OC\Authentication\Token\IProvider as IAuthTokenProvider; +use OC\Authentication\Token\IToken; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\ISession; +use OCP\IUserSession; +use OCP\Session\Exceptions\SessionNotAvailableException; +use OCP\Settings\ISettings; +use function array_map; + +class Authtokens implements ISettings { + + public function __construct( + private IAuthTokenProvider $tokenProvider, + private ISession $session, + private IUserSession $userSession, + private IInitialState $initialState, + private ?string $userId, + ) { + } + + public function getForm(): TemplateResponse { + $this->initialState->provideInitialState( + 'app_tokens', + $this->getAppTokens() + ); + + $this->initialState->provideInitialState( + 'can_create_app_token', + $this->userSession->getImpersonatingUserID() === null + ); + + return new TemplateResponse('settings', 'settings/personal/security/authtokens'); + } + + public function getSection(): string { + return 'security'; + } + + public function getPriority(): int { + return 100; + } + + private function getAppTokens(): array { + $tokens = $this->tokenProvider->getTokenByUser($this->userId); + + try { + $sessionId = $this->session->getId(); + } catch (SessionNotAvailableException $ex) { + return []; + } + try { + $sessionToken = $this->tokenProvider->getToken($sessionId); + } catch (InvalidTokenException $ex) { + return []; + } + + return array_map(function (IToken $token) use ($sessionToken) { + $data = $token->jsonSerialize(); + $data['canDelete'] = true; + $data['canRename'] = $token instanceof INamedToken && $data['type'] !== IToken::WIPE_TOKEN; + if ($sessionToken->getId() === $token->getId()) { + $data['canDelete'] = false; + $data['canRename'] = false; + $data['current'] = true; + } + return $data; + }, $tokens); + } +} diff --git a/apps/settings/lib/Settings/Personal/Security/Password.php b/apps/settings/lib/Settings/Personal/Security/Password.php new file mode 100644 index 00000000000..8184dae9560 --- /dev/null +++ b/apps/settings/lib/Settings/Personal/Security/Password.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Personal\Security; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IUserManager; +use OCP\Settings\ISettings; + +class Password implements ISettings { + + public function __construct( + private IUserManager $userManager, + private ?string $userId, + ) { + } + + public function getForm(): TemplateResponse { + $user = $this->userManager->get($this->userId); + $passwordChangeSupported = false; + if ($user !== null) { + $passwordChangeSupported = $user->canChangePassword(); + } + + return new TemplateResponse('settings', 'settings/personal/security/password', [ + 'passwordChangeSupported' => $passwordChangeSupported, + ]); + } + + public function getSection(): string { + return 'security'; + } + + public function getPriority(): int { + return 10; + } +} diff --git a/apps/settings/lib/Settings/Personal/Security/TwoFactor.php b/apps/settings/lib/Settings/Personal/Security/TwoFactor.php new file mode 100644 index 00000000000..0c419cb6fa7 --- /dev/null +++ b/apps/settings/lib/Settings/Personal/Security/TwoFactor.php @@ -0,0 +1,108 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Personal\Security; + +use Exception; +use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; +use OC\Authentication\TwoFactorAuth\ProviderLoader; +use OCA\TwoFactorBackupCodes\Provider\BackupCodesProvider; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\Authentication\TwoFactorAuth\IProvidesPersonalSettings; +use OCP\IConfig; +use OCP\IUserSession; +use OCP\Settings\ISettings; +use function array_filter; +use function array_map; +use function is_null; + +class TwoFactor implements ISettings { + + /** @var ProviderLoader */ + private $providerLoader; + + /** @var MandatoryTwoFactor */ + private $mandatoryTwoFactor; + + public function __construct( + ProviderLoader $providerLoader, + MandatoryTwoFactor $mandatoryTwoFactor, + private IUserSession $userSession, + private IConfig $config, + private ?string $userId, + ) { + $this->providerLoader = $providerLoader; + $this->mandatoryTwoFactor = $mandatoryTwoFactor; + } + + public function getForm(): TemplateResponse { + return new TemplateResponse('settings', 'settings/personal/security/twofactor', [ + 'twoFactorProviderData' => $this->getTwoFactorProviderData(), + ]); + } + + public function getSection(): ?string { + if (!$this->shouldShow()) { + return null; + } + return 'security'; + } + + public function getPriority(): int { + return 15; + } + + private function shouldShow(): bool { + $user = $this->userSession->getUser(); + if (is_null($user)) { + // Actually impossible, but still … + return false; + } + + // Anyone who's supposed to use 2FA should see 2FA settings + if ($this->mandatoryTwoFactor->isEnforcedFor($user)) { + return true; + } + + // If there is at least one provider with personal settings but it's not + // the backup codes provider, then these settings should show. + try { + $providers = $this->providerLoader->getProviders($user); + } catch (Exception $e) { + // Let's hope for the best + return true; + } + foreach ($providers as $provider) { + if ($provider instanceof IProvidesPersonalSettings + && !($provider instanceof BackupCodesProvider)) { + return true; + } + } + return false; + } + + private function getTwoFactorProviderData(): array { + $user = $this->userSession->getUser(); + if (is_null($user)) { + // Actually impossible, but still … + return []; + } + + return [ + 'providers' => array_map(function (IProvidesPersonalSettings $provider) use ($user) { + return [ + 'provider' => $provider, + 'settings' => $provider->getPersonalSettings($user) + ]; + }, array_filter($this->providerLoader->getProviders($user), function (IProvider $provider) { + return $provider instanceof IProvidesPersonalSettings; + })) + ]; + } +} diff --git a/apps/settings/lib/Settings/Personal/Security/WebAuthn.php b/apps/settings/lib/Settings/Personal/Security/WebAuthn.php new file mode 100644 index 00000000000..a6ba4e9522a --- /dev/null +++ b/apps/settings/lib/Settings/Personal/Security/WebAuthn.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Personal\Security; + +use OC\Authentication\WebAuthn\Db\PublicKeyCredentialMapper; +use OC\Authentication\WebAuthn\Manager; +use OCA\Settings\AppInfo\Application; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IInitialStateService; +use OCP\Settings\ISettings; + +class WebAuthn implements ISettings { + + /** @var PublicKeyCredentialMapper */ + private $mapper; + + /** @var Manager */ + private $manager; + + public function __construct( + PublicKeyCredentialMapper $mapper, + private string $userId, + private IInitialStateService $initialStateService, + Manager $manager, + ) { + $this->mapper = $mapper; + $this->manager = $manager; + } + + public function getForm() { + $this->initialStateService->provideInitialState( + Application::APP_ID, + 'webauthn-devices', + $this->mapper->findAllForUid($this->userId) + ); + + return new TemplateResponse('settings', 'settings/personal/security/webauthn'); + } + + public function getSection(): ?string { + if (!$this->manager->isWebAuthnAvailable()) { + return null; + } + + return 'security'; + } + + public function getPriority(): int { + return 20; + } +} diff --git a/apps/settings/lib/Settings/Personal/ServerDevNotice.php b/apps/settings/lib/Settings/Personal/ServerDevNotice.php new file mode 100644 index 00000000000..c9993484abd --- /dev/null +++ b/apps/settings/lib/Settings/Personal/ServerDevNotice.php @@ -0,0 +1,79 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Settings\Personal; + +use OCA\Viewer\Event\LoadViewer; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\IRootFolder; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\Settings\ISettings; +use OCP\Support\Subscription\IRegistry; +use OCP\Util; + +class ServerDevNotice implements ISettings { + + public function __construct( + private IRegistry $registry, + private IEventDispatcher $eventDispatcher, + private IRootFolder $rootFolder, + private IUserSession $userSession, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm(): TemplateResponse { + $userFolder = $this->rootFolder->getUserFolder($this->userSession->getUser()->getUID()); + + $hasInitialState = false; + + // If the Reasons to use Nextcloud.pdf file is here, let's init Viewer, also check that Viewer is there + if (class_exists(LoadViewer::class) && $userFolder->nodeExists('Reasons to use Nextcloud.pdf')) { + /** + * @psalm-suppress UndefinedClass, InvalidArgument + */ + $this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer()); + $hasInitialState = true; + } + + // Always load the script + Util::addScript('settings', 'vue-settings-nextcloud-pdf'); + $this->initialState->provideInitialState('has-reasons-use-nextcloud-pdf', $hasInitialState); + + return new TemplateResponse('settings', 'settings/personal/development.notice', [ + 'reasons-use-nextcloud-pdf-link' => $this->urlGenerator->linkToRoute('settings.Reasons.getPdf') + ]); + } + + /** + * @return string|null the section ID, e.g. 'sharing' + */ + public function getSection(): ?string { + if ($this->registry->delegateHasValidSubscription()) { + return null; + } + + return 'personal-info'; + } + + /** + * @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 1000; + } +} |