@saltcorn/server 0.7.1-beta.2 → 0.7.2-beta.0

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/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.7.1-beta.2",
3
+ "version": "0.7.2-beta.0",
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
- "@saltcorn/base-plugin": "0.7.1-beta.2",
10
- "@saltcorn/builder": "0.7.1-beta.2",
11
- "@saltcorn/data": "0.7.1-beta.2",
12
- "@saltcorn/admin-models": "0.7.1-beta.2",
13
- "@saltcorn/markup": "0.7.1-beta.2",
14
- "@saltcorn/sbadmin2": "0.7.1-beta.2",
9
+ "@saltcorn/base-plugin": "0.7.2-beta.0",
10
+ "@saltcorn/builder": "0.7.2-beta.0",
11
+ "@saltcorn/data": "0.7.2-beta.0",
12
+ "@saltcorn/admin-models": "0.7.2-beta.0",
13
+ "@saltcorn/markup": "0.7.2-beta.0",
14
+ "@saltcorn/sbadmin2": "0.7.2-beta.0",
15
15
  "@socket.io/cluster-adapter": "^0.1.0",
16
16
  "@socket.io/sticky": "^1.0.1",
17
17
  "aws-sdk": "^2.1037.0",
@@ -46,6 +46,7 @@
46
46
  "pg": "^8.2.1",
47
47
  "pluralize": "^8.0.0",
48
48
  "qrcode": "1.5.0",
49
+ "sharp": "0.30.3",
49
50
  "socket.io": "4.2.0",
50
51
  "thirty-two": "1.0.2",
51
52
  "tmp-promise": "^3.0.2",
@@ -57,7 +58,8 @@
57
58
  },
58
59
  "repository": "github:saltcorn/saltcorn",
59
60
  "devDependencies": {
60
- "jest": "^25.1.0",
61
+ "jest": "26.6.3",
62
+ "jest-environment-jsdom": "^26.6.2",
61
63
  "supertest": "^4.0.2"
62
64
  },
63
65
  "scripts": {
@@ -117,6 +117,7 @@ function MenuEditor(e, t) {
117
117
  (n.prev("div").children(".sortableListsOpener").first().remove(),
118
118
  n.remove()),
119
119
  MenuEditor.updateButtons(s);
120
+ l.onUpdate();
120
121
  }
121
122
  }),
122
123
  $(document).on("click", ".btnEdit", function (e) {
@@ -145,11 +146,13 @@ function MenuEditor(e, t) {
145
146
  e.preventDefault();
146
147
  var t = $(this).closest("li");
147
148
  t.prev("li").before(t), MenuEditor.updateButtons(s);
149
+ l.onUpdate();
148
150
  }),
149
151
  s.on("click", ".btnDown", function (e) {
150
152
  e.preventDefault();
151
153
  var t = $(this).closest("li");
152
154
  t.next("li").after(t), MenuEditor.updateButtons(s);
155
+ l.onUpdate();
153
156
  }),
154
157
  s.on("click", ".btnOut", function (e) {
155
158
  e.preventDefault();
@@ -161,6 +164,7 @@ function MenuEditor(e, t) {
161
164
  t.remove()),
162
165
  MenuEditor.updateButtons(s),
163
166
  s.updateLevels();
167
+ l.onUpdate();
164
168
  }),
165
169
  s.on("click", ".btnIn", function (e) {
166
170
  e.preventDefault();
@@ -174,6 +178,7 @@ function MenuEditor(e, t) {
174
178
  l.append(n), n.append(t), l.addClass("sortableListsOpen"), f(l);
175
179
  }
176
180
  MenuEditor.updateButtons(s), s.updateLevels();
181
+ l.onUpdate();
177
182
  }),
178
183
  (this.setForm = function (e) {
179
184
  i = e;
@@ -224,3 +224,12 @@ footer.bs-mobile-nav-footer {
224
224
  .form-group {
225
225
  margin-bottom: 1rem;
226
226
  }
227
+
228
+ .containerbgimage {
229
+ position: absolute;
230
+ top: 0;
231
+ left: 0;
232
+ width: 100%;
233
+ height: 100%;
234
+ z-index: -1;
235
+ }
@@ -329,17 +329,36 @@ function enable_codemirror(f) {
329
329
  }
330
330
 
331
331
  //https://stackoverflow.com/a/6021027
332
- function updateQueryStringParameter(uri, key, value) {
332
+ function updateQueryStringParameter(uri1, key, value) {
333
+ let hash = "";
334
+ let uri = uri1;
335
+ if (uri && uri.includes("#")) {
336
+ let uris = uri1.split("#");
337
+ hash = "#" + uris[1];
338
+ uri = uris[0];
339
+ }
340
+
333
341
  var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
334
342
  var separator = uri.indexOf("?") !== -1 ? "&" : "?";
335
343
  if (uri.match(re)) {
336
- return uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2");
344
+ return (
345
+ uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2") +
346
+ hash
347
+ );
337
348
  } else {
338
- return uri + separator + key + "=" + encodeURIComponent(value);
349
+ return uri + separator + key + "=" + encodeURIComponent(value) + hash;
339
350
  }
340
351
  }
341
352
 
342
- function removeQueryStringParameter(uri, key) {
353
+ function removeQueryStringParameter(uri1, key) {
354
+ let hash = "";
355
+ let uri = uri1;
356
+ if (uri && uri.includes("#")) {
357
+ let uris = uri1.split("#");
358
+ hash = "#" + uris[1];
359
+ uri = uris[0];
360
+ }
361
+
343
362
  var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
344
363
  var separator = uri.indexOf("?") !== -1 ? "&" : "?";
345
364
  if (uri.match(re)) {
@@ -347,7 +366,7 @@ function removeQueryStringParameter(uri, key) {
347
366
  }
348
367
  if (uri[uri.length - 1] === "?" || uri[uri.length - 1] === "&")
349
368
  uri = uri.substring(0, uri.length - 1);
350
- return uri;
369
+ return uri + hash;
351
370
  }
352
371
 
353
372
  function select_id(id) {
@@ -548,6 +567,10 @@ function ajax_modal(url, opts = {}) {
548
567
  </div>
549
568
  </div>
550
569
  </div>`);
570
+ } else if ($("#scmodal").hasClass("show")) {
571
+ var myModalEl = document.getElementById("scmodal");
572
+ var modal = bootstrap.Modal.getInstance(myModalEl);
573
+ modal.dispose();
551
574
  }
552
575
  if (opts.submitReload === false) $("#scmodal").addClass("no-submit-reload");
553
576
  else $("#scmodal").removeClass("no-submit-reload");
@@ -595,6 +618,47 @@ function saveAndContinue(e, k) {
595
618
  return false;
596
619
  }
597
620
 
621
+ function applyViewConfig(e, url) {
622
+ var form = $(e).closest("form");
623
+ var form_data = form.serializeArray();
624
+ const cfg = {};
625
+ form_data.forEach((item) => {
626
+ cfg[item.name] = item.value;
627
+ });
628
+ $.ajax(url, {
629
+ type: "POST",
630
+ dataType: "json",
631
+ contentType: "application/json",
632
+ headers: {
633
+ "CSRF-Token": _sc_globalCsrf,
634
+ },
635
+ data: JSON.stringify(cfg),
636
+ error: function (request) {},
637
+ });
638
+
639
+ return false;
640
+ }
641
+
642
+ const repeaterCopyValuesToForm = (form, editor) => {
643
+ const vs = JSON.parse(editor.getString());
644
+ //console.log(vs)
645
+ const setVal = (k, ix, v) => {
646
+ const $e = form.find(`input[name="${k}_${ix}"]`);
647
+ if ($e.length) $e.val(v);
648
+ else
649
+ form.append(
650
+ `<input type="hidden" name="${k}_${ix}" value="${v}"></input>`
651
+ );
652
+ };
653
+ vs.forEach((v, ix) => {
654
+ Object.entries(v).forEach(([k, v]) => {
655
+ //console.log(ix, k, typeof v, v)
656
+ if (typeof v === "boolean") setVal(k, ix, v ? "on" : "");
657
+ else setVal(k, ix, v);
658
+ });
659
+ });
660
+ };
661
+
598
662
  function ajaxSubmitForm(e) {
599
663
  var form = $(e).closest("form");
600
664
  var url = form.attr("action");
@@ -811,13 +875,13 @@ const columnSummary = (col) => {
811
875
  if (!col) return "Unknown";
812
876
  switch (col.type) {
813
877
  case "Field":
814
- return `Field ${col.field_name} ${col.fieldview}`;
878
+ return `Field ${col.field_name} ${col.fieldview || ""}`;
815
879
  case "Link":
816
880
  return `Link ${col.link_text}`;
817
881
  case "JoinField":
818
882
  return `Join ${col.join_field}`;
819
883
  case "ViewLink":
820
- return `View ${col.view_label || col.view.split(":")[1] || ""}`;
884
+ return `View link ${col.view_label || col.view.split(":")[1] || ""}`;
821
885
  case "Action":
822
886
  return `Action ${col.action_label || col.action_name}`;
823
887
  case "Aggregation":
package/routes/fields.js CHANGED
@@ -629,10 +629,14 @@ router.post(
629
629
  */
630
630
  router.post(
631
631
  "/show-calculated/:tableName/:fieldName/:fieldview",
632
- isAdmin,
633
632
  error_catcher(async (req, res) => {
634
633
  const { tableName, fieldName, fieldview } = req.params;
635
634
  const table = await Table.findOne({ name: tableName });
635
+ const role = req.user && req.user.id ? req.user.role_id : 10;
636
+ if (role > table.min_role_read) {
637
+ res.status(401).send("");
638
+ return;
639
+ }
636
640
  const fields = await table.getFields();
637
641
  const row = { ...req.body };
638
642
  readState(row, fields);
@@ -643,6 +647,10 @@ router.post(
643
647
  if (kpath.length === 2 && row[kpath[0]]) {
644
648
  const field = fields.find((f) => f.name === kpath[0]);
645
649
  const reftable = await Table.findOne({ name: field.reftable_name });
650
+ if (role > reftable.min_role_read) {
651
+ res.status(401).send("");
652
+ return;
653
+ }
646
654
  const targetField = (await reftable.getFields()).find(
647
655
  (f) => f.name === kpath[1]
648
656
  );
@@ -671,6 +679,10 @@ router.post(
671
679
  if (field.is_fkey) {
672
680
  const reftable = await Table.findOne({ name: field.reftable_name });
673
681
  if (!oldRow[ref]) break;
682
+ if (role > reftable.min_role_read) {
683
+ res.status(401).send("");
684
+ return;
685
+ }
674
686
  const q = { [reftable.pk_name]: oldRow[ref] };
675
687
  oldRow = await reftable.getRow(q);
676
688
  oldTable = reftable;
package/routes/files.js CHANGED
@@ -9,6 +9,7 @@ const File = require("@saltcorn/data/models/file");
9
9
  const User = require("@saltcorn/data/models/user");
10
10
  const { getState } = require("@saltcorn/data/db/state");
11
11
  const s3storage = require("../s3storage");
12
+ const sharp = require("sharp");
12
13
 
13
14
  const {
14
15
  mkTable,
@@ -43,6 +44,8 @@ const {
43
44
  config_fields_form,
44
45
  save_config_from_form,
45
46
  } = require("../markup/admin");
47
+ const fsp = require("fs").promises;
48
+ const fs = require("fs");
46
49
 
47
50
  /**
48
51
  * @type {object}
@@ -187,6 +190,54 @@ router.get(
187
190
  })
188
191
  );
189
192
 
193
+ /**
194
+ * @name get/resize/:id
195
+ * @function
196
+ * @memberof module:routes/files~filesRouter
197
+ * @function
198
+ */
199
+ router.get(
200
+ "/resize/:id/:width_str",
201
+ error_catcher(async (req, res) => {
202
+ const role = req.user && req.user.id ? req.user.role_id : 10;
203
+ const user_id = req.user && req.user.id;
204
+ const { id, width_str } = req.params;
205
+ let file;
206
+ if (typeof strictParseInt(id) !== "undefined")
207
+ file = await File.findOne({ id });
208
+ else file = await File.findOne({ filename: id });
209
+
210
+ if (!file) {
211
+ res
212
+ .status(404)
213
+ .sendWrap(req.__("Not found"), h1(req.__("File not found")));
214
+ return;
215
+ }
216
+ if (role <= file.min_role_read || (user_id && user_id === file.user_id)) {
217
+ res.type(file.mimetype);
218
+ const cacheability = file.min_role_read === 10 ? "public" : "private";
219
+ res.set("Cache-Control", `${cacheability}, max-age=86400`);
220
+ //TODO s3
221
+ if (file.s3_store) s3storage.serveObject(file, res, false);
222
+ else {
223
+ const width = strictParseInt(width_str);
224
+ if (!width) {
225
+ res.sendFile(file.location);
226
+ return;
227
+ }
228
+ const fnm = `${file.location}_w${width}`;
229
+ if (!fs.existsSync(fnm)) {
230
+ await sharp(file.location).resize({ width }).toFile(fnm);
231
+ }
232
+ res.sendFile(fnm);
233
+ }
234
+ } else {
235
+ req.flash("warning", req.__("Not authorized"));
236
+ res.redirect("/");
237
+ }
238
+ })
239
+ );
240
+
190
241
  /**
191
242
  * @name post/setrole/:id
192
243
  * @function
package/routes/plugins.js CHANGED
@@ -149,6 +149,7 @@ const local_has_theme = (name) => {
149
149
  */
150
150
  const get_store_items = async () => {
151
151
  const installed_plugins = await Plugin.find({});
152
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
152
153
 
153
154
  const instore = await Plugin.store_plugins_available();
154
155
  const packs_available = await fetch_available_packs();
@@ -156,15 +157,18 @@ const get_store_items = async () => {
156
157
  const schema = db.getTenantSchema();
157
158
  const installed_plugin_names = installed_plugins.map((p) => p.name);
158
159
  const store_plugin_names = instore.map((p) => p.name);
159
- const plugins_item = instore.map((plugin) => ({
160
- name: plugin.name,
161
- installed: installed_plugin_names.includes(plugin.name),
162
- plugin: true,
163
- description: plugin.description,
164
- documentation_link: plugin.documentation_link,
165
- has_theme: plugin.has_theme,
166
- has_auth: plugin.has_auth,
167
- }));
160
+ const plugins_item = instore
161
+ .map((plugin) => ({
162
+ name: plugin.name,
163
+ installed: installed_plugin_names.includes(plugin.name),
164
+ plugin: true,
165
+ description: plugin.description,
166
+ documentation_link: plugin.documentation_link,
167
+ has_theme: plugin.has_theme,
168
+ has_auth: plugin.has_auth,
169
+ unsafe: plugin.unsafe,
170
+ }))
171
+ .filter((p) => !p.unsafe || isRoot);
168
172
  const local_logins = installed_plugins
169
173
  .filter((p) => !store_plugin_names.includes(p.name) && p.name !== "base")
170
174
  .map((plugin) => ({
@@ -648,16 +652,16 @@ router.get(
648
652
  error_catcher(async (req, res) => {
649
653
  const { plugin } = req.params;
650
654
  const filepath = req.params[0];
655
+ const hasVersion = plugin.includes("@");
651
656
  const location =
652
- getState().plugin_locations[
653
- plugin.includes("@") ? plugin.split("@")[0] : plugin
654
- ];
657
+ getState().plugin_locations[hasVersion ? plugin.split("@")[0] : plugin];
655
658
  if (location) {
656
659
  const safeFile = path
657
660
  .normalize(filepath)
658
661
  .replace(/^(\.\.(\/|\\|$))+/, "");
659
662
  const fullpath = path.join(location, "public", safeFile);
660
- if (fs.existsSync(fullpath)) res.sendFile(fullpath, { maxAge: "1d" });
663
+ if (fs.existsSync(fullpath))
664
+ res.sendFile(fullpath, { maxAge: hasVersion ? "100d" : "1d" });
661
665
  else res.status(404).send(req.__("Not found"));
662
666
  } else {
663
667
  res.status(404).send(req.__("Not found"));
@@ -961,6 +965,15 @@ router.post(
961
965
  res.redirect(`/plugins`);
962
966
  return;
963
967
  }
968
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
969
+ if (!isRoot && plugin.unsafe) {
970
+ req.flash(
971
+ "error",
972
+ req.__("Cannot install unsafe plugins on subdomain tenants")
973
+ );
974
+ res.redirect(`/plugins`);
975
+ return;
976
+ }
964
977
  delete plugin.id;
965
978
  await load_plugins.loadAndSaveNewPlugin(plugin);
966
979
  const plugin_module = getState().plugins[name];
@@ -582,8 +582,11 @@ router.get(
582
582
  return;
583
583
  }
584
584
  const configFlow = await view.get_config_flow(req);
585
+ const hasConfig =
586
+ view.configuration && Object.keys(view.configuration).length > 0;
585
587
  const wfres = await configFlow.run(
586
588
  {
589
+ id: hasConfig ? view.id : undefined,
587
590
  table_id: view.table_id,
588
591
  exttable_name: view.exttable_name,
589
592
  viewname: name,
@@ -704,6 +707,48 @@ router.post(
704
707
  })
705
708
  );
706
709
 
710
+ /**
711
+ * @name post/saveconfig/:id
712
+ * @function
713
+ * @memberof module:routes/viewedit~vieweditRouter
714
+ * @function
715
+ */
716
+ router.post(
717
+ "/saveconfig/:viewname",
718
+ isAdmin,
719
+ error_catcher(async (req, res) => {
720
+ const { viewname } = req.params;
721
+
722
+ if (viewname && req.body) {
723
+ const view = await View.findOne({ name: viewname });
724
+ const configFlow = await view.get_config_flow(req);
725
+ const step = await configFlow.singleStepForm(req.body, req);
726
+ if (step?.renderForm) {
727
+ if (!step.renderForm.hasErrors) {
728
+ let newcfg;
729
+ if (step.contextField)
730
+ newcfg = {
731
+ ...view.configuration,
732
+ [step.contextField]: {
733
+ ...view.configuration?.[step.contextField],
734
+ ...step.renderForm.values,
735
+ },
736
+ };
737
+ else newcfg = { ...view.configuration, ...step.renderForm.values };
738
+ await View.update({ configuration: newcfg }, view.id);
739
+ res.json({ success: "ok" });
740
+ } else {
741
+ res.json({ error: step.renderForm.errorSummary });
742
+ }
743
+ } else {
744
+ res.json({ error: "no form" });
745
+ }
746
+ } else {
747
+ res.json({ error: "no view" });
748
+ }
749
+ })
750
+ );
751
+
707
752
  /**
708
753
  * @name post/setrole/:id
709
754
  * @function
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const load_script = (fnm) => {
8
+ const srcFile = fs.readFileSync(path.join(__dirname, "..", "public", fnm), {
9
+ encoding: "utf-8",
10
+ });
11
+ const scriptEl = document.createElement("script");
12
+ scriptEl.textContent = srcFile;
13
+ document.body.appendChild(scriptEl);
14
+ };
15
+
16
+ load_script("jquery-3.6.0.min.js");
17
+ load_script("saltcorn.js");
18
+
19
+ test("updateQueryStringParameter", () => {
20
+ const element = document.createElement("div");
21
+ expect(element).not.toBeNull();
22
+ expect(updateQueryStringParameter("/foo", "age", 43)).toBe("/foo?age=43");
23
+ expect(updateQueryStringParameter("/foo?age=44", "age", 43)).toBe(
24
+ "/foo?age=43"
25
+ );
26
+ expect(updateQueryStringParameter("/foo?name=Bar", "age", 43)).toBe(
27
+ "/foo?name=Bar&age=43"
28
+ );
29
+ expect(removeQueryStringParameter("/foo?age=44", "age")).toBe("/foo");
30
+ expect(removeQueryStringParameter("/foo?name=Bar", "age")).toBe(
31
+ "/foo?name=Bar"
32
+ );
33
+ expect(removeQueryStringParameter("/foo?name=Bar&age=45", "age")).toBe(
34
+ "/foo?name=Bar"
35
+ );
36
+ });
37
+
38
+ test("updateQueryStringParameter hash", () => {
39
+ expect(updateQueryStringParameter("/foo#baz", "age", 43)).toBe(
40
+ "/foo?age=43#baz"
41
+ );
42
+ expect(updateQueryStringParameter("/foo?age=44#Baz", "age", 43)).toBe(
43
+ "/foo?age=43#Baz"
44
+ );
45
+ expect(updateQueryStringParameter("/foo?name=Bar#Zap", "age", 43)).toBe(
46
+ "/foo?name=Bar&age=43#Zap"
47
+ );
48
+ expect(removeQueryStringParameter("/foo?age=44#Baz", "age")).toBe("/foo#Baz");
49
+ expect(removeQueryStringParameter("/foo?name=Bar#Baz", "age")).toBe(
50
+ "/foo?name=Bar#Baz"
51
+ );
52
+ expect(removeQueryStringParameter("/foo?name=Bar&age=45#Baz", "age")).toBe(
53
+ "/foo?name=Bar#Baz"
54
+ );
55
+ });
@@ -90,7 +90,7 @@ describe("Plugin Endpoints", () => {
90
90
  .expect(toInclude("testfilecontents"));
91
91
  await request(app)
92
92
  .get(
93
- "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.0/css/sb-admin-2.min.css"
93
+ "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.4/css/sb-admin-2.min.css"
94
94
  )
95
95
  .expect(toInclude("Start Bootstrap"));
96
96
 
@@ -100,7 +100,7 @@ describe("Plugin Endpoints", () => {
100
100
  .expect(toRedirect("/plugins"));
101
101
  await request(app)
102
102
  .get(
103
- "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.0/css/sb-admin-2.min.css"
103
+ "/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.4/css/sb-admin-2.min.css"
104
104
  )
105
105
  .expect(toInclude("Start Bootstrap"));
106
106
  });
@@ -82,7 +82,7 @@ describe("edit view", () => {
82
82
  .post("/view/authoredit")
83
83
  .send("author=Chekov")
84
84
 
85
- .expect(toRedirect("/"));
85
+ .expect(toRedirect("/view/authorlist"));
86
86
  });
87
87
  });
88
88