Readonly Settings

This commit is contained in:
Roman committed 2020-06-26 19:31:31 +02:00
1 parent 33dd990b71
commit 7bf88a7c98
5 files changed
+237 -50

No files matched your search

+66 -10
View File
@@ -14,8 +14,8 @@ namespace Api\Settings {
use Api\Parameter\StringType;
use Api\SettingsAPI;
use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondLike;
use Driver\SQL\Condition\CondBool;
use Driver\SQL\Condition\CondIn;
use Driver\SQL\Condition\CondNot;
use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Strategy\UpdateStrategy;
@@ -89,29 +89,85 @@ namespace Api\Settings {
}
$paramKey = new StringType('key', 32);
$paramValue = new StringType('value', 1024);
$paramValue = new StringType('value', 1024, true, NULL);
$sql = $this->user->getSQL();
$query = $sql->insert("Settings", array("name", "value"));
$keys = array();
$deleteKeys = array();
foreach($values as $key => $value) {
if (!$paramKey->parseParam($key)) {
$key = print_r($key, true);
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);
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 {
$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(
array("name"),
array("value" => new Column("value")))
);
if ($this->isExternalCall()) {
$column = $this->checkReadonly($keys);
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();
return $this->success;
}
+10 -9
View File
@@ -145,18 +145,19 @@ class CreateDatabase {
->addString("name", 32)
->addString("value", 1024, true)
->addBool("private", false)
->addBool("readonly", false)
->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_host", "", false)
->addRow("mail_port", "", false)
->addRow("mail_username", "", false)
->addRow("mail_password", "", true)
->addRow("mail_from", "", false)
->addRow("message_confirm_email", self::MessageConfirmEmail(), false)
->addRow("message_accept_invite", self::MessageAcceptInvite(), false)
->addRow("message_reset_password", self::MessageResetPassword(), false);
->addRow("mail_host", "", false, false)
->addRow("mail_port", "", false, false)
->addRow("mail_username", "", false, false)
->addRow("mail_password", "", true, false)
->addRow("mail_from", "", false, false)
->addRow("message_confirm_email", self::MessageConfirmEmail(), false, false)
->addRow("message_accept_invite", self::MessageAcceptInvite(), false, false)
->addRow("message_reset_password", self::MessageResetPassword(), false, false);
(Settings::loadDefaults())->addRows($settingsQuery);
$queries[] = $settingsQuery;
+5 -5
View File
@@ -62,11 +62,11 @@ class Settings {
}
public function addRows(Insert $query) {
$query->addRow("site_name", $this->siteName, false)
->addRow("base_url", $this->baseUrl, false)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true)
->addRow("jwt_secret", $this->jwtSecret, true);
$query->addRow("site_name", $this->siteName, false, false)
->addRow("base_url", $this->baseUrl, false, false)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
->addRow("jwt_secret", $this->jwtSecret, true, true);
}
public function getSiteName() {
+1 -1
View File
@@ -11451,7 +11451,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Settings; });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-router-dom */ \"./node_modules/react-router-dom/esm/react-router-dom.js\");\n/* harmony import */ var _elements_alert__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../elements/alert */ \"./src/elements/alert.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-collapse/lib/Collapse */ \"./node_modules/react-collapse/lib/Collapse.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _elements_icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../elements/icon */ \"./src/elements/icon.js\");\n/* harmony import */ var draft_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! draft-js */ \"./node_modules/draft-js/lib/Draft.js\");\n/* harmony import */ var draft_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(draft_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var react_draft_wysiwyg__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! react-draft-wysiwyg */ \"./node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.js\");\n/* harmony import */ var react_draft_wysiwyg__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react_draft_wysiwyg__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var draftjs_to_html__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! draftjs-to-html */ \"./node_modules/draftjs-to-html/lib/draftjs-to-html.js\");\n/* harmony import */ var draftjs_to_html__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(draftjs_to_html__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var html_to_draftjs__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! html-to-draftjs */ \"./node_modules/html-to-draftjs/dist/html-to-draftjs.js\");\n/* harmony import */ var html_to_draftjs__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(html_to_draftjs__WEBPACK_IMPORTED_MODULE_8__);\n/* harmony import */ var sanitize_html__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! sanitize-html */ \"./node_modules/sanitize-html/dist/sanitize-html.js\");\n/* harmony import */ var sanitize_html__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(sanitize_html__WEBPACK_IMPORTED_MODULE_9__);\n/* harmony import */ var react_draft_wysiwyg_dist_react_draft_wysiwyg_css__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! react-draft-wysiwyg/dist/react-draft-wysiwyg.css */ \"./node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css\");\n/* harmony import */ var react_draft_wysiwyg_dist_react_draft_wysiwyg_css__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(react_draft_wysiwyg_dist_react_draft_wysiwyg_css__WEBPACK_IMPORTED_MODULE_10__);\n/* harmony import */ var react_tooltip__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! react-tooltip */ \"./node_modules/react-tooltip/dist/index.es.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === \"undefined\" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } }Line truncated
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Settings; });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_router_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-router-dom */ \"./node_modules/react-router-dom/esm/react-router-dom.js\");\n/* harmony import */ var _elements_alert__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../elements/alert */ \"./src/elements/alert.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-collapse/lib/Collapse */ \"./node_modules/react-collapse/lib/Collapse.js\");\n/* harmony import */ var react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react_collapse_lib_Collapse__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _elements_icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../elements/icon */ \"./src/elements/icon.js\");\n/* harmony import */ var draft_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! draft-js */ \"./node_modules/draft-js/lib/Draft.js\");\n/* harmony import */ var draft_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(draft_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var react_draft_wysiwyg__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! react-draft-wysiwyg */ \"./node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.js\");\n/* harmony import */ var react_draft_wysiwyg__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react_draft_wysiwyg__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var draftjs_to_html__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! draftjs-to-html */ \"./node_modules/draftjs-to-html/lib/draftjs-to-html.js\");\n/* harmony import */ var draftjs_to_html__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(draftjs_to_html__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var html_to_draftjs__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! html-to-draftjs */ \"./node_modules/html-to-draftjs/dist/html-to-draftjs.js\");\n/* harmony import */ var html_to_draftjs__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(html_to_draftjs__WEBPACK_IMPORTED_MODULE_8__);\n/* harmony import */ var sanitize_html__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! sanitize-html */ \"./node_modules/sanitize-html/dist/sanitize-html.js\");\n/* harmony import */ var sanitize_html__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(sanitize_html__WEBPACK_IMPORTED_MODULE_9__);\n/* harmony import */ var react_draft_wysiwyg_dist_react_draft_wysiwyg_css__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! react-draft-wysiwyg/dist/react-draft-wysiwyg.css */ \"./node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css\");\n/* harmony import */ var react_draft_wysiwyg_dist_react_draft_wysiwyg_css__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(react_draft_wysiwyg_dist_react_draft_wysiwyg_css__WEBPACK_IMPORTED_MODULE_10__);\n/* harmony import */ var react_tooltip__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! react-tooltip */ \"./node_modules/react-tooltip/dist/index.es.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === \"undefined\" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } }Line truncated
/***/ }),
+155 -25
View File
@@ -50,18 +50,80 @@ export default class Settings extends React.Component {
isOpen: true,
isSaving: false,
isResetting: false,
settings: []
},
};
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() {
this.parent.api.getSettings().then((res) => {
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 {
let errors = this.state.errors.slice();
errors.push({title: "Error fetching settings", message: res.msg});
@@ -341,7 +403,52 @@ export default class Settings extends React.Component {
}
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() {
@@ -427,11 +534,6 @@ export default class Settings extends React.Component {
onReset(category) {
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) => {
if (!res.success) {
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}
});
} else {
let newSettings = {...this.state.settings};
let newState = { ...this.state };
let categoryUpdated = {...this.state[category], isResetting: false};
let newSettings = {...this.state.settings};
for (let key of this.state[category].keys) {
newSettings[key] = res.settings[key] ?? "";
if (category === "uncategorised") {
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") {
categoryUpdated.unsavedMailSettings = false;
} else if (category === "messages") {
categoryUpdated.isEditing = null;
}
this.setState({
...this.state, settings: newSettings,
[category]: categoryUpdated
});
newState.settings = newSettings;
newState[category] = categoryUpdated;
this.setState(newState);
}
});
}
@@ -470,12 +581,31 @@ export default class Settings extends React.Component {
}
let values = {};
for (let key of this.state[category].keys) {
if (key === "mail_password" && !this.state.settings[key]) {
continue;
if (category === "uncategorised") {
for (let prop of this.state.uncategorised.settings) {
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) => {