domma-cms 1.10.16 → 1.10.18

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.
@@ -9,6 +9,38 @@ import {fileURLToPath} from 'url';
9
9
  import * as cache from './cache/index.js';
10
10
  import {buttonSettingsProblems, formClassProblems} from '../../public/js/form-submit.mjs';
11
11
  import {fileIn} from './safePath.js';
12
+ import {getConfig} from '../config.js';
13
+
14
+ /** What one submission's files may hold in memory together when server.json sets nothing: 50 MB. */
15
+ export const DEFAULT_MAX_SUBMISSION_BYTES = 50 * 1024 * 1024;
16
+
17
+ /**
18
+ * The most bytes one submission's files may hold in memory, all fields together:
19
+ * server.json `uploads.maxSubmissionBytes` (read fresh) when set; otherwise 50 MB or
20
+ * `uploads.maxFileSize`, whichever is larger, so one file the site takes is never
21
+ * refused for the total.
22
+ *
23
+ * @returns {number}
24
+ */
25
+ export function maxSubmissionBytes() {
26
+ let uploads = {};
27
+ try { uploads = getConfig('server')?.uploads || {}; } catch { /* no server.json */ }
28
+ const set = Number(uploads.maxSubmissionBytes);
29
+ if (set > 0) return set;
30
+ return Math.max(DEFAULT_MAX_SUBMISSION_BYTES, Number(uploads.maxFileSize) || 0);
31
+ }
32
+
33
+ /**
34
+ * A form as a page draws it, told the per-submission file cap when it has a file field,
35
+ * so the page refuses a file that would pass it before anything is sent.
36
+ *
37
+ * @param {object} form
38
+ * @returns {object}
39
+ */
40
+ export function publicFileLimits(form) {
41
+ const hasFiles = (form?.fields || []).some(f => f && f.type === 'file');
42
+ return hasFiles ? {...form, fileLimits: {maxTotalBytes: maxSubmissionBytes()}} : form;
43
+ }
12
44
 
13
45
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
46
  const ROOT = path.resolve(__dirname, '..', '..');
@@ -44,6 +76,11 @@ export async function readForm(slug) {
44
76
  */
45
77
  export async function writeForm(slug, data) {
46
78
  await ensureFormsDir();
79
+ // fileLimits is the server's config, added to the copy a page is given - never stored
80
+ if (data && 'fileLimits' in data) {
81
+ const {fileLimits: _limits, ...rest} = data;
82
+ data = rest;
83
+ }
47
84
  const file = fileIn(FORMS_DIR, slug, '.json');
48
85
  await fs.writeFile(file, JSON.stringify(data, null, 4) + '\n', 'utf8');
49
86
  await cache.invalidateTags([`form:${slug}`]);
@@ -198,7 +235,9 @@ function toFormFieldType(type) {
198
235
  string: 'string', text: 'string', email: 'email', tel: 'tel',
199
236
  number: 'number', textarea: 'textarea', select: 'select', radio: 'radio',
200
237
  checkbox: 'checkbox', 'checkbox-group': 'checkbox-group',
201
- date: 'date', time: 'time', url: 'url', hidden: 'hidden'
238
+ date: 'date', time: 'time', url: 'url', hidden: 'hidden',
239
+ // {url, name, size, mime} - a list of them when the field is `multiple`
240
+ file: 'file'
202
241
  };
203
242
  return map[type] || 'string';
204
243
  }
@@ -223,6 +262,7 @@ function buildFormFromCollection(schema) {
223
262
  };
224
263
  if (f.options) field.options = f.options;
225
264
  if (f.validation) field.validation = f.validation;
265
+ if (f.type === 'file' && f.multiple) field.multiple = true;
226
266
  return field;
227
267
  });
228
268
 
@@ -286,7 +326,9 @@ function toCollectionFieldType(type) {
286
326
  string: 'string', email: 'email', tel: 'tel', number: 'number',
287
327
  textarea: 'textarea', select: 'select', radio: 'radio',
288
328
  checkbox: 'checkbox', 'checkbox-group': 'checkbox-group',
289
- date: 'date', time: 'time', url: 'url', hidden: 'hidden'
329
+ date: 'date', time: 'time', url: 'url', hidden: 'hidden',
330
+ // {url, name, size, mime} - a list of them when the field is `multiple`
331
+ file: 'file'
290
332
  };
291
333
  return map[type] || 'string';
292
334
  }
@@ -329,9 +371,12 @@ export async function ensureCollectionForForm(form) {
329
371
  name: f.name,
330
372
  type: toCollectionFieldType(f.type),
331
373
  label: f.label || f.name,
332
- required: !!f.required
374
+ // A file field's `required` is the form's to enforce (it sees the upload);
375
+ // copied here it would stop the entry editor saving an entry without one.
376
+ required: f.type === 'file' ? false : !!f.required
333
377
  };
334
378
  if (f.options) field.options = f.options;
379
+ if (f.type === 'file' && f.multiple) field.multiple = true;
335
380
  if (f.validation) field.validation = f.validation;
336
381
  return field;
337
382
  });
@@ -23,6 +23,7 @@ import {buildTextStyle} from '../../public/js/text-style.mjs';
23
23
  import {buildScribeStyle} from '../../public/js/scribe-style.mjs';
24
24
  import {CHARACTERS, resolveCharacterSize} from '../../public/js/celebrations/character-catalog.mjs';
25
25
  import {publicButtonSettings} from '../../public/js/form-submit.mjs';
26
+ import {publicFileLimits} from './forms.js';
26
27
  import {applyShortcodeExpiry} from './shortcodeExpiry.js';
27
28
 
28
29
  const __dirname_md = path.dirname(fileURLToPath(import.meta.url));
@@ -631,13 +632,29 @@ function isFileRef(v) {
631
632
  return v && typeof v === 'object' && typeof v.url === 'string' && typeof v.name === 'string';
632
633
  }
633
634
 
635
+ /** A `multiple` file field's value: a non-empty list of nothing but file references. */
636
+ function isFileRefList(v) {
637
+ return Array.isArray(v) && v.length > 0 && v.every(isFileRef);
638
+ }
639
+
640
+ /** One file reference as HTML: a thumbnail link for an image, a download link otherwise. */
641
+ function fileRefHtml(ref) {
642
+ const url = escapeAttr(ref.url);
643
+ const name = escapeHtmlText(ref.name);
644
+ if (String(ref.mime || '').startsWith('image/')) {
645
+ return `<a href="${url}" target="_blank" rel="noopener"><img src="${url}" alt="${name}" loading="lazy" class="dm-file-thumb"></a>`;
646
+ }
647
+ return `<a href="${url}" download>${name}</a>`;
648
+ }
649
+
634
650
  /**
635
651
  * Resolve the display value for one field on one entry.
636
652
  *
637
653
  * For a `type: reference` field, returns the resolved label from `_refs`
638
654
  * (set by `references.resolveReferences()`); arrays join with ", ", missing
639
655
  * targets render as "<id> (missing)". For a `type: file` field (or any field
640
- * holding a stored file ref), returns the filename. Everything else returns
656
+ * holding a stored file ref), returns the filename - names joined with ", " for a
657
+ * `multiple` field's list. Everything else returns
641
658
  * the raw value with `null`/`undefined` collapsed to '' and arrays joined.
642
659
  *
643
660
  * @param {object} entry
@@ -654,6 +671,7 @@ function displayValue(entry, field) {
654
671
  }
655
672
  const raw = entry.data?.[field.name];
656
673
  if (isFileRef(raw)) return raw.name;
674
+ if (isFileRefList(raw)) return raw.map(f => f.name).join(', ');
657
675
  if (raw == null) return '';
658
676
  if (Array.isArray(raw)) return raw.join(', ');
659
677
  return String(raw);
@@ -679,14 +697,8 @@ function displayValueHtml(entry, field) {
679
697
  }
680
698
  }
681
699
  const raw = entry.data?.[field.name];
682
- if (isFileRef(raw)) {
683
- const url = escapeAttr(raw.url);
684
- const name = escapeHtmlText(raw.name);
685
- if (String(raw.mime || '').startsWith('image/')) {
686
- return `<a href="${url}" target="_blank" rel="noopener"><img src="${url}" alt="${name}" loading="lazy" class="dm-file-thumb"></a>`;
687
- }
688
- return `<a href="${url}" download>${name}</a>`;
689
- }
700
+ if (isFileRef(raw)) return fileRefHtml(raw);
701
+ if (isFileRefList(raw)) return raw.map(fileRefHtml).join(' ');
690
702
  return escapeHtmlText(displayValue(entry, field));
691
703
  }
692
704
 
@@ -4450,7 +4462,7 @@ async function processFormBlocks(markdown, tagSet) {
4450
4462
  const {actions: _actions, ...publicForm} = form;
4451
4463
  // A Cancel link that is not safe to draw stays out of the page source.
4452
4464
  if (publicForm.settings) publicForm.settings = publicButtonSettings(publicForm.settings);
4453
- const encoded = Buffer.from(JSON.stringify(publicForm)).toString('base64');
4465
+ const encoded = Buffer.from(JSON.stringify(publicFileLimits(publicForm))).toString('base64');
4454
4466
  const extraClass = attrs.class ? ` ${escapeAttr(attrs.class)}` : '';
4455
4467
  const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
4456
4468
  replacement = `<div class="cms-form-embed${extraClass}" data-form-inline="${escapeAttr(encoded)}"${idAttr}>${formSummaryHtml(form)}</div>`;
@@ -185,7 +185,7 @@
185
185
  <script src="/public/js/form-logic-engine.js?v=20261025-dropzone-disable" defer></script>
186
186
 
187
187
  <!-- Site initialisation -->
188
- <script src="/public/js/site.js?v=20261025-dropzone-range" type="module"></script>
188
+ <script src="/public/js/site.js?v=20261025-files-total" type="module"></script>
189
189
  <script src="/public/js/collection-context.js?v=20261010-utf8-payloads" type="module"></script>
190
190
  {{ctxMenusModule}}
191
191
 
@@ -195,7 +195,7 @@
195
195
  <script src="/public/js/menu-float.js?v=20260927-float" type="module"></script>
196
196
 
197
197
  <!-- Core Forms renderer -->
198
- <script src="/public/js/forms.js?v=20261025-form-dropzone" type="module"></script>
198
+ <script src="/public/js/forms.js?v=20261025-forms-files-total" type="module"></script>
199
199
 
200
200
  <!-- Core effects runtime -->
201
201
  <script src="/public/js/effects.js?v=20261015-occasions" defer></script>