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.
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/app.js +2 -2
- package/admin/js/templates/blocks.html +1 -1
- package/admin/js/templates/docs/tutorial-crud.html +1 -1
- package/admin/js/templates/docs/tutorial-plugin.html +114 -11
- package/admin/js/templates/docs/usage-blocks.html +243 -0
- package/admin/js/templates/docs/usage-plugins.html +1 -1
- package/admin/js/templates/documentation.html +5 -1
- package/admin/js/templates/tutorials.html +1 -1
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/plugins.js +6 -6
- package/config/menus/admin-sidebar.json +1 -1
- package/package.json +2 -2
- package/server/server.js +1 -0
- package/server/services/docs.js +2 -1
- package/server/services/markdown.js +87 -72
- package/server/services/sidebar-migration.js +13 -1
|
@@ -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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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 =
|
|
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
|
|
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.
|