mirror of https://github.com/nextcloud/server.git
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>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