@saltcorn/server 1.0.0-beta.13 → 1.0.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +3 -0
- package/load_plugins.js +40 -12
- package/locales/en.json +7 -2
- package/package.json +9 -9
- package/public/saltcorn-common.js +15 -2
- package/public/saltcorn.js +22 -5
- package/routes/actions.js +7 -23
- package/routes/admin.js +147 -9
- package/routes/infoarch.js +8 -1
- package/routes/menu.js +3 -0
- package/routes/pageedit.js +7 -1
- package/routes/plugins.js +70 -15
- package/routes/tables.js +9 -3
- package/serve.js +16 -2
package/app.js
CHANGED
|
@@ -447,6 +447,9 @@ Sitemap: ${base}sitemap.xml
|
|
|
447
447
|
app.get("*", function (req, res) {
|
|
448
448
|
res.status(404).sendWrap(req.__("Not found"), h1(req.__("Page not found")));
|
|
449
449
|
});
|
|
450
|
+
|
|
451
|
+
//prevent prototype pollution
|
|
452
|
+
delete Object.prototype.__proto__;
|
|
450
453
|
return app;
|
|
451
454
|
};
|
|
452
455
|
module.exports = getApp;
|
package/load_plugins.js
CHANGED
|
@@ -19,17 +19,46 @@ const {
|
|
|
19
19
|
resolveLatest,
|
|
20
20
|
} = require("@saltcorn/plugins-loader/stable_versioning");
|
|
21
21
|
|
|
22
|
+
const isFixedPlugin = (plugin) =>
|
|
23
|
+
plugin.location === "@saltcorn/sbadmin2" ||
|
|
24
|
+
plugin.location === "@saltcorn/base-plugin";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* return the cached engine infos or fetch them from npm and update the cache
|
|
28
|
+
* @param plugin plugin to load
|
|
29
|
+
*/
|
|
30
|
+
const getEngineInfos = async (plugin, forceFetch) => {
|
|
31
|
+
const rootState = getRootState();
|
|
32
|
+
const cached = rootState.getConfig("engines_cache", {}) || {};
|
|
33
|
+
if (cached[plugin.location] && !forceFetch) {
|
|
34
|
+
return cached[plugin.location];
|
|
35
|
+
} else {
|
|
36
|
+
getState().log(5, `Fetching versions for '${plugin.location}'`);
|
|
37
|
+
const pkgInfo = await npmFetch.json(
|
|
38
|
+
`https://registry.npmjs.org/${plugin.location}`
|
|
39
|
+
);
|
|
40
|
+
const versions = pkgInfo.versions;
|
|
41
|
+
const newCached = {};
|
|
42
|
+
for (const [k, v] of Object.entries(versions)) {
|
|
43
|
+
newCached[k] = v.engines?.saltcorn
|
|
44
|
+
? { engines: { saltcorn: v.engines.saltcorn } }
|
|
45
|
+
: {};
|
|
46
|
+
}
|
|
47
|
+
cached[plugin.location] = newCached;
|
|
48
|
+
await rootState.setConfig("engines_cache", { ...cached });
|
|
49
|
+
return newCached;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
22
53
|
/**
|
|
23
54
|
* checks the saltcorn engine property and changes the plugin version if necessary
|
|
24
55
|
* @param plugin plugin to load
|
|
25
56
|
*/
|
|
26
|
-
const ensurePluginSupport = async (plugin) => {
|
|
27
|
-
const
|
|
28
|
-
`https://registry.npmjs.org/${plugin.location}`
|
|
29
|
-
);
|
|
57
|
+
const ensurePluginSupport = async (plugin, forceFetch) => {
|
|
58
|
+
const versions = await getEngineInfos(plugin, forceFetch);
|
|
30
59
|
const supported = supportedVersion(
|
|
31
60
|
plugin.version || "latest",
|
|
32
|
-
|
|
61
|
+
versions,
|
|
33
62
|
packagejson.version
|
|
34
63
|
);
|
|
35
64
|
if (!supported)
|
|
@@ -38,8 +67,7 @@ const ensurePluginSupport = async (plugin) => {
|
|
|
38
67
|
);
|
|
39
68
|
else if (
|
|
40
69
|
supported !== plugin.version ||
|
|
41
|
-
(plugin.version === "latest" &&
|
|
42
|
-
supported !== resolveLatest(pkgInfo.versions))
|
|
70
|
+
(plugin.version === "latest" && supported !== resolveLatest(versions))
|
|
43
71
|
)
|
|
44
72
|
plugin.version = supported;
|
|
45
73
|
};
|
|
@@ -50,10 +78,10 @@ const ensurePluginSupport = async (plugin) => {
|
|
|
50
78
|
* @param plugin - plugin to load
|
|
51
79
|
* @param force - force flag
|
|
52
80
|
*/
|
|
53
|
-
const loadPlugin = async (plugin, force) => {
|
|
54
|
-
if (plugin.source === "npm" &&
|
|
81
|
+
const loadPlugin = async (plugin, force, forceFetch) => {
|
|
82
|
+
if (plugin.source === "npm" && !isFixedPlugin(plugin)) {
|
|
55
83
|
try {
|
|
56
|
-
await ensurePluginSupport(plugin);
|
|
84
|
+
await ensurePluginSupport(plugin, forceFetch);
|
|
57
85
|
} catch (e) {
|
|
58
86
|
console.log(
|
|
59
87
|
`Warning: Unable to find a supported version for '${plugin.location}' Continuing with the installed version`
|
|
@@ -145,7 +173,7 @@ const loadAllPlugins = async (force) => {
|
|
|
145
173
|
}
|
|
146
174
|
}
|
|
147
175
|
await getState().refreshUserLayouts();
|
|
148
|
-
await getState().refresh(true);
|
|
176
|
+
await getState().refresh(true, true);
|
|
149
177
|
if (!isRoot()) reloadAuthFromRoot();
|
|
150
178
|
};
|
|
151
179
|
|
|
@@ -279,6 +307,6 @@ module.exports = {
|
|
|
279
307
|
loadAllPlugins,
|
|
280
308
|
loadPlugin,
|
|
281
309
|
requirePlugin,
|
|
282
|
-
|
|
310
|
+
getEngineInfos,
|
|
283
311
|
ensurePluginSupport,
|
|
284
312
|
};
|
package/locales/en.json
CHANGED
|
@@ -1468,5 +1468,10 @@
|
|
|
1468
1468
|
"Time to run": "Time to run",
|
|
1469
1469
|
"Mobile": "Mobile",
|
|
1470
1470
|
"Plain password trigger row": "Plain password trigger row",
|
|
1471
|
-
"Send plaintext password changes to Users table triggers (Insert, Update and Validate).": "Send plaintext password changes to Users table triggers (Insert, Update and Validate)."
|
|
1472
|
-
|
|
1471
|
+
"Send plaintext password changes to Users table triggers (Insert, Update and Validate).": "Send plaintext password changes to Users table triggers (Insert, Update and Validate).",
|
|
1472
|
+
"Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>": "Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>",
|
|
1473
|
+
"Select tag": "Select tag",
|
|
1474
|
+
"Invalid build directory path": "Invalid build directory path",
|
|
1475
|
+
"Invalid build directory name": "Invalid build directory name",
|
|
1476
|
+
"clean node_modules": "clean node_modules"
|
|
1477
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.15",
|
|
4
4
|
"description": "Server app for Saltcorn, open-source no-code platform",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@aws-sdk/client-s3": "^3.451.0",
|
|
10
|
-
"@saltcorn/base-plugin": "1.0.0-beta.
|
|
11
|
-
"@saltcorn/builder": "1.0.0-beta.
|
|
12
|
-
"@saltcorn/data": "1.0.0-beta.
|
|
13
|
-
"@saltcorn/admin-models": "1.0.0-beta.
|
|
14
|
-
"@saltcorn/filemanager": "1.0.0-beta.
|
|
15
|
-
"@saltcorn/markup": "1.0.0-beta.
|
|
16
|
-
"@saltcorn/plugins-loader": "1.0.0-beta.
|
|
17
|
-
"@saltcorn/sbadmin2": "1.0.0-beta.
|
|
10
|
+
"@saltcorn/base-plugin": "1.0.0-beta.15",
|
|
11
|
+
"@saltcorn/builder": "1.0.0-beta.15",
|
|
12
|
+
"@saltcorn/data": "1.0.0-beta.15",
|
|
13
|
+
"@saltcorn/admin-models": "1.0.0-beta.15",
|
|
14
|
+
"@saltcorn/filemanager": "1.0.0-beta.15",
|
|
15
|
+
"@saltcorn/markup": "1.0.0-beta.15",
|
|
16
|
+
"@saltcorn/plugins-loader": "1.0.0-beta.15",
|
|
17
|
+
"@saltcorn/sbadmin2": "1.0.0-beta.15",
|
|
18
18
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
19
19
|
"@socket.io/sticky": "^1.0.1",
|
|
20
20
|
"adm-zip": "0.5.10",
|
|
@@ -18,7 +18,7 @@ function monospace_block_click(e) {
|
|
|
18
18
|
function copy_monospace_block(e) {
|
|
19
19
|
let e1 = $(e).next("pre");
|
|
20
20
|
let e2 = $(e1).next("pre");
|
|
21
|
-
if (!e2.length) return navigator.clipboard.writeText($(
|
|
21
|
+
if (!e2.length) return navigator.clipboard.writeText($(e1).text());
|
|
22
22
|
const e1t = e1.text();
|
|
23
23
|
const e2t = e2.text();
|
|
24
24
|
if (e1t.length > e2t.length) return navigator.clipboard.writeText(e1t);
|
|
@@ -184,8 +184,21 @@ function apply_showif() {
|
|
|
184
184
|
var current = e.attr("data-selected") || e.val();
|
|
185
185
|
//console.log({ field: e.attr("name"), target: data[0], val, current });
|
|
186
186
|
e.empty();
|
|
187
|
+
//TODO clean repetition in following cose
|
|
187
188
|
(options || []).forEach((o) => {
|
|
188
|
-
if (
|
|
189
|
+
if (o && o.optgroup) {
|
|
190
|
+
const opts = o.options
|
|
191
|
+
.map(
|
|
192
|
+
(innero) =>
|
|
193
|
+
`<option ${
|
|
194
|
+
`${current}` === `${innero.value || innero}` ? "selected " : ""
|
|
195
|
+
}value="${innero.value || innero}">${
|
|
196
|
+
innero.label || innero
|
|
197
|
+
}</option>`
|
|
198
|
+
)
|
|
199
|
+
.join("");
|
|
200
|
+
e.append($(`<optgroup label="${o.label}">` + opts + "</optgroup>"));
|
|
201
|
+
} else if (
|
|
189
202
|
!(o && typeof o.label !== "undefined" && typeof o.value !== "undefined")
|
|
190
203
|
) {
|
|
191
204
|
if (`${current}` === `${o}`)
|
package/public/saltcorn.js
CHANGED
|
@@ -225,8 +225,10 @@ function ajax_done(res, viewname) {
|
|
|
225
225
|
function spin_action_link(e) {
|
|
226
226
|
const $e = $(e);
|
|
227
227
|
const width = $e.width();
|
|
228
|
+
const height = $e.height();
|
|
229
|
+
|
|
228
230
|
$e.attr("data-innerhtml-prespin", $e.html());
|
|
229
|
-
$e.html('<i class="fas fa-spinner fa-spin"></i>').width(width);
|
|
231
|
+
$e.html('<i class="fas fa-spinner fa-spin"></i>').width(width).height(height);
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
function reset_spinners() {
|
|
@@ -870,7 +872,7 @@ function build_mobile_app(button) {
|
|
|
870
872
|
|
|
871
873
|
if (
|
|
872
874
|
params.useDocker &&
|
|
873
|
-
!cordovaBuilderAvailable &&
|
|
875
|
+
!window.cordovaBuilderAvailable &&
|
|
874
876
|
!confirm(
|
|
875
877
|
"Docker is selected but the Cordova builder seems not to be installed. " +
|
|
876
878
|
"Do you really want to continue?"
|
|
@@ -878,6 +880,21 @@ function build_mobile_app(button) {
|
|
|
878
880
|
) {
|
|
879
881
|
return;
|
|
880
882
|
}
|
|
883
|
+
|
|
884
|
+
const notSupportedPlugins = params.includedPlugins.filter(
|
|
885
|
+
(plugin) => !window.pluginsReadyForMobile.includes(plugin)
|
|
886
|
+
);
|
|
887
|
+
if (
|
|
888
|
+
notSupportedPlugins.length > 0 &&
|
|
889
|
+
!confirm(
|
|
890
|
+
`It seems that the plugins '${notSupportedPlugins.join(
|
|
891
|
+
", "
|
|
892
|
+
)}' are not ready for mobile. Do you really want to continue?`
|
|
893
|
+
)
|
|
894
|
+
) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
881
898
|
ajax_post("/admin/build-mobile-app", {
|
|
882
899
|
data: params,
|
|
883
900
|
success: (data) => {
|
|
@@ -949,8 +966,8 @@ function check_cordova_builder() {
|
|
|
949
966
|
$.ajax("/admin/mobile-app/check-cordova-builder", {
|
|
950
967
|
type: "GET",
|
|
951
968
|
success: function (res) {
|
|
952
|
-
cordovaBuilderAvailable = !!res.installed;
|
|
953
|
-
if (cordovaBuilderAvailable) {
|
|
969
|
+
window.cordovaBuilderAvailable = !!res.installed;
|
|
970
|
+
if (window.cordovaBuilderAvailable) {
|
|
954
971
|
$("#dockerBuilderStatusId").html(
|
|
955
972
|
`<span>
|
|
956
973
|
installed<i class="ps-2 fas fa-check text-success"></i>
|
|
@@ -1044,7 +1061,7 @@ function toggle_tbl_sync() {
|
|
|
1044
1061
|
function toggle_android_platform() {
|
|
1045
1062
|
if ($("#androidCheckboxId")[0].checked === true) {
|
|
1046
1063
|
$("#dockerCheckboxId").attr("hidden", false);
|
|
1047
|
-
$("#dockerCheckboxId").attr("checked", cordovaBuilderAvailable);
|
|
1064
|
+
$("#dockerCheckboxId").attr("checked", window.cordovaBuilderAvailable);
|
|
1048
1065
|
$("#dockerLabelId").removeClass("d-none");
|
|
1049
1066
|
} else {
|
|
1050
1067
|
$("#dockerCheckboxId").attr("hidden", true);
|
package/routes/actions.js
CHANGED
|
@@ -57,21 +57,6 @@ const {
|
|
|
57
57
|
blocklyToolbox,
|
|
58
58
|
} = require("../markup/blockly.js");
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
* @returns {Promise<object>}
|
|
62
|
-
*/
|
|
63
|
-
const getActions = async () => {
|
|
64
|
-
return Object.entries(getState().actions).map(([k, v]) => {
|
|
65
|
-
const hasConfig = !!v.configFields;
|
|
66
|
-
const requireRow = !!v.requireRow;
|
|
67
|
-
return {
|
|
68
|
-
name: k,
|
|
69
|
-
hasConfig,
|
|
70
|
-
requireRow,
|
|
71
|
-
};
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
|
|
75
60
|
/**
|
|
76
61
|
* Show list of Actions (Triggers) (HTTP GET)
|
|
77
62
|
* @name get
|
|
@@ -96,7 +81,7 @@ router.get(
|
|
|
96
81
|
triggers = triggers.filter((t) => tagged_trigger_ids.has(t.id));
|
|
97
82
|
filterOnTag = await Tag.findOne({ id: +req.query._tag });
|
|
98
83
|
}
|
|
99
|
-
const actions =
|
|
84
|
+
const actions = Trigger.abbreviated_actions;
|
|
100
85
|
send_events_page({
|
|
101
86
|
res,
|
|
102
87
|
req,
|
|
@@ -156,7 +141,7 @@ const triggerForm = async (req, trigger) => {
|
|
|
156
141
|
value: r.id,
|
|
157
142
|
label: r.role,
|
|
158
143
|
}));
|
|
159
|
-
const actions =
|
|
144
|
+
const actions = Trigger.abbreviated_actions;
|
|
160
145
|
const tables = await Table.find({});
|
|
161
146
|
let id;
|
|
162
147
|
let form_action;
|
|
@@ -168,14 +153,13 @@ const triggerForm = async (req, trigger) => {
|
|
|
168
153
|
const hasChannel = Object.entries(getState().eventTypes)
|
|
169
154
|
.filter(([k, v]) => v.hasChannel)
|
|
170
155
|
.map(([k, v]) => k);
|
|
171
|
-
|
|
172
|
-
allActions.
|
|
156
|
+
|
|
157
|
+
const allActions = Trigger.action_options({ notRequireRow: false });
|
|
173
158
|
const table_triggers = ["Insert", "Update", "Delete", "Validate"];
|
|
174
159
|
const action_options = {};
|
|
175
|
-
const actionsNotRequiringRow =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
actionsNotRequiringRow.push("Multi-step action");
|
|
160
|
+
const actionsNotRequiringRow = Trigger.action_options({
|
|
161
|
+
notRequireRow: true,
|
|
162
|
+
});
|
|
179
163
|
|
|
180
164
|
Trigger.when_options.forEach((t) => {
|
|
181
165
|
if (table_triggers.includes(t)) action_options[t] = allActions;
|
package/routes/admin.js
CHANGED
|
@@ -673,7 +673,10 @@ router.get(
|
|
|
673
673
|
const backup_file_prefix = getState().getConfig("backup_file_prefix");
|
|
674
674
|
if (
|
|
675
675
|
!isRoot ||
|
|
676
|
-
!(
|
|
676
|
+
!(
|
|
677
|
+
path.resolve(filename).startsWith(backup_file_prefix) &&
|
|
678
|
+
filename.endsWith(".zip")
|
|
679
|
+
)
|
|
677
680
|
) {
|
|
678
681
|
res.redirect("/admin/backup");
|
|
679
682
|
return;
|
|
@@ -1278,6 +1281,7 @@ router.get(
|
|
|
1278
1281
|
throw new Error(req.__("Unable to fetch versions"));
|
|
1279
1282
|
const versions = Object.keys(pkgInfo.versions);
|
|
1280
1283
|
if (versions.length === 0) throw new Error(req.__("No versions found"));
|
|
1284
|
+
const tags = pkgInfo["dist-tags"] || {};
|
|
1281
1285
|
res.set("Page-Title", req.__("%s versions", "Saltcorn"));
|
|
1282
1286
|
let selected = packagejson.version;
|
|
1283
1287
|
res.send(
|
|
@@ -1287,6 +1291,7 @@ router.get(
|
|
|
1287
1291
|
method: "post",
|
|
1288
1292
|
},
|
|
1289
1293
|
input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
|
|
1294
|
+
// version select
|
|
1290
1295
|
div(
|
|
1291
1296
|
{ class: "form-group" },
|
|
1292
1297
|
label(
|
|
@@ -1312,6 +1317,54 @@ router.get(
|
|
|
1312
1317
|
)
|
|
1313
1318
|
)
|
|
1314
1319
|
),
|
|
1320
|
+
// tag select
|
|
1321
|
+
div(
|
|
1322
|
+
{ class: "form-group" },
|
|
1323
|
+
label(
|
|
1324
|
+
{
|
|
1325
|
+
for: "tag_select",
|
|
1326
|
+
class: "form-label fw-bold",
|
|
1327
|
+
},
|
|
1328
|
+
req.__("Tags")
|
|
1329
|
+
),
|
|
1330
|
+
select(
|
|
1331
|
+
{
|
|
1332
|
+
id: "tag_select",
|
|
1333
|
+
class: "form-control form-select",
|
|
1334
|
+
},
|
|
1335
|
+
option({
|
|
1336
|
+
id: "empty_opt",
|
|
1337
|
+
value: "",
|
|
1338
|
+
label: req.__("Select tag"),
|
|
1339
|
+
selected: true,
|
|
1340
|
+
}),
|
|
1341
|
+
Object.keys(tags).map((tag) =>
|
|
1342
|
+
option({
|
|
1343
|
+
id: `${tag}_opt`,
|
|
1344
|
+
value: tags[tag],
|
|
1345
|
+
label: `${tag} (${tags[tag]})`,
|
|
1346
|
+
})
|
|
1347
|
+
)
|
|
1348
|
+
)
|
|
1349
|
+
),
|
|
1350
|
+
// deep clean checkbox
|
|
1351
|
+
div(
|
|
1352
|
+
{ class: "form-group" },
|
|
1353
|
+
input({
|
|
1354
|
+
id: "deep_clean",
|
|
1355
|
+
class: "form-check-input",
|
|
1356
|
+
type: "checkbox",
|
|
1357
|
+
name: "deep_clean",
|
|
1358
|
+
checked: false,
|
|
1359
|
+
}),
|
|
1360
|
+
label(
|
|
1361
|
+
{
|
|
1362
|
+
for: "deep_clean",
|
|
1363
|
+
class: "form-label ms-2",
|
|
1364
|
+
},
|
|
1365
|
+
req.__("clean node_modules")
|
|
1366
|
+
)
|
|
1367
|
+
),
|
|
1315
1368
|
div(
|
|
1316
1369
|
{ class: "d-flex justify-content-end" },
|
|
1317
1370
|
button(
|
|
@@ -1331,7 +1384,18 @@ router.get(
|
|
|
1331
1384
|
req.__("Install")
|
|
1332
1385
|
)
|
|
1333
1386
|
)
|
|
1334
|
-
)
|
|
1387
|
+
) +
|
|
1388
|
+
script(
|
|
1389
|
+
domReady(`
|
|
1390
|
+
document.getElementById('tag_select').addEventListener('change', () => {
|
|
1391
|
+
const version = document.getElementById('tag_select').value;
|
|
1392
|
+
if (version) document.getElementById('version_select').value = version;
|
|
1393
|
+
});
|
|
1394
|
+
document.getElementById('version_select').addEventListener('change', () => {
|
|
1395
|
+
document.getElementById('tag_select').value = '';
|
|
1396
|
+
});
|
|
1397
|
+
`)
|
|
1398
|
+
)
|
|
1335
1399
|
);
|
|
1336
1400
|
} catch (error) {
|
|
1337
1401
|
getState().log(
|
|
@@ -1343,7 +1407,17 @@ router.get(
|
|
|
1343
1407
|
})
|
|
1344
1408
|
);
|
|
1345
1409
|
|
|
1346
|
-
const
|
|
1410
|
+
const cleanNodeModules = async () => {
|
|
1411
|
+
const topSaltcornDir = path.join(__dirname, "..", "..", "..", "..", "..");
|
|
1412
|
+
if (path.basename(topSaltcornDir) === "@saltcorn")
|
|
1413
|
+
await fs.promises.rm(topSaltcornDir, { recursive: true, force: true });
|
|
1414
|
+
else
|
|
1415
|
+
throw new Error(
|
|
1416
|
+
`'${topSaltcornDir}' is not a Saltcorn installation directory`
|
|
1417
|
+
);
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const doInstall = async (req, res, version, deepClean, runPull) => {
|
|
1347
1421
|
if (db.getTenantSchema() !== db.connectObj.default_schema) {
|
|
1348
1422
|
req.flash("error", req.__("Not possible for tenant"));
|
|
1349
1423
|
res.redirect("/admin");
|
|
@@ -1353,6 +1427,14 @@ const doInstall = async (req, res, version, runPull) => {
|
|
|
1353
1427
|
? req.__("Starting upgrade, please wait...\n")
|
|
1354
1428
|
: req.__("Installing %s, please wait...\n", version)
|
|
1355
1429
|
);
|
|
1430
|
+
if (deepClean) {
|
|
1431
|
+
res.write(req.__("Cleaning node_modules...\n"));
|
|
1432
|
+
try {
|
|
1433
|
+
await cleanNodeModules();
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
res.write(req.__("Error cleaning node_modules: %s\n", e.message));
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1356
1438
|
const child = spawn(
|
|
1357
1439
|
"npm",
|
|
1358
1440
|
["install", "-g", `@saltcorn/cli@${version}`, "--unsafe"],
|
|
@@ -1390,8 +1472,8 @@ const doInstall = async (req, res, version, runPull) => {
|
|
|
1390
1472
|
};
|
|
1391
1473
|
|
|
1392
1474
|
router.post("/install", isAdmin, async (req, res) => {
|
|
1393
|
-
const { version } = req.body;
|
|
1394
|
-
await doInstall(req, res, version, false);
|
|
1475
|
+
const { version, deep_clean } = req.body;
|
|
1476
|
+
await doInstall(req, res, version, deep_clean === "on", false);
|
|
1395
1477
|
});
|
|
1396
1478
|
|
|
1397
1479
|
/**
|
|
@@ -1404,7 +1486,7 @@ router.post(
|
|
|
1404
1486
|
"/upgrade",
|
|
1405
1487
|
isAdmin,
|
|
1406
1488
|
error_catcher(async (req, res) => {
|
|
1407
|
-
await doInstall(req, res, "latest", true);
|
|
1489
|
+
await doInstall(req, res, "latest", false, true);
|
|
1408
1490
|
})
|
|
1409
1491
|
);
|
|
1410
1492
|
/**
|
|
@@ -1742,8 +1824,8 @@ router.get(
|
|
|
1742
1824
|
});
|
|
1743
1825
|
})
|
|
1744
1826
|
);
|
|
1745
|
-
const buildDialogScript = (cordovaBuilderAvailable) =>
|
|
1746
|
-
|
|
1827
|
+
const buildDialogScript = (cordovaBuilderAvailable) =>
|
|
1828
|
+
`<script>
|
|
1747
1829
|
var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
|
|
1748
1830
|
function showEntrySelect(type) {
|
|
1749
1831
|
for( const currentType of ["view", "page", "pagegroup"]) {
|
|
@@ -1769,7 +1851,6 @@ const buildDialogScript = (cordovaBuilderAvailable) => {
|
|
|
1769
1851
|
notifyAlert("Building the app, please wait.", true)
|
|
1770
1852
|
}
|
|
1771
1853
|
</script>`;
|
|
1772
|
-
};
|
|
1773
1854
|
|
|
1774
1855
|
const imageAvailable = async () => {
|
|
1775
1856
|
try {
|
|
@@ -1827,6 +1908,9 @@ router.get(
|
|
|
1827
1908
|
const plugins = (await Plugin.find()).filter(
|
|
1828
1909
|
(plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
|
|
1829
1910
|
);
|
|
1911
|
+
const pluginsReadyForMobile = plugins
|
|
1912
|
+
.filter((plugin) => plugin.ready_for_mobile())
|
|
1913
|
+
.map((plugin) => plugin.name);
|
|
1830
1914
|
const builderSettings =
|
|
1831
1915
|
getState().getConfig("mobile_builder_settings") || {};
|
|
1832
1916
|
const dockerAvailable = await imageAvailable();
|
|
@@ -1841,6 +1925,11 @@ router.get(
|
|
|
1841
1925
|
{
|
|
1842
1926
|
headerTag: buildDialogScript(dockerAvailable),
|
|
1843
1927
|
},
|
|
1928
|
+
{
|
|
1929
|
+
headerTag: `<script>var pluginsReadyForMobile = ${JSON.stringify(
|
|
1930
|
+
pluginsReadyForMobile
|
|
1931
|
+
)}</script>`,
|
|
1932
|
+
},
|
|
1844
1933
|
],
|
|
1845
1934
|
contents: {
|
|
1846
1935
|
above: [
|
|
@@ -2877,17 +2966,66 @@ router.get(
|
|
|
2877
2966
|
})
|
|
2878
2967
|
);
|
|
2879
2968
|
|
|
2969
|
+
const validateBuildDirName = (buildDirName) => {
|
|
2970
|
+
// ensure characters
|
|
2971
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(buildDirName)) {
|
|
2972
|
+
getState().log(
|
|
2973
|
+
4,
|
|
2974
|
+
`Invalid characters in build directory name '${buildDirName}'`
|
|
2975
|
+
);
|
|
2976
|
+
return false;
|
|
2977
|
+
}
|
|
2978
|
+
// ensure format is 'build_1234567890'
|
|
2979
|
+
if (!/^build_\d+$/.test(buildDirName)) {
|
|
2980
|
+
getState().log(4, `Invalid build directory name format '${buildDirName}'`);
|
|
2981
|
+
return false;
|
|
2982
|
+
}
|
|
2983
|
+
return true;
|
|
2984
|
+
};
|
|
2985
|
+
|
|
2986
|
+
const validateBuildDir = (buildDir, rootPath) => {
|
|
2987
|
+
const resolvedBuildDir = path.resolve(buildDir);
|
|
2988
|
+
if (!resolvedBuildDir.startsWith(path.join(rootPath, "mobile_app"))) {
|
|
2989
|
+
getState().log(4, `Invalid build directory path '${buildDir}'`);
|
|
2990
|
+
return false;
|
|
2991
|
+
}
|
|
2992
|
+
return true;
|
|
2993
|
+
};
|
|
2994
|
+
|
|
2880
2995
|
router.get(
|
|
2881
2996
|
"/build-mobile-app/result",
|
|
2882
2997
|
isAdmin,
|
|
2883
2998
|
error_catcher(async (req, res) => {
|
|
2884
2999
|
const { build_dir_name } = req.query;
|
|
3000
|
+
if (!validateBuildDirName(build_dir_name)) {
|
|
3001
|
+
return res.sendWrap(req.__(`Admin`), {
|
|
3002
|
+
above: [
|
|
3003
|
+
{
|
|
3004
|
+
type: "card",
|
|
3005
|
+
title: req.__("Build Result"),
|
|
3006
|
+
contents: div(req.__("Invalid build directory name")),
|
|
3007
|
+
},
|
|
3008
|
+
],
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
2885
3011
|
const rootFolder = await File.rootFolder();
|
|
2886
3012
|
const buildDir = path.join(
|
|
2887
3013
|
rootFolder.location,
|
|
2888
3014
|
"mobile_app",
|
|
2889
3015
|
build_dir_name
|
|
2890
3016
|
);
|
|
3017
|
+
if (!validateBuildDir(buildDir, rootFolder.location)) {
|
|
3018
|
+
return res.sendWrap(req.__(`Admin`), {
|
|
3019
|
+
above: [
|
|
3020
|
+
{
|
|
3021
|
+
type: "card",
|
|
3022
|
+
title: req.__("Build Result"),
|
|
3023
|
+
contents: div(req.__("Invalid build directory path")),
|
|
3024
|
+
},
|
|
3025
|
+
],
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
|
|
2891
3029
|
const files = await Promise.all(
|
|
2892
3030
|
fs
|
|
2893
3031
|
.readdirSync(buildDir)
|
package/routes/infoarch.js
CHANGED
|
@@ -234,7 +234,14 @@ router.post(
|
|
|
234
234
|
isAdmin,
|
|
235
235
|
error_catcher(async (req, res) => {
|
|
236
236
|
const { lang, defstring } = req.params;
|
|
237
|
-
|
|
237
|
+
if (
|
|
238
|
+
lang === "__proto__" ||
|
|
239
|
+
defstring === "__proto__" ||
|
|
240
|
+
lang === "constructor"
|
|
241
|
+
) {
|
|
242
|
+
res.redirect(`/`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
238
245
|
const cfgStrings = getState().getConfigCopy("localizer_strings");
|
|
239
246
|
if (cfgStrings[lang]) cfgStrings[lang][defstring] = text(req.body.value);
|
|
240
247
|
else cfgStrings[lang] = { [defstring]: text(req.body.value) };
|
package/routes/menu.js
CHANGED
package/routes/pageedit.js
CHANGED
|
@@ -183,6 +183,12 @@ const pageBuilderData = async (req, context) => {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
const actionsNotRequiringRow = Trigger.action_options({
|
|
187
|
+
notRequireRow: true,
|
|
188
|
+
apiNeverTriggers: true,
|
|
189
|
+
builtInLabel: "Page Actions",
|
|
190
|
+
builtIns: ["GoBack"],
|
|
191
|
+
});
|
|
186
192
|
const library = (await Library.find({})).filter((l) => l.suitableFor("page"));
|
|
187
193
|
const fixed_state_fields = {};
|
|
188
194
|
for (const view of views) {
|
|
@@ -228,7 +234,7 @@ const pageBuilderData = async (req, context) => {
|
|
|
228
234
|
images,
|
|
229
235
|
pages,
|
|
230
236
|
page_groups,
|
|
231
|
-
actions,
|
|
237
|
+
actions: actionsNotRequiringRow,
|
|
232
238
|
builtInActions: ["GoBack"],
|
|
233
239
|
library,
|
|
234
240
|
min_role: context.min_role,
|
package/routes/plugins.js
CHANGED
|
@@ -49,6 +49,8 @@ const {
|
|
|
49
49
|
input,
|
|
50
50
|
label,
|
|
51
51
|
text,
|
|
52
|
+
script,
|
|
53
|
+
domReady,
|
|
52
54
|
} = require("@saltcorn/markup/tags");
|
|
53
55
|
const { search_bar } = require("@saltcorn/markup/helpers");
|
|
54
56
|
const fs = require("fs");
|
|
@@ -614,13 +616,14 @@ router.get(
|
|
|
614
616
|
res.set("Page-Title", req.__("%s versions", text(withoutOrg)));
|
|
615
617
|
const versions = Object.keys(pkgInfo.versions);
|
|
616
618
|
if (versions.length === 0) throw new Error(req.__("No versions found"));
|
|
619
|
+
const tags = pkgInfo["dist-tags"] || {};
|
|
617
620
|
let selected = null;
|
|
618
621
|
if (getState().plugins[plugin.name]) {
|
|
619
622
|
const mod = await load_plugins.requirePlugin(plugin);
|
|
620
623
|
if (mod) selected = mod.version;
|
|
621
624
|
}
|
|
622
625
|
if (!selected) selected = versions[versions.length - 1];
|
|
623
|
-
const
|
|
626
|
+
const scVersion = getState().scVersion;
|
|
624
627
|
return res.send(
|
|
625
628
|
form(
|
|
626
629
|
{
|
|
@@ -630,6 +633,7 @@ router.get(
|
|
|
630
633
|
input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
|
|
631
634
|
div(
|
|
632
635
|
{ class: "form-group" },
|
|
636
|
+
// version
|
|
633
637
|
label(
|
|
634
638
|
{
|
|
635
639
|
for: "version_select",
|
|
@@ -645,7 +649,7 @@ router.get(
|
|
|
645
649
|
},
|
|
646
650
|
versions
|
|
647
651
|
.filter((v) =>
|
|
648
|
-
isVersionSupported(v, pkgInfo.versions,
|
|
652
|
+
isVersionSupported(v, pkgInfo.versions, scVersion)
|
|
649
653
|
)
|
|
650
654
|
.map((version) =>
|
|
651
655
|
option({
|
|
@@ -655,6 +659,37 @@ router.get(
|
|
|
655
659
|
selected: version === selected,
|
|
656
660
|
})
|
|
657
661
|
)
|
|
662
|
+
),
|
|
663
|
+
// tag
|
|
664
|
+
label(
|
|
665
|
+
{
|
|
666
|
+
for: "tag_select",
|
|
667
|
+
class: "form-label fw-bold mt-2",
|
|
668
|
+
},
|
|
669
|
+
req.__("Tags")
|
|
670
|
+
),
|
|
671
|
+
select(
|
|
672
|
+
{
|
|
673
|
+
id: "tag_select",
|
|
674
|
+
class: "form-control form-select",
|
|
675
|
+
},
|
|
676
|
+
option({
|
|
677
|
+
id: "empty_opt",
|
|
678
|
+
value: "",
|
|
679
|
+
label: req.__("Select tag"),
|
|
680
|
+
selected: true,
|
|
681
|
+
}),
|
|
682
|
+
Object.keys(tags)
|
|
683
|
+
.filter((tag) =>
|
|
684
|
+
isVersionSupported(tags[tag], pkgInfo.versions, scVersion)
|
|
685
|
+
)
|
|
686
|
+
.map((tag) =>
|
|
687
|
+
option({
|
|
688
|
+
id: `${tag}_opt`,
|
|
689
|
+
value: tags[tag],
|
|
690
|
+
label: `${tag} (${tags[tag]})`,
|
|
691
|
+
})
|
|
692
|
+
)
|
|
658
693
|
)
|
|
659
694
|
),
|
|
660
695
|
div(
|
|
@@ -676,7 +711,19 @@ router.get(
|
|
|
676
711
|
req.__("Install")
|
|
677
712
|
)
|
|
678
713
|
)
|
|
679
|
-
)
|
|
714
|
+
) +
|
|
715
|
+
script(
|
|
716
|
+
domReady(`
|
|
717
|
+
document.getElementById('tag_select').onchange = () => {
|
|
718
|
+
const version = document.getElementById('tag_select').value;
|
|
719
|
+
if (version) document.getElementById('version_select').value = version;
|
|
720
|
+
};
|
|
721
|
+
document.getElementById('version_select').onchange = () => {
|
|
722
|
+
const tagSelect = document.getElementById('tag_select');
|
|
723
|
+
tagSelect.value = '';
|
|
724
|
+
};
|
|
725
|
+
`)
|
|
726
|
+
)
|
|
680
727
|
);
|
|
681
728
|
} catch (error) {
|
|
682
729
|
getState().log(
|
|
@@ -1184,9 +1231,20 @@ router.get(
|
|
|
1184
1231
|
const update_permitted =
|
|
1185
1232
|
db.getTenantSchema() === db.connectObj.default_schema &&
|
|
1186
1233
|
plugin_db.source === "npm";
|
|
1187
|
-
|
|
1234
|
+
|
|
1235
|
+
let latest =
|
|
1188
1236
|
update_permitted &&
|
|
1189
1237
|
(await get_latest_npm_version(plugin_db.location, 1000));
|
|
1238
|
+
if (
|
|
1239
|
+
latest &&
|
|
1240
|
+
!isVersionSupported(latest, await load_plugins.getEngineInfos(plugin_db)) // with cache
|
|
1241
|
+
) {
|
|
1242
|
+
// with force fetch
|
|
1243
|
+
latest = supportedVersion(
|
|
1244
|
+
latest,
|
|
1245
|
+
await load_plugins.getEngineInfos(plugin_db, true)
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1190
1248
|
const can_update = update_permitted && latest && mod.version !== latest;
|
|
1191
1249
|
const can_select_version = update_permitted && plugin_db.source === "npm";
|
|
1192
1250
|
let pkgjson;
|
|
@@ -1332,8 +1390,8 @@ router.get(
|
|
|
1332
1390
|
error_catcher(async (req, res) => {
|
|
1333
1391
|
const schema = db.getTenantSchema();
|
|
1334
1392
|
if (schema === db.connectObj.default_schema) {
|
|
1335
|
-
await upgrade_all_tenants_plugins((p, f) =>
|
|
1336
|
-
load_plugins.loadPlugin(p, f)
|
|
1393
|
+
await upgrade_all_tenants_plugins((p, f, forceFetch) =>
|
|
1394
|
+
load_plugins.loadPlugin(p, f, forceFetch)
|
|
1337
1395
|
);
|
|
1338
1396
|
req.flash(
|
|
1339
1397
|
"success",
|
|
@@ -1344,7 +1402,9 @@ router.get(
|
|
|
1344
1402
|
} else {
|
|
1345
1403
|
const installed_plugins = await Plugin.find({});
|
|
1346
1404
|
for (const plugin of installed_plugins) {
|
|
1347
|
-
await plugin.upgrade_version((p, f) =>
|
|
1405
|
+
await plugin.upgrade_version((p, f, forceFetch) =>
|
|
1406
|
+
load_plugins.loadPlugin(p, f, forceFetch)
|
|
1407
|
+
);
|
|
1348
1408
|
}
|
|
1349
1409
|
req.flash("success", req.__(`Modules up-to-date`));
|
|
1350
1410
|
await restart_tenant(loadAllPlugins);
|
|
@@ -1370,16 +1430,11 @@ router.get(
|
|
|
1370
1430
|
const { name } = req.params;
|
|
1371
1431
|
|
|
1372
1432
|
const plugin = await Plugin.findOne({ name });
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
);
|
|
1433
|
+
const versions = await load_plugins.getEngineInfos(plugin, true);
|
|
1434
|
+
|
|
1376
1435
|
await plugin.upgrade_version(
|
|
1377
1436
|
(p, f) => load_plugins.loadPlugin(p, f),
|
|
1378
|
-
supportedVersion(
|
|
1379
|
-
"latest",
|
|
1380
|
-
pkgInfo.versions,
|
|
1381
|
-
require("../package.json").version
|
|
1382
|
-
)
|
|
1437
|
+
supportedVersion("latest", versions, require("../package.json").version)
|
|
1383
1438
|
);
|
|
1384
1439
|
req.flash("success", req.__(`Module up-to-date`));
|
|
1385
1440
|
|
package/routes/tables.js
CHANGED
|
@@ -670,7 +670,10 @@ router.get(
|
|
|
670
670
|
* @param {string} lbl
|
|
671
671
|
* @returns {string}
|
|
672
672
|
*/
|
|
673
|
-
const badge = (col, lbl) =>
|
|
673
|
+
const badge = (col, lbl, title) =>
|
|
674
|
+
`<span ${
|
|
675
|
+
title ? `title="${title}" ` : ""
|
|
676
|
+
}class="badge bg-${col}">${lbl}</span> `;
|
|
674
677
|
|
|
675
678
|
/**
|
|
676
679
|
* @param {object} f
|
|
@@ -708,8 +711,11 @@ const attribBadges = (f) => {
|
|
|
708
711
|
].includes(k)
|
|
709
712
|
)
|
|
710
713
|
return;
|
|
711
|
-
if(Array.isArray(v) && !v.length) return;
|
|
712
|
-
|
|
714
|
+
if (Array.isArray(v) && !v.length) return;
|
|
715
|
+
const title = ["string", "number", "boolean"].includes(typeof v)
|
|
716
|
+
? `${v}`
|
|
717
|
+
: null;
|
|
718
|
+
if (v || v === 0) s += badge("secondary", k, title);
|
|
713
719
|
});
|
|
714
720
|
}
|
|
715
721
|
return s;
|
package/serve.js
CHANGED
|
@@ -27,7 +27,7 @@ const {
|
|
|
27
27
|
loadAndSaveNewPlugin,
|
|
28
28
|
loadPlugin,
|
|
29
29
|
} = require("./load_plugins");
|
|
30
|
-
const { getConfig } = require("@saltcorn/data/models/config");
|
|
30
|
+
const { getConfig, setConfig } = require("@saltcorn/data/models/config");
|
|
31
31
|
const { migrate } = require("@saltcorn/data/migrate");
|
|
32
32
|
const socketio = require("socket.io");
|
|
33
33
|
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
|
|
@@ -77,6 +77,17 @@ const ensureJwtSecret = () => {
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Ensure the engines cache is up to date with the current sc version
|
|
82
|
+
*/
|
|
83
|
+
const ensureEnginesCache = async () => {
|
|
84
|
+
const cacheScVersion = await getConfig("engines_cache_sc_version", "");
|
|
85
|
+
if (!cacheScVersion || cacheScVersion !== getState().scVersion) {
|
|
86
|
+
await setConfig("engines_cache", {});
|
|
87
|
+
await setConfig("engines_cache_sc_version", getState().scVersion);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
80
91
|
// helpful https://gist.github.com/jpoehls/2232358
|
|
81
92
|
/**
|
|
82
93
|
* @param {object} opts
|
|
@@ -222,7 +233,10 @@ module.exports =
|
|
|
222
233
|
dev,
|
|
223
234
|
...appargs
|
|
224
235
|
} = {}) => {
|
|
225
|
-
|
|
236
|
+
if (cluster.isMaster) {
|
|
237
|
+
ensureJwtSecret();
|
|
238
|
+
await ensureEnginesCache();
|
|
239
|
+
}
|
|
226
240
|
process.on("unhandledRejection", (reason, p) => {
|
|
227
241
|
console.error(reason, "Unhandled Rejection at Promise");
|
|
228
242
|
});
|