feat: Add declarative settings
Signed-off-by: jld3103 <jld3103yt@gmail.com> Signed-off-by: Julien Veyssier <julien-nc@posteo.net> Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
This commit is contained in:
parent
c42397358f
commit
4ac2375ca2
|
@ -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": {
|
||||
"application/octet-stream": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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'),
|
||||
|
|
Loading…
Reference in New Issue