@saltcorn/server 1.0.0 → 1.1.0-beta.1

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 CHANGED
@@ -74,7 +74,7 @@ const noCsrfLookup = (state) => {
74
74
  if (!state.plugin_routes) return null;
75
75
  else {
76
76
  const result = new Set();
77
- for (const [plugin, routes] of Object.entries(state.plugin_routes)) {
77
+ for (const routes of Object.values(state.plugin_routes)) {
78
78
  for (const url of routes
79
79
  .filter((r) => r.noCsrf === true)
80
80
  .map((r) => r.url)) {
@@ -87,7 +87,7 @@ const noCsrfLookup = (state) => {
87
87
 
88
88
  const prepPluginRouter = (pluginRoutes) => {
89
89
  const router = express.Router();
90
- for (const [plugin, routes] of Object.entries(pluginRoutes)) {
90
+ for (const routes of Object.values(pluginRoutes)) {
91
91
  for (const route of routes) {
92
92
  switch (route.method) {
93
93
  case "post":
@@ -133,17 +133,32 @@ const getApp = async (opts = {}) => {
133
133
  );
134
134
 
135
135
  const helmetOptions = {
136
- contentSecurityPolicy: false,
136
+ contentSecurityPolicy: {
137
+ directives: {
138
+ defaultSrc: ["'self'"],
139
+ connectSrc: ["'self'", "data:"],
140
+ scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
141
+ "script-src-attr": ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
142
+ styleSrc: ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "'unsafe-inline'"],
143
+ imgSrc: ["'self'", "data:"],
144
+ fontSrc: ["'self'", "data:", "https://fonts.gstatic.com",],
145
+ "form-action": ["'self'", "javascript:"],
146
+ },
147
+ },
137
148
  referrerPolicy: {
138
149
  policy: ["same-origin"],
139
150
  },
140
151
  };
152
+ if (
153
+ getState().getConfig("content_security_policy", "Disabled") === "Disabled"
154
+ )
155
+ helmetOptions.contentSecurityPolicy = false;
141
156
 
142
157
  if (cross_domain_iframe) helmetOptions.xFrameOptions = false;
143
158
  app.use(helmet(helmetOptions));
144
159
 
145
160
  // TODO ch find a better solution
146
- app.use(cors());
161
+ if (getState().getConfig("cors_enabled", true)) app.use(cors());
147
162
  const bodyLimit = getState().getConfig("body_limit");
148
163
  app.use(
149
164
  express.json({
@@ -366,7 +381,9 @@ const getApp = async (opts = {}) => {
366
381
  req.url === "/auth/login-with/jwt" ||
367
382
  req.url === "/auth/signup")) ||
368
383
  jwt_extractor(req) ||
369
- req.url === "/auth/callback/saml"
384
+ req.url === "/auth/callback/saml" ||
385
+ req.url.startsWith("/notifications/share-handler") ||
386
+ req.url.startsWith("/notifications/manifest")
370
387
  )
371
388
  return disabledCsurf(req, res, next);
372
389
  csurf(req, res, next);
package/auth/admin.js CHANGED
@@ -356,11 +356,16 @@ const auth_settings_form = async (req) =>
356
356
  "allow_signup",
357
357
  "login_menu",
358
358
  "allow_forgot",
359
- "new_user_form",
359
+ {
360
+ section_header: req.__("Signup and login views"),
361
+ sublabel: "Login and signup views should be accessible by public users",
362
+ },
360
363
  "login_form",
361
364
  "signup_form",
365
+ "new_user_form",
362
366
  "user_settings_form",
363
367
  "verification_view",
368
+ { section_header: req.__("Additional login and signup settings") },
364
369
  "logout_url",
365
370
  "signup_role",
366
371
  "elevate_verified",
@@ -383,7 +388,9 @@ const http_settings_form = async (req) =>
383
388
  "timeout",
384
389
  "cookie_duration",
385
390
  "cookie_duration_remember",
386
- //"cookie_sessions",
391
+ "cookie_samesite",
392
+ "content_security_policy",
393
+ "cors_enabled",
387
394
  "public_cache_maxage",
388
395
  "custom_http_headers",
389
396
  "cross_domain_iframe",
package/auth/routes.js CHANGED
@@ -306,8 +306,20 @@ router.get(
306
306
  getAuthLinks("login")
307
307
  );
308
308
  else {
309
- const resp = await login_form.run_possibly_on_page({}, req, res);
310
- if (login_form.default_render_page) {
309
+ const resp = await login_form.run_possibly_on_page(
310
+ {},
311
+ req,
312
+ res,
313
+ false,
314
+ true
315
+ );
316
+ if (!resp) {
317
+ res.sendAuthWrap(
318
+ req.__(`Login`),
319
+ loginForm(req),
320
+ getAuthLinks("login")
321
+ );
322
+ } else if (login_form.default_render_page) {
311
323
  const page = Page.findOne({ name: login_form.default_render_page });
312
324
  res.sendWrap(
313
325
  { title: req.__(`Login`), no_menu: page?.attributes?.no_menu },
@@ -509,7 +521,8 @@ router.get(
509
521
  if (!signup_form) await defaultSignup();
510
522
  else {
511
523
  const resp = await signup_form.run_possibly_on_page({}, req, res);
512
- if (signup_form.default_render_page) {
524
+ if (!resp) await defaultSignup();
525
+ else if (signup_form.default_render_page) {
513
526
  const page = Page.findOne({ name: signup_form.default_render_page });
514
527
  res.sendWrap(
515
528
  { title: req.__(`Sign up`), no_menu: page?.attributes?.no_menu },
@@ -673,7 +686,7 @@ const getNewUserForm = async (new_user_view_name, req, askEmail) => {
673
686
  layout,
674
687
  submitLabel: req.__("Sign up"),
675
688
  });
676
- await form.fill_fkey_options();
689
+ await form.fill_fkey_options(false, undefined, req.user || { role_id: 100 });
677
690
  if (askEmail) {
678
691
  form.fields.push(
679
692
  new Field({
package/locales/en.json CHANGED
@@ -1477,5 +1477,21 @@
1477
1477
  "After delete": "After delete",
1478
1478
  "Search only on exact match, not substring match. Useful for large tables": "Search only on exact match, not substring match. Useful for large tables",
1479
1479
  "Please select an entry point.": "Please select an entry point.",
1480
- "Group by": "Group by"
1480
+ "Group by": "Group by",
1481
+ "View %s added to menu. Adjust access permissions in <a href=\"/menu\">Settings &raquo; Menu</a>": "View %s added to menu. Adjust access permissions in <a href=\"/menu\">Settings &raquo; Menu</a>",
1482
+ "Page %s added to menu. Adjust access permissions in <a href=\"/menu\">Settings &raquo; Menu</a>": "Page %s added to menu. Adjust access permissions in <a href=\"/menu\">Settings &raquo; Menu</a>",
1483
+ "SameSite": "SameSite",
1484
+ "Restrict use of cookie to third-party sites. Strict is more secure, but may impact authentication": "Restrict use of cookie to third-party sites. Strict is more secure, but may impact authentication",
1485
+ "Content Security Policy": "Content Security Policy",
1486
+ "CORS": "CORS",
1487
+ "Cross-origin resource sharing": "Cross-origin resource sharing",
1488
+ "Signup and login views": "Signup and login views",
1489
+ "Additional login and signup settings": "Additional login and signup settings",
1490
+ "Login and signup views should be accessible by public users": "Login and signup views should be accessible by public users",
1491
+ "Shared: %s": "Shared: %s",
1492
+ "Sharing not enabled": "Sharing not enabled",
1493
+ "You must be logged in to share": "You must be logged in to share",
1494
+ "Fluid layout": "Fluid layout",
1495
+ "Request fluid layout from theme for a wider display for this page": "Request fluid layout from theme for a wider display for this page",
1496
+ "Location of view to create new row": "Location of view to create new row"
1481
1497
  }
package/locales/pl.json CHANGED
@@ -1475,5 +1475,21 @@
1475
1475
  "Invalid build directory path": "Nieprawidłowa ścieżka katalogu kompilacji",
1476
1476
  "Invalid build directory name": "Nieprawidłowa nazwa katalogu kompilacji",
1477
1477
  "clean node_modules": "wyczyść node_modules",
1478
- "After delete": "Po usunięciu"
1478
+ "After delete": "Po usunięciu",
1479
+ "Search only on exact match, not substring match. Useful for large tables": "Wyszukuj tylko dokładne dopasowania, nie dopasowania do podciągów. Przydatne w przypadku dużych tabel",
1480
+ "Please select an entry point.": "Proszę wybrać punkt wejścia",
1481
+ "Group by": "Grupuj według",
1482
+ "View %s added to menu. Adjust access permissions in <a href=\"/menu\">Settings &raquo; Menu</a>": "Widok %s został dodany do menu. Dostosuj uprawnienia dostępu w <a href=\"/menu\">Ustawienia &raquo; Menu</a>",
1483
+ "Page %s added to menu. Adjust access permissions in <a href=\"/menu\">Settings &raquo; Menu</a>": "Strona %s została dodana do menu. Dostosuj uprawnienia dostępu w <a href=\"/menu\">Ustawienia &raquo; Menu</a>",
1484
+ "SameSite": "SameSite",
1485
+ "Restrict use of cookie to third-party sites. Strict is more secure, but may impact authentication": "Ogranicz użycie ciasteczka na stronach trzecich. Tryb „Strict” jest bardziej bezpieczny, ale może wpływać na uwierzytelnianie",
1486
+ "Content Security Policy": "Polityka bezpieczeństwa treści",
1487
+ "CORS": "CORS",
1488
+ "Cross-origin resource sharing": "Udostępnianie zasobów między źródłami (CORS)",
1489
+ "Signup and login views": "Widoki rejestracji i logowania",
1490
+ "Additional login and signup settings": "Dodatkowe ustawienia logowania i rejestracji",
1491
+ "Login and signup views should be accessible by public users": "Widoki logowania i rejestracji powinny być dostępne dla użytkowników publicznych",
1492
+ "Shared: %s": "Udostępnione: %s",
1493
+ "Sharing not enabled": "Udostępnianie nie jest włączone",
1494
+ "You must be logged in to share": "Musisz być zalogowany, aby udostępniać"
1479
1495
  }
package/markup/admin.js CHANGED
@@ -420,6 +420,7 @@ const config_fields_form = async ({
420
420
  fields.push({
421
421
  input_type: "section_header",
422
422
  label: req.__(name0.section_header),
423
+ sublabel: name0.sublabel ? req.__(name0.sublabel) : undefined,
423
424
  });
424
425
  continue;
425
426
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "1.0.0",
3
+ "version": "1.1.0-beta.1",
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",
11
- "@saltcorn/builder": "1.0.0",
12
- "@saltcorn/data": "1.0.0",
13
- "@saltcorn/admin-models": "1.0.0",
14
- "@saltcorn/filemanager": "1.0.0",
15
- "@saltcorn/markup": "1.0.0",
16
- "@saltcorn/plugins-loader": "1.0.0",
17
- "@saltcorn/sbadmin2": "1.0.0",
10
+ "@saltcorn/base-plugin": "1.1.0-beta.1",
11
+ "@saltcorn/builder": "1.1.0-beta.1",
12
+ "@saltcorn/data": "1.1.0-beta.1",
13
+ "@saltcorn/admin-models": "1.1.0-beta.1",
14
+ "@saltcorn/filemanager": "1.1.0-beta.1",
15
+ "@saltcorn/markup": "1.1.0-beta.1",
16
+ "@saltcorn/plugins-loader": "1.1.0-beta.1",
17
+ "@saltcorn/sbadmin2": "1.1.0-beta.1",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -33,7 +33,7 @@ function setScreenInfoCookie() {
33
33
  innerHeight: window.innerHeight,
34
34
  })}; expires=Thu, 01 Jan 2100 00:00:00 GMT; path=/; domain=.${
35
35
  window.location.hostname
36
- }`;
36
+ }; samesite=strict`;
37
37
  }
38
38
  setScreenInfoCookie();
39
39
  $(window).resize(() => {
@@ -607,3 +607,7 @@ button.monospace-copy-btn {
607
607
  .mobile-thumbnail-container img {
608
608
  display: block;
609
609
  }
610
+
611
+ i[class^="unicode-"], i[class*=" unicode-"] {
612
+ font-style: normal;
613
+ }
package/routes/actions.js CHANGED
@@ -564,10 +564,17 @@ router.get(
564
564
 
565
565
  const subtitle =
566
566
  span(
567
- { class: "ms-3" },
567
+ { class: "ms-2" },
568
568
  trigger.action,
569
+ "&nbsp;",
570
+ trigger.when_trigger,
569
571
  table ? ` on ` + a({ href: `/table/${table.name}` }, table.name) : ""
570
572
  ) +
573
+ a(
574
+ { href: `/actions/edit/${id}`, class: "ms-2" },
575
+ req.__("Edit"),
576
+ '&nbsp;<i class="fas fa-edit"></i>'
577
+ ) +
571
578
  a(
572
579
  { href: `/actions/testrun/${id}`, class: "ms-2" },
573
580
  req.__("Test run") + "&nbsp;&raquo;"
package/routes/admin.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  setTenant,
13
13
  admin_config_route,
14
14
  get_sys_info,
15
+ tenant_letsencrypt_name,
15
16
  } = require("./utils.js");
16
17
  const Table = require("@saltcorn/data/models/table");
17
18
  const Plugin = require("@saltcorn/data/models/plugin");
@@ -90,7 +91,10 @@ const {
90
91
  } = require("../markup/admin.js");
91
92
  const packagejson = require("../package.json");
92
93
  const Form = require("@saltcorn/data/models/form");
93
- const { get_latest_npm_version } = require("@saltcorn/data/models/config");
94
+ const {
95
+ get_latest_npm_version,
96
+ isFixedConfig,
97
+ } = require("@saltcorn/data/models/config");
94
98
  const { getMailTransport } = require("@saltcorn/data/models/email");
95
99
  const {
96
100
  getBaseDomain,
@@ -976,6 +980,40 @@ router.post(
976
980
  }
977
981
  })
978
982
  );
983
+
984
+ router.post(
985
+ "/save-config",
986
+ isAdmin,
987
+ error_catcher(async (req, res) => {
988
+ const state = getState();
989
+
990
+ //TODO check this is a config key
991
+ const validKeyName = (k) =>
992
+ k !== "_csrf" && k !== "constructor" && k !== "__proto__";
993
+
994
+ for (const [k, v] of Object.entries(req.body)) {
995
+ if (!isFixedConfig(k) && typeof v !== "undefined" && validKeyName(k)) {
996
+ //TODO read value from type
997
+ await state.setConfig(k, v);
998
+ }
999
+ }
1000
+
1001
+ // checkboxes that are false are not sent in post body. Check here
1002
+ const { boolcheck } = req.query;
1003
+ const boolchecks =
1004
+ typeof boolcheck === "undefined"
1005
+ ? []
1006
+ : Array.isArray(boolcheck)
1007
+ ? boolcheck
1008
+ : [boolcheck];
1009
+ for (const k of boolchecks) {
1010
+ if (typeof req.body[k] === "undefined" && validKeyName(k))
1011
+ await state.setConfig(k, false);
1012
+ }
1013
+ res.json({ success: "ok" });
1014
+ })
1015
+ );
1016
+
979
1017
  /**
980
1018
  * Do Auto backup now
981
1019
  */
@@ -1689,6 +1727,64 @@ const clearAllForm = (req) =>
1689
1727
  ],
1690
1728
  });
1691
1729
 
1730
+ router.post(
1731
+ "/acq-ssl-tenant/:subdomain",
1732
+ isAdmin,
1733
+ error_catcher(async (req, res) => {
1734
+ if (
1735
+ db.is_it_multi_tenant() &&
1736
+ db.getTenantSchema() === db.connectObj.default_schema
1737
+ ) {
1738
+ const { subdomain } = req.params;
1739
+
1740
+ const domain = getBaseDomain();
1741
+
1742
+ let altname = await tenant_letsencrypt_name(subdomain);
1743
+
1744
+ if (!altname || domain) {
1745
+ req.json({ error: "Set Base URL for both tenant and root first." });
1746
+ return;
1747
+ }
1748
+
1749
+ try {
1750
+ const file_store = db.connectObj.file_store;
1751
+ const admin_users = await User.find({ role_id: 1 }, { orderBy: "id" });
1752
+ // greenlock logic
1753
+ const Greenlock = require("greenlock");
1754
+ const greenlock = Greenlock.create({
1755
+ packageRoot: path.resolve(__dirname, ".."),
1756
+ configDir: path.join(file_store, "greenlock.d"),
1757
+ maintainerEmail: admin_users[0].email,
1758
+ });
1759
+
1760
+ await greenlock.sites.add({
1761
+ subject: altname,
1762
+ });
1763
+ // letsencrypt
1764
+ const tenant_letsencrypt_sites = getState().getConfig(
1765
+ "tenant_letsencrypt_sites",
1766
+ []
1767
+ );
1768
+ await getState().setConfig(tenant_letsencrypt_sites, [
1769
+ altname,
1770
+ ...tenant_letsencrypt_sites,
1771
+ ]);
1772
+
1773
+ res.json({
1774
+ success: true,
1775
+ notify: "Certificate added, please restart server",
1776
+ });
1777
+ } catch (e) {
1778
+ req.flash("error", e.message);
1779
+ res.redirect("/useradmin/ssl");
1780
+ }
1781
+ } else {
1782
+ req.flash("error", req.__("Not possible for tenant"));
1783
+ res.redirect("/useradmin/ssl");
1784
+ }
1785
+ })
1786
+ );
1787
+
1692
1788
  /**
1693
1789
  * Do Enable letsencrypt
1694
1790
  * @name post/enable-letsencrypt
@@ -1749,6 +1845,15 @@ router.post(
1749
1845
  });
1750
1846
  // letsencrypt
1751
1847
  await getState().setConfig("letsencrypt", true);
1848
+ const tenant_letsencrypt_sites = getState().getConfig(
1849
+ "tenant_letsencrypt_sites",
1850
+ []
1851
+ );
1852
+ await getState().setConfig(tenant_letsencrypt_sites, [
1853
+ ...altnames,
1854
+ ...tenant_letsencrypt_sites,
1855
+ ]);
1856
+
1752
1857
  req.flash(
1753
1858
  "success",
1754
1859
  req.__(
@@ -3844,6 +3949,10 @@ admin_config_route({
3844
3949
  { section_header: "Progressive Web Application" },
3845
3950
  "pwa_enabled",
3846
3951
  { name: "pwa_display", showIf: { pwa_enabled: true } },
3952
+ {
3953
+ name: "pwa_share_to_enabled",
3954
+ showIf: { pwa_enabled: true },
3955
+ },
3847
3956
  { name: "pwa_set_colors", showIf: { pwa_enabled: true } },
3848
3957
  {
3849
3958
  name: "pwa_theme_color",
package/routes/fields.js CHANGED
@@ -1158,6 +1158,8 @@ router.post(
1158
1158
  try {
1159
1159
  if (!field.calculated) {
1160
1160
  result = row[field.name];
1161
+ } else if (field.stored && field.expression === "__aggregation") {
1162
+ result = row[field.name];
1161
1163
  } else if (field.stored) {
1162
1164
  const f = get_async_expression_function(formula, fields);
1163
1165
  //are there join fields in formula?
package/routes/list.js CHANGED
@@ -303,7 +303,10 @@ router.get(
303
303
  type: "breadcrumbs",
304
304
  crumbs: [
305
305
  { text: req.__("Tables"), href: "/table" },
306
- { href: `/table/${table.id || table.name}`, text: table.name },
306
+ {
307
+ href: `/table/${table.id || encodeURIComponent(table.name)}`,
308
+ text: table.name,
309
+ },
307
310
  { text: req.__("Data") },
308
311
  ],
309
312
  right: div(
@@ -376,7 +379,7 @@ router.get(
376
379
  })
377
380
  })
378
381
  window.tabulator_table = new Tabulator("#jsGrid", {
379
- ajaxURL:"/api/${table.name}${
382
+ ajaxURL:"/api/${encodeURIComponent(table.name)}${
380
383
  table.versioned ? "?versioncount=on" : ""
381
384
  }",
382
385
  layout:"fitColumns",
package/routes/menu.js CHANGED
@@ -20,11 +20,19 @@ const { save_menu_items } = require("@saltcorn/data/models/config");
20
20
  const db = require("@saltcorn/data/db");
21
21
 
22
22
  const { renderForm } = require("@saltcorn/markup");
23
- const { script, domReady, div, ul, i } = require("@saltcorn/markup/tags");
23
+ const {
24
+ script,
25
+ domReady,
26
+ div,
27
+ ul,
28
+ i,
29
+ style,
30
+ } = require("@saltcorn/markup/tags");
24
31
  const { send_infoarch_page } = require("../markup/admin.js");
25
32
  const Table = require("@saltcorn/data/models/table");
26
33
  const Trigger = require("@saltcorn/data/models/trigger");
27
34
  const { run_action_column } = require("@saltcorn/data/plugin-helper");
35
+ const path = require("path");
28
36
 
29
37
  /**
30
38
  * @type {object}
@@ -497,7 +505,7 @@ router.get(
497
505
  script: static_pre + "/jquery-menu-editor.min.js",
498
506
  },
499
507
  {
500
- script: static_pre + "/iconset-fontawesome5-3-1.min.js",
508
+ script: "/menu/icon-options?format=bootstrap-iconpicker",
501
509
  },
502
510
  {
503
511
  script: static_pre + "/bootstrap-iconpicker.js",
@@ -526,7 +534,8 @@ router.get(
526
534
  ),
527
535
  div(
528
536
  renderForm(form, req.csrfToken()),
529
- script(domReady(menuEditorScript(menu_items)))
537
+ script(domReady(menuEditorScript(menu_items))),
538
+ style(setIconStyle())
530
539
  ),
531
540
  ],
532
541
  },
@@ -599,3 +608,45 @@ router.post(
599
608
  else res.status(404).json({ error: "Action not found" });
600
609
  })
601
610
  );
611
+
612
+ const getIcons = () => {
613
+ return getState().icons;
614
+ };
615
+
616
+ const setIconStyle = () => {
617
+ const icons = getIcons();
618
+ return icons
619
+ .filter((icon) => icon.startsWith("unicode-"))
620
+ .map(
621
+ (icon) =>
622
+ `i.${icon}:after {content: '${String.fromCharCode(
623
+ parseInt(icon.substring(8, 12), 16)
624
+ )}'}`
625
+ )
626
+ .join("\n");
627
+ };
628
+
629
+ router.get(
630
+ "/icon-options",
631
+ isAdmin,
632
+ error_catcher(async (req, res) => {
633
+ const { format } = req.query;
634
+ const icons = getIcons();
635
+ switch (format) {
636
+ case "bootstrap-iconpicker":
637
+ res.type("text/javascript");
638
+ res.send(
639
+ `jQuery.iconset_fontawesome_5={iconClass:"",iconClassFix:"",version:"5.3.1",icons:${JSON.stringify(
640
+ icons
641
+ )}}`
642
+ );
643
+ break;
644
+ case "json":
645
+ res.json(icons);
646
+
647
+ default:
648
+ res.send("");
649
+ break;
650
+ }
651
+ })
652
+ );
@@ -13,6 +13,7 @@ const { getState } = require("@saltcorn/data/db/state");
13
13
  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
+ const Trigger = require("@saltcorn/data/models/trigger");
16
17
  const { renderForm, post_btn } = require("@saltcorn/markup");
17
18
  const db = require("@saltcorn/data/db");
18
19
 
@@ -193,6 +194,32 @@ router.post(
193
194
  })
194
195
  );
195
196
 
197
+ router.post(
198
+ "/share-handler",
199
+ error_catcher(async (req, res) => {
200
+ const role = req.user?.role_id || 100;
201
+ if (role === 100) {
202
+ req.flash("error", req.__("You must be logged in to share"));
203
+ res.redirect("/auth/login");
204
+ } else if (!getState().getConfig("pwa_share_to_enabled", false)) {
205
+ req.flash("error", req.__("Sharing not enabled"));
206
+ res.redirect("/");
207
+ } else {
208
+ Trigger.emitEvent("ReceiveMobileShareData", null, req.user, {
209
+ row: req.body,
210
+ });
211
+ req.flash(
212
+ "success",
213
+ req.__(
214
+ "Shared: %s",
215
+ req.body.title || req.body.text || req.body.url || ""
216
+ )
217
+ );
218
+ res.status(303).redirect("/");
219
+ }
220
+ })
221
+ );
222
+
196
223
  router.get(
197
224
  "/manifest.json:opt_cache_bust?",
198
225
  error_catcher(async (req, res) => {
@@ -205,6 +232,19 @@ router.get(
205
232
  };
206
233
  const site_logo = state.getConfig("site_logo_id");
207
234
  const pwa_icons = state.getConfig("pwa_icons");
235
+ const pwa_share_to_enabled = state.getConfig("pwa_share_to_enabled", false);
236
+ if (pwa_share_to_enabled) {
237
+ manifest.share_target = {
238
+ action: "/notifications/share-handler",
239
+ method: "POST",
240
+ enctype: "multipart/form-data",
241
+ params: {
242
+ title: "title",
243
+ text: "text",
244
+ url: "url",
245
+ },
246
+ };
247
+ }
208
248
  if (Array.isArray(pwa_icons) && pwa_icons.length > 0)
209
249
  manifest.icons = pwa_icons.map(({ image, size, maskable }) => ({
210
250
  src: `/files/serve/${image}`,
package/routes/page.js CHANGED
@@ -66,6 +66,7 @@ const runPage = async (page, req, res, tic) => {
66
66
  description: page.description,
67
67
  bodyClass: "page_" + db.sqlsanitize(page.name),
68
68
  no_menu: page.attributes?.no_menu,
69
+ requestFluidLayout: page.attributes?.request_fluid_layout,
69
70
  } || `${page.name} page`,
70
71
  add_edit_bar({
71
72
  role,
@@ -200,6 +201,7 @@ router.post(
200
201
  });
201
202
  res.json({ success: "ok", ...(result || {}) });
202
203
  } catch (e) {
204
+ getState().log(2, e?.stack)
203
205
  res.status(400).json({ error: e.message || e });
204
206
  }
205
207
  } else res.status(404).json({ error: "Action not found" });
@@ -678,7 +678,7 @@ router.post(
678
678
  req.flash(
679
679
  "success",
680
680
  req.__(
681
- "Page %s added to menu. Adjust access permissions in Settings &raquo; Menu",
681
+ "Page %s added to menu. Adjust access permissions in <a href=\"/menu\">Settings &raquo; Menu</a>",
682
682
  group.name
683
683
  )
684
684
  );
@@ -143,6 +143,12 @@ const pagePropertiesForm = async (req, isNew) => {
143
143
  sublabel: req.__("Omit the menu from this page"),
144
144
  type: "Bool",
145
145
  },
146
+ {
147
+ name: "request_fluid_layout",
148
+ label: req.__("Fluid layout"),
149
+ sublabel: req.__("Request fluid layout from theme for a wider display for this page"),
150
+ type: "Bool",
151
+ },
146
152
  ],
147
153
  });
148
154
  return form;
@@ -229,6 +235,7 @@ const pageBuilderData = async (req, context) => {
229
235
  }
230
236
 
231
237
  //console.log(fixed_state_fields.ListTasks);
238
+ const icons = getState().icons;
232
239
  return {
233
240
  views: views.map((v) => v.select_option),
234
241
  images,
@@ -244,6 +251,7 @@ const pageBuilderData = async (req, context) => {
244
251
  page_id: context.id,
245
252
  mode: "page",
246
253
  roles,
254
+ icons,
247
255
  fixed_state_fields,
248
256
  next_button_label: "Done",
249
257
  fonts: getState().fonts,
@@ -427,6 +435,7 @@ router.get(
427
435
  form.hidden("id");
428
436
  form.values = page;
429
437
  form.values.no_menu = page.attributes?.no_menu;
438
+ form.values.request_fluid_layout = page.attributes?.request_fluid_layout;
430
439
  form.onChange = `saveAndContinue(this)`;
431
440
  res.sendWrap(
432
441
  req.__(`Page attributes`),
@@ -473,9 +482,16 @@ router.post(
473
482
  wrap(renderForm(form, req.csrfToken()), false, req)
474
483
  );
475
484
  } else {
476
- const { id, columns, no_menu, html_file, ...pageRow } = form.values;
485
+ const {
486
+ id,
487
+ columns,
488
+ no_menu,
489
+ request_fluid_layout,
490
+ html_file,
491
+ ...pageRow
492
+ } = form.values;
477
493
  pageRow.min_role = +pageRow.min_role;
478
- pageRow.attributes = { no_menu };
494
+ pageRow.attributes = { no_menu, request_fluid_layout };
479
495
  if (html_file) {
480
496
  pageRow.layout = {
481
497
  html_file: html_file,
@@ -528,7 +544,10 @@ const getEditNormalPage = async (req, res, page) => {
528
544
  version_tag: db.connectObj.version_tag,
529
545
  };
530
546
  res.sendWrap(
531
- req.__(`%s configuration`, page.name),
547
+ {
548
+ title: req.__(`%s configuration`, page.name),
549
+ requestFluidLayout: true,
550
+ },
532
551
  wrap(renderBuilder(builderData, req.csrfToken()), true, req, page)
533
552
  );
534
553
  };
@@ -780,7 +799,7 @@ router.post(
780
799
  req.flash(
781
800
  "success",
782
801
  req.__(
783
- "Page %s added to menu. Adjust access permissions in Settings &raquo; Menu",
802
+ 'Page %s added to menu. Adjust access permissions in <a href="/menu">Settings &raquo; Menu</a>',
784
803
  page.name
785
804
  )
786
805
  );
@@ -135,25 +135,70 @@ router.get(
135
135
  },
136
136
  ],
137
137
  });
138
+ let cfg_link = "";
138
139
  switch (etype) {
139
140
  case "table":
140
141
  const tpack = await table_pack(
141
142
  all_tables.find((t) => t.name === ename)
142
143
  );
144
+ cfg_link = a(
145
+ { href: `/table/${encodeURIComponent(ename)}` },
146
+ `${ename} ${etype}`
147
+ );
143
148
  edContents = renderForm(mkForm(tpack), req.csrfToken());
144
149
  break;
145
150
  case "view":
151
+ cfg_link =
152
+ `${ename} ${etype}` +
153
+ a(
154
+ {
155
+ class: "ms-2",
156
+ href: `/viewedit/edit/${encodeURIComponent(ename)}`,
157
+ },
158
+ "Edit&nbsp;",
159
+ i({ class: "fas fa-edit" })
160
+ ) +
161
+ a(
162
+ {
163
+ class: "ms-1 me-3",
164
+ href: `/viewedit/config/${encodeURIComponent(ename)}`,
165
+ },
166
+ "Configure&nbsp;",
167
+ i({ class: "fas fa-cog" })
168
+ );
146
169
  const vpack = await view_pack(all_views.find((v) => v.name === ename));
147
170
  edContents = renderForm(mkForm(vpack), req.csrfToken());
148
171
  break;
149
172
  case "page":
173
+ cfg_link = a(
174
+ { href: `/pageedit/edit/${encodeURIComponent(ename)}` },
175
+ `${ename} ${etype}`
176
+ );
150
177
  const ppack = await page_pack(all_pages.find((v) => v.name === ename));
151
178
  edContents = renderForm(mkForm(ppack), req.csrfToken());
152
179
  break;
153
180
  case "trigger":
154
- const trpack = await trigger_pack(
155
- all_triggers.find((t) => t.name === ename)
156
- );
181
+ const trigger = all_triggers.find((t) => t.name === ename);
182
+ const trpack = await trigger_pack(trigger);
183
+ cfg_link =
184
+ `${ename} ${etype}` +
185
+ a(
186
+ {
187
+ class: "ms-2",
188
+ href: `/actions/edit/${trigger?.id}`,
189
+ },
190
+ "Edit&nbsp;",
191
+ i({ class: "fas fa-edit" })
192
+ ) +
193
+ a(
194
+ {
195
+ class: "ms-1 me-3",
196
+ href: `/actions/configure/${trigger?.id}`,
197
+ },
198
+ "Configure&nbsp;",
199
+ i({ class: "fas fa-cog" })
200
+ );
201
+
157
202
  edContents = renderForm(mkForm(trpack), req.csrfToken());
158
203
  break;
159
204
  }
@@ -243,10 +288,11 @@ router.get(
243
288
  },
244
289
  {
245
290
  type: "card",
246
- title:
247
- ename && etype
248
- ? `Registry editor: ${ename} ${etype}`
249
- : "Registry editor",
291
+ title: cfg_link
292
+ ? `Registry editor: ${cfg_link}`
293
+ : ename && etype
294
+ ? `Registry editor: ${ename} ${etype}`
295
+ : "Registry editor",
250
296
  contents: edContents,
251
297
  },
252
298
  ],
package/routes/tables.js CHANGED
@@ -926,7 +926,7 @@ router.get(
926
926
  ? `/useradmin/`
927
927
  : fields.length === 1
928
928
  ? `javascript:;` // Fix problem with edition of table with only one column ID / Primary Key
929
- : `/list/${table.name}`,
929
+ : `/list/${encodeURIComponent(table.name)}`,
930
930
  },
931
931
  i({ class: "fas fa-2x fa-edit" }),
932
932
  "<br/>",
@@ -949,7 +949,7 @@ router.get(
949
949
  div(
950
950
  { class: "mx-auto" },
951
951
  a(
952
- { href: `/table/download/${table.name}` },
952
+ { href: `/table/download/${encodeURIComponent(table.name)}` },
953
953
  i({ class: "fas fa-2x fa-download" }),
954
954
  "<br/>",
955
955
  req.__("Download CSV")
@@ -1014,13 +1014,13 @@ router.get(
1014
1014
  '<i class="fas fa-edit"></i>&nbsp;' + req.__("Rename table")
1015
1015
  ),
1016
1016
  post_dropdown_item(
1017
- `/table/recalc-stored/${table.name}`,
1017
+ `/table/recalc-stored/${encodeURIComponent(table.name)}`,
1018
1018
  '<i class="fas fa-sync"></i>&nbsp;' +
1019
1019
  req.__("Recalculate stored fields"),
1020
1020
  req
1021
1021
  ),
1022
1022
  post_dropdown_item(
1023
- `/table/delete-all-rows/${table.name}`,
1023
+ `/table/delete-all-rows/${encodeURIComponent(table.name)}`,
1024
1024
  '<i class="far fa-trash-alt"></i>&nbsp;' +
1025
1025
  req.__("Delete all rows"),
1026
1026
  req,
@@ -1111,6 +1111,7 @@ router.post(
1111
1111
  const v = req.body;
1112
1112
  if (typeof v.id === "undefined" && typeof v.external === "undefined") {
1113
1113
  // insert
1114
+ v.name = v.name.trim()
1114
1115
  const { name, ...rest } = v;
1115
1116
  const alltables = await Table.find({});
1116
1117
  const existing_tables = [
package/routes/tenant.js CHANGED
@@ -44,7 +44,12 @@ const {
44
44
  const db = require("@saltcorn/data/db");
45
45
 
46
46
  const { loadAllPlugins, loadAndSaveNewPlugin } = require("../load_plugins");
47
- const { isAdmin, error_catcher, is_ip_address } = require("./utils.js");
47
+ const {
48
+ isAdmin,
49
+ error_catcher,
50
+ is_ip_address,
51
+ tenant_letsencrypt_name,
52
+ } = require("./utils.js");
48
53
  const User = require("@saltcorn/data/models/user");
49
54
  const File = require("@saltcorn/data/models/file");
50
55
  const {
@@ -612,8 +617,18 @@ router.get(
612
617
  return;
613
618
  }
614
619
  const { subdomain } = req.params;
620
+
615
621
  // get tenant info
616
622
  const info = await get_tenant_info(subdomain);
623
+ const letsencrypt = getState().getConfig("letsencrypt", false);
624
+
625
+ let altname = await tenant_letsencrypt_name(subdomain);
626
+ const tenant_letsencrypt_sites = getState().getConfig(
627
+ "tenant_letsencrypt_sites",
628
+ []
629
+ );
630
+ const has_cert = tenant_letsencrypt_sites.includes(altname);
631
+
617
632
  // get list of files
618
633
  let files;
619
634
  await db.runWithTenant(subdomain, async () => {
@@ -632,6 +647,7 @@ router.get(
632
647
  // TBD make more pretty view - in ideal with charts
633
648
  contents: [
634
649
  table(
650
+ { class: "table table-sm" },
635
651
  tr(
636
652
  th(req.__("First user E-mail")),
637
653
  td(
@@ -723,6 +739,20 @@ router.get(
723
739
  submitLabel: req.__("Save"),
724
740
  submitButtonClass: "btn-outline-primary",
725
741
  onChange: "remove_outline(this)",
742
+ additionalButtons: [
743
+ ...(letsencrypt && !has_cert
744
+ ? [
745
+ {
746
+ label: req.__("Acquire LetsEncrypt certificate"),
747
+ id: "btnAcqCert",
748
+ class: "btn btn-secondary",
749
+ onclick: `press_store_button(this);ajax_post('/admin/acq-ssl-tenant/${encodeURIComponent(
750
+ subdomain
751
+ )}')`,
752
+ },
753
+ ]
754
+ : []),
755
+ ],
726
756
  fields: [
727
757
  {
728
758
  name: "base_url",
package/routes/utils.js CHANGED
@@ -33,6 +33,7 @@ const {
33
33
  const path = require("path");
34
34
  const { UAParser } = require("ua-parser-js");
35
35
  const crypto = require("crypto");
36
+ const { domain_sanitize } = require("@saltcorn/admin-models/models/tenant");
36
37
 
37
38
  const get_sys_info = async () => {
38
39
  const disks = await si.fsSize();
@@ -312,14 +313,17 @@ const getSessionStore = (pruneInterval) => {
312
313
  maxAge: 30 * 24 * 60 * 60 * 1000,
313
314
  sameSite: "strict",
314
315
  });
315
- } else*/ if (db.isSQLite) {
316
+ } else*/
317
+ let sameSite = getState().getConfig("cookie_samesite", "None").toLowerCase();
318
+ if (sameSite === "unset") sameSite = undefined;
319
+ if (db.isSQLite) {
316
320
  var SQLiteStore = require("connect-sqlite3")(session);
317
321
  return session({
318
322
  store: new SQLiteStore({ db: "sessions.sqlite" }),
319
323
  secret: db.connectObj.session_secret || is.str.generate(),
320
324
  resave: false,
321
325
  saveUninitialized: false,
322
- cookie: { maxAge: 30 * 24 * 60 * 60 * 1000, sameSite: "strict" }, // 30 days
326
+ cookie: { maxAge: 30 * 24 * 60 * 60 * 1000, sameSite }, // 30 days
323
327
  });
324
328
  } else {
325
329
  const pgSession = require("connect-pg-simple")(session);
@@ -333,7 +337,7 @@ const getSessionStore = (pruneInterval) => {
333
337
  secret: db.connectObj.session_secret || is.str.generate(),
334
338
  resave: false,
335
339
  saveUninitialized: false,
336
- cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 }, // 30 days
340
+ cookie: { maxAge: 30 * 24 * 60 * 60 * 1000, sameSite }, // 30 days
337
341
  });
338
342
  }
339
343
  };
@@ -369,6 +373,19 @@ const is_ip_address = (hostname) => {
369
373
  return hostname.split(".").every((s) => +s >= 0 && +s <= 255);
370
374
  };
371
375
 
376
+ const tenant_letsencrypt_name = async (subdomain) => {
377
+ const saneDomain = domain_sanitize(subdomain);
378
+ let altname;
379
+ await db.runWithTenant(saneDomain, async () => {
380
+ altname = getState()
381
+ .getConfig("base_url", "")
382
+ .replace("https://", "")
383
+ .replace("http://", "")
384
+ .replace("/", "");
385
+ });
386
+ return altname;
387
+ };
388
+
372
389
  const admin_config_route = ({
373
390
  router,
374
391
  path,
@@ -584,4 +601,5 @@ module.exports = {
584
601
  setRole,
585
602
  getEligiblePage,
586
603
  getRandomPage,
604
+ tenant_letsencrypt_name,
587
605
  };
@@ -286,7 +286,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
286
286
  parent_field: "attributes",
287
287
  attributes: {
288
288
  inline: true,
289
- options: ["px", "%", "vw", "em", "rem"],
289
+ options: ["px", "%", "vw", "em", "rem", "cm"],
290
290
  },
291
291
  },
292
292
  {
@@ -306,7 +306,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
306
306
  parent_field: "attributes",
307
307
  attributes: {
308
308
  inline: true,
309
- options: ["px", "%", "vw", "em", "rem"],
309
+ options: ["px", "%", "vw", "em", "rem", "cm"],
310
310
  },
311
311
  },
312
312
  {
@@ -586,7 +586,10 @@ const respondWorkflow = (view, wf, wfres, req, res, table) => {
586
586
  text: view.name,
587
587
  postLinkText: `[${view.viewtemplate}${
588
588
  table
589
- ? ` on ${a({ href: `/table/` + table.name }, table.name)}`
589
+ ? ` on ${a(
590
+ { href: `/table/` + encodeURIComponent(table.name) },
591
+ table.name
592
+ )}`
590
593
  : ""
591
594
  }]`,
592
595
  },
@@ -761,7 +764,7 @@ router.post(
761
764
  req.flash(
762
765
  "success",
763
766
  req.__(
764
- "View %s added to menu. Adjust access permissions in Settings &raquo; Menu",
767
+ 'View %s added to menu. Adjust access permissions in <a href="/menu">Settings &raquo; Menu</a>',
765
768
  view.name
766
769
  )
767
770
  );
package/tests/api.test.js CHANGED
@@ -3,9 +3,12 @@ const getApp = require("../app");
3
3
  const Table = require("@saltcorn/data/models/table");
4
4
  const Trigger = require("@saltcorn/data/models/trigger");
5
5
  const File = require("@saltcorn/data/models/file");
6
+ const Field = require("@saltcorn/data/models/field");
7
+ const User = require("@saltcorn/data/models/user");
8
+ const { getState } = require("@saltcorn/data/db/state");
9
+
6
10
  const fs = require("fs").promises;
7
11
 
8
- const Field = require("@saltcorn/data/models/field");
9
12
  const {
10
13
  getStaffLoginCookie,
11
14
  getAdminLoginCookie,
@@ -14,10 +17,11 @@ const {
14
17
  succeedJsonWith,
15
18
  notAuthorized,
16
19
  toRedirect,
20
+ toInclude,
17
21
  succeedJsonWithWholeBody,
18
22
  } = require("../auth/testhelp");
19
23
  const db = require("@saltcorn/data/db");
20
- const User = require("@saltcorn/data/models/user");
24
+ const { sleep } = require("@saltcorn/data/tests/mocks");
21
25
 
22
26
  beforeAll(async () => {
23
27
  await resetToFixtures();
@@ -505,3 +509,85 @@ describe("API action", () => {
505
509
  expect(counts.map((c) => c.thing)).toContain("no body");
506
510
  });
507
511
  });
512
+
513
+ describe("test share handler", () => {
514
+ beforeAll(async () => {
515
+ await getState().setConfig("pwa_share_to_enabled", true);
516
+
517
+ const sharedData = await Table.create("shared_data");
518
+ await Field.create({
519
+ table: sharedData,
520
+ name: "title",
521
+ label: "Title",
522
+ type: "String",
523
+ });
524
+ await Field.create({
525
+ table: sharedData,
526
+ name: "user",
527
+ label: "user",
528
+ type: "String",
529
+ });
530
+ await Trigger.create({
531
+ action: "run_js_code",
532
+ when_trigger: "ReceiveMobileShareData",
533
+ name: "my_receive_share",
534
+ min_role: 100,
535
+ configuration: {
536
+ code: `
537
+ const sharedData = Table.findOne({ name: "shared_data" });
538
+ await sharedData.insertRow({
539
+ title: row.title, user: JSON.stringify(user)
540
+ });`,
541
+ },
542
+ });
543
+ });
544
+
545
+ it("shares as admin", async () => {
546
+ const app = await getApp({ disableCsrf: true });
547
+ const loginCookie = await getAdminLoginCookie();
548
+ await request(app)
549
+ .post("/notifications/share-handler")
550
+ .set("Cookie", loginCookie)
551
+ .send({ title: "share_as_admin" })
552
+ .expect(toRedirect("/"));
553
+ await sleep(1000);
554
+ const sharedData = Table.findOne({ name: "shared_data" });
555
+ const rows = await sharedData.getRows({});
556
+ const row = rows.find(
557
+ (r) =>
558
+ r.title === "share_as_admin" &&
559
+ r.user ===
560
+ '{"email":"admin@foo.com","id":1,"role_id":1,"language":null,"tenant":"public","attributes":{}}'
561
+ );
562
+ expect(row).toBeDefined();
563
+ });
564
+
565
+ it("pwa_disabled as admin", async () => {
566
+ const app = await getApp({ disableCsrf: true });
567
+ const loginCookie = await getAdminLoginCookie();
568
+ await getState().setConfig("pwa_share_to_enabled", false);
569
+ await request(app)
570
+ .post("/notifications/share-handler")
571
+ .set("Cookie", loginCookie)
572
+ .send({ title: "pwa_disabled_as_admin" })
573
+ .expect(toRedirect("/"));
574
+ await sleep(1000);
575
+ const sharedData = Table.findOne({ name: "shared_data" });
576
+ const rows = await sharedData.getRows({});
577
+ const row = rows.find((r) => r.title === "pwa_disabled_as_admin");
578
+ expect(row).toBeUndefined();
579
+ });
580
+
581
+ it("does not share as public", async () => {
582
+ const app = await getApp({ disableCsrf: true });
583
+ await request(app)
584
+ .post("/notifications/share-handler")
585
+ .send({ title: "does_not_share_as_public" })
586
+ .expect(toRedirect("/auth/login"));
587
+ await sleep(1000);
588
+ const sharedData = Table.findOne({ name: "shared_data" });
589
+ const rows = await sharedData.getRows({});
590
+ const row = rows.find((r) => r.title === "does_not_share_as_public");
591
+ expect(row).toBeUndefined();
592
+ });
593
+ });
@@ -372,7 +372,7 @@ describe("User fields", () => {
372
372
  ],
373
373
  },
374
374
  },
375
- min_role: 1,
375
+ min_role: 100,
376
376
  });
377
377
  await getState().setConfig("new_user_form", "newuser");
378
378
  });
@@ -508,7 +508,7 @@ describe("signup with custom login form", () => {
508
508
  viewname: "loginform",
509
509
  view_when_done: "publicissueboard",
510
510
  },
511
- min_role: 1,
511
+ min_role: 100,
512
512
  //default_render_page: "loginpage",
513
513
  });
514
514
 
@@ -598,7 +598,7 @@ describe("signup with custom login form", () => {
598
598
  viewname: "loginform",
599
599
  view_when_done: "publicissueboard",
600
600
  },
601
- min_role: 1,
601
+ min_role: 100,
602
602
  //default_render_page: "signuppage",
603
603
  });
604
604
  await getState().setConfig("signup_form", "signupform");
@@ -161,7 +161,7 @@ describe("Stable versioning install", () => {
161
161
  name: "@christianhugoch/empty_sc_test_plugin",
162
162
  });
163
163
  expect(dbPlugin).not.toBe(null);
164
- expect(dbPlugin.version).toBe("0.0.6");
164
+ expect(dbPlugin.version).toBe("0.0.1");
165
165
  });
166
166
 
167
167
  it("installs a fixed version", async () => {
@@ -178,7 +178,7 @@ describe("Stable versioning install", () => {
178
178
  name: "@christianhugoch/empty_sc_test_plugin",
179
179
  });
180
180
  expect(dbPlugin).not.toBe(null);
181
- expect(dbPlugin.version).toBe("0.0.6");
181
+ expect(dbPlugin.version).toBe("0.0.1");
182
182
  });
183
183
 
184
184
  it("installs and downgrades a fixed version", async () => {
@@ -196,7 +196,7 @@ describe("Stable versioning install", () => {
196
196
  name: "@christianhugoch/empty_sc_test_plugin",
197
197
  });
198
198
  expect(dbPlugin).not.toBe(null);
199
- expect(dbPlugin.version).toBe("0.0.6");
199
+ expect(dbPlugin.version).toBe("0.0.1");
200
200
  });
201
201
  });
202
202
 
@@ -276,7 +276,7 @@ describe("Stable versioning upgrade", () => {
276
276
  name: "@christianhugoch/empty_sc_test_plugin",
277
277
  });
278
278
  expect(newPlugin).not.toBe(null);
279
- expect(newPlugin.version).toBe("0.0.6");
279
+ expect(newPlugin.version).toBe("0.0.1");
280
280
  });
281
281
 
282
282
  it("upgrades to fixed version", async () => {
@@ -344,6 +344,6 @@ describe("Stable versioning upgrade", () => {
344
344
  name: "@christianhugoch/empty_sc_test_plugin",
345
345
  });
346
346
  expect(newPlugin).not.toBe(null);
347
- expect(newPlugin.version).toBe("0.0.6");
347
+ expect(newPlugin.version).toBe("0.0.1");
348
348
  });
349
349
  });
@@ -236,7 +236,7 @@ describe("Pack Endpoints", () => {
236
236
  .send(
237
237
  "pack=les%22%3A+%5B%5D%2C+%22views%22%3A+%5B%5D%2C+%22plugins%22%3A+%5B%5D%2C+%22pages%22%3A+%5B%5D+%7D"
238
238
  )
239
- .expect(toInclude("Unexpected token l in JSON at position 0"));
239
+ .expect(toInclude("Unexpected token"));
240
240
  });
241
241
  it("should install named", async () => {
242
242
  const loginCookie = await getAdminLoginCookie();
@@ -404,7 +404,7 @@ describe("Upgrade plugin to supported version", () => {
404
404
  const upgradedPlugin = await Plugin.findOne({
405
405
  name: "@christianhugoch/empty_sc_test_plugin",
406
406
  });
407
- expect(upgradedPlugin.version).toBe("0.0.6");
407
+ expect(upgradedPlugin.version).toBe("0.0.1");
408
408
  });
409
409
 
410
410
  it("upgrades with a downgrade of the most current fixed version", async () => {
@@ -427,7 +427,7 @@ describe("Upgrade plugin to supported version", () => {
427
427
  const upgradedPlugin = await Plugin.findOne({
428
428
  name: "@christianhugoch/empty_sc_test_plugin",
429
429
  });
430
- expect(upgradedPlugin.version).toBe("0.0.6");
430
+ expect(upgradedPlugin.version).toBe("0.0.1");
431
431
  });
432
432
  });
433
433
 
@@ -452,7 +452,16 @@ describe("install a different version dialog", () => {
452
452
  )}`
453
453
  )
454
454
  .set("Cookie", loginCookie)
455
- .expect(toInclude(["0.0.1", "0.0.2", "0.0.3", "0.0.4", "0.0.5", "0.0.6"]))
456
- .expect(toNotInclude("0.1.0"));
455
+ .expect(
456
+ toInclude([
457
+ "0.0.1",
458
+ "0.0.2",
459
+ "0.0.3",
460
+ "0.0.4",
461
+ "0.0.5",
462
+ "0.0.6",
463
+ "0.1.0",
464
+ ])
465
+ );
457
466
  });
458
467
  });
@@ -1,11 +0,0 @@
1
- /*!========================================================================
2
- * File: bootstrap-iconpicker.min.js v1.10.0 by @victor-valencia
3
- * https://victor-valencia.github.com/bootstrap-iconpicker
4
- * ========================================================================
5
- * Copyright 2013-2018 Victor Valencia Rico.
6
- * Licensed under MIT license.
7
- * https://github.com/victor-valencia/bootstrap-iconpicker/blob/master/LICENSE
8
- * ========================================================================
9
- */
10
-
11
- !function($){"use strict";var Iconpicker=function(element,options){if("undefined"==typeof $.fn.popover||"undefined"==typeof $.fn.popover.Constructor.VERSION)throw new TypeError("Bootstrap iconpicker require Bootstrap popover");this.$element=$(element),this.options=$.extend({},Iconpicker.DEFAULTS,this.$element.data()),this.options=$.extend({},this.options,options)};Iconpicker.VERSION="1.10.0",Iconpicker.ICONSET_EMPTY={iconClass:"",iconClassFix:"",icons:[]},Iconpicker.ICONSET={_custom:null,elusiveicon:$.iconset_elusiveicon||Iconpicker.ICONSET_EMPTY,flagicon:$.iconset_flagicon||Iconpicker.ICONSET_EMPTY,fontawesome4:$.iconset_fontawesome_4||Iconpicker.ICONSET_EMPTY,fontawesome5:$.iconset_fontawesome_5||Iconpicker.ICONSET_EMPTY,glyphicon:$.iconset_glyphicon||Iconpicker.ICONSET_EMPTY,ionicon:$.iconset_ionicon||Iconpicker.ICONSET_EMPTY,mapicon:$.iconset_mapicon||Iconpicker.ICONSET_EMPTY,materialdesign:$.iconset_materialdesign||Iconpicker.ICONSET_EMPTY,octicon:$.iconset_octicon||Iconpicker.ICONSET_EMPTY,typicon:$.iconset_typicon||Iconpicker.ICONSET_EMPTY,weathericon:$.iconset_weathericon||Iconpicker.ICONSET_EMPTY},Iconpicker.DEFAULTS={align:"center",arrowClass:"btn-primary",arrowNextIconClass:"fas fa-arrow-right",arrowPrevIconClass:"fas fa-arrow-left",cols:4,icon:"",iconset:"fontawesome5",iconsetVersion:"lastest",header:!0,labelHeader:"{0} / {1}",footer:!0,labelFooter:"{0} - {1} of {2}",placement:"bottom",rows:4,search:!0,searchText:"Search icon",selectedClass:"btn-warning",unselectedClass:"btn-secondary"},Iconpicker.prototype.bindEvents=function(){var op=this.options,el=this;op.table.find(".btn-previous, .btn-next").off("click").on("click",function(e){if(e.preventDefault(),!$(this).hasClass("disabled")){var inc=parseInt($(this).val(),10);el.changeList(op.page+inc)}}),op.table.find(".btn-icon").off("click").on("click",function(e){e.preventDefault(),el.select($(this).val()),op.inline===!1?el.$element.popover("3.x"===$.fn.bsVersion()?"destroy":"dispose"):op.table.find("i[class$='"+$(this).val()+"']").parent().addClass(op.selectedClass)}),op.table.find(".search-control").off("keyup").on("keyup",function(){el.changeList(1)})},Iconpicker.prototype.changeList=function(page){this.filterIcons(),this.updateLabels(page),this.updateIcons(page),this.options.page=page,this.bindEvents()},Iconpicker.prototype.filterIcons=function(){var op=this.options,search=op.table.find(".search-control").val(),icons=[];if("lastest"!=op.iconsetVersion&&"undefined"!=typeof Iconpicker.ICONSET[op.iconset].allVersions?$.each(Iconpicker.ICONSET[op.iconset].allVersions,function(i,v){op.iconsetVersion==v.version&&(icons=v.icons)}):icons=Iconpicker.ICONSET[op.iconset].icons,""===search)op.icons=icons;else{var result=[];$.each(icons,function(i,v){v.toLowerCase().indexOf(search)>-1&&result.push(v)}),op.icons=result}},Iconpicker.prototype.removeAddClass=function(target,remove,add){return this.options.table.find(target).removeClass(remove).addClass(add),add},Iconpicker.prototype.reset=function(){this.updatePicker(),this.changeList(1)},Iconpicker.prototype.select=function(icon){var op=this.options,el=this.$element;op.selected=$.inArray(icon.replace(op.iconClassFix,""),op.icons),-1===op.selected&&(op.selected=0,icon=op.iconClassFix+op.icons[op.selected]),""!==icon&&op.selected>=0&&(op.icon=icon,op.inline===!1&&(el.find("input").val(icon),el.find("i").attr("class","").addClass(op.iconClass).addClass(icon)),icon===op.iconClassFix?el.trigger({type:"change",icon:"empty"}):(el.trigger({type:"change",icon:icon}),el.find("input").val(icon)),op.table.find("button."+op.selectedClass).removeClass(op.selectedClass))},Iconpicker.prototype.switchPage=function(icon){var op=this.options;if(op.selected=$.inArray(icon.replace(op.iconClassFix,""),op.icons),op.selected>=0){var page=Math.ceil((op.selected+1)/this.totalIconsPerPage());this.changeList(page)}""===icon?op.table.find("i."+op.iconClassFix).parent().addClass(op.selectedClass):op.table.find("i."+icon).parent().addClass(op.selectedClass)},Iconpicker.prototype.totalPages=function(){return Math.ceil(this.totalIcons()/this.totalIconsPerPage())},Iconpicker.prototype.totalIcons=function(){return this.options.icons.length},Iconpicker.prototype.totalIconsPerPage=function(){return 0===this.options.rows?this.options.icons.length:this.options.cols*this.options.rows},Iconpicker.prototype.updateArrows=function(page){var op=this.options,total_pages=this.totalPages();1===page?op.table.find(".btn-previous").addClass("disabled"):op.table.find(".btn-previous").removeClass("disabled"),page===total_pages||0===total_pages?op.table.find(".btn-next").addClass("disabled"):op.table.find(".btn-next").removeClass("disabled")},Iconpicker.prototype.updateIcons=function(page){var op=this.options,tbody=op.table.find("tbody").empty(),offset=(page-1)*this.totalIconsPerPage(),length=op.rows;0===op.rows&&(length=op.icons.length);for(var i=0;length>i;i++){for(var tr=$("<tr></tr>"),j=0;j<op.cols;j++){var pos=offset+i*op.cols+j,btn=$('<button class="btn '+op.unselectedClass+' btn-icon"></button>').hide();if(pos<op.icons.length){var v=op.iconClassFix+op.icons[pos];btn.val(v).attr("title",v).append('<i class="'+op.iconClass+" "+v+'"></i>').show(),op.icon===v&&btn.addClass(op.selectedClass).addClass("btn-icon-selected")}tr.append($("<td></td>").append(btn))}tbody.append(tr)}},Iconpicker.prototype.updateIconsCount=function(){var op=this.options;if(op.footer===!0){var icons_count=["<tr>",' <td colspan="'+op.cols+'" class="text-center">',' <span class="icons-count"></span>'," </td>","</tr>"];op.table.find("tfoot").empty().append(icons_count.join(""))}},Iconpicker.prototype.updateLabels=function(page){var op=this.options,total_icons=this.totalIcons(),total_pages=this.totalPages();op.table.find(".page-count").html(op.labelHeader.replace("{0}",0===total_pages?0:page).replace("{1}",total_pages));var offset=(page-1)*this.totalIconsPerPage(),total=page*this.totalIconsPerPage();op.table.find(".icons-count").html(op.labelFooter.replace("{0}",total_icons?offset+1:0).replace("{1}",total_icons>total?total:total_icons).replace("{2}",total_icons)),this.updateArrows(page)},Iconpicker.prototype.updatePagesCount=function(){var op=this.options;if(op.header===!0){for(var tr=$("<tr></tr>"),i=0;i<op.cols;i++){var td=$('<td class="text-center"></td>');if(0===i||i===op.cols-1){var arrow=['<button class="btn btn-arrow '+(0===i?"btn-previous":"btn-next")+" "+op.arrowClass+'" value="'+(0===i?-1:1)+'">','<span class="'+(0===i?op.arrowPrevIconClass:op.arrowNextIconClass)+'"></span>',"</button>"];td.append(arrow.join("")),tr.append(td)}else 0===tr.find(".page-count").length&&(td.attr("colspan",op.cols-2).append('<span class="page-count"></span>'),tr.append(td))}op.table.find("thead").empty().append(tr)}},Iconpicker.prototype.updatePicker=function(){var op=this.options;if(op.cols<4)throw"Iconpicker => The number of columns must be greater than or equal to 4. [option.cols = "+op.cols+"]";if(op.rows<0)throw"Iconpicker => The number of rows must be greater than or equal to 0. [option.rows = "+op.rows+"]";this.updatePagesCount(),this.updateSearch(),this.updateIconsCount()},Iconpicker.prototype.updateSearch=function(){var op=this.options,search=["<tr>",' <td colspan="'+op.cols+'">',' <input type="text" class="form-control search-control" style="width: '+op.cols*("3.x"===$.fn.bsVersion()?39:41)+'px;" placeholder="'+op.searchText+'">'," </td>","</tr>"];search=$(search.join("")),op.search===!0?search.show():search.hide(),op.table.find("thead").append(search)},Iconpicker.prototype.setAlign=function(value){this.$element.removeClass(this.options.align).addClass(value),this.options.align=value},Iconpicker.prototype.setArrowClass=function(value){this.options.arrowClass=this.removeAddClass(".btn-arrow",this.options.arrowClass,value)},Iconpicker.prototype.setArrowNextIconClass=function(value){this.options.arrowNextIconClass=this.removeAddClass(".btn-next > span",this.options.arrowNextIconClass,value)},Iconpicker.prototype.setArrowPrevIconClass=function(value){this.options.arrowPrevIconClass=this.removeAddClass(".btn-previous > span",this.options.arrowPrevIconClass,value)},Iconpicker.prototype.setCols=function(value){this.options.cols=value,this.reset()},Iconpicker.prototype.setFooter=function(value){var footer=this.options.table.find("tfoot");value===!0?footer.show():footer.hide(),this.options.footer=value},Iconpicker.prototype.setHeader=function(value){var header=this.options.table.find("thead");value===!0?header.show():header.hide(),this.options.header=value},Iconpicker.prototype.setIcon=function(value){this.select(value)},Iconpicker.prototype.setIconset=function(value){var op=this.options;$.isPlainObject(value)?(Iconpicker.ICONSET._custom=$.extend(Iconpicker.ICONSET_EMPTY,value),op.iconset="_custom"):Iconpicker.ICONSET.hasOwnProperty(value)?op.iconset=value:op.iconset=Iconpicker.DEFAULTS.iconset,op=$.extend(op,Iconpicker.ICONSET[op.iconset]),this.reset(),this.select(op.icon)},Iconpicker.prototype.setLabelHeader=function(value){this.options.labelHeader=value,this.updateLabels(this.options.page)},Iconpicker.prototype.setLabelFooter=function(value){this.options.labelFooter=value,this.updateLabels(this.options.page)},Iconpicker.prototype.setPlacement=function(value){this.options.placement=value},Iconpicker.prototype.setRows=function(value){this.options.rows=value,this.reset()},Iconpicker.prototype.setSearch=function(value){var search=this.options.table.find(".search-control");value===!0?search.show():search.hide(),search.val(""),this.changeList(1),this.options.search=value},Iconpicker.prototype.setSearchText=function(value){this.options.table.find(".search-control").attr("placeholder",value),this.options.searchText=value},Iconpicker.prototype.setSelectedClass=function(value){this.options.selectedClass=this.removeAddClass(".btn-icon-selected",this.options.selectedClass,value)},Iconpicker.prototype.setUnselectedClass=function(value){this.options.unselectedClass=this.removeAddClass(".btn-icon",this.options.unselectedClass,value)};var old=$.fn.iconpicker;$.fn.iconpicker=function(option,params){return this.each(function(){var $this=$(this),data=$this.data("bs.iconpicker"),options="object"==typeof option&&option;if(data||$this.data("bs.iconpicker",data=new Iconpicker(this,options)),"string"==typeof option){if("undefined"==typeof data[option])throw'Iconpicker => The "'+option+'" method does not exists.';data[option](params)}else{var op=data.options;op=$.extend(op,{inline:!1,page:1,selected:-1,table:$('<table class="table-icons"><thead></thead><tbody></tbody><tfoot></tfoot></table>')});var name="undefined"!=typeof $this.attr("name")?'name="'+$this.attr("name")+'"':"";"BUTTON"===$this.prop("tagName")?($this.empty().append("<i></i>").append('<input type="hidden" '+name+"></input>").append('<span class="caret"></span>').addClass("iconpicker "+("3.x"===$.fn.bsVersion()?"":"dropdown-toggle")),data.setIconset(op.iconset),$this.on("click",function(e){e.preventDefault(),$this.popover({animation:!1,trigger:"manual",html:!0,content:op.table,container:"body",placement:op.placement}).on("inserted.bs.popover",function(){var el=$this.data("bs.popover"),tip="3.x"===$.fn.bsVersion()?el.tip():$(el.getTipElement());tip.addClass("iconpicker-popover")}).on("shown.bs.popover",function(){data.switchPage(op.icon),data.bindEvents()}),$this.popover("show")})):(op.inline=!0,data.setIconset(op.iconset),$this.empty().append('<input type="hidden" '+name+"></input>").append(op.table).addClass("iconpicker").addClass(op.align),data.switchPage(op.icon),data.bindEvents())}})},$.fn.iconpicker.Constructor=Iconpicker,$.fn.iconpicker.noConflict=function(){return $.fn.iconpicker=old,this},$.fn.bsVersion=function(){return $.fn.popover.Constructor.VERSION.substr(0,2)+"x"},$(document).on("click","body",function(e){$(".iconpicker").each(function(){$(this).is(e.target)||0!==$(this).has(e.target).length||0!==$(".popover").has(e.target).length||$(this).popover("3.x"===$.fn.bsVersion()?"destroy":"dispose")})}),$('button[role="iconpicker"],div[role="iconpicker"]').iconpicker()}(jQuery);