From 3eb82ab2e3f9d921095af05cf0bc284f335aaa35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 10:11:44 -0300 Subject: content: misc. changes to handle HTML sanitization --- src/content/dependencies/generateCoverGrid.js | 12 ++++++--- .../generateFooterLocalizationLinks.js | 2 +- src/content/dependencies/generatePageLayout.js | 6 ++--- src/content/dependencies/linkTemplate.js | 2 +- src/content/dependencies/linkThing.js | 2 +- src/data/things/language.js | 29 ++++++++++++++++++++++ 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 9822e1ae..5636e4f3 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -2,7 +2,7 @@ import {stitchArrays} from '#sugar'; export default { contentDependencies: ['generateGridActionLinks'], - extraDependencies: ['html'], + extraDependencies: ['html', 'language'], relations(relation) { return { @@ -20,7 +20,7 @@ export default { actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, - generate(relations, slots, {html}) { + generate(relations, slots, {html, language}) { return ( html.tag('div', {class: 'grid-listing'}, [ stitchArrays({ @@ -42,8 +42,12 @@ export default { ? slots.lazy : false), }), - html.tag('span', {[html.onlyIfContent]: true}, name), - html.tag('span', {[html.onlyIfContent]: true}, info), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(name)), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(info)), ], })), diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index b4970b17..5df83566 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -38,7 +38,7 @@ export default { return html.tag('div', {class: 'footer-localization-links'}, language.$('misc.uiLanguage', { - languages: links.join('\n'), + languages: language.formatListWithoutSeparator(links), })); }, }; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 95a5dbec..5377f80d 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -105,7 +105,7 @@ export default { color: {validate: v => v.isColor}, styleRules: { - validate: v => v.sparseArrayOf(v.isString), + validate: v => v.sparseArrayOf(v.isHTML), default: [], }, @@ -183,7 +183,7 @@ export default { } else { aggregate.call(v.validateProperties({ path: v.strictArrayOf(v.isString), - title: v.isString, + title: v.isHTML, }), { path: object.path, title: object.title, @@ -521,7 +521,7 @@ export default { ]), slots.bannerPosition === 'bottom' && slots.banner, footerHTML, - ].filter(Boolean).join('\n'); + ]; const pageHTML = html.tags([ ``, diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index ba7c7cda..7206e960 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -16,7 +16,7 @@ export default { path: {validate: v => v.validateArrayItems(v.isString)}, hash: {type: 'string'}, - tooltip: {validate: v => v.isString}, + tooltip: {type: 'string'}, attributes: {validate: v => v.isAttributes}, color: {validate: v => v.isColor}, content: {type: 'html'}, diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index e3e2608f..643bf4b1 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -26,7 +26,7 @@ export default { preferShortName: {type: 'boolean', default: false}, tooltip: { - validate: v => v.oneOf(v.isBoolean, v.isString), + validate: v => v.oneOf(v.isBoolean, v.isHTML), default: false, }, diff --git a/src/data/things/language.js b/src/data/things/language.js index cc49b735..afa9f1ee 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -214,6 +214,28 @@ export class Language extends Thing { return new Tag(null, null, output); } + // Similar to the above internal methods, but this one is public. + // It should be used when embedding content that may not have previously + // been sanitized directly into an HTML tag or template's contents. + // The templating engine usually handles this on its own, as does passing + // a value (sanitized or not) directly as an argument to formatString, + // but if you used a custom validation function ({validate: v => v.isHTML} + // instead of {type: 'string'} / {type: 'html'}) and are embedding the + // contents of a slot directly, it should be manually sanitized with this + // function first. + sanitize(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + return ( + (typeof arg === 'string' + ? new Tag(null, null, escapeHTML(arg)) + : arg)); + } + formatDate(date) { this.assertIntlAvailable('intl_date'); return this.intl_date.format(date); @@ -301,6 +323,13 @@ export class Language extends Thing { array.map(item => this.#sanitizeStringArg(item)))); } + // Lists without separator: A B C + formatListWithoutSeparator(array) { + return this.#wrapSanitized( + array.map(item => this.#sanitizeStringArg(item)) + .join(' ')); + } + // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB formatFileSize(bytes) { if (!bytes) return ''; -- cgit 1.3.0-6-gf8a5