diff options
author | jld3103 <jld3103yt@gmail.com> | 2023-12-07 16:39:16 +0100 |
---|---|---|
committer | Andrey Borysenko <andrey18106x@gmail.com> | 2024-03-12 13:56:54 +0200 |
commit | 4ac2375ca2082750432ccc9cff46bf5888b4db30 (patch) | |
tree | bca24a21f4dfa0184f8e400e9508fc5600ade8d4 /apps | |
parent | c42397358f05aa60ae91ed11e7754fddba182cce (diff) | |
download | nextcloud-server-4ac2375ca2082750432ccc9cff46bf5888b4db30.tar.gz nextcloud-server-4ac2375ca2082750432ccc9cff46bf5888b4db30.zip |
feat: Add declarative settings
Signed-off-by: jld3103 <jld3103yt@gmail.com>
Signed-off-by: Julien Veyssier <julien-nc@posteo.net>
Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
Diffstat (limited to 'apps')
23 files changed, 1736 insertions, 40 deletions
diff --git a/apps/settings/appinfo/routes.php b/apps/settings/appinfo/routes.php index e8a3869fe11..7bb946d1934 100644 --- a/apps/settings/appinfo/routes.php +++ b/apps/settings/appinfo/routes.php @@ -81,5 +81,9 @@ return [ ['name' => 'WebAuthn#deleteRegistration', 'url' => '/settings/api/personal/webauthn/registration/{id}', 'verb' => 'DELETE' , 'root' => ''], ['name' => 'Reasons#getPdf', 'url' => '/settings/download/reasons', 'verb' => 'GET', 'root' => ''], - ] + ], + 'ocs' => [ + ['name' => 'DeclarativeSettings#setValue', 'url' => '/settings/api/declarative/value', 'verb' => 'POST', 'root' => ''], + ['name' => 'DeclarativeSettings#getForms', 'url' => '/settings/api/declarative/forms', 'verb' => 'GET', 'root' => ''], + ], ]; diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index c3cf0763ed9..b9709c8ad28 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -27,6 +27,7 @@ return array( 'OCA\\Settings\\Controller\\ChangePasswordController' => $baseDir . '/../lib/Controller/ChangePasswordController.php', 'OCA\\Settings\\Controller\\CheckSetupController' => $baseDir . '/../lib/Controller/CheckSetupController.php', 'OCA\\Settings\\Controller\\CommonSettingsTrait' => $baseDir . '/../lib/Controller/CommonSettingsTrait.php', + 'OCA\\Settings\\Controller\\DeclarativeSettingsController' => $baseDir . '/../lib/Controller/DeclarativeSettingsController.php', 'OCA\\Settings\\Controller\\HelpController' => $baseDir . '/../lib/Controller/HelpController.php', 'OCA\\Settings\\Controller\\LogSettingsController' => $baseDir . '/../lib/Controller/LogSettingsController.php', 'OCA\\Settings\\Controller\\MailSettingsController' => $baseDir . '/../lib/Controller/MailSettingsController.php', @@ -43,6 +44,7 @@ return array( 'OCA\\Settings\\Listener\\UserRemovedFromGroupActivityListener' => $baseDir . '/../lib/Listener/UserRemovedFromGroupActivityListener.php', 'OCA\\Settings\\Mailer\\NewUserMailHelper' => $baseDir . '/../lib/Mailer/NewUserMailHelper.php', 'OCA\\Settings\\Middleware\\SubadminMiddleware' => $baseDir . '/../lib/Middleware/SubadminMiddleware.php', + 'OCA\\Settings\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', 'OCA\\Settings\\Search\\AppSearch' => $baseDir . '/../lib/Search/AppSearch.php', 'OCA\\Settings\\Search\\SectionSearch' => $baseDir . '/../lib/Search/SectionSearch.php', 'OCA\\Settings\\Search\\UserSearch' => $baseDir . '/../lib/Search/UserSearch.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index 88f71b026f5..67808ad23f2 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -42,6 +42,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Controller\\ChangePasswordController' => __DIR__ . '/..' . '/../lib/Controller/ChangePasswordController.php', 'OCA\\Settings\\Controller\\CheckSetupController' => __DIR__ . '/..' . '/../lib/Controller/CheckSetupController.php', 'OCA\\Settings\\Controller\\CommonSettingsTrait' => __DIR__ . '/..' . '/../lib/Controller/CommonSettingsTrait.php', + 'OCA\\Settings\\Controller\\DeclarativeSettingsController' => __DIR__ . '/..' . '/../lib/Controller/DeclarativeSettingsController.php', 'OCA\\Settings\\Controller\\HelpController' => __DIR__ . '/..' . '/../lib/Controller/HelpController.php', 'OCA\\Settings\\Controller\\LogSettingsController' => __DIR__ . '/..' . '/../lib/Controller/LogSettingsController.php', 'OCA\\Settings\\Controller\\MailSettingsController' => __DIR__ . '/..' . '/../lib/Controller/MailSettingsController.php', @@ -58,6 +59,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Listener\\UserRemovedFromGroupActivityListener' => __DIR__ . '/..' . '/../lib/Listener/UserRemovedFromGroupActivityListener.php', 'OCA\\Settings\\Mailer\\NewUserMailHelper' => __DIR__ . '/..' . '/../lib/Mailer/NewUserMailHelper.php', 'OCA\\Settings\\Middleware\\SubadminMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SubadminMiddleware.php', + 'OCA\\Settings\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', 'OCA\\Settings\\Search\\AppSearch' => __DIR__ . '/..' . '/../lib/Search/AppSearch.php', 'OCA\\Settings\\Search\\SectionSearch' => __DIR__ . '/..' . '/../lib/Search/SectionSearch.php', 'OCA\\Settings\\Search\\UserSearch' => __DIR__ . '/..' . '/../lib/Search/UserSearch.php', diff --git a/apps/settings/composer/composer/installed.php b/apps/settings/composer/composer/installed.php index 1a66c7f2416..d2b87e1bdfd 100644 --- a/apps/settings/composer/composer/installed.php +++ b/apps/settings/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), diff --git a/apps/settings/lib/Controller/AdminSettingsController.php b/apps/settings/lib/Controller/AdminSettingsController.php index 7b0313c9fa7..8da03607a79 100644 --- a/apps/settings/lib/Controller/AdminSettingsController.php +++ b/apps/settings/lib/Controller/AdminSettingsController.php @@ -30,12 +30,14 @@ use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Group\ISubAdmin; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; use OCP\Settings\IManager as ISettingsManager; use OCP\Template; @@ -50,7 +52,9 @@ class AdminSettingsController extends Controller { ISettingsManager $settingsManager, IUserSession $userSession, IGroupManager $groupManager, - ISubAdmin $subAdmin + ISubAdmin $subAdmin, + IDeclarativeManager $declarativeSettingsManager, + IInitialState $initialState, ) { parent::__construct($appName, $request); $this->navigationManager = $navigationManager; @@ -58,6 +62,8 @@ class AdminSettingsController extends Controller { $this->userSession = $userSession; $this->groupManager = $groupManager; $this->subAdmin = $subAdmin; + $this->declarativeSettingsManager = $declarativeSettingsManager; + $this->initialState = $initialState; } /** @@ -80,7 +86,8 @@ class AdminSettingsController extends Controller { $user = $this->userSession->getUser(); $isSubAdmin = !$this->groupManager->isAdmin($user->getUID()) && $this->subAdmin->isSubAdmin($user); $settings = $this->settingsManager->getAllowedAdminSettings($section, $user); - if (empty($settings)) { + $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); diff --git a/apps/settings/lib/Controller/CommonSettingsTrait.php b/apps/settings/lib/Controller/CommonSettingsTrait.php index 5d683d7d824..ab51deadfc3 100644 --- a/apps/settings/lib/Controller/CommonSettingsTrait.php +++ b/apps/settings/lib/Controller/CommonSettingsTrait.php @@ -26,17 +26,26 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OCA\Settings\Controller; +use OCA\Settings\AppInfo\Application; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Group\ISubAdmin; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; +use OCP\Settings\IDeclarativeSettingsForm; use OCP\Settings\IIconSection; use OCP\Settings\IManager as ISettingsManager; use OCP\Settings\ISettings; +use OCP\Util; +/** + * @psalm-import-type DeclarativeSettingsFormField from IDeclarativeSettingsForm + */ trait CommonSettingsTrait { /** @var ISettingsManager */ @@ -54,28 +63,26 @@ trait CommonSettingsTrait { /** @var ISubAdmin */ private $subAdmin; + private IDeclarativeManager $declarativeSettingsManager; + + /** @var IInitialState */ + private $initialState; + /** * @return array{forms: array{personal: array, admin: array}} */ private function getNavigationParameters(string $currentType, string $currentSection): array { - $templateParameters = [ - 'personal' => $this->formatPersonalSections($currentType, $currentSection), - 'admin' => [] - ]; - - $templateParameters['admin'] = $this->formatAdminSections( - $currentType, - $currentSection - ); - return [ - 'forms' => $templateParameters + 'forms' => [ + 'personal' => $this->formatPersonalSections($currentType, $currentSection), + 'admin' => $this->formatAdminSections($currentType, $currentSection), + ], ]; } /** * @param IIconSection[][] $sections - * @psam-param 'admin'|'personal' $type + * @psalm-param 'admin'|'personal' $type * @return list<array{anchor: string, section-name: string, active: bool, icon: string}> */ protected function formatSections(array $sections, string $currentSection, string $type, string $currentType): array { @@ -87,7 +94,11 @@ trait CommonSettingsTrait { } elseif ($type === 'personal') { $settings = $this->settingsManager->getPersonalSettings($section->getID()); } - if (empty($settings) && !($section->getID() === 'additional' && count(\OC_App::getForms('admin')) > 0)) { + + /** @psalm-suppress PossiblyNullArgument */ + $declarativeFormIDs = $this->declarativeSettingsManager->getFormIDs($this->userSession->getUser(), $type, $section->getID()); + + if (empty($settings) && empty($declarativeFormIDs) && !($section->getID() === 'additional' && count(\OC_App::getForms('admin')) > 0)) { continue; } @@ -107,14 +118,14 @@ trait CommonSettingsTrait { return $templateParameters; } - protected function formatPersonalSections(string $currentType, string $currentSections): array { + protected function formatPersonalSections(string $currentType, string $currentSection): array { $sections = $this->settingsManager->getPersonalSections(); - return $this->formatSections($sections, $currentSections, 'personal', $currentType); + return $this->formatSections($sections, $currentSection, 'personal', $currentType); } - protected function formatAdminSections(string $currentType, string $currentSections): array { + protected function formatAdminSections(string $currentType, string $currentSection): array { $sections = $this->settingsManager->getAdminSections(); - return $this->formatSections($sections, $currentSections, 'admin', $currentType); + return $this->formatSections($sections, $currentSection, 'admin', $currentType); } /** @@ -133,6 +144,9 @@ trait CommonSettingsTrait { return ['content' => $html]; } + /** + * @psalm-param 'admin'|'personal' $type + */ private function getIndexResponse(string $type, string $section): TemplateResponse { if ($type === 'personal') { if ($section === 'theming') { @@ -144,9 +158,24 @@ trait CommonSettingsTrait { $this->navigationManager->setActiveEntry('admin_settings'); } + $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)); + } + 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)); + } + $activeSection = $this->settingsManager->getSection($type, $section); if ($activeSection) { $templateParams['pageTitle'] = $activeSection->getName(); diff --git a/apps/settings/lib/Controller/DeclarativeSettingsController.php b/apps/settings/lib/Controller/DeclarativeSettingsController.php new file mode 100644 index 00000000000..71347066290 --- /dev/null +++ b/apps/settings/lib/Controller/DeclarativeSettingsController.php @@ -0,0 +1,105 @@ +<?php +/** + * @copyright Copyright (c) 2023 Kate Döen <kate.doeen@nextcloud.com> + * + * @author Kate Döen <kate.doeen@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Settings\Controller; + +use Exception; +use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; +use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException; +use OCA\Settings\ResponseDefinitions; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; +use Psr\Log\LoggerInterface; + +/** + * @psalm-import-type SettingsDeclarativeForm from ResponseDefinitions + */ +class DeclarativeSettingsController extends OCSController { + public function __construct( + string $appName, + IRequest $request, + private IUserSession $userSession, + private IDeclarativeManager $declarativeManager, + private LoggerInterface $logger, + ) { + parent::__construct($appName, $request); + } + + /** + * Sets a declarative settings value + * + * @param string $app ID of the app + * @param string $formId ID of the form + * @param string $fieldId ID of the field + * @param mixed $value Value to be saved + * @return DataResponse<Http::STATUS_OK, null, array{}> + * @throws NotLoggedInException Not logged in or not an admin user + * @throws NotAdminException Not logged in or not an admin user + * @throws OCSBadRequestException Invalid arguments to save value + * + * 200: Value set successfully + */ + #[NoAdminRequired] + public function setValue(string $app, string $formId, string $fieldId, mixed $value): DataResponse { + $user = $this->userSession->getUser(); + if ($user === null) { + throw new NotLoggedInException(); + } + + try { + $this->declarativeManager->loadSchemas(); + $this->declarativeManager->setValue($user, $app, $formId, $fieldId, $value); + return new DataResponse(null); + } catch (NotAdminException $e) { + throw $e; + } catch (Exception $e) { + $this->logger->error('Failed to set declarative settings value: ' . $e->getMessage()); + throw new OCSBadRequestException(); + } + } + + /** + * Gets all declarative forms with the values prefilled. + * + * @return DataResponse<Http::STATUS_OK, list<SettingsDeclarativeForm>, array{}> + * @throws NotLoggedInException + * @NoSubAdminRequired + * + * 200: Forms returned + */ + #[NoAdminRequired] + public function getForms(): DataResponse { + $user = $this->userSession->getUser(); + if ($user === null) { + throw new NotLoggedInException(); + } + $this->declarativeManager->loadSchemas(); + return new DataResponse($this->declarativeManager->getFormsWithValues($user, null, null)); + } +} diff --git a/apps/settings/lib/Controller/PersonalSettingsController.php b/apps/settings/lib/Controller/PersonalSettingsController.php index 7d219f5c165..57da74cd99a 100644 --- a/apps/settings/lib/Controller/PersonalSettingsController.php +++ b/apps/settings/lib/Controller/PersonalSettingsController.php @@ -29,11 +29,13 @@ namespace OCA\Settings\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Group\ISubAdmin; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IRequest; use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; use OCP\Settings\IManager as ISettingsManager; use OCP\Template; @@ -48,7 +50,9 @@ class PersonalSettingsController extends Controller { ISettingsManager $settingsManager, IUserSession $userSession, IGroupManager $groupManager, - ISubAdmin $subAdmin + ISubAdmin $subAdmin, + IDeclarativeManager $declarativeSettingsManager, + IInitialState $initialState, ) { parent::__construct($appName, $request); $this->navigationManager = $navigationManager; @@ -56,6 +60,8 @@ class PersonalSettingsController extends Controller { $this->userSession = $userSession; $this->subAdmin = $subAdmin; $this->groupManager = $groupManager; + $this->declarativeSettingsManager = $declarativeSettingsManager; + $this->initialState = $initialState; } /** diff --git a/apps/settings/lib/ResponseDefinitions.php b/apps/settings/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..1887a04de08 --- /dev/null +++ b/apps/settings/lib/ResponseDefinitions.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2024 Kate Döen <kate.doeen@nextcloud.com> + * + * @author Kate Döen <kate.doeen@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Settings; + +/** + * @psalm-type SettingsDeclarativeFormField = array{ + * id: string, + * title: string, + * description?: string, + * type: 'text'|'password'|'email'|'tel'|'url'|'number'|'checkbox'|'multi-checkbox'|'radio'|'select'|'multi-select', + * placeholder?: string, + * label?: string, + * default: mixed, + * options?: list<string|array{name: string, value: mixed}>, + * value: string|int|float|bool|list<string>, + * } + * + * @psalm-type SettingsDeclarativeForm = array{ + * id: string, + * priority: int, + * section_type: 'admin'|'personal', + * section_id: string, + * storage_type: 'internal'|'external', + * title: string, + * description?: string, + * doc_url?: string, + * app: string, + * fields: list<SettingsDeclarativeFormField>, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/settings/openapi-administration.json b/apps/settings/openapi-administration.json new file mode 100644 index 00000000000..5d39237779a --- /dev/null +++ b/apps/settings/openapi-administration.json @@ -0,0 +1,65 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "settings-administration", + "version": "0.0.1", + "description": "Nextcloud settings", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": {} + }, + "paths": { + "/index.php/settings/admin/log/download": { + "get": { + "operationId": "log_settings-download", + "summary": "download logfile", + "description": "This endpoint requires admin access", + "tags": [ + "log_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "responses": { + "200": { + "description": "Logfile returned", + "headers": { + "Content-Disposition": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + } + }, + "tags": [] +}
\ No newline at end of file diff --git a/apps/settings/openapi-full.json b/apps/settings/openapi-full.json new file mode 100644 index 00000000000..99a2b8b97c1 --- /dev/null +++ b/apps/settings/openapi-full.json @@ -0,0 +1,433 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "settings-full", + "version": "0.0.1", + "description": "Nextcloud settings", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "DeclarativeForm": { + "type": "object", + "required": [ + "id", + "priority", + "section_type", + "section_id", + "storage_type", + "title", + "app", + "fields" + ], + "properties": { + "id": { + "type": "string" + }, + "priority": { + "type": "integer", + "format": "int64" + }, + "section_type": { + "type": "string", + "enum": [ + "admin", + "personal" + ] + }, + "section_id": { + "type": "string" + }, + "storage_type": { + "type": "string", + "enum": [ + "internal", + "external" + ] + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "doc_url": { + "type": "string" + }, + "app": { + "type": "string" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarativeFormField" + } + } + } + }, + "DeclarativeFormField": { + "type": "object", + "required": [ + "id", + "title", + "type", + "default", + "value" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "text", + "password", + "email", + "tel", + "url", + "number", + "checkbox", + "multi-checkbox", + "radio", + "select", + "multi-select" + ] + }, + "placeholder": { + "type": "string" + }, + "label": { + "type": "string" + }, + "default": { + "type": "object" + }, + "options": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "object" + } + } + } + ] + } + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer", + "format": "int64" + }, + { + "type": "number", + "format": "float" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } + }, + "paths": { + "/index.php/settings/admin/log/download": { + "get": { + "operationId": "log_settings-download", + "summary": "download logfile", + "description": "This endpoint requires admin access", + "tags": [ + "log_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "responses": { + "200": { + "description": "Logfile returned", + "headers": { + "Content-Disposition": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/ocs/v2.php/settings/api/declarative/value": { + "post": { + "operationId": "declarative_settings-set-value", + "summary": "Sets a declarative settings value", + "tags": [ + "declarative_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "app", + "in": "query", + "description": "ID of the app", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "formId", + "in": "query", + "description": "ID of the form", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fieldId", + "in": "query", + "description": "ID of the field", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "value", + "in": "query", + "description": "Value to be saved", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Value set successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + }, + "500": { + "description": "Not logged in or not an admin user", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid arguments to save value", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ocs/v2.php/settings/api/declarative/forms": { + "get": { + "operationId": "declarative_settings-get-forms", + "summary": "Gets all declarative forms with the values prefilled.", + "tags": [ + "declarative_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Forms returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarativeForm" + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "tags": [] +}
\ No newline at end of file diff --git a/apps/settings/openapi.json b/apps/settings/openapi.json index 217a0fae9f7..e9591eaf346 100644 --- a/apps/settings/openapi.json +++ b/apps/settings/openapi.json @@ -19,16 +19,192 @@ "scheme": "bearer" } }, - "schemas": {} + "schemas": { + "DeclarativeForm": { + "type": "object", + "required": [ + "id", + "priority", + "section_type", + "section_id", + "storage_type", + "title", + "app", + "fields" + ], + "properties": { + "id": { + "type": "string" + }, + "priority": { + "type": "integer", + "format": "int64" + }, + "section_type": { + "type": "string", + "enum": [ + "admin", + "personal" + ] + }, + "section_id": { + "type": "string" + }, + "storage_type": { + "type": "string", + "enum": [ + "internal", + "external" + ] + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "doc_url": { + "type": "string" + }, + "app": { + "type": "string" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarativeFormField" + } + } + } + }, + "DeclarativeFormField": { + "type": "object", + "required": [ + "id", + "title", + "type", + "default", + "value" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "text", + "password", + "email", + "tel", + "url", + "number", + "checkbox", + "multi-checkbox", + "radio", + "select", + "multi-select" + ] + }, + "placeholder": { + "type": "string" + }, + "label": { + "type": "string" + }, + "default": { + "type": "object" + }, + "options": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "object" + } + } + } + ] + } + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer", + "format": "int64" + }, + { + "type": "number", + "format": "float" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } }, "paths": { - "/index.php/settings/admin/log/download": { - "get": { - "operationId": "log_settings-download", - "summary": "download logfile", - "description": "This endpoint requires admin access", + "/ocs/v2.php/settings/api/declarative/value": { + "post": { + "operationId": "declarative_settings-set-value", + "summary": "Sets a declarative settings value", "tags": [ - "log_settings" + "declarative_settings" ], "security": [ { @@ -38,21 +214,175 @@ "basic_auth": [] } ], + "parameters": [ + { + "name": "app", + "in": "query", + "description": "ID of the app", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "formId", + "in": "query", + "description": "ID of the form", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fieldId", + "in": "query", + "description": "ID of the field", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "value", + "in": "query", + "description": "Value to be saved", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], "responses": { "200": { - "description": "Logfile returned", - "headers": { - "Content-Disposition": { + "description": "Value set successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + }, + "500": { + "description": "Not logged in or not an admin user", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid arguments to save value", + "content": { + "text/plain": { "schema": { "type": "string" } } - }, + } + } + } + } + }, + "/ocs/v2.php/settings/api/declarative/forms": { + "get": { + "operationId": "declarative_settings-get-forms", + "summary": "Gets all declarative forms with the values prefilled.", + "tags": [ + "declarative_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Forms returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarativeForm" + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", "content": { - "application/octet-stream": { + "text/plain": { "schema": { - "type": "string", - "format": "binary" + "type": "string" } } } diff --git a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue new file mode 100644 index 00000000000..879eb8e62d8 --- /dev/null +++ b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue @@ -0,0 +1,268 @@ +<template> + <NcSettingsSection + class="declarative-settings-section" + :name="t(formApp, form.title)" + :description="t(formApp, form.description)" + :doc-url="form.doc_url || ''"> + <div v-for="formField in formFields" + :key="formField.id" + class="declarative-form-field" + :aria-label="t('settings', '{app}\'s declarative setting field: {name}', { app: formApp, name: t(formApp, formField.title) })" + :class="{ + 'declarative-form-field-text': isTextFormField(formField), + 'declarative-form-field-select': formField.type === 'select', + 'declarative-form-field-multi-select': formField.type === 'multi-select', + 'declarative-form-field-checkbox': formField.type === 'checkbox', + 'declarative-form-field-multi_checkbox': formField.type === 'multi-checkbox', + 'declarative-form-field-radio': formField.type === 'radio' + }"> + + <template v-if="isTextFormField(formField)"> + <div class="input-wrapper"> + <NcInputField + :type="formField.type" + :label="t(formApp, formField.title)" + :value.sync="formFieldsData[formField.id].value" + :placeholder="t(formApp, formField.placeholder)" + @update:value="onChangeDebounced(formField)" + @submit="updateDeclarativeSettingsValue(formField)"/> + </div> + <span class="hint">{{ t(formApp, formField.description) }}</span> + </template> + + <template v-if="formField.type === 'select'"> + <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label> + <div class="input-wrapper"> + <NcSelect + :id="formField.id + '_field'" + :options="formField.options" + :placeholder="t(formApp, formField.placeholder)" + :label-outside="true" + :value="formFieldsData[formField.id].value" + @input="(value) => updateFormFieldDataValue(value, formField, true)"/> + </div> + <span class="hint">{{ t(formApp, formField.description) }}</span> + </template> + + <template v-if="formField.type === 'multi-select'"> + <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label> + <div class="input-wrapper"> + <NcSelect + :id="formField.id + '_field'" + :options="formField.options" + :placeholder="t(formApp, formField.placeholder)" + :multiple="true" + :label-outside="true" + :value="formFieldsData[formField.id].value" + @input="(value) => { + formFieldsData[formField.id].value = value + updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value)) + } + "/> + </div> + <span class="hint">{{ t(formApp, formField.description) }}</span> + </template> + + <template v-if="formField.type === 'checkbox'"> + <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label> + <NcCheckboxRadioSwitch + :id="formField.id + '_field'" + :checked="Boolean(formFieldsData[formField.id].value)" + @update:checked="(value) => { + formField.value = value + updateFormFieldDataValue(+value, formField, true) + } + "> + {{ t(formApp, formField.label) }} + </NcCheckboxRadioSwitch> + <span class="hint">{{ t(formApp, formField.description) }}</span> + </template> + + <template v-if="formField.type === 'multi-checkbox'"> + <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label> + <NcCheckboxRadioSwitch + v-for="option in formField.options" + :id="formField.id + '_field_' + option.value" + :key="option.value" + :checked="formFieldsData[formField.id].value[option.value]" + @update:checked="(value) => { + formFieldsData[formField.id].value[option.value] = value + // Update without re-generating initial formFieldsData.value object as the link to components are lost + updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value)) + } + "> + {{ t(formApp, option.name) }} + </NcCheckboxRadioSwitch> + <span class="hint">{{ t(formApp, formField.description) }}</span> + </template> + + <template v-if="formField.type === 'radio'"> + <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label> + <NcCheckboxRadioSwitch + v-for="option in formField.options" + :key="option.value" + :value="option.value" + type="radio" + :checked="formFieldsData[formField.id].value" + @update:checked="(value) => updateFormFieldDataValue(value, formField, true)"> + {{ t(formApp, option.name) }} + </NcCheckboxRadioSwitch> + <span class="hint">{{ t(formApp, formField.description) }}</span> + </template> + </div> + </NcSettingsSection> +</template> + +<script> +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' +import { showError } from '@nextcloud/dialogs' +import debounce from 'debounce' +import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' +import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' + +export default { + name: 'DeclarativeSection', + components: { + NcSettingsSection, + NcInputField, + NcSelect, + NcCheckboxRadioSwitch, + }, + props: { + form: { + type: Object, + required: true, + }, + }, + data() { + return { + formFieldsData: {}, + } + }, + beforeMount() { + this.initFormFieldsData() + }, + computed: { + formApp() { + return this.form.app || '' + }, + formFields() { + return this.form.fields || [] + }, + }, + methods: { + initFormFieldsData() { + this.form.fields.forEach((formField) => { + if (formField.type === 'checkbox') { + // convert bool to number using unary plus (+) operator + this.$set(formField, 'value', +formField.value) + } + if (formField.type === 'multi-checkbox') { + if (formField.value === '') { + // Init formFieldsData from options + this.$set(formField, 'value', {}) + formField.options.forEach(option => { + this.$set(formField.value, option.value, false) + }) + } else { + this.$set(formField, 'value', JSON.parse(formField.value)) + // Merge possible new options + formField.options.forEach(option => { + if (!formField.value.hasOwnProperty(option.value)) { + this.$set(formField.value, option.value, false) + } + }) + // Remove options that are not in the form anymore + Object.keys(formField.value).forEach(key => { + if (!formField.options.find(option => option.value === key)) { + delete formField.value[key] + } + }) + } + } + if (formField.type === 'multi-select') { + if (formField.value === '') { + // Init empty array for multi-select + this.$set(formField, 'value', []) + } else { + // JSON decode an array of multiple values set + this.$set(formField, 'value', JSON.parse(formField.value)) + } + } + this.$set(this.formFieldsData, formField.id, { + value: formField.value, + }) + }) + }, + + updateFormFieldDataValue(value, formField, update = false) { + this.formFieldsData[formField.id].value = value + if (update) { + this.updateDeclarativeSettingsValue(formField) + } + }, + + updateDeclarativeSettingsValue(formField, value = null) { + try { + return axios.post(generateOcsUrl('settings/api/declarative/value'), { + app: this.formApp, + formId: this.form.id.replace(this.formApp + '_', ''), // Remove app prefix to send clean form id + fieldId: formField.id, + value: value === null ? this.formFieldsData[formField.id].value : value, + }); + } catch (err) { + console.debug(err) + showError(t('settings', 'Failed to save setting')) + } + }, + + onChangeDebounced: debounce(function(formField) { + this.updateDeclarativeSettingsValue(formField) + }, 1000), + + isTextFormField(formField) { + return ['text', 'password', 'email', 'tel', 'url', 'number'].includes(formField.type) + }, + }, +} +</script> + +<style lang="scss" scoped> +.declarative-form-field { + margin: 20px 0; + padding: 10px 0; + + .input-wrapper { + width: 100%; + max-width: 400px; + } + + &:last-child { + border-bottom: none; + } + + .hint { + display: inline-block; + color: var(--color-text-maxcontrast); + margin-left: 8px; + padding-top: 5px; + } + + &-radio, &-multi_checkbox { + max-height: 250px; + overflow-y: auto; + } + + &-multi-select, &-select { + display: flex; + flex-direction: column; + + label { + margin-bottom: 5px; + } + } +} +</style> diff --git a/apps/settings/src/main-declarative-settings-forms.ts b/apps/settings/src/main-declarative-settings-forms.ts new file mode 100644 index 00000000000..0b37fee476e --- /dev/null +++ b/apps/settings/src/main-declarative-settings-forms.ts @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import { loadState } from '@nextcloud/initial-state'; +import { translate as t, translatePlural as n } from '@nextcloud/l10n'; +import DeclarativeSection from './components/DeclarativeSettings/DeclarativeSection.vue'; + +interface DeclarativeFormField { + id: string, + title: string, + description: string, + type: string, + placeholder: string, + label: string, + options: Array<any>|null, + value: any, + default: any, +} + +interface DeclarativeForm { + id: number, + priority: number, + section_type: string, + section_id: string, + storage_type: string, + title: string, + description: string, + doc_url: string, + app: string, + fields: Array<DeclarativeFormField>, +} + +const forms = loadState('settings', 'declarative-settings-forms', []) as Array<DeclarativeForm>; +console.debug('Loaded declarative forms:', forms); + +function renderDeclarativeSettingsSections(forms: Array<DeclarativeForm>): void { + Vue.mixin({ methods: { t, n } }) + const DeclarativeSettingsSection = Vue.extend(<any>DeclarativeSection); + for (const form of forms) { + const el = `#${form.app}_${form.id}` + new DeclarativeSettingsSection({ + el: el, + propsData: { + form, + }, + }) + } +} + +document.addEventListener('DOMContentLoaded', () => { + renderDeclarativeSettingsSections(forms); +}); diff --git a/apps/settings/tests/Controller/AdminSettingsControllerTest.php b/apps/settings/tests/Controller/AdminSettingsControllerTest.php index acdcaa136aa..6a11ceb9fca 100644 --- a/apps/settings/tests/Controller/AdminSettingsControllerTest.php +++ b/apps/settings/tests/Controller/AdminSettingsControllerTest.php @@ -29,12 +29,14 @@ namespace OCA\Settings\Tests\Controller; use OCA\Settings\Controller\AdminSettingsController; use OCA\Settings\Settings\Personal\ServerDevNotice; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Group\ISubAdmin; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; +use OCP\Settings\IDeclarativeManager; use OCP\Settings\IManager; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -62,6 +64,10 @@ class AdminSettingsControllerTest extends TestCase { private $groupManager; /** @var ISubAdmin|MockObject */ private $subAdmin; + /** @var IDeclarativeManager|MockObject */ + private $declarativeSettingsManager; + /** @var IInitialState|MockObject */ + private $initialState; /** @var string */ private $adminUid = 'lololo'; @@ -74,6 +80,8 @@ class AdminSettingsControllerTest extends TestCase { $this->userSession = $this->createMock(IUserSession::class); $this->groupManager = $this->createMock(IGroupManager::class); $this->subAdmin = $this->createMock(ISubAdmin::class); + $this->declarativeSettingsManager = $this->createMock(IDeclarativeManager::class); + $this->initialState = $this->createMock(IInitialState::class); $this->adminSettingsController = new AdminSettingsController( 'settings', @@ -82,7 +90,9 @@ class AdminSettingsControllerTest extends TestCase { $this->settingsManager, $this->userSession, $this->groupManager, - $this->subAdmin + $this->subAdmin, + $this->declarativeSettingsManager, + $this->initialState, ); $user = \OC::$server->getUserManager()->createUser($this->adminUid, 'mylongrandompassword'); @@ -123,6 +133,11 @@ class AdminSettingsControllerTest extends TestCase { ->method('getAllowedAdminSettings') ->with('test') ->willReturn([5 => $this->createMock(ServerDevNotice::class)]); + $this->declarativeSettingsManager + ->expects($this->any()) + ->method('getFormIDs') + ->with($user, 'admin', 'test') + ->willReturn([]); $idx = $this->adminSettingsController->index('test'); diff --git a/apps/testing/composer/composer/autoload_classmap.php b/apps/testing/composer/composer/autoload_classmap.php index cd2a052bbac..079f8877881 100644 --- a/apps/testing/composer/composer/autoload_classmap.php +++ b/apps/testing/composer/composer/autoload_classmap.php @@ -12,9 +12,13 @@ return array( 'OCA\\Testing\\Controller\\ConfigController' => $baseDir . '/../lib/Controller/ConfigController.php', 'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => $baseDir . '/../lib/Controller/RateLimitTestController.php', + 'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/GetDeclarativeSettingsValueListener.php', + 'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => $baseDir . '/../lib/Listener/RegisterDeclarativeSettingsListener.php', + 'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/SetDeclarativeSettingsValueListener.php', 'OCA\\Testing\\Locking\\FakeDBLockingProvider' => $baseDir . '/../lib/Locking/FakeDBLockingProvider.php', 'OCA\\Testing\\Provider\\FakeText2ImageProvider' => $baseDir . '/../lib/Provider/FakeText2ImageProvider.php', 'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => $baseDir . '/../lib/Provider/FakeTextProcessingProvider.php', 'OCA\\Testing\\Provider\\FakeTextProcessingProviderSync' => $baseDir . '/../lib/Provider/FakeTextProcessingProviderSync.php', 'OCA\\Testing\\Provider\\FakeTranslationProvider' => $baseDir . '/../lib/Provider/FakeTranslationProvider.php', + 'OCA\\Testing\\Settings\\DeclarativeSettingsForm' => $baseDir . '/../lib/Settings/DeclarativeSettingsForm.php', ); diff --git a/apps/testing/composer/composer/autoload_static.php b/apps/testing/composer/composer/autoload_static.php index ce07262200a..2332da70da9 100644 --- a/apps/testing/composer/composer/autoload_static.php +++ b/apps/testing/composer/composer/autoload_static.php @@ -27,11 +27,15 @@ class ComposerStaticInitTesting 'OCA\\Testing\\Controller\\ConfigController' => __DIR__ . '/..' . '/../lib/Controller/ConfigController.php', 'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => __DIR__ . '/..' . '/../lib/Controller/RateLimitTestController.php', + 'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/GetDeclarativeSettingsValueListener.php', + 'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => __DIR__ . '/..' . '/../lib/Listener/RegisterDeclarativeSettingsListener.php', + 'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/SetDeclarativeSettingsValueListener.php', 'OCA\\Testing\\Locking\\FakeDBLockingProvider' => __DIR__ . '/..' . '/../lib/Locking/FakeDBLockingProvider.php', 'OCA\\Testing\\Provider\\FakeText2ImageProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeText2ImageProvider.php', 'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeTextProcessingProvider.php', 'OCA\\Testing\\Provider\\FakeTextProcessingProviderSync' => __DIR__ . '/..' . '/../lib/Provider/FakeTextProcessingProviderSync.php', 'OCA\\Testing\\Provider\\FakeTranslationProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeTranslationProvider.php', + 'OCA\\Testing\\Settings\\DeclarativeSettingsForm' => __DIR__ . '/..' . '/../lib/Settings/DeclarativeSettingsForm.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/testing/composer/composer/installed.php b/apps/testing/composer/composer/installed.php index 1a66c7f2416..d2b87e1bdfd 100644 --- a/apps/testing/composer/composer/installed.php +++ b/apps/testing/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), diff --git a/apps/testing/lib/AppInfo/Application.php b/apps/testing/lib/AppInfo/Application.php index cbbbf6fc4ea..7b51a8469db 100644 --- a/apps/testing/lib/AppInfo/Application.php +++ b/apps/testing/lib/AppInfo/Application.php @@ -25,14 +25,21 @@ namespace OCA\Testing\AppInfo; use OCA\Testing\AlternativeHomeUserBackend; +use OCA\Testing\Listener\GetDeclarativeSettingsValueListener; +use OCA\Testing\Listener\RegisterDeclarativeSettingsListener; +use OCA\Testing\Listener\SetDeclarativeSettingsValueListener; use OCA\Testing\Provider\FakeText2ImageProvider; use OCA\Testing\Provider\FakeTextProcessingProvider; use OCA\Testing\Provider\FakeTextProcessingProviderSync; use OCA\Testing\Provider\FakeTranslationProvider; +use OCA\Testing\Settings\DeclarativeSettingsForm; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; +use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; class Application extends App implements IBootstrap { public function __construct(array $urlParams = []) { @@ -44,6 +51,11 @@ class Application extends App implements IBootstrap { $context->registerTextProcessingProvider(FakeTextProcessingProvider::class); $context->registerTextProcessingProvider(FakeTextProcessingProviderSync::class); $context->registerTextToImageProvider(FakeText2ImageProvider::class); + + $context->registerDeclarativeSettings(DeclarativeSettingsForm::class); + $context->registerEventListener(DeclarativeSettingsRegisterFormEvent::class, RegisterDeclarativeSettingsListener::class); + $context->registerEventListener(DeclarativeSettingsGetValueEvent::class, GetDeclarativeSettingsValueListener::class); + $context->registerEventListener(DeclarativeSettingsSetValueEvent::class, SetDeclarativeSettingsValueListener::class); } public function boot(IBootContext $context): void { diff --git a/apps/testing/lib/Listener/GetDeclarativeSettingsValueListener.php b/apps/testing/lib/Listener/GetDeclarativeSettingsValueListener.php new file mode 100644 index 00000000000..ff55ba77104 --- /dev/null +++ b/apps/testing/lib/Listener/GetDeclarativeSettingsValueListener.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Testing\Listener; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IConfig; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; + +/** + * @template-implements IEventListener<DeclarativeSettingsGetValueEvent> + */ +class GetDeclarativeSettingsValueListener implements IEventListener { + + public function __construct(private IConfig $config) { + } + + public function handle(Event $event): void { + if (!$event instanceof DeclarativeSettingsGetValueEvent) { + return; + } + + if ($event->getApp() !== 'testing') { + return; + } + + $value = $this->config->getUserValue($event->getUser()->getUID(), $event->getApp(), $event->getFieldId()); + $event->setValue($value); + } +} diff --git a/apps/testing/lib/Listener/RegisterDeclarativeSettingsListener.php b/apps/testing/lib/Listener/RegisterDeclarativeSettingsListener.php new file mode 100644 index 00000000000..e11205904e7 --- /dev/null +++ b/apps/testing/lib/Listener/RegisterDeclarativeSettingsListener.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Testing\Listener; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Settings\DeclarativeSettingsTypes; +use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent; + +/** + * @template-implements IEventListener<DeclarativeSettingsRegisterFormEvent> + */ +class RegisterDeclarativeSettingsListener implements IEventListener { + + public function __construct() { + } + + public function handle(Event $event): void { + if (!($event instanceof DeclarativeSettingsRegisterFormEvent)) { + // Unrelated + return; + } + + $event->registerSchema('testing', [ + 'id' => 'test_declarative_form_event', + 'priority' => 20, + 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, + 'section_id' => 'additional', + 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, + 'title' => 'Test declarative settings event', // NcSettingsSection name + 'description' => 'This form is registered via the RegisterDeclarativeSettingsFormEvent', // NcSettingsSection description + 'fields' => [ + [ + 'id' => 'event_field_1', + 'title' => 'Why is 42 this answer to all questions?', + 'description' => 'Hint: It\'s not', + 'type' => DeclarativeSettingsTypes::TEXT, + 'placeholder' => 'Enter your answer', + 'default' => 'Because it is', + ], + [ + 'id' => 'feature_rating', + 'title' => 'How would you rate this feature?', + 'description' => 'Your vote is not anonymous', + 'type' => DeclarativeSettingsTypes::RADIO, // radio, radio-button (NcCheckboxRadioSwitch button-variant) + 'label' => 'Select single toggle', + 'default' => '3', + 'options' => [ + [ + 'name' => 'Awesome', // NcCheckboxRadioSwitch display name + 'value' => '1' // NcCheckboxRadioSwitch value + ], + [ + 'name' => 'Very awesome', + 'value' => '2' + ], + [ + 'name' => 'Super awesome', + 'value' => '3' + ], + ], + ], + ], + ]); + } +} diff --git a/apps/testing/lib/Listener/SetDeclarativeSettingsValueListener.php b/apps/testing/lib/Listener/SetDeclarativeSettingsValueListener.php new file mode 100644 index 00000000000..d9931455934 --- /dev/null +++ b/apps/testing/lib/Listener/SetDeclarativeSettingsValueListener.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Testing\Listener; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IConfig; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; + +/** + * @template-implements IEventListener<DeclarativeSettingsSetValueEvent> + */ +class SetDeclarativeSettingsValueListener implements IEventListener { + + public function __construct(private IConfig $config) { + } + + public function handle(Event $event): void { + if (!$event instanceof DeclarativeSettingsSetValueEvent) { + return; + } + + if ($event->getApp() !== 'testing') { + return; + } + + error_log('Testing app wants to store ' . $event->getValue() . ' for field ' . $event->getFieldId() . ' for user ' . $event->getUser()->getUID()); + $this->config->setUserValue($event->getUser()->getUID(), $event->getApp(), $event->getFieldId(), $event->getValue()); + } +} diff --git a/apps/testing/lib/Settings/DeclarativeSettingsForm.php b/apps/testing/lib/Settings/DeclarativeSettingsForm.php new file mode 100644 index 00000000000..a717de59ea1 --- /dev/null +++ b/apps/testing/lib/Settings/DeclarativeSettingsForm.php @@ -0,0 +1,172 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Testing\Settings; + +use OCP\Settings\DeclarativeSettingsTypes; +use OCP\Settings\IDeclarativeSettingsForm; + +class DeclarativeSettingsForm implements IDeclarativeSettingsForm { + public function getSchema(): array { + return [ + 'id' => 'test_declarative_form', + 'priority' => 10, + 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, // admin, personal + 'section_id' => 'additional', + 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, // external, internal (handled by core to store in appconfig and preferences) + 'title' => 'Test declarative settings class', // NcSettingsSection name + 'description' => 'This form is registered with a DeclarativeSettingsForm class', // NcSettingsSection description + 'doc_url' => '', // NcSettingsSection doc_url for documentation or help page, empty string if not needed + 'fields' => [ + [ + 'id' => 'test_ex_app_field_7', // configkey + 'title' => 'Multi-selection', // name or label + 'description' => 'Select some option setting', // hint + 'type' => DeclarativeSettingsTypes::MULTI_SELECT, // select, radio, multi-select + 'options' => ['foo', 'bar', 'baz'], // simple options for select, radio, multi-select + 'placeholder' => 'Select some multiple options', // input placeholder + 'default' => ['foo', 'bar'], + ], + [ + 'id' => 'some_real_setting', + 'title' => 'Choose init status check background job interval', + 'description' => 'How often AppAPI should check for initialization status', + 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio) + 'placeholder' => 'Choose init status check background job interval', + 'default' => '40m', + 'options' => [ + [ + 'name' => 'Each 40 minutes', // NcCheckboxRadioSwitch display name + 'value' => '40m' // NcCheckboxRadioSwitch value + ], + [ + 'name' => 'Each 60 minutes', + 'value' => '60m' + ], + [ + 'name' => 'Each 120 minutes', + 'value' => '120m' + ], + [ + 'name' => 'Each day', + 'value' => 60 * 24 . 'm' + ], + ], + ], + [ + 'id' => 'test_ex_app_field_1', // configkey + 'title' => 'Default text field', // label + 'description' => 'Set some simple text setting', // hint + 'type' => DeclarativeSettingsTypes::TEXT, // text, password, email, tel, url, number + 'placeholder' => 'Enter text setting', // placeholder + 'default' => 'foo', + ], + [ + 'id' => 'test_ex_app_field_1_1', + 'title' => 'Email field', + 'description' => 'Set email config', + 'type' => DeclarativeSettingsTypes::EMAIL, + 'placeholder' => 'Enter email', + 'default' => '', + ], + [ + 'id' => 'test_ex_app_field_1_2', + 'title' => 'Tel field', + 'description' => 'Set tel config', + 'type' => DeclarativeSettingsTypes::TEL, + 'placeholder' => 'Enter your tel', + 'default' => '', + ], + [ + 'id' => 'test_ex_app_field_1_3', + 'title' => 'Url (website) field', + 'description' => 'Set url config', + 'type' => 'url', + 'placeholder' => 'Enter url', + 'default' => '', + ], + [ + 'id' => 'test_ex_app_field_1_4', + 'title' => 'Number field', + 'description' => 'Set number config', + 'type' => DeclarativeSettingsTypes::NUMBER, + 'placeholder' => 'Enter number value', + 'default' => 0, + ], + [ + 'id' => 'test_ex_app_field_2', + 'title' => 'Password', + 'description' => 'Set some secure value setting', + 'type' => 'password', + 'placeholder' => 'Set secure value', + 'default' => '', + ], + [ + 'id' => 'test_ex_app_field_3', + 'title' => 'Selection', + 'description' => 'Select some option setting', + 'type' => DeclarativeSettingsTypes::SELECT, // select, radio, multi-select + 'options' => ['foo', 'bar', 'baz'], + 'placeholder' => 'Select some option setting', + 'default' => 'foo', + ], + [ + 'id' => 'test_ex_app_field_4', + 'title' => 'Toggle something', + 'description' => 'Select checkbox option setting', + 'type' => DeclarativeSettingsTypes::CHECKBOX, // checkbox, multiple-checkbox + 'label' => 'Verify something if enabled', + 'default' => false, + ], + [ + 'id' => 'test_ex_app_field_5', + 'title' => 'Multiple checkbox toggles, describing one setting, checked options are saved as an JSON object {foo: true, bar: false}', + 'description' => 'Select checkbox option setting', + 'type' => DeclarativeSettingsTypes::MULTI_CHECKBOX, // checkbox, multi-checkbox + 'default' => ['foo' => true, 'bar' => true, 'baz' => true], + 'options' => [ + [ + 'name' => 'Foo', + 'value' => 'foo', // multiple-checkbox configkey + ], + [ + 'name' => 'Bar', + 'value' => 'bar', + ], + [ + 'name' => 'Baz', + 'value' => 'baz', + ], + [ + 'name' => 'Qux', + 'value' => 'qux', + ], + ], + ], + [ + 'id' => 'test_ex_app_field_6', + 'title' => 'Radio toggles, describing one setting like single select', + 'description' => 'Select radio option setting', + 'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio) + 'label' => 'Select single toggle', + 'default' => 'foo', + 'options' => [ + [ + 'name' => 'First radio', // NcCheckboxRadioSwitch display name + 'value' => 'foo' // NcCheckboxRadioSwitch value + ], + [ + 'name' => 'Second radio', + 'value' => 'bar' + ], + [ + 'name' => 'Third radio', + 'value' => 'baz' + ], + ], + ], + ], + ]; + } +} |