@saltcorn/server 1.0.0-beta.2 → 1.0.0-beta.4

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.
@@ -8,7 +8,7 @@ To build an Android app for the Play Store, you need to create an Android Applic
8
8
 
9
9
  *Note: You need a [Play Console developer account](https://support.google.com/googleplay/android-developer/answer/6112435?hl=en&ref_topic=3450769&sjid=11090022771305927482-EU) to publish your app on the Play Store.*
10
10
 
11
- ### Create a Keysore file
11
+ ### Create a Keystore file
12
12
  On any Unix-based system, you can use the `keytool` command to create the keystore file. For example:
13
13
  ```sh
14
14
  keytool -genkey -v -keystore my-app-key.jks
package/locales/en.json CHANGED
@@ -1459,5 +1459,10 @@
1459
1459
  "Hourly": "Hourly",
1460
1460
  "Daily": "Daily",
1461
1461
  "Weekly": "Weekly",
1462
- "Code pages": "Code pages"
1462
+ "Code pages": "Code pages",
1463
+ "Please select a file": "Please select a file",
1464
+ "Zip compression level": "Zip compression level",
1465
+ "1=Fast, larger file, 9=Slow, smaller files": "1=Fast, larger file, 9=Slow, smaller files",
1466
+ "Use system zip": "Use system zip",
1467
+ "Recommended. Executable <code>zip</code> must be installed": "Recommended. Executable <code>zip</code> must be installed"
1463
1468
  }
package/markup/blockly.js CHANGED
@@ -22,16 +22,16 @@ const db = require("@saltcorn/data/db");
22
22
  */
23
23
  const blocklyImportScripts = ({ locale }) =>
24
24
  script({
25
- src: "/plugins/pubdeps/base/blockly/6.20210701.0/blockly_compressed.js",
25
+ src: "/plugins/pubdeps/base/blockly/8.0.5/blockly_compressed.js",
26
26
  }) +
27
27
  script({
28
- src: "/plugins/pubdeps/base/blockly/6.20210701.0/blocks_compressed.js",
28
+ src: "/plugins/pubdeps/base/blockly/8.0.5/blocks_compressed.js",
29
29
  }) +
30
30
  script({
31
- src: `/plugins/pubdeps/base/blockly/6.20210701.0/msg/${locale}.js`,
31
+ src: `/plugins/pubdeps/base/blockly/8.0.5/msg/${locale}.js`,
32
32
  }) +
33
33
  script({
34
- src: "/plugins/pubdeps/base/blockly/6.20210701.0/javascript_compressed.js",
34
+ src: "/plugins/pubdeps/base/blockly/8.0.5/javascript_compressed.js",
35
35
  }) +
36
36
  script({
37
37
  src: `/static_assets/${db.connectObj.version_tag}/blockly.js`,
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
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.2",
11
- "@saltcorn/builder": "1.0.0-beta.2",
12
- "@saltcorn/data": "1.0.0-beta.2",
13
- "@saltcorn/admin-models": "1.0.0-beta.2",
14
- "@saltcorn/filemanager": "1.0.0-beta.2",
15
- "@saltcorn/markup": "1.0.0-beta.2",
16
- "@saltcorn/plugins-loader": "1.0.0-beta.2",
17
- "@saltcorn/sbadmin2": "1.0.0-beta.2",
10
+ "@saltcorn/base-plugin": "1.0.0-beta.4",
11
+ "@saltcorn/builder": "1.0.0-beta.4",
12
+ "@saltcorn/data": "1.0.0-beta.4",
13
+ "@saltcorn/admin-models": "1.0.0-beta.4",
14
+ "@saltcorn/filemanager": "1.0.0-beta.4",
15
+ "@saltcorn/markup": "1.0.0-beta.4",
16
+ "@saltcorn/plugins-loader": "1.0.0-beta.4",
17
+ "@saltcorn/sbadmin2": "1.0.0-beta.4",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -63,7 +63,7 @@
63
63
  "tmp-promise": "^3.0.2",
64
64
  "ua-parser-js": "^1.0.37",
65
65
  "underscore": "1.13.6",
66
- "uuid": "^8.2.0"
66
+ "uuid": "^10.0.0"
67
67
  },
68
68
  "optionalDependencies": {
69
69
  "connect-sqlite3": "^0.9.11",
@@ -71,9 +71,9 @@
71
71
  },
72
72
  "repository": "github:saltcorn/saltcorn",
73
73
  "devDependencies": {
74
- "jest": "^28.1.3",
75
- "jest-environment-jsdom": "28.1.3",
76
- "supertest": "^6.3.3"
74
+ "jest": "^29.7.0",
75
+ "jest-environment-jsdom": "29.7.0",
76
+ "supertest": "7.0.0"
77
77
  },
78
78
  "scripts": {
79
79
  "dev": "nodemon index.js",
@@ -421,7 +421,14 @@ function apply_showif() {
421
421
  navigator.systemLanguage ||
422
422
  "en";
423
423
  window.detected_locale = locale;
424
- const parse = (s) => JSON.parse(decodeURIComponent(s));
424
+ const parse = (s, def = {}) => {
425
+ try {
426
+ return JSON.parse(decodeURIComponent(s));
427
+ } catch (e) {
428
+ console.error("failed to parse time format", e);
429
+ return def;
430
+ }
431
+ };
425
432
  $("time[locale-time-options]").each(function () {
426
433
  var el = $(this);
427
434
  var date = new Date(el.attr("datetime"));
@@ -443,8 +450,9 @@ function apply_showif() {
443
450
  $("time[locale-date-format]").each(function () {
444
451
  var el = $(this);
445
452
  var date = el.attr("datetime");
446
- const format = parse(el.attr("locale-date-format"));
447
- el.text(dayjs(date).format(format));
453
+ const format = parse(el.attr("locale-date-format"), "");
454
+ if (format) el.text(dayjs(date).format(format));
455
+ else el.text(dayjs(date));
448
456
  });
449
457
 
450
458
  _apply_showif_plugins.forEach((p) => p());
@@ -391,6 +391,9 @@ function ajax_modal(url, opts = {}) {
391
391
  : {}),
392
392
  });
393
393
  }
394
+ function closeModal() {
395
+ $("#scmodal").modal("toggle");
396
+ }
394
397
 
395
398
  function selectVersionError(res, btnId) {
396
399
  notifyAlert({
package/routes/admin.js CHANGED
@@ -312,6 +312,12 @@ router.get(
312
312
  backupForm.values.backup_with_event_log = getState().getConfig(
313
313
  "backup_with_event_log"
314
314
  );
315
+ backupForm.values.backup_with_system_zip = getState().getConfig(
316
+ "backup_with_system_zip"
317
+ );
318
+ backupForm.values.backup_system_zip_level = getState().getConfig(
319
+ "backup_system_zip_level"
320
+ );
315
321
  //
316
322
  const aSnapshotForm = snapshotForm(req);
317
323
  aSnapshotForm.values.snapshots_enabled =
@@ -838,6 +844,31 @@ const autoBackupForm = (req) => {
838
844
  auto_backup_frequency: ["Daily", "Weekly"],
839
845
  },
840
846
  },
847
+ {
848
+ type: "Bool",
849
+ label: req.__("Use system zip"),
850
+ sublabel: req.__(
851
+ "Recommended. Executable <code>zip</code> must be installed"
852
+ ),
853
+ name: "backup_with_system_zip",
854
+ showIf: {
855
+ auto_backup_frequency: ["Daily", "Weekly"],
856
+ },
857
+ },
858
+ {
859
+ type: "Integer",
860
+ label: req.__("Zip compression level"),
861
+ sublabel: req.__("1=Fast, larger file, 9=Slow, smaller files"),
862
+ name: "backup_system_zip_level",
863
+ attributes: {
864
+ min: 1,
865
+ max: 9,
866
+ },
867
+ showIf: {
868
+ auto_backup_frequency: ["Daily", "Weekly"],
869
+ backup_with_system_zip: true,
870
+ },
871
+ },
841
872
  ],
842
873
  });
843
874
  };
package/routes/files.js CHANGED
@@ -20,7 +20,15 @@ const {
20
20
  setTenant,
21
21
  is_relative_url,
22
22
  } = require("./utils.js");
23
- const { h1, div, text } = require("@saltcorn/markup/tags");
23
+ const {
24
+ h1,
25
+ div,
26
+ text,
27
+ script,
28
+ style,
29
+ link,
30
+ domReady,
31
+ } = require("@saltcorn/markup/tags");
24
32
  const { editRoleForm, fileUploadForm } = require("../markup/forms.js");
25
33
  const { strictParseInt } = require("@saltcorn/data/plugin-helper");
26
34
  const {
@@ -43,20 +51,98 @@ const { extract } = require("@saltcorn/admin-models/models/backup");
43
51
  const router = new Router();
44
52
  module.exports = router;
45
53
 
46
- /**
47
- * Edit file Role form
48
- * @param {*} file
49
- * @param {*} roles
50
- * @param {*} req
51
- * @returns {Form}
52
- */
53
- const editFileRoleForm = (file, roles, req) =>
54
- editRoleForm({
55
- url: `/files/setrole/${file.path_to_serve}`,
56
- current_role: file.min_role_read,
57
- roles,
58
- req,
54
+ const send_files_picker = async (folder, noSubdirs, inputId, req, res) => {
55
+ res.set("SaltcornModalWidth", "1200px");
56
+ res.sendWrap(req.__("Please select a file"), {
57
+ above: [
58
+ script({
59
+ src: `/static_assets/${db.connectObj.version_tag}/bundle.js`,
60
+ defer: true,
61
+ }),
62
+ script(
63
+ domReady(
64
+ `$("head").append('${link({
65
+ rel: "stylesheet",
66
+ href: `/static_assets/${db.connectObj.version_tag}/bundle.css`,
67
+ })}')`
68
+ )
69
+ ),
70
+ div({
71
+ id: "saltcorn-file-manager",
72
+ full_manager: "false",
73
+ folder: folder,
74
+ input_id: inputId,
75
+ ...(noSubdirs ? { no_subdirs: "true" } : {}),
76
+ }),
77
+ ],
59
78
  });
79
+ };
80
+
81
+ router.get(
82
+ "/picker",
83
+ error_catcher(async (req, res) => {
84
+ const { folder, input_id, no_subdirs } = req.query;
85
+ send_files_picker(folder, no_subdirs, input_id, req, res);
86
+ })
87
+ );
88
+
89
+ router.get(
90
+ "/visible_entries",
91
+ error_catcher(async (req, res) => {
92
+ const role = req.user?.role_id ? req.user.role_id : 100;
93
+ const userId = req.user?.id;
94
+ const { dir, no_subdirs } = req.query;
95
+ const noSubdirs = no_subdirs === "true";
96
+ const safeDir = File.normalise(dir || "/");
97
+ const absFolder = path.join(
98
+ db.connectObj.file_store,
99
+ db.getTenantSchema(),
100
+ safeDir
101
+ );
102
+ const dirOnDisk = await File.from_file_on_disk(
103
+ path.basename(absFolder),
104
+ path.dirname(absFolder)
105
+ );
106
+ if (dirOnDisk.min_role_read < role) {
107
+ getState().log(5, `Directory denied. path=${dir} role=${role}`);
108
+ res.json({ files: [], roles: [], directories: [] });
109
+ return;
110
+ }
111
+ const rows = (
112
+ await File.find({ folder: dir }, { orderBy: "filename" })
113
+ ).filter((f) => {
114
+ if (noSubdirs && f.isDirectory) return false;
115
+ else return role <= f.min_role_read || (userId && userId === f.user_id);
116
+ });
117
+ const roles = await User.get_roles();
118
+ if (!no_subdirs && safeDir && safeDir !== "/" && safeDir !== ".") {
119
+ let dirname = path.dirname(safeDir);
120
+ if (dirname === ".") dirname = "/";
121
+ rows.unshift(
122
+ new File({
123
+ filename: "..",
124
+ location: dirname,
125
+ isDirectory: true,
126
+ mime_super: "",
127
+ mime_sub: "",
128
+ })
129
+ );
130
+ }
131
+
132
+ for (const file of rows) {
133
+ file.location = file.path_to_serve;
134
+ }
135
+ const directories = !noSubdirs
136
+ ? (await File.allDirectories(true)).filter(
137
+ (dir) => role <= dir.min_role_read
138
+ )
139
+ : [];
140
+ for (const dir of directories) {
141
+ dir.location = dir.path_to_serve;
142
+ }
143
+ res.json({ files: rows, roles, directories });
144
+ })
145
+ );
60
146
 
61
147
  /**
62
148
  * @name get
@@ -116,7 +202,7 @@ router.get(
116
202
  contents: {
117
203
  type: "card",
118
204
  contents: [
119
- div({ id: "saltcorn-file-manager" }),
205
+ div({ full_manager: "true", id: "saltcorn-file-manager" }),
120
206
  fileUploadForm(req, safeDir),
121
207
  ],
122
208
  },
@@ -14,6 +14,7 @@ const Form = require("@saltcorn/data/models/form");
14
14
  const File = require("@saltcorn/data/models/file");
15
15
  const User = require("@saltcorn/data/models/user");
16
16
  const { renderForm, post_btn } = require("@saltcorn/markup");
17
+ const db = require("@saltcorn/data/db");
17
18
 
18
19
  const router = new Router();
19
20
  module.exports = router;
@@ -39,9 +40,18 @@ router.get(
39
40
  orderDesc: true,
40
41
  limit: 20,
41
42
  });
42
- await Notification.mark_as_read({
43
- id: { in: nots.filter((n) => !n.read).map((n) => n.id) },
44
- });
43
+ const unreads = nots.filter((n) => !n.read);
44
+ if (unreads.length > 0)
45
+ await Notification.mark_as_read(
46
+ !db.isSQLite
47
+ ? {
48
+ id: { in: unreads.map((n) => n.id) },
49
+ }
50
+ : {
51
+ or: unreads.map((n) => ({ id: n.id })),
52
+ }
53
+ );
54
+
45
55
  const form = notificationSettingsForm();
46
56
  const user = await User.findOne({ id: req.user?.id });
47
57
  form.values = { notify_email: user?._attributes?.notify_email };
@@ -186,7 +186,11 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
186
186
  sublabel: req.__("Display data from this table"),
187
187
  options: tableOptions,
188
188
  disabled: isEdit,
189
- showIf: { viewtemplate: hasTable },
189
+ showIf: isEdit
190
+ ? hasTable.includes(values.viewtemplate)
191
+ ? undefined
192
+ : { nosuchvar: true }
193
+ : { viewtemplate: hasTable },
190
194
  }),
191
195
  new Field({
192
196
  name: "min_role",
@@ -242,7 +246,11 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
242
246
  mapObjectValues(slugOptions, (lvs) => lvs.map((lv) => lv.label)),
243
247
  ],
244
248
  },
245
- showIf: { viewtemplate: hasTable },
249
+ showIf: isEdit
250
+ ? hasTable.includes(values.viewtemplate)
251
+ ? undefined
252
+ : { nosuchvar: true }
253
+ : { viewtemplate: hasTable },
246
254
  }),
247
255
  new Field({
248
256
  name: "no_menu",
@@ -30,6 +30,14 @@ const createTestFile = async (folder, name, mimetype, content) => {
30
30
  }
31
31
  };
32
32
 
33
+ const checkFiles = (files, expecteds) =>
34
+ files.length >= expecteds.length &&
35
+ expecteds.every(({ filename, location }) =>
36
+ files.find(
37
+ (file) => file.filename === filename && file.location === location
38
+ )
39
+ );
40
+
33
41
  beforeAll(async () => {
34
42
  await resetToFixtures();
35
43
  await File.ensure_file_store();
@@ -186,13 +194,7 @@ describe("files admin", () => {
186
194
  it("search files by name", async () => {
187
195
  const app = await getApp({ disableCsrf: true });
188
196
  const loginCookie = await getAdminLoginCookie();
189
- const checkFiles = (files, expecteds) =>
190
- files.length >= expecteds.length &&
191
- expecteds.every(({ filename, location }) =>
192
- files.find(
193
- (file) => file.filename === filename && file.location === location
194
- )
195
- );
197
+
196
198
  const searchTestHelper = async (dir, search, expected) => {
197
199
  await request(app)
198
200
  .get("/files")
@@ -345,3 +347,81 @@ describe("files edit", () => {
345
347
  expect(!!file).toBe(true);
346
348
  });
347
349
  });
350
+
351
+ describe("visible_entries test", () => {
352
+ const setRole = async (role, entry) => {
353
+ const app = await getApp({ disableCsrf: true });
354
+ const adminCookie = await getAdminLoginCookie();
355
+ await request(app)
356
+ .post(`/files/setrole/${entry}`)
357
+ .set("Cookie", adminCookie)
358
+ .send(`role=${role}`)
359
+ .expect(toRedirect("/files?dir=_sc_test_subfolder_one"));
360
+ };
361
+
362
+ it("shows allowed files", async () => {
363
+ await setRole(100, path.join("_sc_test_subfolder_one", "foo_image.png"));
364
+
365
+ const app = await getApp({ disableCsrf: true });
366
+ const staffCookie = await getStaffLoginCookie();
367
+ await request(app)
368
+ .get("/files/visible_entries?dir=_sc_test_subfolder_one")
369
+ .set("Cookie", staffCookie)
370
+ .expect(
371
+ respondJsonWith(200, (data) =>
372
+ checkFiles(data.files, [
373
+ {
374
+ filename: "foo_image.png",
375
+ location: path.join("_sc_test_subfolder_one", "foo_image.png"),
376
+ },
377
+ {
378
+ filename: "bar_image.png",
379
+ location: path.join("_sc_test_subfolder_one", "bar_image.png"),
380
+ },
381
+ ])
382
+ )
383
+ );
384
+ });
385
+
386
+ it("shows no disallowed files", async () => {
387
+ await setRole(1, path.join("_sc_test_subfolder_one", "foo_image.png"));
388
+ const app = await getApp({ disableCsrf: true });
389
+ const staffCookie = await getStaffLoginCookie();
390
+ const resp = await request(app)
391
+ .get("/files/visible_entries?dir=_sc_test_subfolder_one")
392
+ .set("Cookie", staffCookie);
393
+ expect(resp.statusCode).toBe(200);
394
+ const files = resp.body.files;
395
+ expect(
396
+ files.find((file) => file.filename === "foo_image.png")
397
+ ).toBeUndefined();
398
+ });
399
+
400
+ it("shows allowed directories", async () => {
401
+ const dir = path.join("_sc_test_subfolder_one", "subsubfolder");
402
+ await setRole(80, dir);
403
+ const app = await getApp({ disableCsrf: true });
404
+ const staffCookie = await getStaffLoginCookie();
405
+ const resp = await request(app)
406
+ .get(`/files/visible_entries?dir=${dir}`)
407
+ .set("Cookie", staffCookie);
408
+ expect(resp.statusCode).toBe(200);
409
+ expect(
410
+ resp.body.directories.find((file) => file.filename === "subsubfolder")
411
+ ).toBeDefined();
412
+ });
413
+
414
+ it("shows no disallowed directories", async () => {
415
+ const dir = path.join("_sc_test_subfolder_one", "subsubfolder");
416
+ await setRole(1, dir);
417
+ const app = await getApp({ disableCsrf: true });
418
+ const staffCookie = await getStaffLoginCookie();
419
+ const resp = await request(app)
420
+ .get(`/files/visible_entries?dir=${dir}`)
421
+ .set("Cookie", staffCookie);
422
+ expect(resp.statusCode).toBe(200);
423
+ const body = resp.body;
424
+ expect(body.files.length).toBe(0);
425
+ expect(body.directories.length).toBe(0);
426
+ });
427
+ });