From 6889c764caef5542ba9ad8362acf6e8b7b879ea9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 12:06:06 -0300 Subject: data, infra: import validators directly --- src/data/things/language.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src/data/things/language.js') diff --git a/src/data/things/language.js b/src/data/things/language.js index 7755c505..0638afa2 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,11 +1,9 @@ +import {isLanguageCode} from '#validators'; + import Thing from './thing.js'; export class Language extends Thing { - static [Thing.getPropertyDescriptors] = ({ - validators: { - isLanguageCode, - }, - }) => ({ + static [Thing.getPropertyDescriptors] = () => ({ // Update & expose // General language code. This is used to identify the language distinctly -- cgit 1.3.0-6-gf8a5 From eb00f2993a1aaaba171ad6c918656552f80bb748 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 7 Sep 2023 12:38:34 -0300 Subject: data: import Thing.common utilities directly Also rename 'color' (from #cli) to 'colors'. --- src/data/things/language.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src/data/things/language.js') diff --git a/src/data/things/language.js b/src/data/things/language.js index 0638afa2..c98495dc 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,6 +1,10 @@ import {isLanguageCode} from '#validators'; -import Thing from './thing.js'; +import Thing, { + externalFunction, + flag, + simpleString, +} from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ @@ -16,7 +20,7 @@ export class Language extends Thing { // Human-readable name. This should be the language's own native name, not // localized to any other language. - name: Thing.common.simpleString(), + name: simpleString(), // Language code specific to JavaScript's Internationalization (Intl) API. // Usually this will be the same as the language's general code, but it @@ -38,7 +42,7 @@ export class Language extends Thing { // with languages that are currently in development and not ready for // formal release, or which are just kept hidden as "experimental zones" // for wiki development or content testing. - hidden: Thing.common.flag(false), + hidden: flag(false), // Mapping of translation keys to values (strings). Generally, don't // access this object directly - use methods instead. @@ -66,7 +70,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: externalFunction(), // Expose only -- cgit 1.3.0-6-gf8a5 From d878ab29f20c0727acafb4b1150d4e31d69c55c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 10:09:48 -0300 Subject: data, html, infra: supporting changes for sanitizing content --- src/data/things/language.js | 88 +++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 27 deletions(-) (limited to 'src/data/things/language.js') diff --git a/src/data/things/language.js b/src/data/things/language.js index 7755c505..cc49b735 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,5 +1,10 @@ import Thing from './thing.js'; +import {Tag} from '#html'; +import {isLanguageCode} from '#validators'; + +import CacheableObject from './cacheable-object.js'; + export class Language extends Thing { static [Thing.getPropertyDescriptors] = ({ validators: { @@ -68,7 +73,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: Thing.common.externalFunction({expose: true}), // Expose only @@ -140,19 +145,9 @@ export class Language extends Thing { } formatString(key, args = {}) { - if (this.strings && !this.strings_htmlEscaped) { - throw new Error(`HTML-escaped strings unavailable - please ensure escapeHTML function is provided`); - } - - return this.formatStringHelper(this.strings_htmlEscaped, key, args); - } + const strings = this.strings_htmlEscaped; - formatStringNoHTMLEscape(key, args = {}) { - return this.formatStringHelper(this.strings, key, args); - } - - formatStringHelper(strings, key, args = {}) { - if (!strings) { + if (!this.strings) { throw new Error(`Strings unavailable`); } @@ -160,22 +155,25 @@ export class Language extends Thing { throw new Error(`Invalid key ${key} accessed`); } - const template = strings[key]; + const template = this.strings[key]; // Convert the keys on the args dict from camelCase to CONSTANT_CASE. // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. - const processedArgs = Object.entries(args).map(([k, v]) => [ - k.replace(/[A-Z]/g, '_$&').toUpperCase(), - v, - ]); + // for the iterating we're a8out to do. Also strip HTML from arguments + // that are literal strings - real HTML content should always be proper + // HTML objects (see html.js). + const processedArgs = + Object.entries(args).map(([k, v]) => [ + k.replace(/[A-Z]/g, '_$&').toUpperCase(), + this.#sanitizeStringArg(v), + ]); // Replacement time! Woot. Reduce comes in handy here! - const output = processedArgs.reduce( - (x, [k, v]) => x.replaceAll(`{${k}}`, v), - template - ); + const output = + processedArgs.reduce( + (x, [k, v]) => x.replaceAll(`{${k}}`, v), + template); // Post-processing: if any expected arguments *weren't* replaced, that // is almost definitely an error. @@ -183,7 +181,37 @@ export class Language extends Thing { throw new Error(`Args in ${key} were missing - output: ${output}`); } - return output; + // Last caveat: Wrap the output in an HTML tag so that it doesn't get + // treated as unsanitized HTML if *it* gets passed as an argument to + // *another* formatString call. + return this.#wrapSanitized(output); + } + + // Escapes HTML special characters so they're displayed as-are instead of + // treated by the browser as a tag. This does *not* have an effect on actual + // html.Tag objects, which are treated as sanitized by default (so that they + // can be nested inside strings at all). + #sanitizeStringArg(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + if (typeof arg !== 'string') { + return arg.toString(); + } + + return escapeHTML(arg); + } + + // Wraps the output of a formatting function in a no-name-nor-attributes + // HTML tag, which will indicate to other calls to formatString that this + // content is a string *that may contain HTML* and doesn't need to + // sanitized any further. It'll still .toString() to just the string + // contents, if needed. + #wrapSanitized(output) { + return new Tag(null, null, output); } formatDate(date) { @@ -252,19 +280,25 @@ export class Language extends Thing { // Conjunction list: A, B, and C formatConjunctionList(array) { this.assertIntlAvailable('intl_listConjunction'); - return this.intl_listConjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listConjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Disjunction lists: A, B, or C formatDisjunctionList(array) { this.assertIntlAvailable('intl_listDisjunction'); - return this.intl_listDisjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listDisjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Unit lists: A, B, C formatUnitList(array) { this.assertIntlAvailable('intl_listUnit'); - return this.intl_listUnit.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listUnit.format( + array.map(item => this.#sanitizeStringArg(item)))); } // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB -- cgit 1.3.0-6-gf8a5 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/data/things/language.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'src/data/things/language.js') 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 From ab7591e45e7e31b4e2c0e2f81e224672145993fa Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 1 Oct 2023 17:01:21 -0300 Subject: data, test: refactor utilities into own file Primarily for more precies test coverage mapping, but also to make navigation a bit easier and consolidate complex functions with lots of imports out of the same space as other, more simple or otherwise specialized files. --- src/data/things/language.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src/data/things/language.js') diff --git a/src/data/things/language.js b/src/data/things/language.js index a325d6a6..fe74f7bf 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,13 +1,14 @@ import {Tag} from '#html'; import {isLanguageCode} from '#validators'; -import CacheableObject from './cacheable-object.js'; - -import Thing, { +import { externalFunction, flag, simpleString, -} from './thing.js'; +} from '#composite/wiki-properties'; + +import CacheableObject from './cacheable-object.js'; +import Thing from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ -- cgit 1.3.0-6-gf8a5 From 3cd6f9edc58171e33ed6af565db84113e2488f25 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 9 Oct 2023 14:58:14 -0300 Subject: data: language: allow passing multiple key parts directly --- src/data/things/language.js | 66 ++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 22 deletions(-) (limited to 'src/data/things/language.js') diff --git a/src/data/things/language.js b/src/data/things/language.js index fe74f7bf..646eb6d1 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -101,6 +101,7 @@ export class Language extends Thing { }, }, + // TODO: This currently isn't used. Is it still needed? strings_htmlEscaped: { flags: {expose: true}, expose: { @@ -130,8 +131,8 @@ export class Language extends Thing { }; } - $(key, args = {}) { - return this.formatString(key, args); + $(...args) { + return this.formatString(...args); } assertIntlAvailable(property) { @@ -145,8 +146,20 @@ export class Language extends Thing { return this.intl_pluralCardinal.select(value); } - formatString(key, args = {}) { - const strings = this.strings_htmlEscaped; + formatString(...args) { + const hasOptions = + typeof args.at(-1) === 'object' && + args.at(-1) !== null; + + const key = + (hasOptions ? args.slice(0, -1) : args) + .filter(Boolean) + .join('.'); + + const options = + (hasOptions + ? args.at(-1) + : null); if (!this.strings) { throw new Error(`Strings unavailable`); @@ -158,27 +171,36 @@ export class Language extends Thing { const template = this.strings[key]; - // Convert the keys on the args dict from camelCase to CONSTANT_CASE. - // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut - // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. Also strip HTML from arguments - // that are literal strings - real HTML content should always be proper - // HTML objects (see html.js). - const processedArgs = - Object.entries(args).map(([k, v]) => [ - k.replace(/[A-Z]/g, '_$&').toUpperCase(), - this.#sanitizeStringArg(v), - ]); - - // Replacement time! Woot. Reduce comes in handy here! - const output = - processedArgs.reduce( - (x, [k, v]) => x.replaceAll(`{${k}}`, v), - template); + let output; + + if (hasOptions) { + // Convert the keys on the options dict from camelCase to CONSTANT_CASE. + // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut + // like, who cares, dude?) Also, this is an array, 8ecause it's handy + // for the iterating we're a8out to do. Also strip HTML from arguments + // that are literal strings - real HTML content should always be proper + // HTML objects (see html.js). + const processedOptions = + Object.entries(options).map(([k, v]) => [ + k.replace(/[A-Z]/g, '_$&').toUpperCase(), + this.#sanitizeStringArg(v), + ]); + + // Replacement time! Woot. Reduce comes in handy here! + output = + processedOptions.reduce( + (x, [k, v]) => x.replaceAll(`{${k}}`, v), + template); + } else { + // Without any options provided, just use the template as-is. This will + // still error if the template expected arguments, and otherwise will be + // the right value. + output = template; + } // Post-processing: if any expected arguments *weren't* replaced, that // is almost definitely an error. - if (output.match(/\{[A-Z_]+\}/)) { + if (output.match(/\{[A-Z][A-Z0-9_]*\}/)) { throw new Error(`Args in ${key} were missing - output: ${output}`); } -- cgit 1.3.0-6-gf8a5