Readonly Settings
This commit is contained in:
parent
33dd990b71
commit
7bf88a7c98
@ -14,8 +14,8 @@ namespace Api\Settings {
|
|||||||
use Api\Parameter\StringType;
|
use Api\Parameter\StringType;
|
||||||
use Api\SettingsAPI;
|
use Api\SettingsAPI;
|
||||||
use Driver\SQL\Column\Column;
|
use Driver\SQL\Column\Column;
|
||||||
use Driver\SQL\Condition\Compare;
|
use Driver\SQL\Condition\CondBool;
|
||||||
use Driver\SQL\Condition\CondLike;
|
use Driver\SQL\Condition\CondIn;
|
||||||
use Driver\SQL\Condition\CondNot;
|
use Driver\SQL\Condition\CondNot;
|
||||||
use Driver\SQL\Condition\CondRegex;
|
use Driver\SQL\Condition\CondRegex;
|
||||||
use Driver\SQL\Strategy\UpdateStrategy;
|
use Driver\SQL\Strategy\UpdateStrategy;
|
||||||
@ -89,29 +89,85 @@ namespace Api\Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$paramKey = new StringType('key', 32);
|
$paramKey = new StringType('key', 32);
|
||||||
$paramValue = new StringType('value', 1024);
|
$paramValue = new StringType('value', 1024, true, NULL);
|
||||||
|
|
||||||
$sql = $this->user->getSQL();
|
$sql = $this->user->getSQL();
|
||||||
$query = $sql->insert("Settings", array("name", "value"));
|
$query = $sql->insert("Settings", array("name", "value"));
|
||||||
|
$keys = array();
|
||||||
|
$deleteKeys = array();
|
||||||
|
|
||||||
foreach($values as $key => $value) {
|
foreach($values as $key => $value) {
|
||||||
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() . ")");
|
||||||
} else if(!$paramValue->parseParam($value)) {
|
} else if(!is_null($value) && !$paramValue->parseParam($value)) {
|
||||||
$value = print_r($value, true);
|
$value = print_r($value, true);
|
||||||
return $this->createError("Invalid Type for value in parameter settings: '$value' (Required: " . $paramValue->getTypeName() . ")");
|
return $this->createError("Invalid Type for value in parameter settings: '$value' (Required: " . $paramValue->getTypeName() . ")");
|
||||||
|
} else if(preg_match("/^[a-zA-Z_][a-zA-Z_0-9-]*$/", $paramKey->value) !== 1) {
|
||||||
|
return $this->createError("The property key should only contain alphanumeric characters, underscores and dashes");
|
||||||
} else {
|
} else {
|
||||||
$query->addRow($paramKey->value, $paramValue->value);
|
if (!is_null($paramValue->value)) {
|
||||||
|
$query->addRow($paramKey->value, $paramValue->value);
|
||||||
|
} else {
|
||||||
|
$deleteKeys[] = $paramKey->value;
|
||||||
|
}
|
||||||
|
$keys[] = $paramKey->value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->onDuplicateKeyStrategy(new UpdateStrategy(
|
if ($this->isExternalCall()) {
|
||||||
array("name"),
|
$column = $this->checkReadonly($keys);
|
||||||
array("value" => new Column("value")))
|
if(!$this->success) {
|
||||||
);
|
return false;
|
||||||
|
} else if($column !== null) {
|
||||||
|
return $this->createError("Column '$column' is readonly.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->success = ($query->execute() !== FALSE);
|
if (!empty($deleteKeys) && !$this->deleteKeys($keys)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($deleteKeys) !== count($keys)) {
|
||||||
|
$query->onDuplicateKeyStrategy(new UpdateStrategy(
|
||||||
|
array("name"),
|
||||||
|
array("value" => new Column("value")))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
$this->success = ($query->execute() !== FALSE);
|
||||||
|
$this->lastError = $sql->getLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkReadonly(array $keys) {
|
||||||
|
$sql = $this->user->getSQL();
|
||||||
|
$res = $sql->select("name")
|
||||||
|
->from("Settings")
|
||||||
|
->where(new CondBool("readonly"))
|
||||||
|
->where(new CondIn("name", $keys))
|
||||||
|
->limit(1)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$this->success = ($res !== FALSE);
|
||||||
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
|
if ($this->success && !empty($res)) {
|
||||||
|
return $res[0]["name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteKeys(array $keys) {
|
||||||
|
$sql = $this->user->getSQL();
|
||||||
|
$res = $sql->delete("Settings")
|
||||||
|
->where(new CondIn("name", $keys))
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$this->success = ($res !== FALSE);
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
@ -145,18 +145,19 @@ class CreateDatabase {
|
|||||||
->addString("name", 32)
|
->addString("name", 32)
|
||||||
->addString("value", 1024, true)
|
->addString("value", 1024, true)
|
||||||
->addBool("private", false)
|
->addBool("private", false)
|
||||||
|
->addBool("readonly", false)
|
||||||
->primaryKey("name");
|
->primaryKey("name");
|
||||||
|
|
||||||
$settingsQuery = $sql->insert("Settings", array("name", "value", "private"))
|
$settingsQuery = $sql->insert("Settings", array("name", "value", "private", "readonly"))
|
||||||
// ->addRow("mail_enabled", "0") # this key will be set during installation
|
// ->addRow("mail_enabled", "0") # this key will be set during installation
|
||||||
->addRow("mail_host", "", false)
|
->addRow("mail_host", "", false, false)
|
||||||
->addRow("mail_port", "", false)
|
->addRow("mail_port", "", false, false)
|
||||||
->addRow("mail_username", "", false)
|
->addRow("mail_username", "", false, false)
|
||||||
->addRow("mail_password", "", true)
|
->addRow("mail_password", "", true, false)
|
||||||
->addRow("mail_from", "", false)
|
->addRow("mail_from", "", false, false)
|
||||||
->addRow("message_confirm_email", self::MessageConfirmEmail(), false)
|
->addRow("message_confirm_email", self::MessageConfirmEmail(), false, false)
|
||||||
->addRow("message_accept_invite", self::MessageAcceptInvite(), false)
|
->addRow("message_accept_invite", self::MessageAcceptInvite(), false, false)
|
||||||
->addRow("message_reset_password", self::MessageResetPassword(), false);
|
->addRow("message_reset_password", self::MessageResetPassword(), false, false);
|
||||||
|
|
||||||
(Settings::loadDefaults())->addRows($settingsQuery);
|
(Settings::loadDefaults())->addRows($settingsQuery);
|
||||||
$queries[] = $settingsQuery;
|
$queries[] = $settingsQuery;
|
||||||
|
@ -62,11 +62,11 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function addRows(Insert $query) {
|
public function addRows(Insert $query) {
|
||||||
$query->addRow("site_name", $this->siteName, false)
|
$query->addRow("site_name", $this->siteName, false, false)
|
||||||
->addRow("base_url", $this->baseUrl, false)
|
->addRow("base_url", $this->baseUrl, false, false)
|
||||||
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false)
|
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
|
||||||
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true)
|
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
|
||||||
->addRow("jwt_secret", $this->jwtSecret, true);
|
->addRow("jwt_secret", $this->jwtSecret, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSiteName() {
|
public function getSiteName() {
|
||||||
|
2
js/admin.min.js
vendored
2
js/admin.min.js
vendored
File diff suppressed because one or more lines are too long
@ -50,18 +50,80 @@ export default class Settings extends React.Component {
|
|||||||
isOpen: true,
|
isOpen: true,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
isResetting: false,
|
isResetting: false,
|
||||||
|
settings: []
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.parent = {
|
this.parent = {
|
||||||
api: props.api
|
api: props.api,
|
||||||
|
showDialog: props.showDialog
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.hiddenKeys = [
|
||||||
|
"mail_password",
|
||||||
|
"jwt_secret"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
isDefaultKey(key) {
|
||||||
|
key = key.trim();
|
||||||
|
return this.state.general.keys.includes(key)
|
||||||
|
|| this.state.mail.keys.includes(key)
|
||||||
|
|| this.state.messages.keys.includes(key)
|
||||||
|
|| this.hiddenKeys.includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUncategorisedValues(res) {
|
||||||
|
let uncategorised = [];
|
||||||
|
for(let key in res.settings) {
|
||||||
|
if (res.settings.hasOwnProperty(key) && !this.isDefaultKey(key)) {
|
||||||
|
uncategorised.push({key: key, value: res.settings[key]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uncategorised;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteUncategorisedProp(index) {
|
||||||
|
if (index < 0 || index >= this.state.uncategorised.settings.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let props = this.state.uncategorised.settings.slice();
|
||||||
|
props.splice(index, 1);
|
||||||
|
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeUncategorisedValue(event, index, isKey) {
|
||||||
|
if (index < 0 || index >= this.state.uncategorised.settings.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let props = this.state.uncategorised.settings.slice();
|
||||||
|
if (isKey) {
|
||||||
|
props[index].key = event.target.value;
|
||||||
|
} else {
|
||||||
|
props[index].value = event.target.value;
|
||||||
|
}
|
||||||
|
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddUncategorisedProperty() {
|
||||||
|
let props = this.state.uncategorised.settings.slice();
|
||||||
|
props.push({key: "", value: ""});
|
||||||
|
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.parent.api.getSettings().then((res) => {
|
this.parent.api.getSettings().then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.setState({ ...this.state, settings: res.settings });
|
let newState = {
|
||||||
|
...this.state,
|
||||||
|
settings: res.settings,
|
||||||
|
uncategorised: { ...this.state.uncategorised, settings: this.getUncategorisedValues(res) }
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState(newState);
|
||||||
} else {
|
} else {
|
||||||
let errors = this.state.errors.slice();
|
let errors = this.state.errors.slice();
|
||||||
errors.push({title: "Error fetching settings", message: res.msg});
|
errors.push({title: "Error fetching settings", message: res.msg});
|
||||||
@ -341,7 +403,52 @@ export default class Settings extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUncategorizedForm() {
|
getUncategorizedForm() {
|
||||||
return <b>Coming soon…</b>
|
let tr = [];
|
||||||
|
|
||||||
|
for(let i = 0; i < this.state.uncategorised.settings.length; i++) {
|
||||||
|
let key = this.state.uncategorised.settings[i].key;
|
||||||
|
let value = this.state.uncategorised.settings[i].value;
|
||||||
|
tr.push(
|
||||||
|
<tr key={"uncategorised-" + i} className={(i % 2 === 0) ? "even" : "odd"}>
|
||||||
|
<td>
|
||||||
|
<input className={"form-control"} type={"text"} value={key} maxLength={32} placeholder={"Key"}
|
||||||
|
onChange={(e) => this.onChangeUncategorisedValue(e, i, true)} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input className={"form-control"} type={"text"} value={value} placeholder={"value"}
|
||||||
|
onChange={(e) => this.onChangeUncategorisedValue(e, i, false)} />
|
||||||
|
</td>
|
||||||
|
<td className={"text-center align-middle"}>
|
||||||
|
<ReactTooltip id={"tooltip-uncategorised-" + i} />
|
||||||
|
<Icon icon={"trash"} className={"text-danger"} style={{cursor: "pointer"}}
|
||||||
|
onClick={() => this.onDeleteUncategorisedProp(i)} data-type={"error"}
|
||||||
|
data-tip={"Delete property"} data-place={"right"} data-effect={"solid"}
|
||||||
|
data-for={"tooltip-uncategorised-" + i}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<table className={"table table-bordered table-hover dataTable dtr-inline"}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th className={"text-center"}><Icon icon={"tools"}/></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tr}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className={"mt-2 mb-3"}>
|
||||||
|
<button className={"btn btn-info"} onClick={() => this.onAddUncategorisedProperty()} >
|
||||||
|
<Icon icon={"plus"} className={"mr-2"} /> Add property
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -427,11 +534,6 @@ export default class Settings extends React.Component {
|
|||||||
onReset(category) {
|
onReset(category) {
|
||||||
this.setState({...this.state, [category]: {...this.state[category], isResetting: true}});
|
this.setState({...this.state, [category]: {...this.state[category], isResetting: true}});
|
||||||
|
|
||||||
let values = {};
|
|
||||||
for (let key of this.state[category].keys) {
|
|
||||||
values[key] = this.state.settings[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.parent.api.getSettings().then((res) => {
|
this.parent.api.getSettings().then((res) => {
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
let alerts = this.state[category].alerts.slice();
|
let alerts = this.state[category].alerts.slice();
|
||||||
@ -441,23 +543,32 @@ export default class Settings extends React.Component {
|
|||||||
[category]: {...this.state[category], alerts: alerts, isResetting: false}
|
[category]: {...this.state[category], alerts: alerts, isResetting: false}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let newSettings = {...this.state.settings};
|
let newState = { ...this.state };
|
||||||
let categoryUpdated = {...this.state[category], isResetting: false};
|
let categoryUpdated = {...this.state[category], isResetting: false};
|
||||||
|
let newSettings = {...this.state.settings};
|
||||||
|
|
||||||
for (let key of this.state[category].keys) {
|
if (category === "uncategorised") {
|
||||||
newSettings[key] = res.settings[key] ?? "";
|
categoryUpdated.settings = this.getUncategorisedValues(res);
|
||||||
|
for (let key in res.settings) {
|
||||||
|
if (res.settings.hasOwnProperty(key) && !this.isDefaultKey(key)) {
|
||||||
|
newSettings[key] = res.settings[key] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let key of this.state[category].keys) {
|
||||||
|
newSettings[key] = res.settings[key] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category === "mail") {
|
||||||
|
categoryUpdated.unsavedMailSettings = false;
|
||||||
|
} else if (category === "messages") {
|
||||||
|
categoryUpdated.isEditing = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category === "mail") {
|
newState.settings = newSettings;
|
||||||
categoryUpdated.unsavedMailSettings = false;
|
newState[category] = categoryUpdated;
|
||||||
} else if (category === "messages") {
|
this.setState(newState);
|
||||||
categoryUpdated.isEditing = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
...this.state, settings: newSettings,
|
|
||||||
[category]: categoryUpdated
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -470,12 +581,31 @@ export default class Settings extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let values = {};
|
let values = {};
|
||||||
for (let key of this.state[category].keys) {
|
if (category === "uncategorised") {
|
||||||
if (key === "mail_password" && !this.state.settings[key]) {
|
for (let prop of this.state.uncategorised.settings) {
|
||||||
continue;
|
if (prop.key) {
|
||||||
|
values[prop.key] = prop.value;
|
||||||
|
if (this.isDefaultKey(prop.key)) {
|
||||||
|
this.parent.showDialog("You cannot use this key as property key: " + prop.key, "System specific key");
|
||||||
|
this.setState({...this.state, [category]: {...this.state[category], isSaving: false}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
values[key] = this.state.settings[key];
|
for (let key in this.state.settings) {
|
||||||
|
if (this.state.settings.hasOwnProperty(key) && !this.isDefaultKey(key) && !values.hasOwnProperty(key)) {
|
||||||
|
values[key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let key of this.state[category].keys) {
|
||||||
|
if (this.hiddenKeys.includes(key) && !this.state.settings[key]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[key] = this.state.settings[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.parent.api.saveSettings(values).then((res) => {
|
this.parent.api.saveSettings(values).then((res) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user