umb-localize encode HTML arguments (#18960)
* Moves `escapeHTML` call from localization controller to `umb-localize` element * Adds supporting unit-test * Removed unit-test as it is now expected that the localization controller will return literal HTML markup. * Updated import path * Removed extra call to `text()`
This commit is contained in:
@@ -175,12 +175,6 @@ describe('UmbLocalizeController', () => {
|
||||
expect((controller.term as any)('logout', 'Hello', 'World')).to.equal('Log out');
|
||||
});
|
||||
|
||||
it('should encode HTML entities', () => {
|
||||
expect(controller.term('withInlineToken', 'Hello', '<script>alert("XSS")</script>'), 'XSS detected').to.equal(
|
||||
'Hello <script>alert("XSS")</script>',
|
||||
);
|
||||
});
|
||||
|
||||
it('only reacts to changes of its own localization-keys', async () => {
|
||||
const element: UmbLocalizationRenderCountElement = await fixture(
|
||||
html`<umb-localization-render-count></umb-localization-render-count>`,
|
||||
|
||||
@@ -20,7 +20,6 @@ import type {
|
||||
import { umbLocalizationManager } from './localization.manager.js';
|
||||
import type { LitElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { escapeHTML } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
const LocalizationControllerAlias = Symbol();
|
||||
/**
|
||||
@@ -137,20 +136,16 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
|
||||
return String(key);
|
||||
}
|
||||
|
||||
// As translated texts can contain HTML, we will need to render with unsafeHTML.
|
||||
// But arguments can come from user input, so they should be escaped.
|
||||
const sanitizedArgs = args.map((a) => escapeHTML(a));
|
||||
|
||||
if (typeof term === 'function') {
|
||||
return term(...sanitizedArgs) as string;
|
||||
return term(...args) as string;
|
||||
}
|
||||
|
||||
if (typeof term === 'string') {
|
||||
if (sanitizedArgs.length) {
|
||||
if (args.length) {
|
||||
// Replace placeholders of format "%index%" and "{index}" with provided values
|
||||
term = term.replace(/(%(\d+)%|\{(\d+)\})/g, (match, _p1, p2, p3): string => {
|
||||
const index = p2 || p3;
|
||||
return typeof sanitizedArgs[index] !== 'undefined' ? String(sanitizedArgs[index]) : match;
|
||||
return typeof args[index] !== 'undefined' ? String(args[index]) : match;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,14 @@ describe('umb-localize', () => {
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('Hello World');
|
||||
});
|
||||
|
||||
it('should localize a key with multiple arguments as encoded HTML', async () => {
|
||||
element.key = 'general_moreThanOneArgument';
|
||||
element.args = ['<strong>Hello</strong>', '<em>World</em>'];
|
||||
await elementUpdated(element);
|
||||
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('<strong>Hello</strong> <em>World</em>');
|
||||
});
|
||||
|
||||
it('should localize a key with args as an attribute', async () => {
|
||||
element.key = 'general_moreThanOneArgument';
|
||||
element.setAttribute('args', '["Hello","World"]');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { css, customElement, html, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { escapeHTML } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
/**
|
||||
@@ -34,7 +35,11 @@ export class UmbLocalizeElement extends UmbLitElement {
|
||||
|
||||
@state()
|
||||
protected get text(): string {
|
||||
const localizedValue = this.localize.term(this.key, ...(this.args ?? []));
|
||||
// As translated texts can contain HTML, we will need to render with unsafeHTML.
|
||||
// But arguments can come from user input, so they should be escaped.
|
||||
const escapedArgs = (this.args ?? []).map((a) => escapeHTML(a));
|
||||
|
||||
const localizedValue = this.localize.term(this.key, ...escapedArgs);
|
||||
|
||||
// If the value is the same as the key, it means the key was not found.
|
||||
if (localizedValue === this.key) {
|
||||
@@ -44,12 +49,13 @@ export class UmbLocalizeElement extends UmbLitElement {
|
||||
|
||||
(this.getHostElement() as HTMLElement).removeAttribute('data-localize-missing');
|
||||
|
||||
return localizedValue;
|
||||
return localizedValue.trim();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this.text.trim()
|
||||
? html`${unsafeHTML(this.text)}`
|
||||
const text = this.text;
|
||||
return text
|
||||
? unsafeHTML(text)
|
||||
: this.debug
|
||||
? html`<span style="color:red">${this.key}</span>`
|
||||
: html`<slot></slot>`;
|
||||
|
||||
@@ -4,7 +4,7 @@ const NON_ALPHANUMERIC_REGEXP = /([^#-~| |!])/g;
|
||||
|
||||
/**
|
||||
* Escapes HTML entities in a string.
|
||||
* @example escapeHTML('<script>alert("XSS")</script>'), // "<script>alert("XSS")</script>"
|
||||
* @example escapeHTML('<script>alert("XSS")</script>'), // "<script>alert("XSS")</script>"
|
||||
* @param html The HTML string to escape.
|
||||
* @returns The sanitized HTML string.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user