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