domma-cms 1.6.46 → 1.6.48

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.
@@ -1178,9 +1178,11 @@ async function processCollectionBlocks(markdown, tagSet) {
1178
1178
  */
1179
1179
  export async function renderScopedCollection(attrs, userId) {
1180
1180
  const opts = {extraFilter: {createdBy: userId}, scope: 'mine'};
1181
- return isInteractiveCollection(attrs)
1182
- ? renderCollectionBrowserShell(attrs, opts)
1183
- : renderCollectionFragment(attrs, opts);
1181
+ // Drawn into the page with innerHTML by site.js, after the page itself was
1182
+ // sanitised - so it is cleaned here, the same way.
1183
+ return sanitizePageHtml(isInteractiveCollection(attrs)
1184
+ ? await renderCollectionBrowserShell(attrs, opts)
1185
+ : await renderCollectionFragment(attrs, opts));
1184
1186
  }
1185
1187
 
1186
1188
  /**
@@ -4528,6 +4530,87 @@ function processCtaBlocks(markdown) {
4528
4530
  return restore(processed);
4529
4531
  }
4530
4532
 
4533
+ /**
4534
+ * Clean rendered page HTML - the one sanitiser every page fragment goes through,
4535
+ * whether it is part of a full page (parseMarkdown) or drawn later in the
4536
+ * browser (the scope="mine" fragment from renderScopedCollection), so a block
4537
+ * or a {{{raw}}} field cannot carry a handler onto the page by the second road.
4538
+ *
4539
+ * @param {string} html
4540
+ * @returns {string}
4541
+ */
4542
+ export function sanitizePageHtml(html) {
4543
+ const extensions = getSanitizeExtensions();
4544
+ return sanitizeHtml(html, {
4545
+ // `style` is intentionally allowed below so per-block custom CSS can
4546
+ // reach the page. Admins write block templates; same trust model as
4547
+ // content/custom.css. This flag silences sanitize-html's warning.
4548
+ allowVulnerableTags: true,
4549
+ // [embed] emits <iframe>/<video>; hostnames are gated by
4550
+ // allowedIframeHostnames below (defence in depth over the render-time
4551
+ // host allow-list).
4552
+ allowedIframeHostnames: [
4553
+ 'www.youtube-nocookie.com', 'www.youtube.com', 'youtube.com',
4554
+ 'player.vimeo.com',
4555
+ ...getEmbedAllowedHosts(),
4556
+ ],
4557
+ // sanitize-html drops a RELATIVE iframe src whenever allowedIframeHostnames
4558
+ // is set, because it cannot match a hostname it has not been given. That
4559
+ // silently emptied every [demo] iframe. A relative src is same-origin by
4560
+ // definition, so the hostname gate has nothing to say about it: the gate
4561
+ // exists to stop embedding somebody else's site, not your own page.
4562
+ allowIframeRelativeUrls: true,
4563
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat([
4564
+ 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
4565
+ 'form', 'input', 'textarea', 'select', 'option', 'optgroup',
4566
+ 'button', 'label', 'fieldset', 'legend',
4567
+ // [embed] shortcode output
4568
+ 'iframe', 'video', 'source', 'figure', 'figcaption',
4569
+ // <style> tags are emitted by block rendering (buildBlockStyleTag) so
4570
+ // per-block custom CSS can reach the page. Admin-trust: block authors
4571
+ // are admins, same security model as site-wide content/custom.css.
4572
+ 'style',
4573
+ // dm-<name> custom-element tags - one per registered component. Refreshed
4574
+ // by refreshComponentTagAllowlist() on startup and after create/delete.
4575
+ // Component authors are super-admins, same trust model as plugin code.
4576
+ ..._dmTagAllowlist,
4577
+ ...extensions.tags
4578
+ ]),
4579
+ allowedAttributes: {
4580
+ ...sanitizeHtml.defaults.allowedAttributes,
4581
+ // aria-hidden marks decorative markup (menu spacers, the floating panel's
4582
+ // drag grip) so it isn't announced as an empty item. aria-current marks
4583
+ // the [menu] item for the page you are on. Both presentational only, no
4584
+ // script surface - unlike the on* handlers stripped globally.
4585
+ '*': ['class', 'id', 'style', 'data-*', 'aria-hidden', 'aria-current'],
4586
+ img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
4587
+ form: ['action', 'method'],
4588
+ input: ['type', 'name', 'placeholder', 'value', 'required', 'disabled',
4589
+ 'readonly', 'min', 'max', 'step', 'pattern', 'maxlength',
4590
+ 'minlength', 'checked', 'autocomplete'],
4591
+ textarea: ['name', 'placeholder', 'rows', 'cols', 'required',
4592
+ 'disabled', 'readonly', 'maxlength'],
4593
+ select: ['name', 'required', 'disabled', 'multiple'],
4594
+ option: ['value', 'selected', 'disabled'],
4595
+ optgroup: ['label', 'disabled'],
4596
+ button: ['type', 'disabled', 'aria-label', 'data-action', 'data-entry', 'data-confirm'],
4597
+ label: ['for'],
4598
+ fieldset: ['disabled'],
4599
+ iframe: ['src', 'title', 'width', 'height', 'allow', 'allowfullscreen',
4600
+ 'loading', 'referrerpolicy', 'frameborder', 'style'],
4601
+ video: ['controls', 'autoplay', 'muted', 'loop', 'playsinline',
4602
+ 'preload', 'poster', 'width', 'height', 'style'],
4603
+ source: ['src', 'type'],
4604
+ // Allow any attribute on every registered dm-* tag. Props are
4605
+ // author-defined and enumerating them at sanitise time has no security
4606
+ // benefit - custom-element attributes cannot execute JS on their own,
4607
+ // and `on*` handlers + `javascript:` URLs are still stripped globally.
4608
+ ..._dmTagAllowlist.reduce((acc, tag) => { acc[tag] = ['*']; return acc; }, {}),
4609
+ ...extensions.attributes
4610
+ }
4611
+ });
4612
+ }
4613
+
4531
4614
  /**
4532
4615
  * Parse a Markdown file string into frontmatter data and rendered HTML.
4533
4616
  *
@@ -4548,7 +4631,6 @@ function processCtaBlocks(markdown) {
4548
4631
  */
4549
4632
  export async function parseMarkdown(raw, opts = {}) {
4550
4633
  const {data, content} = matter(raw);
4551
- const extensions = getSanitizeExtensions();
4552
4634
 
4553
4635
  // Pipeline (earlier stages are already HTML when later ones run; keep in step
4554
4636
  // with "Shortcode Nesting Rules" in CLAUDE.md and docs/markdown-shortcodes.md):
@@ -4607,74 +4689,7 @@ export async function parseMarkdown(raw, opts = {}) {
4607
4689
  const withSlideover = processSlideoverBlocks(withBanner);
4608
4690
  const rendered = marked.parse(withSlideover);
4609
4691
 
4610
- const sanitized = sanitizeHtml(rendered, {
4611
- // `style` is intentionally allowed below so per-block custom CSS can
4612
- // reach the page. Admins write block templates; same trust model as
4613
- // content/custom.css. This flag silences sanitize-html's warning.
4614
- allowVulnerableTags: true,
4615
- // [embed] emits <iframe>/<video>; hostnames are gated by
4616
- // allowedIframeHostnames below (defence in depth over the render-time
4617
- // host allow-list).
4618
- allowedIframeHostnames: [
4619
- 'www.youtube-nocookie.com', 'www.youtube.com', 'youtube.com',
4620
- 'player.vimeo.com',
4621
- ...getEmbedAllowedHosts(),
4622
- ],
4623
- // sanitize-html drops a RELATIVE iframe src whenever allowedIframeHostnames
4624
- // is set, because it cannot match a hostname it has not been given. That
4625
- // silently emptied every [demo] iframe. A relative src is same-origin by
4626
- // definition, so the hostname gate has nothing to say about it: the gate
4627
- // exists to stop embedding somebody else's site, not your own page.
4628
- allowIframeRelativeUrls: true,
4629
- allowedTags: sanitizeHtml.defaults.allowedTags.concat([
4630
- 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
4631
- 'form', 'input', 'textarea', 'select', 'option', 'optgroup',
4632
- 'button', 'label', 'fieldset', 'legend',
4633
- // [embed] shortcode output
4634
- 'iframe', 'video', 'source', 'figure', 'figcaption',
4635
- // <style> tags are emitted by block rendering (buildBlockStyleTag) so
4636
- // per-block custom CSS can reach the page. Admin-trust: block authors
4637
- // are admins, same security model as site-wide content/custom.css.
4638
- 'style',
4639
- // dm-<name> custom-element tags - one per registered component. Refreshed
4640
- // by refreshComponentTagAllowlist() on startup and after create/delete.
4641
- // Component authors are super-admins, same trust model as plugin code.
4642
- ..._dmTagAllowlist,
4643
- ...extensions.tags
4644
- ]),
4645
- allowedAttributes: {
4646
- ...sanitizeHtml.defaults.allowedAttributes,
4647
- // aria-hidden marks decorative markup (menu spacers, the floating panel's
4648
- // drag grip) so it isn't announced as an empty item. aria-current marks
4649
- // the [menu] item for the page you are on. Both presentational only, no
4650
- // script surface - unlike the on* handlers stripped globally.
4651
- '*': ['class', 'id', 'style', 'data-*', 'aria-hidden', 'aria-current'],
4652
- img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
4653
- form: ['action', 'method'],
4654
- input: ['type', 'name', 'placeholder', 'value', 'required', 'disabled',
4655
- 'readonly', 'min', 'max', 'step', 'pattern', 'maxlength',
4656
- 'minlength', 'checked', 'autocomplete'],
4657
- textarea: ['name', 'placeholder', 'rows', 'cols', 'required',
4658
- 'disabled', 'readonly', 'maxlength'],
4659
- select: ['name', 'required', 'disabled', 'multiple'],
4660
- option: ['value', 'selected', 'disabled'],
4661
- optgroup: ['label', 'disabled'],
4662
- button: ['type', 'disabled', 'aria-label', 'data-action', 'data-entry', 'data-confirm'],
4663
- label: ['for'],
4664
- fieldset: ['disabled'],
4665
- iframe: ['src', 'title', 'width', 'height', 'allow', 'allowfullscreen',
4666
- 'loading', 'referrerpolicy', 'frameborder', 'style'],
4667
- video: ['controls', 'autoplay', 'muted', 'loop', 'playsinline',
4668
- 'preload', 'poster', 'width', 'height', 'style'],
4669
- source: ['src', 'type'],
4670
- // Allow any attribute on every registered dm-* tag. Props are
4671
- // author-defined and enumerating them at sanitise time has no security
4672
- // benefit - custom-element attributes cannot execute JS on their own,
4673
- // and `on*` handlers + `javascript:` URLs are still stripped globally.
4674
- ..._dmTagAllowlist.reduce((acc, tag) => { acc[tag] = ['*']; return acc; }, {}),
4675
- ...extensions.attributes
4676
- }
4677
- });
4692
+ const sanitized = sanitizePageHtml(rendered);
4678
4693
 
4679
4694
  const html = applyTransforms('markdown:afterParse', sanitized);
4680
4695
 
@@ -111,7 +111,7 @@ function DOC_TUTORIALS_SUBMENU() {
111
111
  text: 'Tutorials', icon: 'document',
112
112
  items: [
113
113
  {text: 'Building a CRUD App', url: '#/tutorials/crud', icon: 'database'},
114
- {text: 'Writing a Plugin', url: '#/tutorials/plugin', icon: 'package'},
114
+ {text: 'Writing a Tool', url: '#/tutorials/plugin', icon: 'package'},
115
115
  {text: 'Form Follow-Up', url: '#/tutorials/forms', icon: 'layout'}
116
116
  ]
117
117
  };
@@ -385,6 +385,18 @@ export async function ensureFolderKeys(opts = {}) {
385
385
  changed = true;
386
386
  }
387
387
 
388
+ // Plugins are Tools, so the tutorial became "Writing a Tool" in 1.6.48. Renamed once
389
+ // (`meta.writingToolName`) and only where the item still says Writing a Plugin.
390
+ if (!menu.meta?.writingToolName && items.length) {
391
+ const rename = (list) => (list || []).forEach(n => {
392
+ if (n && n.url === '#/tutorials/plugin' && String(n.text || '').trim() === 'Writing a Plugin') n.text = 'Writing a Tool';
393
+ if (Array.isArray(n?.items)) rename(n.items);
394
+ });
395
+ rename(items);
396
+ menu.meta = {...(menu.meta || {}), writingToolName: true};
397
+ changed = true;
398
+ }
399
+
388
400
  // An empty menu is left empty: the renderer's minimal fallback (Dashboard,
389
401
  // Menus) is what an admin needs to repair it, and a lone Tools folder would
390
402
  // stop that fallback from applying.