settings values json instead of strings

This commit is contained in:
Roman Hergenreder 2024-04-11 14:41:03 -04:00
parent 3851b7f289
commit 3888e7fcde
9 changed files with 111 additions and 47 deletions

@ -19,7 +19,7 @@ namespace Core\API {
if ($this->success) { if ($this->success) {
$settings = $req->getResult()["settings"]; $settings = $req->getResult()["settings"];
if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") { if (!isset($settings["mail_enabled"]) || !$settings["mail_enabled"]) {
$this->createError("Mailing is not configured on this server yet."); $this->createError("Mailing is not configured on this server yet.");
return null; return null;
} }

@ -171,6 +171,8 @@ abstract class Request {
if ($this->externalCall) { if ($this->externalCall) {
$trustedDomains = $this->getCORS(); $trustedDomains = $this->getCORS();
if (!empty($trustedDomains)) { if (!empty($trustedDomains)) {
// TODO: origins require a protocol, e.g. https:// or http:// as prefix.
// should we force https for all origins? or make exceptions for localhost?
header("Access-Control-Allow-Origin: " . implode(", ", $trustedDomains)); header("Access-Control-Allow-Origin: " . implode(", ", $trustedDomains));
} }
} }

@ -3,10 +3,27 @@
namespace Core\API { namespace Core\API {
use Core\Objects\Context; use Core\Objects\Context;
use Core\API\Parameter\ArrayType;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
abstract class SettingsAPI extends Request { abstract class SettingsAPI extends Request {
protected array $predefinedKeys;
public function __construct(Context $context, bool $externalCall = false, array $params = array()) { public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params); parent::__construct($context, $externalCall, $params);
// TODO: improve this, additional validation for allowed chars etc.
// API parameters should be more configurable, e.g. allow regexes, min/max values for numbers, etc.
$this->predefinedKeys = [
"allowed_extensions" => new ArrayType("allowed_extensions", Parameter::TYPE_STRING),
"trusted_domains" => new ArrayType("allowed_extensions", Parameter::TYPE_STRING),
"user_registration_enabled" => new Parameter("user_registration_enabled", Parameter::TYPE_BOOLEAN),
"recaptcha_enabled" => new Parameter("recaptcha_enabled", Parameter::TYPE_BOOLEAN),
"mail_enabled" => new Parameter("mail_enabled", Parameter::TYPE_BOOLEAN),
"mail_port" => new Parameter("mail_port", Parameter::TYPE_INT)
];
} }
} }
} }
@ -53,7 +70,6 @@ namespace Core\API\Settings {
} }
} }
// TODO: we need additional validation for built-in settings here, e.g. csv-values, bool values, etc.
class Set extends SettingsAPI { class Set extends SettingsAPI {
public function __construct(Context $context, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array( parent::__construct($context, $externalCall, array(
@ -68,14 +84,16 @@ namespace Core\API\Settings {
} }
$paramKey = new StringType('key', 32); $paramKey = new StringType('key', 32);
$paramValue = new StringType('value', 1024, true, NULL); $paramValueDefault = new StringType('value', 1024, true, NULL);
$sql = $this->context->getSQL(); $sql = $this->context->getSQL();
$query = $sql->insert("Settings", array("name", "value")); $query = $sql->insert("Settings", ["name", "value"]);
$keys = array(); $keys = array();
$deleteKeys = array(); $deleteKeys = array();
foreach ($values as $key => $value) { foreach ($values as $key => $value) {
$paramValue = $this->predefinedKeys[$key] ?? $paramValueDefault;
if (!$paramKey->parseParam($key)) { if (!$paramKey->parseParam($key)) {
$key = print_r($key, true); $key = print_r($key, true);
return $this->createError("Invalid Type for key in parameter settings: '$key' (Required: " . $paramKey->getTypeName() . ")"); return $this->createError("Invalid Type for key in parameter settings: '$key' (Required: " . $paramKey->getTypeName() . ")");
@ -86,7 +104,7 @@ namespace Core\API\Settings {
return $this->createError("The property key should only contain alphanumeric characters, underscores and dashes"); return $this->createError("The property key should only contain alphanumeric characters, underscores and dashes");
} else { } else {
if (!is_null($paramValue->value)) { if (!is_null($paramValue->value)) {
$query->addRow($paramKey->value, $paramValue->value); $query->addRow($paramKey->value, json_encode($paramValue->value));
} else { } else {
$deleteKeys[] = $paramKey->value; $deleteKeys[] = $paramKey->value;
} }
@ -111,8 +129,8 @@ namespace Core\API\Settings {
if (count($deleteKeys) !== count($keys)) { if (count($deleteKeys) !== count($keys)) {
$query->onDuplicateKeyStrategy(new UpdateStrategy( $query->onDuplicateKeyStrategy(new UpdateStrategy(
array("name"), ["name"],
array("value" => new Column("value"))) ["value" => new Column("value")])
); );

@ -29,8 +29,8 @@ class Stats extends Request {
if ($this->success) { if ($this->success) {
$settings = $req->getResult()["settings"]; $settings = $req->getResult()["settings"];
$this->mailConfigured = ($settings["mail_enabled"] ?? "0") === "1"; $this->mailConfigured = $settings["mail_enabled"];
$this->recaptchaConfigured = ($settings["recaptcha_enabled"] ?? "0") === "1"; $this->recaptchaConfigured = $settings["recaptcha_enabled"];
} }
return $this->success; return $this->success;

@ -60,7 +60,7 @@ class Settings {
if ($res !== false && $res !== null) { if ($res !== false && $res !== null) {
$settings = array(); $settings = array();
foreach($res as $row) { foreach($res as $row) {
$settings[$row["name"]] = $row["value"]; $settings[$row["name"]] = json_decode($row["value"], true);
} }
return $settings; return $settings;
} else { } else {
@ -76,7 +76,7 @@ class Settings {
if ($res === false || $res === null) { if ($res === false || $res === null) {
return null; return null;
} else { } else {
return (empty($res)) ? $defaultValue : $res[0]["value"]; return (empty($res)) ? $defaultValue : json_decode($res[0]["value"], true);
} }
} }
@ -131,8 +131,8 @@ class Settings {
$this->mailSender = $result["mail_from"] ?? $this->mailSender; $this->mailSender = $result["mail_from"] ?? $this->mailSender;
$this->mailFooter = $result["mail_footer"] ?? $this->mailFooter; $this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
$this->mailAsync = $result["mail_async"] ?? $this->mailAsync; $this->mailAsync = $result["mail_async"] ?? $this->mailAsync;
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions))); $this->allowedExtensions = $result["allowed_extensions"] ?? $this->allowedExtensions;
$this->trustedDomains = explode(",", $result["trusted_domains"] ?? strtolower(implode(",", $this->trustedDomains))); $this->trustedDomains = $result["trusted_domains"] ?? $this->trustedDomains;
date_default_timezone_set($this->timeZone); date_default_timezone_set($this->timeZone);
} }
@ -140,23 +140,23 @@ class Settings {
} }
public function addRows(Insert $query): void { public function addRows(Insert $query): void {
$query->addRow("site_name", $this->siteName, false, false) $query->addRow("site_name", json_encode($this->siteName), false, false)
->addRow("base_url", $this->baseUrl, false, false) ->addRow("base_url", json_encode($this->baseUrl), false, false)
->addRow("trusted_domains", implode(",", $this->trustedDomains), false, false) ->addRow("trusted_domains", json_encode($this->trustedDomains), false, false)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false) ->addRow("user_registration_enabled", json_encode($this->registrationAllowed), false, false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true) ->addRow("installation_completed", json_encode($this->installationComplete), true, true)
->addRow("time_zone", $this->timeZone, false, false) ->addRow("time_zone", json_encode($this->timeZone), false, false)
->addRow("recaptcha_enabled", $this->recaptchaEnabled ? "1" : "0", false, false) ->addRow("recaptcha_enabled", json_encode($this->recaptchaEnabled), false, false)
->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false) ->addRow("recaptcha_public_key", json_encode($this->recaptchaPublicKey), false, false)
->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false) ->addRow("recaptcha_private_key", json_encode($this->recaptchaPrivateKey), true, false)
->addRow("allowed_extensions", implode(",", $this->allowedExtensions), false, false) ->addRow("allowed_extensions", json_encode($this->allowedExtensions), false, false)
->addRow("mail_host", "", false, false) ->addRow("mail_host", '""', false, false)
->addRow("mail_port", "", false, false) ->addRow("mail_port", '587', false, false)
->addRow("mail_username", "", false, false) ->addRow("mail_username", '""', false, false)
->addRow("mail_password", "", true, false) ->addRow("mail_password", '""', true, false)
->addRow("mail_from", "", false, false) ->addRow("mail_from", '""', false, false)
->addRow("mail_last_sync", "", false, false) ->addRow("mail_last_sync", '""', false, false)
->addRow("mail_footer", "", false, false) ->addRow("mail_footer", '""', false, false)
->addRow("mail_async", false, false, false); ->addRow("mail_async", false, false, false);
} }

@ -472,7 +472,7 @@ namespace Documents\Install {
if ($this->getParameter("skip") === "true") { if ($this->getParameter("skip") === "true") {
$req = new \Core\API\Settings\Set($context); $req = new \Core\API\Settings\Set($context);
$success = $req->execute(array("settings" => array("mail_enabled" => "0"))); $success = $req->execute(["settings" => ["mail_enabled" => false]]);
$msg = $req->getLastError(); $msg = $req->getLastError();
} else { } else {
@ -538,13 +538,13 @@ namespace Documents\Install {
if ($success) { if ($success) {
$req = new \Core\API\Settings\Set($context); $req = new \Core\API\Settings\Set($context);
$success = $req->execute(array("settings" => array( $success = $req->execute(["settings" => [
"mail_enabled" => "1", "mail_enabled" => true,
"mail_host" => "$address", "mail_host" => $address,
"mail_port" => "$port", "mail_port" => $port,
"mail_username" => "$username", "mail_username" => $username,
"mail_password" => "$password", "mail_password" => $password,
))); ]]);
$msg = $req->getLastError(); $msg = $req->getLastError();
} }
} }

@ -25,7 +25,7 @@ return [
"base_url" => "Basis URL", "base_url" => "Basis URL",
"user_registration_enabled" => "Benutzerregistrierung erlauben", "user_registration_enabled" => "Benutzerregistrierung erlauben",
"allowed_extensions" => "Erlaubte Dateierweiterungen", "allowed_extensions" => "Erlaubte Dateierweiterungen",
"trusted_domains" => "Vertraute Ursprungs-Domains (Komma getrennt, * als Subdomain-Wildcard)", "trusted_domains" => "Vertraute Ursprungs-Domains (* als Subdomain-Wildcard)",
"time_zone" => "Zeitzone", "time_zone" => "Zeitzone",
# mail settings # mail settings

@ -25,7 +25,7 @@ return [
"base_url" => "Base URL", "base_url" => "Base URL",
"user_registration_enabled" => "Allow user registration", "user_registration_enabled" => "Allow user registration",
"allowed_extensions" => "Allowed file extensions", "allowed_extensions" => "Allowed file extensions",
"trusted_domains" => "Trusted origin domains (comma separated, * as subdomain-wildcard)", "trusted_domains" => "Trusted origin domains (* as subdomain-wildcard)",
"time_zone" => "Time zone", "time_zone" => "Time zone",
# mail settings # mail settings

@ -12,7 +12,9 @@ import {
TableHead, TableHead,
TableContainer, TableContainer,
TableRow, TableRow,
Tabs, TextField Tabs, TextField,
Autocomplete,
Chip
} from "@mui/material"; } from "@mui/material";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import { import {
@ -69,6 +71,7 @@ export default function SettingsView(props) {
// data // data
const [fetchSettings, setFetchSettings] = useState(true); const [fetchSettings, setFetchSettings] = useState(true);
const [settings, setSettings] = useState(null); const [settings, setSettings] = useState(null);
const [extra, setExtra] = useState({});
const [uncategorizedKeys, setUncategorizedKeys] = useState([]); const [uncategorizedKeys, setUncategorizedKeys] = useState([]);
// ui // ui
@ -194,12 +197,9 @@ export default function SettingsView(props) {
setFetchSettings(true); setFetchSettings(true);
setNewKey(""); setNewKey("");
setChanged(false); setChanged(false);
setExtra({});
}, []); }, []);
if (settings === null) {
return <CircularProgress />
}
const parseBool = (v) => v !== undefined && (v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase())); const parseBool = (v) => v !== undefined && (v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase()));
const renderTextInput = (key_name, disabled=false, props={}) => { const renderTextInput = (key_name, disabled=false, props={}) => {
@ -271,14 +271,54 @@ export default function SettingsView(props) {
</SettingsFormGroup> </SettingsFormGroup>
} }
const renderTextValuesInput = (key_name, disabled=false, props={}) => {
const finishTyping = () => {
console.log("finishTyping", key_name);
setExtra({...extra, [key_name]: ""});
if (extra[key_name]) {
setSettings({...settings, [key_name]: [...settings[key_name], extra[key_name]]});
}
}
return <SettingsFormGroup key={"form-" + key_name} {...props}>
<FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
<Autocomplete
clearIcon={false}
options={[]}
freeSolo
multiple
value={settings[key_name]}
onChange={(e, v) => setSettings({...settings, [key_name]: v})}
renderTags={(values, props) =>
values.map((option, index) => (
<Chip label={option} {...props({ index })} />
))
}
renderInput={(params) => <TextField
{...params}
value={extra[key_name] ?? ""}
onChange={e => setExtra({...extra, [key_name]: e.target.value.trim()})}
onKeyDown={e => {
if (["Enter", "Tab", " "].includes(e.key)) {
e.preventDefault();
e.stopPropagation();
finishTyping();
}
}}
onBlur={finishTyping} />}
/>
</SettingsFormGroup>
}
const renderTab = () => { const renderTab = () => {
if (selectedTab === "general") { if (selectedTab === "general") {
return [ return [
renderTextInput("site_name"), renderTextInput("site_name"),
renderTextInput("base_url"), renderTextInput("base_url"),
renderTextInput("trusted_domains"), renderTextValuesInput("trusted_domains"),
renderCheckBox("user_registration_enabled"), renderCheckBox("user_registration_enabled"),
renderTextInput("allowed_extensions"), renderTextValuesInput("allowed_extensions"),
renderSelection("time_zone", TIME_ZONES), renderSelection("time_zone", TIME_ZONES),
]; ];
} else if (selectedTab === "mail") { } else if (selectedTab === "mail") {
@ -373,6 +413,10 @@ export default function SettingsView(props) {
} }
} }
if (settings === null) {
return <CircularProgress />
}
return <> return <>
<div className={"content-header"}> <div className={"content-header"}>
<div className={"container-fluid"}> <div className={"container-fluid"}>