diff options
Diffstat (limited to 'lib/private/Settings')
-rw-r--r-- | lib/private/Settings/AuthorizedGroup.php | 31 | ||||
-rw-r--r-- | lib/private/Settings/AuthorizedGroupMapper.php | 111 | ||||
-rw-r--r-- | lib/private/Settings/DeclarativeManager.php | 482 | ||||
-rw-r--r-- | lib/private/Settings/Manager.php | 352 | ||||
-rw-r--r-- | lib/private/Settings/Section.php | 70 |
5 files changed, 1046 insertions, 0 deletions
diff --git a/lib/private/Settings/AuthorizedGroup.php b/lib/private/Settings/AuthorizedGroup.php new file mode 100644 index 00000000000..0171e020171 --- /dev/null +++ b/lib/private/Settings/AuthorizedGroup.php @@ -0,0 +1,31 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Settings; + +use OCP\AppFramework\Db\Entity; + +/** + * @method setGroupId(string $groupId) + * @method setClass(string $class) + * @method getGroupId(): string + * @method getClass(): string + */ +class AuthorizedGroup extends Entity implements \JsonSerializable { + /** @var string $group_id */ + protected $groupId; + + /** @var string $class */ + protected $class; + + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'group_id' => $this->groupId, + 'class' => $this->class + ]; + } +} diff --git a/lib/private/Settings/AuthorizedGroupMapper.php b/lib/private/Settings/AuthorizedGroupMapper.php new file mode 100644 index 00000000000..b622459426b --- /dev/null +++ b/lib/private/Settings/AuthorizedGroupMapper.php @@ -0,0 +1,111 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Settings; + +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; + +/** + * @template-extends QBMapper<AuthorizedGroup> + */ +class AuthorizedGroupMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'authorized_groups', AuthorizedGroup::class); + } + + /** + * @throws Exception + */ + public function findAllClassesForUser(IUser $user): array { + $qb = $this->db->getQueryBuilder(); + + /** @var IGroupManager $groupManager */ + $groupManager = \OC::$server->get(IGroupManager::class); + $groups = $groupManager->getUserGroups($user); + if (count($groups) === 0) { + return []; + } + + $result = $qb->select('class') + ->from($this->getTableName(), 'auth') + ->where($qb->expr()->in('group_id', array_map(function (IGroup $group) use ($qb) { + return $qb->createNamedParameter($group->getGID()); + }, $groups), IQueryBuilder::PARAM_STR)) + ->executeQuery(); + + $classes = []; + while ($row = $result->fetch()) { + $classes[] = $row['class']; + } + $result->closeCursor(); + return $classes; + } + + /** + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws \OCP\DB\Exception + */ + public function find(int $id): AuthorizedGroup { + $queryBuilder = $this->db->getQueryBuilder(); + $queryBuilder->select('*') + ->from($this->getTableName()) + ->where($queryBuilder->expr()->eq('id', $queryBuilder->createNamedParameter($id))); + /** @var AuthorizedGroup $authorizedGroup */ + $authorizedGroup = $this->findEntity($queryBuilder); + return $authorizedGroup; + } + + /** + * Get all the authorizations stored in the database. + * + * @return AuthorizedGroup[] + * @throws \OCP\DB\Exception + */ + public function findAll(): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*')->from($this->getTableName()); + return $this->findEntities($qb); + } + + public function findByGroupIdAndClass(string $groupId, string $class) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($groupId))) + ->andWhere($qb->expr()->eq('class', $qb->createNamedParameter($class))); + return $this->findEntity($qb); + } + + /** + * @return Entity[] + * @throws \OCP\DB\Exception + */ + public function findExistingGroupsForClass(string $class): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('class', $qb->createNamedParameter($class))); + return $this->findEntities($qb); + } + + /** + * @throws Exception + */ + public function removeGroup(string $gid) { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($gid))) + ->executeStatement(); + } +} diff --git a/lib/private/Settings/DeclarativeManager.php b/lib/private/Settings/DeclarativeManager.php new file mode 100644 index 00000000000..534b4b19984 --- /dev/null +++ b/lib/private/Settings/DeclarativeManager.php @@ -0,0 +1,482 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Settings; + +use Exception; +use OC\AppFramework\Bootstrap\Coordinator; +use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\Security\ICrypto; +use OCP\Server; +use OCP\Settings\DeclarativeSettingsTypes; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; +use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; +use OCP\Settings\IDeclarativeManager; +use OCP\Settings\IDeclarativeSettingsForm; +use OCP\Settings\IDeclarativeSettingsFormWithHandlers; +use Psr\Log\LoggerInterface; + +/** + * @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsStorageType from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsSectionType from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithValues from IDeclarativeSettingsForm + * @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm + */ +class DeclarativeManager implements IDeclarativeManager { + + /** @var array<string, list<IDeclarativeSettingsForm>> */ + private array $declarativeForms = []; + + /** + * @var array<string, list<DeclarativeSettingsFormSchemaWithoutValues>> + */ + private array $appSchemas = []; + + public function __construct( + private IEventDispatcher $eventDispatcher, + private IGroupManager $groupManager, + private Coordinator $coordinator, + private IConfig $config, + private IAppConfig $appConfig, + private LoggerInterface $logger, + private ICrypto $crypto, + ) { + } + + /** + * @inheritdoc + */ + public function registerSchema(string $app, array $schema): void { + $this->appSchemas[$app] ??= []; + + if (!$this->validateSchema($app, $schema)) { + throw new Exception('Invalid schema. Please check the logs for more details.'); + } + + foreach ($this->appSchemas[$app] as $otherSchema) { + if ($otherSchema['id'] === $schema['id']) { + throw new Exception('Duplicate form IDs detected: ' . $schema['id']); + } + } + + $fieldIDs = array_map(fn ($field) => $field['id'], $schema['fields']); + $otherFieldIDs = array_merge(...array_map(fn ($schema) => array_map(fn ($field) => $field['id'], $schema['fields']), $this->appSchemas[$app])); + $intersectionFieldIDs = array_intersect($fieldIDs, $otherFieldIDs); + if (count($intersectionFieldIDs) > 0) { + throw new Exception('Non unique field IDs detected: ' . join(', ', $intersectionFieldIDs)); + } + + $this->appSchemas[$app][] = $schema; + } + + /** + * @inheritdoc + */ + public function loadSchemas(): void { + if (empty($this->declarativeForms)) { + $declarativeSettings = $this->coordinator->getRegistrationContext()->getDeclarativeSettings(); + foreach ($declarativeSettings as $declarativeSetting) { + $app = $declarativeSetting->getAppId(); + /** @var IDeclarativeSettingsForm $declarativeForm */ + $declarativeForm = Server::get($declarativeSetting->getService()); + $this->registerSchema($app, $declarativeForm->getSchema()); + $this->declarativeForms[$app][] = $declarativeForm; + } + } + + $this->eventDispatcher->dispatchTyped(new DeclarativeSettingsRegisterFormEvent($this)); + } + + /** + * @inheritdoc + */ + public function getFormIDs(IUser $user, string $type, string $section): array { + $isAdmin = $this->groupManager->isAdmin($user->getUID()); + /** @var array<string, list<string>> $formIds */ + $formIds = []; + + foreach ($this->appSchemas as $app => $schemas) { + $ids = []; + usort($schemas, [$this, 'sortSchemasByPriorityCallback']); + foreach ($schemas as $schema) { + if ($schema['section_type'] === DeclarativeSettingsTypes::SECTION_TYPE_ADMIN && !$isAdmin) { + continue; + } + if ($schema['section_type'] === $type && $schema['section_id'] === $section) { + $ids[] = $schema['id']; + } + } + + if (!empty($ids)) { + $formIds[$app] = array_merge($formIds[$app] ?? [], $ids); + } + } + + return $formIds; + } + + /** + * @inheritdoc + * @throws Exception + */ + public function getFormsWithValues(IUser $user, ?string $type, ?string $section): array { + $isAdmin = $this->groupManager->isAdmin($user->getUID()); + $forms = []; + + foreach ($this->appSchemas as $app => $schemas) { + foreach ($schemas as $schema) { + if ($type !== null && $schema['section_type'] !== $type) { + continue; + } + if ($section !== null && $schema['section_id'] !== $section) { + continue; + } + // If listing all fields skip the admin fields which a non-admin user has no access to + if ($type === null && $schema['section_type'] === 'admin' && !$isAdmin) { + continue; + } + + $s = $schema; + $s['app'] = $app; + + foreach ($s['fields'] as &$field) { + $field['value'] = $this->getValue($user, $app, $schema['id'], $field['id']); + } + unset($field); + + /** @var DeclarativeSettingsFormSchemaWithValues $s */ + $forms[] = $s; + } + } + + usort($forms, [$this, 'sortSchemasByPriorityCallback']); + + return $forms; + } + + private function sortSchemasByPriorityCallback(mixed $a, mixed $b): int { + if ($a['priority'] === $b['priority']) { + return 0; + } + return $a['priority'] > $b['priority'] ? -1 : 1; + } + + /** + * @return DeclarativeSettingsStorageType + */ + private function getStorageType(string $app, string $fieldId): string { + if (array_key_exists($app, $this->appSchemas)) { + foreach ($this->appSchemas[$app] as $schema) { + foreach ($schema['fields'] as $field) { + if ($field['id'] == $fieldId) { + if (array_key_exists('storage_type', $field)) { + return $field['storage_type']; + } + } + } + + if (array_key_exists('storage_type', $schema)) { + return $schema['storage_type']; + } + } + } + + return DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL; + } + + /** + * @return DeclarativeSettingsSectionType + * @throws Exception + */ + private function getSectionType(string $app, string $fieldId): string { + if (array_key_exists($app, $this->appSchemas)) { + foreach ($this->appSchemas[$app] as $schema) { + foreach ($schema['fields'] as $field) { + if ($field['id'] == $fieldId) { + return $schema['section_type']; + } + } + } + } + + throw new Exception('Unknown fieldId "' . $fieldId . '"'); + } + + /** + * @psalm-param DeclarativeSettingsSectionType $sectionType + * @throws NotAdminException + */ + private function assertAuthorized(IUser $user, string $sectionType): void { + if ($sectionType === 'admin' && !$this->groupManager->isAdmin($user->getUID())) { + throw new NotAdminException('Logged in user does not have permission to access these settings.'); + } + } + + /** + * @return DeclarativeSettingsValueTypes + * @throws Exception + * @throws NotAdminException + */ + private function getValue(IUser $user, string $app, string $formId, string $fieldId): mixed { + $sectionType = $this->getSectionType($app, $fieldId); + $this->assertAuthorized($user, $sectionType); + + $storageType = $this->getStorageType($app, $fieldId); + switch ($storageType) { + case DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL: + $form = $this->getForm($app, $formId); + if ($form !== null && $form instanceof IDeclarativeSettingsFormWithHandlers) { + return $form->getValue($fieldId, $user); + } + $event = new DeclarativeSettingsGetValueEvent($user, $app, $formId, $fieldId); + $this->eventDispatcher->dispatchTyped($event); + return $event->getValue(); + case DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL: + return $this->getInternalValue($user, $app, $formId, $fieldId); + default: + throw new Exception('Unknown storage type "' . $storageType . '"'); + } + } + + /** + * @inheritdoc + */ + public function setValue(IUser $user, string $app, string $formId, string $fieldId, mixed $value): void { + $sectionType = $this->getSectionType($app, $fieldId); + $this->assertAuthorized($user, $sectionType); + + $storageType = $this->getStorageType($app, $fieldId); + switch ($storageType) { + case DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL: + $form = $this->getForm($app, $formId); + if ($form !== null && $form instanceof IDeclarativeSettingsFormWithHandlers) { + $form->setValue($fieldId, $value, $user); + break; + } + // fall back to event handling + $this->eventDispatcher->dispatchTyped(new DeclarativeSettingsSetValueEvent($user, $app, $formId, $fieldId, $value)); + break; + case DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL: + $this->saveInternalValue($user, $app, $formId, $fieldId, $value); + break; + default: + throw new Exception('Unknown storage type "' . $storageType . '"'); + } + } + + /** + * If a declarative setting was registered as a form and not just a schema + * then this will yield the registering form. + */ + private function getForm(string $app, string $formId): ?IDeclarativeSettingsForm { + $allForms = $this->declarativeForms[$app] ?? []; + foreach ($allForms as $form) { + if ($form->getSchema()['id'] === $formId) { + return $form; + } + } + return null; + } + + private function getInternalValue(IUser $user, string $app, string $formId, string $fieldId): mixed { + $sectionType = $this->getSectionType($app, $fieldId); + $defaultValue = $this->getDefaultValue($app, $formId, $fieldId); + + $field = $this->getSchemaField($app, $formId, $fieldId); + $isSensitive = $field !== null && isset($field['sensitive']) && $field['sensitive'] === true; + + switch ($sectionType) { + case DeclarativeSettingsTypes::SECTION_TYPE_ADMIN: + $value = $this->config->getAppValue($app, $fieldId, $defaultValue); + break; + case DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL: + $value = $this->config->getUserValue($user->getUID(), $app, $fieldId, $defaultValue); + break; + default: + throw new Exception('Unknown section type "' . $sectionType . '"'); + } + if ($isSensitive && $value !== '') { + try { + $value = $this->crypto->decrypt($value); + } catch (Exception $e) { + $this->logger->warning('Failed to decrypt sensitive value for field {field} in app {app}: {message}', [ + 'field' => $fieldId, + 'app' => $app, + 'message' => $e->getMessage(), + ]); + $value = $defaultValue; + } + } + return $value; + } + + private function saveInternalValue(IUser $user, string $app, string $formId, string $fieldId, mixed $value): void { + $sectionType = $this->getSectionType($app, $fieldId); + + $field = $this->getSchemaField($app, $formId, $fieldId); + if ($field !== null && isset($field['sensitive']) && $field['sensitive'] === true && $value !== '' && $value !== 'dummySecret') { + try { + $value = $this->crypto->encrypt($value); + } catch (Exception $e) { + $this->logger->warning('Failed to decrypt sensitive value for field {field} in app {app}: {message}', [ + 'field' => $fieldId, + 'app' => $app, + 'message' => $e->getMessage()] + ); + throw new Exception('Failed to encrypt sensitive value'); + } + } + + switch ($sectionType) { + case DeclarativeSettingsTypes::SECTION_TYPE_ADMIN: + $this->appConfig->setValueString($app, $fieldId, $value); + break; + case DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL: + $this->config->setUserValue($user->getUID(), $app, $fieldId, $value); + break; + default: + throw new Exception('Unknown section type "' . $sectionType . '"'); + } + } + + private function getSchemaField(string $app, string $formId, string $fieldId): ?array { + $form = $this->getForm($app, $formId); + if ($form !== null) { + foreach ($form->getSchema()['fields'] as $field) { + if ($field['id'] === $fieldId) { + return $field; + } + } + } + foreach ($this->appSchemas[$app] ?? [] as $schema) { + if ($schema['id'] === $formId) { + foreach ($schema['fields'] as $field) { + if ($field['id'] === $fieldId) { + return $field; + } + } + } + } + return null; + } + + private function getDefaultValue(string $app, string $formId, string $fieldId): mixed { + foreach ($this->appSchemas[$app] as $schema) { + if ($schema['id'] === $formId) { + foreach ($schema['fields'] as $field) { + if ($field['id'] === $fieldId) { + if (isset($field['default'])) { + if (is_array($field['default'])) { + return json_encode($field['default']); + } + return $field['default']; + } + } + } + } + } + return null; + } + + private function validateSchema(string $appId, array $schema): bool { + if (!isset($schema['id'])) { + $this->logger->warning('Attempt to register a declarative settings schema with no id', ['app' => $appId]); + return false; + } + $formId = $schema['id']; + if (!isset($schema['section_type'])) { + $this->logger->warning('Declarative settings: missing section_type', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!in_array($schema['section_type'], [DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL])) { + $this->logger->warning('Declarative settings: invalid section_type', ['app' => $appId, 'form_id' => $formId, 'section_type' => $schema['section_type']]); + return false; + } + if (!isset($schema['section_id'])) { + $this->logger->warning('Declarative settings: missing section_id', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!isset($schema['storage_type'])) { + $this->logger->warning('Declarative settings: missing storage_type', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!in_array($schema['storage_type'], [DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL, DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL])) { + $this->logger->warning('Declarative settings: invalid storage_type', ['app' => $appId, 'form_id' => $formId, 'storage_type' => $schema['storage_type']]); + return false; + } + if (!isset($schema['title'])) { + $this->logger->warning('Declarative settings: missing title', ['app' => $appId, 'form_id' => $formId]); + return false; + } + if (!isset($schema['fields']) || !is_array($schema['fields'])) { + $this->logger->warning('Declarative settings: missing or invalid fields', ['app' => $appId, 'form_id' => $formId]); + return false; + } + foreach ($schema['fields'] as $field) { + if (!isset($field['id'])) { + $this->logger->warning('Declarative settings: missing field id', ['app' => $appId, 'form_id' => $formId, 'field' => $field]); + return false; + } + $fieldId = $field['id']; + if (!isset($field['title'])) { + $this->logger->warning('Declarative settings: missing field title', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + if (!isset($field['type'])) { + $this->logger->warning('Declarative settings: missing field type', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + if (!in_array($field['type'], [ + DeclarativeSettingsTypes::MULTI_SELECT, DeclarativeSettingsTypes::MULTI_CHECKBOX, DeclarativeSettingsTypes::RADIO, + DeclarativeSettingsTypes::SELECT, DeclarativeSettingsTypes::CHECKBOX, + DeclarativeSettingsTypes::URL, DeclarativeSettingsTypes::EMAIL, DeclarativeSettingsTypes::NUMBER, + DeclarativeSettingsTypes::TEL, DeclarativeSettingsTypes::TEXT, DeclarativeSettingsTypes::PASSWORD, + ])) { + $this->logger->warning('Declarative settings: invalid field type', [ + 'app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId, 'type' => $field['type'], + ]); + return false; + } + if (isset($field['sensitive']) && $field['sensitive'] === true && !in_array($field['type'], [DeclarativeSettingsTypes::TEXT, DeclarativeSettingsTypes::PASSWORD])) { + $this->logger->warning('Declarative settings: sensitive field type is supported only for TEXT and PASSWORD types ({app}, {form_id}, {field_id})', [ + 'app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId, + ]); + return false; + } + if (!$this->validateField($appId, $formId, $field)) { + return false; + } + } + + return true; + } + + private function validateField(string $appId, string $formId, array $field): bool { + $fieldId = $field['id']; + if (in_array($field['type'], [ + DeclarativeSettingsTypes::MULTI_SELECT, DeclarativeSettingsTypes::MULTI_CHECKBOX, DeclarativeSettingsTypes::RADIO, + DeclarativeSettingsTypes::SELECT + ])) { + if (!isset($field['options'])) { + $this->logger->warning('Declarative settings: missing field options', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + if (!is_array($field['options'])) { + $this->logger->warning('Declarative settings: field options should be an array', ['app' => $appId, 'form_id' => $formId, 'field_id' => $fieldId]); + return false; + } + } + return true; + } +} diff --git a/lib/private/Settings/Manager.php b/lib/private/Settings/Manager.php new file mode 100644 index 00000000000..78dc64c3c3f --- /dev/null +++ b/lib/private/Settings/Manager.php @@ -0,0 +1,352 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Settings; + +use Closure; +use OCP\AppFramework\QueryException; +use OCP\Group\ISubAdmin; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IServerContainer; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\L10N\IFactory; +use OCP\Settings\IIconSection; +use OCP\Settings\IManager; +use OCP\Settings\ISettings; +use OCP\Settings\ISubAdminSettings; +use Psr\Log\LoggerInterface; + +class Manager implements IManager { + /** @var LoggerInterface */ + private $log; + + /** @var IL10N */ + private $l; + + /** @var IFactory */ + private $l10nFactory; + + /** @var IURLGenerator */ + private $url; + + /** @var IServerContainer */ + private $container; + + /** @var AuthorizedGroupMapper $mapper */ + private $mapper; + + /** @var IGroupManager $groupManager */ + private $groupManager; + + /** @var ISubAdmin $subAdmin */ + private $subAdmin; + + public function __construct( + LoggerInterface $log, + IFactory $l10nFactory, + IURLGenerator $url, + IServerContainer $container, + AuthorizedGroupMapper $mapper, + IGroupManager $groupManager, + ISubAdmin $subAdmin, + ) { + $this->log = $log; + $this->l10nFactory = $l10nFactory; + $this->url = $url; + $this->container = $container; + $this->mapper = $mapper; + $this->groupManager = $groupManager; + $this->subAdmin = $subAdmin; + } + + /** @var array<self::SETTINGS_*, list<class-string<IIconSection>>> */ + protected $sectionClasses = []; + + /** @var array<self::SETTINGS_*, array<string, IIconSection>> */ + protected $sections = []; + + /** + * @inheritdoc + */ + public function registerSection(string $type, string $section) { + if (!isset($this->sectionClasses[$type])) { + $this->sectionClasses[$type] = []; + } + + $this->sectionClasses[$type][] = $section; + } + + /** + * @psalm-param self::SETTINGS_* $type + * + * @return IIconSection[] + */ + protected function getSections(string $type): array { + if (!isset($this->sections[$type])) { + $this->sections[$type] = []; + } + + if (!isset($this->sectionClasses[$type])) { + return $this->sections[$type]; + } + + foreach (array_unique($this->sectionClasses[$type]) as $index => $class) { + try { + /** @var IIconSection $section */ + $section = $this->container->get($class); + } catch (QueryException $e) { + $this->log->info($e->getMessage(), ['exception' => $e]); + continue; + } + + $sectionID = $section->getID(); + + if (!$this->isKnownDuplicateSectionId($sectionID) && isset($this->sections[$type][$sectionID])) { + $e = new \InvalidArgumentException('Section with the same ID already registered: ' . $sectionID . ', class: ' . $class); + $this->log->info($e->getMessage(), ['exception' => $e]); + continue; + } + + $this->sections[$type][$sectionID] = $section; + + unset($this->sectionClasses[$type][$index]); + } + + return $this->sections[$type]; + } + + /** + * @inheritdoc + */ + public function getSection(string $type, string $sectionId): ?IIconSection { + if (isset($this->sections[$type]) && isset($this->sections[$type][$sectionId])) { + return $this->sections[$type][$sectionId]; + } + return null; + } + + protected function isKnownDuplicateSectionId(string $sectionID): bool { + return in_array($sectionID, [ + 'connected-accounts', + 'notifications', + ], true); + } + + /** @var array<class-string<ISettings>, self::SETTINGS_*> */ + protected $settingClasses = []; + + /** @var array<self::SETTINGS_*, array<string, list<ISettings>>> */ + protected $settings = []; + + /** + * @inheritdoc + */ + public function registerSetting(string $type, string $setting) { + $this->settingClasses[$setting] = $type; + } + + /** + * @psalm-param self::SETTINGS_* $type The type of the setting. + * @param string $section + * @param ?Closure $filter optional filter to apply on all loaded ISettings + * + * @return ISettings[] + */ + protected function getSettings(string $type, string $section, ?Closure $filter = null): array { + if (!isset($this->settings[$type])) { + $this->settings[$type] = []; + } + if (!isset($this->settings[$type][$section])) { + $this->settings[$type][$section] = []; + } + + foreach ($this->settingClasses as $class => $settingsType) { + if ($type !== $settingsType) { + continue; + } + + try { + /** @var ISettings $setting */ + $setting = $this->container->get($class); + } catch (QueryException $e) { + $this->log->info($e->getMessage(), ['exception' => $e]); + continue; + } + + if (!$setting instanceof ISettings) { + $e = new \InvalidArgumentException('Invalid settings setting registered (' . $class . ')'); + $this->log->info($e->getMessage(), ['exception' => $e]); + continue; + } + + if ($filter !== null && !$filter($setting)) { + continue; + } + if ($setting->getSection() === null) { + continue; + } + + if (!isset($this->settings[$settingsType][$setting->getSection()])) { + $this->settings[$settingsType][$setting->getSection()] = []; + } + $this->settings[$settingsType][$setting->getSection()][] = $setting; + + unset($this->settingClasses[$class]); + } + + return $this->settings[$type][$section]; + } + + /** + * @inheritdoc + */ + public function getAdminSections(): array { + // built-in sections + $sections = []; + + $appSections = $this->getSections('admin'); + + foreach ($appSections as $section) { + /** @var IIconSection $section */ + if (!isset($sections[$section->getPriority()])) { + $sections[$section->getPriority()] = []; + } + + $sections[$section->getPriority()][] = $section; + } + + ksort($sections); + + return $sections; + } + + /** + * @inheritdoc + */ + public function getAdminSettings(string $section, bool $subAdminOnly = false): array { + if ($subAdminOnly) { + $subAdminSettingsFilter = function (ISettings $settings) { + return $settings instanceof ISubAdminSettings; + }; + $appSettings = $this->getSettings('admin', $section, $subAdminSettingsFilter); + } else { + $appSettings = $this->getSettings('admin', $section); + } + + $settings = []; + foreach ($appSettings as $setting) { + if (!isset($settings[$setting->getPriority()])) { + $settings[$setting->getPriority()] = []; + } + $settings[$setting->getPriority()][] = $setting; + } + + ksort($settings); + return $settings; + } + + /** + * @inheritdoc + */ + public function getPersonalSections(): array { + if ($this->l === null) { + $this->l = $this->l10nFactory->get('lib'); + } + + $sections = []; + + if (count($this->getPersonalSettings('additional')) > 1) { + $sections[98] = [new Section('additional', $this->l->t('Additional settings'), 0, $this->url->imagePath('core', 'actions/settings-dark.svg'))]; + } + + $appSections = $this->getSections('personal'); + + foreach ($appSections as $section) { + /** @var IIconSection $section */ + if (!isset($sections[$section->getPriority()])) { + $sections[$section->getPriority()] = []; + } + + $sections[$section->getPriority()][] = $section; + } + + ksort($sections); + + return $sections; + } + + /** + * @inheritdoc + */ + public function getPersonalSettings(string $section): array { + $settings = []; + $appSettings = $this->getSettings('personal', $section); + + foreach ($appSettings as $setting) { + if (!isset($settings[$setting->getPriority()])) { + $settings[$setting->getPriority()] = []; + } + $settings[$setting->getPriority()][] = $setting; + } + + ksort($settings); + return $settings; + } + + /** + * @inheritdoc + */ + public function getAllowedAdminSettings(string $section, IUser $user): array { + $isAdmin = $this->groupManager->isAdmin($user->getUID()); + if ($isAdmin) { + $appSettings = $this->getSettings('admin', $section); + } else { + $authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user); + if ($this->subAdmin->isSubAdmin($user)) { + $authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) { + return $settings instanceof ISubAdminSettings + || in_array(get_class($settings), $authorizedSettingsClasses) === true; + }; + } else { + $authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) { + return in_array(get_class($settings), $authorizedSettingsClasses) === true; + }; + } + $appSettings = $this->getSettings('admin', $section, $authorizedGroupFilter); + } + + $settings = []; + foreach ($appSettings as $setting) { + if (!isset($settings[$setting->getPriority()])) { + $settings[$setting->getPriority()] = []; + } + $settings[$setting->getPriority()][] = $setting; + } + + ksort($settings); + return $settings; + } + + /** + * @inheritdoc + */ + public function getAllAllowedAdminSettings(IUser $user): array { + $this->getSettings('admin', ''); // Make sure all the settings are loaded + $settings = []; + $authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user); + foreach ($this->settings['admin'] as $section) { + foreach ($section as $setting) { + if (in_array(get_class($setting), $authorizedSettingsClasses) === true) { + $settings[] = $setting; + } + } + } + return $settings; + } +} diff --git a/lib/private/Settings/Section.php b/lib/private/Settings/Section.php new file mode 100644 index 00000000000..6cd8885d2df --- /dev/null +++ b/lib/private/Settings/Section.php @@ -0,0 +1,70 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Settings; + +use OCP\Settings\IIconSection; + +class Section implements IIconSection { + /** @var string */ + private $id; + /** @var string */ + private $name; + /** @var int */ + private $priority; + /** @var string */ + private $icon; + + /** + * @param string $id + * @param string $name + * @param int $priority + * @param string $icon + */ + public function __construct($id, $name, $priority, $icon = '') { + $this->id = $id; + $this->name = $name; + $this->priority = $priority; + $this->icon = $icon; + } + + /** + * @return string The ID of the section. It is supposed to be a lower case string, + * e.g. 'ldap' + */ + public function getID() { + return $this->id; + } + + /** + * @return string The translated name as it should be displayed, e.g. 'LDAP / AD + * integration'. Use the L10N service to translate it. + */ + public function getName() { + return $this->name; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the settings navigation. The sections are arranged in ascending order of + * the priority values. It is required to return a value between 0 and 99. + * + * E.g.: 70 + */ + public function getPriority() { + return $this->priority; + } + + /** + * @return string The relative path to an 16*16 icon describing the section. + * e.g. '/core/img/places/files.svg' + * + * @since 12 + */ + public function getIcon() { + return $this->icon; + } +} |