Signed-off-by: jld3103 <jld3103yt@gmail.com> Signed-off-by: Julien Veyssier <julien-nc@posteo.net> Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>tags/v29.0.0beta2
@@ -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' => ''], | |||
], | |||
]; |
@@ -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', |
@@ -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', |
@@ -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(), |
@@ -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); |
@@ -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(); |
@@ -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)); | |||
} | |||
} |
@@ -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; | |||
} | |||
/** |
@@ -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 { | |||
} |
@@ -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": [] | |||
} |
@@ -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": [] | |||
} |
@@ -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" | |||
} | |||
} | |||
} |
@@ -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> |
@@ -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); | |||
}); |
@@ -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'); | |||
@@ -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', | |||
); |
@@ -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) |
@@ -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(), |
@@ -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 { |
@@ -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); | |||
} | |||
} |
@@ -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' | |||
], | |||
], | |||
], | |||
], | |||
]); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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' | |||
], | |||
], | |||
], | |||
], | |||
]; | |||
} | |||
} |
@@ -1,4 +1,3 @@ | |||
Copyright (c) Nils Adermann, Jordi Boggiano | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
@@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
THE SOFTWARE. | |||
@@ -631,6 +631,12 @@ return array( | |||
'OCP\\Security\\VerificationToken\\InvalidTokenException' => $baseDir . '/lib/public/Security/VerificationToken/InvalidTokenException.php', | |||
'OCP\\Server' => $baseDir . '/lib/public/Server.php', | |||
'OCP\\Session\\Exceptions\\SessionNotAvailableException' => $baseDir . '/lib/public/Session/Exceptions/SessionNotAvailableException.php', | |||
'OCP\\Settings\\DeclarativeSettingsTypes' => $baseDir . '/lib/public/Settings/DeclarativeSettingsTypes.php', | |||
'OCP\\Settings\\Events\\DeclarativeSettingsGetValueEvent' => $baseDir . '/lib/public/Settings/Events/DeclarativeSettingsGetValueEvent.php', | |||
'OCP\\Settings\\Events\\DeclarativeSettingsRegisterFormEvent' => $baseDir . '/lib/public/Settings/Events/DeclarativeSettingsRegisterFormEvent.php', | |||
'OCP\\Settings\\Events\\DeclarativeSettingsSetValueEvent' => $baseDir . '/lib/public/Settings/Events/DeclarativeSettingsSetValueEvent.php', | |||
'OCP\\Settings\\IDeclarativeManager' => $baseDir . '/lib/public/Settings/IDeclarativeManager.php', | |||
'OCP\\Settings\\IDeclarativeSettingsForm' => $baseDir . '/lib/public/Settings/IDeclarativeSettingsForm.php', | |||
'OCP\\Settings\\IDelegatedSettings' => $baseDir . '/lib/public/Settings/IDelegatedSettings.php', | |||
'OCP\\Settings\\IIconSection' => $baseDir . '/lib/public/Settings/IIconSection.php', | |||
'OCP\\Settings\\IManager' => $baseDir . '/lib/public/Settings/IManager.php', | |||
@@ -1755,6 +1761,7 @@ return array( | |||
'OC\\Session\\Session' => $baseDir . '/lib/private/Session/Session.php', | |||
'OC\\Settings\\AuthorizedGroup' => $baseDir . '/lib/private/Settings/AuthorizedGroup.php', | |||
'OC\\Settings\\AuthorizedGroupMapper' => $baseDir . '/lib/private/Settings/AuthorizedGroupMapper.php', | |||
'OC\\Settings\\DeclarativeManager' => $baseDir . '/lib/private/Settings/DeclarativeManager.php', | |||
'OC\\Settings\\Manager' => $baseDir . '/lib/private/Settings/Manager.php', | |||
'OC\\Settings\\Section' => $baseDir . '/lib/private/Settings/Section.php', | |||
'OC\\Setup' => $baseDir . '/lib/private/Setup.php', |
@@ -672,6 +672,12 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 | |||
'OCP\\Security\\VerificationToken\\InvalidTokenException' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/InvalidTokenException.php', | |||
'OCP\\Server' => __DIR__ . '/../../..' . '/lib/public/Server.php', | |||
'OCP\\Session\\Exceptions\\SessionNotAvailableException' => __DIR__ . '/../../..' . '/lib/public/Session/Exceptions/SessionNotAvailableException.php', | |||
'OCP\\Settings\\DeclarativeSettingsTypes' => __DIR__ . '/../../..' . '/lib/public/Settings/DeclarativeSettingsTypes.php', | |||
'OCP\\Settings\\Events\\DeclarativeSettingsGetValueEvent' => __DIR__ . '/../../..' . '/lib/public/Settings/Events/DeclarativeSettingsGetValueEvent.php', | |||
'OCP\\Settings\\Events\\DeclarativeSettingsRegisterFormEvent' => __DIR__ . '/../../..' . '/lib/public/Settings/Events/DeclarativeSettingsRegisterFormEvent.php', | |||
'OCP\\Settings\\Events\\DeclarativeSettingsSetValueEvent' => __DIR__ . '/../../..' . '/lib/public/Settings/Events/DeclarativeSettingsSetValueEvent.php', | |||
'OCP\\Settings\\IDeclarativeManager' => __DIR__ . '/../../..' . '/lib/public/Settings/IDeclarativeManager.php', | |||
'OCP\\Settings\\IDeclarativeSettingsForm' => __DIR__ . '/../../..' . '/lib/public/Settings/IDeclarativeSettingsForm.php', | |||
'OCP\\Settings\\IDelegatedSettings' => __DIR__ . '/../../..' . '/lib/public/Settings/IDelegatedSettings.php', | |||
'OCP\\Settings\\IIconSection' => __DIR__ . '/../../..' . '/lib/public/Settings/IIconSection.php', | |||
'OCP\\Settings\\IManager' => __DIR__ . '/../../..' . '/lib/public/Settings/IManager.php', | |||
@@ -1796,6 +1802,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 | |||
'OC\\Session\\Session' => __DIR__ . '/../../..' . '/lib/private/Session/Session.php', | |||
'OC\\Settings\\AuthorizedGroup' => __DIR__ . '/../../..' . '/lib/private/Settings/AuthorizedGroup.php', | |||
'OC\\Settings\\AuthorizedGroupMapper' => __DIR__ . '/../../..' . '/lib/private/Settings/AuthorizedGroupMapper.php', | |||
'OC\\Settings\\DeclarativeManager' => __DIR__ . '/../../..' . '/lib/private/Settings/DeclarativeManager.php', | |||
'OC\\Settings\\Manager' => __DIR__ . '/../../..' . '/lib/private/Settings/Manager.php', | |||
'OC\\Settings\\Section' => __DIR__ . '/../../..' . '/lib/private/Settings/Section.php', | |||
'OC\\Setup' => __DIR__ . '/../../..' . '/lib/private/Setup.php', |
@@ -3,7 +3,7 @@ | |||
'name' => '__root__', | |||
'pretty_version' => 'dev-master', | |||
'version' => 'dev-master', | |||
'reference' => 'b6abfc4cba2d1ef4fdd8f2c22bbff46796b9485e', | |||
'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', | |||
'type' => 'library', | |||
'install_path' => __DIR__ . '/../../../', | |||
'aliases' => array(), | |||
@@ -13,7 +13,7 @@ | |||
'__root__' => array( | |||
'pretty_version' => 'dev-master', | |||
'version' => 'dev-master', | |||
'reference' => 'b6abfc4cba2d1ef4fdd8f2c22bbff46796b9485e', | |||
'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', | |||
'type' => 'library', | |||
'install_path' => __DIR__ . '/../../../', | |||
'aliases' => array(), |
@@ -49,6 +49,7 @@ use OCP\Http\WellKnown\IHandler; | |||
use OCP\Notification\INotifier; | |||
use OCP\Profile\ILinkAction; | |||
use OCP\Search\IProvider; | |||
use OCP\Settings\IDeclarativeSettingsForm; | |||
use OCP\SetupCheck\ISetupCheck; | |||
use OCP\Share\IPublicShareTemplateProvider; | |||
use OCP\SpeechToText\ISpeechToTextProvider; | |||
@@ -142,9 +143,6 @@ class RegistrationContext { | |||
/** @var ServiceRegistration<\OCP\TextToImage\IProvider>[] */ | |||
private $textToImageProviders = []; | |||
/** @var ParameterRegistration[] */ | |||
private $sensitiveMethods = []; | |||
@@ -159,6 +157,9 @@ class RegistrationContext { | |||
/** @var PreviewProviderRegistration[] */ | |||
private array $previewProviders = []; | |||
/** @var ServiceRegistration<IDeclarativeSettingsForm>[] */ | |||
private array $declarativeSettings = []; | |||
/** @var ServiceRegistration<ITeamResourceProvider>[] */ | |||
private array $teamResourceProviders = []; | |||
@@ -403,6 +404,13 @@ class RegistrationContext { | |||
$setupCheckClass | |||
); | |||
} | |||
public function registerDeclarativeSettings(string $declarativeSettingsClass): void { | |||
$this->context->registerDeclarativeSettings( | |||
$this->appId, | |||
$declarativeSettingsClass | |||
); | |||
} | |||
}; | |||
} | |||
@@ -542,7 +550,6 @@ class RegistrationContext { | |||
); | |||
} | |||
/** | |||
* @psalm-param class-string<ITeamResourceProvider> $class | |||
*/ | |||
@@ -576,6 +583,13 @@ class RegistrationContext { | |||
$this->setupChecks[] = new ServiceRegistration($appId, $setupCheckClass); | |||
} | |||
/** | |||
* @psalm-param class-string<IDeclarativeSettingsForm> $declarativeSettingsClass | |||
*/ | |||
public function registerDeclarativeSettings(string $appId, string $declarativeSettingsClass): void { | |||
$this->declarativeSettings[] = new ServiceRegistration($appId, $declarativeSettingsClass); | |||
} | |||
/** | |||
* @param App[] $apps | |||
*/ | |||
@@ -893,11 +907,10 @@ class RegistrationContext { | |||
return $this->setupChecks; | |||
} | |||
/** | |||
* @return ServiceRegistration<ITeamResourceProvider>[] | |||
* @return ServiceRegistration<IDeclarativeSettingsForm>[] | |||
*/ | |||
public function getTeamResourceProviders(): array { | |||
return $this->teamResourceProviders; | |||
public function getDeclarativeSettings(): array { | |||
return $this->declarativeSettings; | |||
} | |||
} |
@@ -150,6 +150,7 @@ use OC\Security\SecureRandom; | |||
use OC\Security\TrustedDomainHelper; | |||
use OC\Security\VerificationToken\VerificationToken; | |||
use OC\Session\CryptoWrapper; | |||
use OC\Settings\DeclarativeManager; | |||
use OC\SetupCheck\SetupCheckManager; | |||
use OC\Share20\ProviderFactory; | |||
use OC\Share20\ShareHelper; | |||
@@ -259,6 +260,7 @@ use OCP\Security\ISecureRandom; | |||
use OCP\Security\ITrustedDomainHelper; | |||
use OCP\Security\RateLimiting\ILimiter; | |||
use OCP\Security\VerificationToken\IVerificationToken; | |||
use OCP\Settings\IDeclarativeManager; | |||
use OCP\SetupCheck\ISetupCheckManager; | |||
use OCP\Share\IProviderFactory; | |||
use OCP\Share\IShareHelper; | |||
@@ -1430,6 +1432,8 @@ class Server extends ServerContainer implements IServerContainer { | |||
$this->registerAlias(IAvailabilityCoordinator::class, AvailabilityCoordinator::class); | |||
$this->registerAlias(IDeclarativeManager::class, DeclarativeManager::class); | |||
$this->connectDispatcher(); | |||
} | |||
@@ -0,0 +1,402 @@ | |||
<?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 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\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 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 { | |||
public function __construct( | |||
private IEventDispatcher $eventDispatcher, | |||
private IGroupManager $groupManager, | |||
private Coordinator $coordinator, | |||
private IConfig $config, | |||
private IAppConfig $appConfig, | |||
private LoggerInterface $logger, | |||
) { | |||
} | |||
/** | |||
* @var array<string, list<DeclarativeSettingsFormSchemaWithoutValues>> | |||
*/ | |||
private array $appSchemas = []; | |||
/** | |||
* @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 { | |||
$declarativeSettings = $this->coordinator->getRegistrationContext()->getDeclarativeSettings(); | |||
foreach ($declarativeSettings as $declarativeSetting) { | |||
/** @var IDeclarativeSettingsForm $declarativeSettingObject */ | |||
$declarativeSettingObject = Server::get($declarativeSetting->getService()); | |||
$this->registerSchema($declarativeSetting->getAppId(), $declarativeSettingObject->getSchema()); | |||
} | |||
$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: | |||
$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: | |||
$this->eventDispatcher->dispatchTyped(new DeclarativeSettingsSetValueEvent($user, $app, $formId, $fieldId, $value)); | |||
break; | |||
case DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL: | |||
$this->saveInternalValue($user, $app, $fieldId, $value); | |||
break; | |||
default: | |||
throw new Exception('Unknown storage type "' . $storageType . '"'); | |||
} | |||
} | |||
private function getInternalValue(IUser $user, string $app, string $formId, string $fieldId): mixed { | |||
$sectionType = $this->getSectionType($app, $fieldId); | |||
$defaultValue = $this->getDefaultValue($app, $formId, $fieldId); | |||
switch ($sectionType) { | |||
case DeclarativeSettingsTypes::SECTION_TYPE_ADMIN: | |||
return $this->config->getAppValue($app, $fieldId, $defaultValue); | |||
case DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL: | |||
return $this->config->getUserValue($user->getUID(), $app, $fieldId, $defaultValue); | |||
default: | |||
throw new Exception('Unknown section type "' . $sectionType . '"'); | |||
} | |||
} | |||
private function saveInternalValue(IUser $user, string $app, string $fieldId, mixed $value): void { | |||
$sectionType = $this->getSectionType($app, $fieldId); | |||
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 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 (!$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; | |||
} | |||
} |
@@ -399,4 +399,15 @@ interface IRegistrationContext { | |||
* @since 28.0.0 | |||
*/ | |||
public function registerSetupCheck(string $setupCheckClass): void; | |||
/** | |||
* Register an implementation of \OCP\Settings\IDeclarativeSettings that | |||
* will handle the implementation of declarative settings | |||
* | |||
* @param string $declarativeSettingsClass | |||
* @psalm-param class-string<\OCP\Settings\IDeclarativeSettingsForm> $declarativeSettingsClass | |||
* @return void | |||
* @since 29.0.0 | |||
*/ | |||
public function registerDeclarativeSettings(string $declarativeSettingsClass): void; | |||
} |
@@ -0,0 +1,145 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2023 Andrey Borysenko <andrey.borysenko@nextcloud.com> | |||
* | |||
* @author Andrey Borysenko <andrey.borysenko@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 OCP\Settings; | |||
/** | |||
* Declarative settings types supported in the IDeclarativeSettingsForm forms | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
final class DeclarativeSettingsTypes { | |||
/** | |||
* IDeclarativeSettingsForm section_type which is determines where the form is displayed | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const SECTION_TYPE_ADMIN = 'admin'; | |||
/** | |||
* IDeclarativeSettingsForm section_type which is determines where the form is displayed | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const SECTION_TYPE_PERSONAL = 'personal'; | |||
/** | |||
* IDeclarativeSettingsForm storage_type which is determines where and how the config value is stored | |||
* | |||
* | |||
* For `external` storage_type the app implementing \OCP\Settings\SetDeclarativeSettingsValueEvent and \OCP\Settings\GetDeclarativeSettingsValueEvent events is responsible for storing and retrieving the config value. | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const STORAGE_TYPE_EXTERNAL = 'external'; | |||
/** | |||
* IDeclarativeSettingsForm storage_type which is determines where and how the config value is stored | |||
* | |||
* For `internal` storage_type the config value is stored in default `appconfig` and `preferences` tables. | |||
* For `external` storage_type the app implementing \OCP\Settings\SetDeclarativeSettingsValueEvent and \OCP\Settings\GetDeclarativeSettingsValueEvent events is responsible for storing and retrieving the config value. | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const STORAGE_TYPE_INTERNAL = 'internal'; | |||
/** | |||
* NcInputField type text | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const TEXT = 'text'; | |||
/** | |||
* NcInputField type password | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const PASSWORD = 'password'; | |||
/** | |||
* NcInputField type email | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const EMAIL = 'email'; | |||
/** | |||
* NcInputField type tel | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const TEL = 'tel'; | |||
/** | |||
* NcInputField type url | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const URL = 'url'; | |||
/** | |||
* NcInputField type number | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const NUMBER = 'number'; | |||
/** | |||
* NcCheckboxRadioSwitch type checkbox | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const CHECKBOX = 'checkbox'; | |||
/** | |||
* Multiple NcCheckboxRadioSwitch type checkbox representing a one config value (saved as JSON object) | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const MULTI_CHECKBOX = 'multi-checkbox'; | |||
/** | |||
* NcCheckboxRadioSwitch type radio | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const RADIO = 'radio'; | |||
/** | |||
* NcSelect | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const SELECT = 'select'; | |||
/** | |||
* Multiple NcSelect representing a one config value (saved as JSON array) | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public const MULTI_SELECT = 'multi-select'; | |||
} |
@@ -0,0 +1,81 @@ | |||
<?php | |||
namespace OCP\Settings\Events; | |||
use Exception; | |||
use OCP\EventDispatcher\Event; | |||
use OCP\IUser; | |||
use OCP\Settings\IDeclarativeSettingsForm; | |||
/** | |||
* @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
class DeclarativeSettingsGetValueEvent extends Event { | |||
/** | |||
* @var ?DeclarativeSettingsValueTypes | |||
*/ | |||
private mixed $value = null; | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function __construct( | |||
private IUser $user, | |||
private string $app, | |||
private string $formId, | |||
private string $fieldId, | |||
) { | |||
parent::__construct(); | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getUser(): IUser { | |||
return $this->user; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getApp(): string { | |||
return $this->app; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getFormId(): string { | |||
return $this->formId; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getFieldId(): string { | |||
return $this->fieldId; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function setValue(mixed $value): void { | |||
$this->value = $value; | |||
} | |||
/** | |||
* @return DeclarativeSettingsValueTypes | |||
* @throws Exception | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public function getValue(): mixed { | |||
if ($this->value === null) { | |||
throw new Exception('Value not set'); | |||
} | |||
return $this->value; | |||
} | |||
} |
@@ -0,0 +1,29 @@ | |||
<?php | |||
namespace OCP\Settings\Events; | |||
use OCP\EventDispatcher\Event; | |||
use OCP\Settings\IDeclarativeManager; | |||
use OCP\Settings\IDeclarativeSettingsForm; | |||
/** | |||
* @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
class DeclarativeSettingsRegisterFormEvent extends Event { | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function __construct(private IDeclarativeManager $manager) { | |||
parent::__construct(); | |||
} | |||
/** | |||
* @param DeclarativeSettingsFormSchemaWithoutValues $schema | |||
* @since 29.0.0 | |||
*/ | |||
public function registerSchema(string $app, array $schema): void { | |||
$this->manager->registerSchema($app, $schema); | |||
} | |||
} |
@@ -0,0 +1,63 @@ | |||
<?php | |||
namespace OCP\Settings\Events; | |||
use OCP\EventDispatcher\Event; | |||
use OCP\IUser; | |||
use OCP\Settings\IDeclarativeSettingsForm; | |||
/** | |||
* @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
class DeclarativeSettingsSetValueEvent extends Event { | |||
/** | |||
* @param DeclarativeSettingsValueTypes $value | |||
* @since 29.0.0 | |||
*/ | |||
public function __construct( | |||
private IUser $user, | |||
private string $app, | |||
private string $formId, | |||
private string $fieldId, | |||
private mixed $value, | |||
) { | |||
parent::__construct(); | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getUser(): IUser { | |||
return $this->user; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getApp(): string { | |||
return $this->app; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getFormId(): string { | |||
return $this->formId; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getFieldId(): string { | |||
return $this->fieldId; | |||
} | |||
/** | |||
* @since 29.0.0 | |||
*/ | |||
public function getValue(): mixed { | |||
return $this->value; | |||
} | |||
} |
@@ -0,0 +1,89 @@ | |||
<?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 OCP\Settings; | |||
use Exception; | |||
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; | |||
use OCP\IUser; | |||
/** | |||
* @since 29.0.0 | |||
* | |||
* @psalm-import-type DeclarativeSettingsValueTypes from IDeclarativeSettingsForm | |||
* @psalm-import-type DeclarativeSettingsSectionType from IDeclarativeSettingsForm | |||
* @psalm-import-type DeclarativeSettingsFormSchemaWithValues from IDeclarativeSettingsForm | |||
* @psalm-import-type DeclarativeSettingsFormSchemaWithoutValues from IDeclarativeSettingsForm | |||
*/ | |||
interface IDeclarativeManager { | |||
/** | |||
* Registers a new declarative settings schema. | |||
* | |||
* @param DeclarativeSettingsFormSchemaWithoutValues $schema | |||
* @since 29.0.0 | |||
*/ | |||
public function registerSchema(string $app, array $schema): void; | |||
/** | |||
* Load all schemas from the registration context and events. | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public function loadSchemas(): void; | |||
/** | |||
* Gets the IDs of the forms for the given type and section. | |||
* | |||
* @param DeclarativeSettingsSectionType $type | |||
* @param string $section | |||
* @return array<string, list<string>> | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public function getFormIDs(IUser $user, string $type, string $section): array; | |||
/** | |||
* Gets the forms including the field values for the given type and section. | |||
* | |||
* @param IUser $user Used for reading values from the personal section or for authorization for the admin section. | |||
* @param ?DeclarativeSettingsSectionType $type If it is null the forms will not be filtered by type. | |||
* @param ?string $section If it is null the forms will not be filtered by section. | |||
* @return list<DeclarativeSettingsFormSchemaWithValues> | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public function getFormsWithValues(IUser $user, ?string $type, ?string $section): array; | |||
/** | |||
* Sets a value for the given field ID. | |||
* | |||
* @param IUser $user Used for storing values in the personal section or for authorization for the admin section. | |||
* @param DeclarativeSettingsValueTypes $value | |||
* | |||
* @throws Exception | |||
* @throws NotAdminException | |||
* | |||
* @since 29.0.0 | |||
*/ | |||
public function setValue(IUser $user, string $app, string $formId, string $fieldId, mixed $value): void; | |||
} |
@@ -0,0 +1,78 @@ | |||
<?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 OCP\Settings; | |||
/** | |||
* @since 29.0.0 | |||
* | |||
* @psalm-type DeclarativeSettingsSectionType = 'admin'|'personal' | |||
* | |||
* @psalm-type DeclarativeSettingsStorageType = 'internal'|'external' | |||
* | |||
* @psalm-type DeclarativeSettingsValueTypes = string|int|float|bool|list<string> | |||
* | |||
* @psalm-type DeclarativeSettingsFormField = 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}>, | |||
* } | |||
* | |||
* @psalm-type DeclarativeSettingsFormFieldWithValue = DeclarativeSettingsFormField&array{ | |||
* value: DeclarativeSettingsValueTypes, | |||
* } | |||
* | |||
* @psalm-type DeclarativeSettingsFormSchema = array{ | |||
* id: string, | |||
* priority: int, | |||
* section_type: DeclarativeSettingsSectionType, | |||
* section_id: string, | |||
* storage_type: DeclarativeSettingsStorageType, | |||
* title: string, | |||
* description?: string, | |||
* doc_url?: string, | |||
* } | |||
* | |||
* @psalm-type DeclarativeSettingsFormSchemaWithValues = DeclarativeSettingsFormSchema&array{ | |||
* app: string, | |||
* fields: list<DeclarativeSettingsFormFieldWithValue>, | |||
* } | |||
* | |||
* @psalm-type DeclarativeSettingsFormSchemaWithoutValues = DeclarativeSettingsFormSchema&array{ | |||
* fields: list<DeclarativeSettingsFormField>, | |||
* } | |||
*/ | |||
interface IDeclarativeSettingsForm { | |||
/** | |||
* Gets the schema that defines the declarative settings form | |||
* | |||
* @return DeclarativeSettingsFormSchemaWithoutValues | |||
* @since 29.0.0 | |||
*/ | |||
public function getSchema(): array; | |||
} |
@@ -0,0 +1,536 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2023 Andrey Borysenko <andrey.borysenko@nextcloud.com> | |||
* | |||
* @author Andrey Borysenko <andrey.borysenko@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 Test\Settings; | |||
use OC\AppFramework\Bootstrap\Coordinator; | |||
use OC\Settings\DeclarativeManager; | |||
use OCP\EventDispatcher\IEventDispatcher; | |||
use OCP\IAppConfig; | |||
use OCP\IConfig; | |||
use OCP\IGroupManager; | |||
use OCP\IUser; | |||
use OCP\Settings\DeclarativeSettingsTypes; | |||
use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; | |||
use OCP\Settings\IDeclarativeManager; | |||
use PHPUnit\Framework\MockObject\MockObject; | |||
use Psr\Log\LoggerInterface; | |||
use Test\TestCase; | |||
class DeclarativeManagerTest extends TestCase { | |||
/** @var IDeclarativeManager|MockObject */ | |||
private $declarativeManager; | |||
/** @var IEventDispatcher|MockObject */ | |||
private $eventDispatcher; | |||
/** @var IGroupManager|MockObject */ | |||
private $groupManager; | |||
/** @var Coordinator|MockObject */ | |||
private $coordinator; | |||
/** @var IConfig|MockObject */ | |||
private $config; | |||
/** @var IAppConfig|MockObject */ | |||
private $appConfig; | |||
/** @var LoggerInterface|MockObject */ | |||
private $logger; | |||
/** @var IUser|MockObject */ | |||
private $user; | |||
/** @var IUser|MockObject */ | |||
private $adminUser; | |||
public const validSchemaAllFields = [ | |||
'id' => 'test_form_1', | |||
'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', // NcSettingsSection name | |||
'description' => 'These fields are rendered dynamically from declarative schema', // NcSettingsSection description | |||
'doc_url' => '', // NcSettingsSection doc_url for documentation or help page, empty string if not needed | |||
'fields' => [ | |||
[ | |||
'id' => 'test_field_7', // configkey | |||
'title' => 'Multi-selection', // name or label | |||
'description' => 'Select some option setting', // hint | |||
'type' => DeclarativeSettingsTypes::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' => 'Select single option', | |||
'description' => 'Single option radio buttons', | |||
'type' => DeclarativeSettingsTypes::RADIO, // radio (NcCheckboxRadioSwitch type radio) | |||
'placeholder' => 'Select single option, test 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_field_1', // configkey | |||
'title' => 'Default text field', // label | |||
'description' => 'Set some simple text setting', // hint | |||
'type' => DeclarativeSettingsTypes::TEXT, | |||
'placeholder' => 'Enter text setting', // placeholder | |||
'default' => 'foo', | |||
], | |||
[ | |||
'id' => 'test_field_1_1', | |||
'title' => 'Email field', | |||
'description' => 'Set email config', | |||
'type' => DeclarativeSettingsTypes::EMAIL, | |||
'placeholder' => 'Enter email', | |||
'default' => '', | |||
], | |||
[ | |||
'id' => 'test_field_1_2', | |||
'title' => 'Tel field', | |||
'description' => 'Set tel config', | |||
'type' => DeclarativeSettingsTypes::TEL, | |||
'placeholder' => 'Enter your tel', | |||
'default' => '', | |||
], | |||
[ | |||
'id' => 'test_field_1_3', | |||
'title' => 'Url (website) field', | |||
'description' => 'Set url config', | |||
'type' => 'url', | |||
'placeholder' => 'Enter url', | |||
'default' => '', | |||
], | |||
[ | |||
'id' => 'test_field_1_4', | |||
'title' => 'Number field', | |||
'description' => 'Set number config', | |||
'type' => DeclarativeSettingsTypes::NUMBER, | |||
'placeholder' => 'Enter number value', | |||
'default' => 0, | |||
], | |||
[ | |||
'id' => 'test_field_2', | |||
'title' => 'Password', | |||
'description' => 'Set some secure value setting', | |||
'type' => 'password', | |||
'placeholder' => 'Set secure value', | |||
'default' => '', | |||
], | |||
[ | |||
'id' => 'test_field_3', | |||
'title' => 'Selection', | |||
'description' => 'Select some option setting', | |||
'type' => DeclarativeSettingsTypes::SELECT, | |||
'options' => ['foo', 'bar', 'baz'], | |||
'placeholder' => 'Select some option setting', | |||
'default' => 'foo', | |||
], | |||
[ | |||
'id' => 'test_field_4', | |||
'title' => 'Toggle something', | |||
'description' => 'Select checkbox option setting', | |||
'type' => DeclarativeSettingsTypes::CHECKBOX, | |||
'label' => 'Verify something if enabled', | |||
'default' => false, | |||
], | |||
[ | |||
'id' => 'test_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, | |||
'default' => ['foo' => true, 'bar' => true], | |||
'options' => [ | |||
[ | |||
'name' => 'Foo', | |||
'value' => 'foo', // multiple-checkbox configkey | |||
], | |||
[ | |||
'name' => 'Bar', | |||
'value' => 'bar', | |||
], | |||
[ | |||
'name' => 'Baz', | |||
'value' => 'baz', | |||
], | |||
[ | |||
'name' => 'Qux', | |||
'value' => 'qux', | |||
], | |||
], | |||
], | |||
[ | |||
'id' => 'test_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' => 'Second radio', | |||
'value' => 'baz' | |||
], | |||
], | |||
], | |||
], | |||
]; | |||
public static bool $testSetInternalValueAfterChange = false; | |||
protected function setUp(): void { | |||
parent::setUp(); | |||
$this->eventDispatcher = $this->createMock(IEventDispatcher::class); | |||
$this->groupManager = $this->createMock(IGroupManager::class); | |||
$this->coordinator = $this->createMock(Coordinator::class); | |||
$this->config = $this->createMock(IConfig::class); | |||
$this->appConfig = $this->createMock(IAppConfig::class); | |||
$this->logger = $this->createMock(LoggerInterface::class); | |||
$this->declarativeManager = new DeclarativeManager( | |||
$this->eventDispatcher, | |||
$this->groupManager, | |||
$this->coordinator, | |||
$this->config, | |||
$this->appConfig, | |||
$this->logger | |||
); | |||
$this->user = $this->createMock(IUser::class); | |||
$this->user->expects($this->any()) | |||
->method('getUID') | |||
->willReturn('test_user'); | |||
$this->adminUser = $this->createMock(IUser::class); | |||
$this->adminUser->expects($this->any()) | |||
->method('getUID') | |||
->willReturn('admin_test_user'); | |||
$this->groupManager->expects($this->any()) | |||
->method('isAdmin') | |||
->willReturnCallback(function ($userId) { | |||
return $userId === 'admin_test_user'; | |||
}); | |||
} | |||
public function testRegisterSchema(): void { | |||
$app = 'testing'; | |||
$schema = self::validSchemaAllFields; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); | |||
} | |||
/** | |||
* Simple test to verify that exception is thrown when trying to register schema with duplicate id | |||
*/ | |||
public function testRegisterDuplicateSchema(): void { | |||
$this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); | |||
$this->expectException(\Exception::class); | |||
$this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); | |||
} | |||
/** | |||
* It's not allowed to register schema with duplicate fields ids for the same app | |||
*/ | |||
public function testRegisterSchemaWithDuplicateFields(): void { | |||
// Register first valid schema | |||
$this->declarativeManager->registerSchema('testing', self::validSchemaAllFields); | |||
// Register second schema with duplicate fields, but different schema id | |||
$this->expectException(\Exception::class); | |||
$schema = self::validSchemaAllFields; | |||
$schema['id'] = 'test_form_2'; | |||
$this->declarativeManager->registerSchema('testing', $schema); | |||
} | |||
public function testRegisterMultipleSchemasAndDuplicate(): void { | |||
$app = 'testing'; | |||
$schema = self::validSchemaAllFields; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
// 1. Check that form is registered for the app | |||
$this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); | |||
$app = 'testing2'; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
// 2. Check that form is registered for the second app | |||
$this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); | |||
$app = 'testing'; | |||
$this->expectException(\Exception::class); // expecting duplicate form id and duplicate fields ids exception | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$schemaDuplicateFields = self::validSchemaAllFields; | |||
$schemaDuplicateFields['id'] = 'test_form_2'; // change form id to test duplicate fields | |||
$this->declarativeManager->registerSchema($app, $schemaDuplicateFields); | |||
// 3. Check that not valid form with duplicate fields is not registered | |||
$formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schemaDuplicateFields['section_type'], $schemaDuplicateFields['section_id']); | |||
$this->assertFalse(isset($formIds[$app]) && in_array($schemaDuplicateFields['id'], $formIds[$app])); | |||
} | |||
/** | |||
* @dataProvider dataValidateSchema | |||
*/ | |||
public function testValidateSchema(bool $expected, bool $expectException, string $app, array $schema): void { | |||
if ($expectException) { | |||
$this->expectException(\Exception::class); | |||
} | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$this->assertEquals($expected, isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); | |||
} | |||
public static function dataValidateSchema(): array { | |||
return [ | |||
'valid schema with all supported fields' => [ | |||
true, | |||
false, | |||
'testing', | |||
self::validSchemaAllFields, | |||
], | |||
'invalid schema with missing id' => [ | |||
false, | |||
true, | |||
'testing', | |||
[ | |||
'priority' => 10, | |||
'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, | |||
'section_id' => 'additional', | |||
'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, | |||
'title' => 'Test declarative settings', | |||
'description' => 'These fields are rendered dynamically from declarative schema', | |||
'doc_url' => '', | |||
'fields' => [ | |||
[ | |||
'id' => 'test_field_7', | |||
'title' => 'Multi-selection', | |||
'description' => 'Select some option setting', | |||
'type' => DeclarativeSettingsTypes::MULTI_SELECT, | |||
'options' => ['foo', 'bar', 'baz'], | |||
'placeholder' => 'Select some multiple options', | |||
'default' => ['foo', 'bar'], | |||
], | |||
], | |||
], | |||
], | |||
'invalid schema with invalid field' => [ | |||
false, | |||
true, | |||
'testing', | |||
[ | |||
'id' => 'test_form_1', | |||
'priority' => 10, | |||
'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, | |||
'section_id' => 'additional', | |||
'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, | |||
'title' => 'Test declarative settings', | |||
'description' => 'These fields are rendered dynamically from declarative schema', | |||
'doc_url' => '', | |||
'fields' => [ | |||
[ | |||
'id' => 'test_invalid_field', | |||
'title' => 'Invalid field', | |||
'description' => 'Some invalid setting description', | |||
'type' => 'some_invalid_type', | |||
'placeholder' => 'Some invalid field placeholder', | |||
'default' => null, | |||
], | |||
], | |||
], | |||
], | |||
]; | |||
} | |||
public function testGetFormIDs(): void { | |||
$app = 'testing'; | |||
$schema = self::validSchemaAllFields; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); | |||
$app = 'testing2'; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$formIds = $this->declarativeManager->getFormIDs($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$this->assertTrue(isset($formIds[$app]) && in_array($schema['id'], $formIds[$app])); | |||
} | |||
/** | |||
* Check that form with default values is returned with internal storage_type | |||
*/ | |||
public function testGetFormsWithDefaultValues(): void { | |||
$app = 'testing'; | |||
$schema = self::validSchemaAllFields; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$this->config->expects($this->any()) | |||
->method('getAppValue') | |||
->willReturnCallback(fn ($app, $configkey, $default) => $default); | |||
$forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$this->assertNotEmpty($forms); | |||
$this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); | |||
// Check some_real_setting field default value | |||
$someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; | |||
$schemaSomeRealSettingField = array_values(array_filter($schema['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; | |||
$this->assertEquals($schemaSomeRealSettingField['default'], $someRealSettingField['default']); | |||
} | |||
/** | |||
* Check values in json format to ensure that they are properly encoded | |||
*/ | |||
public function testGetFormsWithDefaultValuesJson(): void { | |||
$app = 'testing'; | |||
$schema = [ | |||
'id' => 'test_form_1', | |||
'priority' => 10, | |||
'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL, | |||
'section_id' => 'additional', | |||
'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_INTERNAL, | |||
'title' => 'Test declarative settings', | |||
'description' => 'These fields are rendered dynamically from declarative schema', | |||
'doc_url' => '', | |||
'fields' => [ | |||
[ | |||
'id' => 'test_field_json', | |||
'title' => 'Multi-selection', | |||
'description' => 'Select some option setting', | |||
'type' => DeclarativeSettingsTypes::MULTI_SELECT, | |||
'options' => ['foo', 'bar', 'baz'], | |||
'placeholder' => 'Select some multiple options', | |||
'default' => ['foo', 'bar'], | |||
], | |||
], | |||
]; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
// config->getUserValue() should be called with json encoded default value | |||
$this->config->expects($this->once()) | |||
->method('getUserValue') | |||
->with($this->adminUser->getUID(), $app, 'test_field_json', json_encode($schema['fields'][0]['default'])) | |||
->willReturn(json_encode($schema['fields'][0]['default'])); | |||
$forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$this->assertNotEmpty($forms); | |||
$this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); | |||
$testFieldJson = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'test_field_json'))[0]; | |||
$this->assertEquals(json_encode($schema['fields'][0]['default']), $testFieldJson['value']); | |||
} | |||
/** | |||
* Check that saving value for field with internal storage_type is handled by core | |||
*/ | |||
public function testSetInternalValue(): void { | |||
$app = 'testing'; | |||
$schema = self::validSchemaAllFields; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
self::$testSetInternalValueAfterChange = false; | |||
$this->config->expects($this->any()) | |||
->method('getAppValue') | |||
->willReturnCallback(function ($app, $configkey, $default) { | |||
if ($configkey === 'some_real_setting' && self::$testSetInternalValueAfterChange) { | |||
return '120m'; | |||
} | |||
return $default; | |||
}); | |||
$this->appConfig->expects($this->once()) | |||
->method('setValueString') | |||
->with($app, 'some_real_setting', '120m'); | |||
$forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; | |||
$this->assertEquals('40m', $someRealSettingField['value']); // first check that default value (40m) is returned | |||
// Set new value for some_real_setting field | |||
$this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m'); | |||
self::$testSetInternalValueAfterChange = true; | |||
$forms = $this->declarativeManager->getFormsWithValues($this->adminUser, $schema['section_type'], $schema['section_id']); | |||
$this->assertNotEmpty($forms); | |||
$this->assertTrue(array_search($schema['id'], array_column($forms, 'id')) !== false); | |||
// Check some_real_setting field default value | |||
$someRealSettingField = array_values(array_filter(array_filter($forms, fn ($form) => $form['id'] === $schema['id'])[0]['fields'], fn ($field) => $field['id'] === 'some_real_setting'))[0]; | |||
$this->assertEquals('120m', $someRealSettingField['value']); | |||
} | |||
public function testSetExternalValue(): void { | |||
$app = 'testing'; | |||
$schema = self::validSchemaAllFields; | |||
// Change storage_type to external and section_type to personal | |||
$schema['storage_type'] = DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL; | |||
$schema['section_type'] = DeclarativeSettingsTypes::SECTION_TYPE_PERSONAL; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$setDeclarativeSettingsValueEvent = new DeclarativeSettingsSetValueEvent( | |||
$this->adminUser, | |||
$app, | |||
$schema['id'], | |||
'some_real_setting', | |||
'120m' | |||
); | |||
$this->eventDispatcher->expects($this->once()) | |||
->method('dispatchTyped') | |||
->with($setDeclarativeSettingsValueEvent); | |||
$this->declarativeManager->setValue($this->adminUser, $app, $schema['id'], 'some_real_setting', '120m'); | |||
} | |||
public function testAdminFormUserUnauthorized(): void { | |||
$app = 'testing'; | |||
$schema = self::validSchemaAllFields; | |||
$this->declarativeManager->registerSchema($app, $schema); | |||
$this->expectException(\Exception::class); | |||
$this->declarativeManager->getFormsWithValues($this->user, $schema['section_type'], $schema['section_id']); | |||
} | |||
} |
@@ -98,6 +98,7 @@ module.exports = { | |||
'vue-settings-personal-password': path.join(__dirname, 'apps/settings/src', 'main-personal-password.js'), | |||
'vue-settings-personal-security': path.join(__dirname, 'apps/settings/src', 'main-personal-security.js'), | |||
'vue-settings-personal-webauthn': path.join(__dirname, 'apps/settings/src', 'main-personal-webauth.js'), | |||
'declarative-settings-forms': path.join(__dirname, 'apps/settings/src', 'main-declarative-settings-forms.ts'), | |||
}, | |||
sharebymail: { | |||
'vue-settings-admin-sharebymail': path.join(__dirname, 'apps/sharebymail/src', 'main-admin.js'), |