mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] runtime: add markup tag function
Allows markup to be called as a tag function. The interpolated strings
are then safely escaped for injection in HTML code.
Example usage:
```js
const maliciousInput = "<script>alert('💥💥')</script>";
const value = markup`<b>${maliciousInput}</b>`;
// no problem, maliciousInput is properly escaped
```
This commit is contained in:
committed by
Géry Debongnie
parent
ac9ccb81ca
commit
fd3c194525
@@ -193,6 +193,15 @@ The first `t-out` will act as a `t-esc` directive, which means that the content
|
|||||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||||
this will be injected as html.
|
this will be injected as html.
|
||||||
|
|
||||||
|
`markup` can also be used as a tag function, allowing the interpolated values to
|
||||||
|
be safely escaped:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
// <b><script>alert('💥💥')</script></b>
|
||||||
|
const value = markup`<b>${maliciousInput}</b>`;
|
||||||
|
```
|
||||||
|
|
||||||
### Setting Variables
|
### Setting Variables
|
||||||
|
|
||||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||||
|
|||||||
+42
-2
@@ -81,10 +81,50 @@ export async function loadFile(url: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export class Markup extends String {}
|
export class Markup extends String {}
|
||||||
|
|
||||||
|
function _escapeHtml(str: any): string | Markup {
|
||||||
|
if (str instanceof Markup) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (str === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof str === "number") {
|
||||||
|
return String(str);
|
||||||
|
}
|
||||||
|
[
|
||||||
|
["&", "&"],
|
||||||
|
["<", "<"],
|
||||||
|
[">", ">"],
|
||||||
|
["'", "'"],
|
||||||
|
['"', """],
|
||||||
|
["`", "`"],
|
||||||
|
].forEach((pairs) => {
|
||||||
|
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||||
|
});
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
||||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||||||
|
*
|
||||||
|
* If called as a tag function, the interpolated strings are escaped.
|
||||||
*/
|
*/
|
||||||
export function markup(value: any) {
|
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
|
||||||
return new Markup(value);
|
export function markup(value: string): Markup;
|
||||||
|
export function markup(
|
||||||
|
valueOrStrings: string | TemplateStringsArray,
|
||||||
|
...placeholders: unknown[]
|
||||||
|
): Markup {
|
||||||
|
if (!Array.isArray(valueOrStrings)) {
|
||||||
|
return new Markup(valueOrStrings);
|
||||||
|
}
|
||||||
|
const strings = valueOrStrings;
|
||||||
|
let acc = "";
|
||||||
|
let i = 0;
|
||||||
|
for (; i < placeholders.length; ++i) {
|
||||||
|
acc += strings[i] + _escapeHtml(placeholders[i]);
|
||||||
|
}
|
||||||
|
acc += strings[i];
|
||||||
|
return new Markup(acc);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-1
@@ -1,4 +1,4 @@
|
|||||||
import { batched, EventBus } from "../src/runtime/utils";
|
import { batched, EventBus, markup } from "../src/runtime/utils";
|
||||||
import { nextMicroTick } from "./helpers";
|
import { nextMicroTick } from "./helpers";
|
||||||
|
|
||||||
describe("event bus behaviour", () => {
|
describe("event bus behaviour", () => {
|
||||||
@@ -71,3 +71,33 @@ describe("batched", () => {
|
|||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Markup = markup("").constructor;
|
||||||
|
describe("markup", () => {
|
||||||
|
test("string is flagged as safe", () => {
|
||||||
|
const html = markup("<blink>Hello</blink>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
describe("tag function", () => {
|
||||||
|
test("interpolated values are escaped", () => {
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
const html = markup`<b>${maliciousInput}</b>`;
|
||||||
|
expect(html.toString()).toBe("<b><script>alert('💥💥')</script></b>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("interpolated markups aren't escaped", () => {
|
||||||
|
const shouldBeEscaped = "<script>alert('should be escaped')</script>";
|
||||||
|
const shouldnt = markup("<b>this is safe</b>");
|
||||||
|
const html = markup`<div>${shouldBeEscaped} ${shouldnt}</div>`;
|
||||||
|
expect(html.toString()).toBe(
|
||||||
|
"<div><script>alert('should be escaped')</script> <b>this is safe</b></div>"
|
||||||
|
);
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("quotes in interpolated values are escaped", () => {
|
||||||
|
const imgUrl = `lol" onerror="alert('xss')`;
|
||||||
|
const html = markup`<img src="${imgUrl}">`;
|
||||||
|
expect(html.toString()).toBe(`<img src="lol" onerror="alert('xss')">`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user