Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!doctype html>
<meta charset="utf-8" />
<title>HTML partial updates - streaming with sanitizer</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style id="style"></style>
<p id="target"></p>
<script>
for (const baseMethod of [
"",
"Append",
"Prepend",
"Before",
"After",
"ReplaceWith",
]) {
for (const safe of [true, false]) {
const method = `stream${baseMethod}HTML${safe ? "" : "Unsafe"}`;
function assert_html_equals(parent, placeholder, expected) {
if (["", "Append", "Prepend"].includes(baseMethod)) {
assert_equals(placeholder.innerHTML, expected);
} else if (baseMethod === "ReplaceWith") {
assert_equals(parent.innerHTML, expected);
} else if (baseMethod === "Before") {
assert_equals(parent.innerHTML, expected + "<div></div>");
} else if (baseMethod === "After") {
assert_equals(parent.innerHTML, "<div></div>" + expected);
}
}
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: { removeElements: ["span"] },
}).getWriter();
await writer.write("<div>A</div><span>B</span>");
await writer.close();
assert_equals(parent.textContent, "A");
}, `element.${method} with sanitizer {removeElements}`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: { elements: ["div"] },
}).getWriter();
await writer.write("<div>A</div><span>B</span>");
await writer.close();
assert_equals(parent.textContent, "A");
}, `element.${method} with sanitizer {elements}`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["div"],
},
}).getWriter();
await writer.write("<div id=outer><span id=inner>A</span></div>");
await writer.close();
assert_not_equals(parent.querySelector("#inner"), null);
assert_equals(parent.querySelector("#outer"), null);
}, `element.${method} with sanitizer {replaceWithChildrenElements}`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["div"],
},
}).getWriter();
await writer.write("<div id=outer>A</div>");
await writer.close();
assert_equals(parent.textContent, "A");
assert_equals(parent.querySelector("#outer"), null);
}, `element.${method} with sanitizer {replaceWithChildrenElements} and text`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
attributes: ["allowed"],
},
}).getWriter();
await writer.write("<span allowed=a forbidden=b>A</span>");
await writer.close();
const node = parent.querySelector("span");
assert_true(node.hasAttribute("allowed"));
assert_false(node.hasAttribute("forbidden"));
}, `element.${method} with sanitizer {attributes}`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
removeAttributes: ["forbidden"],
},
}).getWriter();
await writer.write("<span allowed=a forbidden=b>A</span>");
await writer.close();
const node = parent.querySelector("span");
assert_true(node.hasAttribute("allowed"));
assert_false(node.hasAttribute("forbidden"));
}, `element.${method} with sanitizer {removeAttributes}`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
elements: [{ name: "span", removeAttributes: ["forbidden"] }],
},
}).getWriter();
await writer.write("<span allowed=a forbidden=b>A</span>");
await writer.close();
const node = parent.querySelector("span");
assert_true(node.hasAttribute("allowed"));
assert_false(node.hasAttribute("forbidden"));
}, `element.${method} with sanitizer {element: [{removeAttributes}]}`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
elements: [{ name: "span", attributes: ["allowed"] }],
},
}).getWriter();
await writer.write("<span allowed=a forbidden=b>A</span>");
await writer.close();
const node = parent.querySelector("span");
assert_true(node.hasAttribute("allowed"));
assert_false(node.hasAttribute("forbidden"));
}, `element.${method} with sanitizer {element: [{attributes}]}`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
elements: ["template", { name: "span", attributes: ["allowed"] }],
},
}).getWriter();
await writer.write(
"<template><span allowed=a forbidden=b>A</span></template>",
);
await writer.close();
const template = parent.querySelector("template");
const firstElementChild = template.content.firstElementChild;
assert_true(firstElementChild.hasAttribute("allowed"));
assert_false(firstElementChild.hasAttribute("forbidden"));
}, `element.${method} with sanitizer should sanitize template contents`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["template"],
},
}).getWriter();
await writer.write(
"<b>before</b><template><span id=inner>A</span><template><i>nested</i></template></template><b>after</b>",
);
await writer.close();
assert_equals(parent.querySelector("template"), null);
// A <template> has no children in the light tree, so replacing it
// with its children drops its contents rather than hoisting them.
assert_equals(parent.querySelector("#inner"), null);
assert_equals(parent.querySelector("span, i"), null);
assert_html_equals(parent, placeholder, "<b>before</b><b>after</b>");
}, `element.${method}: sanitizer shouldn't allow escaping template`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["template"],
},
}).getWriter();
await writer.write(
"<table><template><span id=inner>A</span></template><td>cell</td></table>",
);
await writer.close();
assert_equals(parent.querySelector("template"), null);
assert_equals(parent.querySelector("#inner"), null);
assert_html_equals(
parent,
placeholder,
"<table><tbody><tr><td>cell</td></tr></tbody></table>",
);
}, `element.${method}: replaced <template> inside a table drops its contents`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["tr"],
},
}).getWriter();
await writer.write("<table><td>Content");
await writer.close();
assert_html_equals(
parent,
placeholder,
"<table><tbody><td>Content</td></tbody></table>",
);
}, `element.${method}: sanitizer takes effect after foster parenting`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["tr"],
},
}).getWriter();
await writer.write("<table><p><p>Content<td>");
await writer.close();
assert_html_equals(
parent,
placeholder,
"<p></p><p>Content</p><table><tbody><td></td></tbody></table>",
);
}, `element.${method}: sanitizer with foster parenting: replaceWithChildrenElements`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
removeElements: ["img"],
},
}).getWriter();
await writer.write("<table><img id=forbidden>");
await writer.close();
assert_html_equals(
parent,
placeholder,
"<table></table>",
);
}, `element.${method}: sanitizer with foster parenting: removeElements`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
removeElements: ["i"],
},
}).getWriter();
await writer.write("<p>1<b>2<i>3</b>4</i>5</p>");
await writer.close();
assert_html_equals(parent, placeholder, "<p>1<b>2</b>5</p>");
}, `element.${method}: sanitizer takes effect after adoption agency (1)`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
removeElements: ["b"],
},
}).getWriter();
await writer.write("<p>1<b>2<i>3</b>4</i>5</p>");
await writer.close();
assert_html_equals(parent, placeholder, "<p>1<i>4</i>5</p>");
}, `element.${method}: sanitizer takes effect after adoption agency (2)`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const sanitizer = new Sanitizer();
const writer = placeholder[method]({ sanitizer }).getWriter();
await writer.write("<p>1</p>");
sanitizer.removeElement("p");
await writer.write("<p>2</p>");
await writer.close();
assert_equals(parent.textContent, "12");
}, `element.${method}: mutating the sanitizer while streaming does not effect the originally given sanitizer`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
const existingChild = document.createElement("span");
existingChild.textContent = "keep";
placeholder.appendChild(existingChild);
parent.appendChild(placeholder);
assert_throws_js(TypeError, _ => {
placeholder[method]({sanitizer: {elements: ["div"], removeElements: ["div"]}});
});
assert_equals(placeholder.parentNode, parent, "Target element must not be removed when sanitizer config throws");
assert_equals(placeholder.firstChild, existingChild, "Existing children must not be removed when sanitizer config throws");
assert_equals(placeholder.textContent, "keep");
}, `element.${method}: passing an invalid sanitizer config should not create the stream or mutate the DOM`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
removeAttributes: ["onclick"],
},
}).getWriter();
// The sanitizer behaves as if it ran after parsing: the Noah's Ark
// clause compares the attributes from the start tag tokens, so these
// four <b> elements are all distinct and all four are reconstructed,
// even though the sanitizer strips the attribute that tells them apart.
await writer.write('<p><b onclick="1"><b onclick="2"><b onclick="3"><b onclick="4"></p>X');
await writer.close();
assert_html_equals(
parent,
placeholder,
"<p><b><b><b><b></b></b></b></b></p><b><b><b><b>X</b></b></b></b>",
);
}, `element.${method}: Noah's Ark clause compares attributes before sanitization`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
removeAttributes: ["onclick"],
},
}).getWriter();
// Control: with identical attributes, the Noah's Ark clause caps the
// reconstructed formatting elements at three.
await writer.write('<p><b onclick="1"><b onclick="1"><b onclick="1"><b onclick="1"></p>X');
await writer.close();
assert_html_equals(
parent,
placeholder,
"<p><b><b><b><b></b></b></b></b></p><b><b><b>X</b></b></b>",
);
}, `element.${method}: Noah's Ark clause still applies to identical sanitized attributes`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["div"],
},
}).getWriter();
await writer.write("<div><table><tr>foster<td>cell</td></tr></table></div>");
await writer.close();
assert_html_equals(
parent,
placeholder,
"foster<table><tbody><tr><td>cell</td></tr></tbody></table>",
);
}, `element.${method}: foster parenting when table's parent in stack is in replaceWithChildrenElements`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["div"],
},
}).getWriter();
await writer.write("<table><div><span>Content</span></div><td>cell</td></table>");
await writer.close();
assert_html_equals(
parent,
placeholder,
"<span>Content</span><table><tbody><tr><td>cell</td></tr></tbody></table>",
);
}, `element.${method}: foster-parented replaceWithChildrenElements element unwraps before table`);
promise_test(async (t) => {
const parent = document.createElement("div");
const placeholder = document.createElement("div");
parent.appendChild(placeholder);
const writer = placeholder[method]({
sanitizer: {
replaceWithChildrenElements: ["div"],
},
}).getWriter();
await writer.write('<div><span id="child">text</span><template shadowrootmode="open"><p>dsd</p></template></div>');
await writer.close();
const span = parent.querySelector("#child");
assert_not_equals(span, null);
assert_equals(span.ownerDocument, document, "Child of replaced element has target ownerDocument");
assert_equals(placeholder.shadowRoot, null, "DSD inside replaced element must not attach to placeholder");
assert_equals(parent.shadowRoot, null, "DSD inside replaced element must not attach to parent");
if (!safe) {
const tpl = parent.querySelector("template");
assert_not_equals(tpl, null, "DSD template inside replaced element remains a plain <template>");
}
}, `element.${method}: replaceWithChildrenElements preserves ownerDocument and does not attach inner declarative shadow root`);
}
}
</script>