Signed-off-by: jld3103 <jld3103yt@gmail.com> Signed-off-by: Julien Veyssier <julien-nc@posteo.net> Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>pull/42661/head
parent
c42397358f
commit
4ac2375ca2
@ -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)); |
||||
} |
||||
} |
@ -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": [] |
||||
} |
@ -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); |
||||
}); |
@ -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
@ -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; |
||||
} |
||||
} |
@ -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']); |
||||
} |
||||
} |
Loading…
Reference in new issue