Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
XML Parser should invoke reactions when creating/inserting new custom…
… elements

https://bugs.webkit.org/show_bug.cgi?id=188190

Reviewed by Chris Dumez.

Based on a patch written by Frédéric Wang.

Made our XML parser respect custom elements semantics. Namely, perform a microtask checkpoint before constructing
a custom element and throw exceptions in document.open, document.write, document.writeln, document.close inside
custom element constructors and reaction callbacks.

Also fixed a bug that enqueueUpgradeInShadowIncludingTreeOrder and JSCustomElementInterface::upgradeElement
were checking the identity of qualified names instead of whether they match or not (ignoring prefix string).

* LayoutTests/fast/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser-expected.txt: Added.
* LayoutTests/fast/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser.xhtml: Added.
* LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser-expected.txt: Added.
* LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser.xhtml: Added.
* LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser-expected.txt: Added.
* LayoutTests/fast/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser.xhtml: Added.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/innerHTML-setter-expected.txt:
* LayoutTests/platform/win/TestExpectations:

* Source/WebCore/bindings/js/JSCustomElementInterface.cpp:
(WebCore::JSCustomElementInterface::upgradeElement):
* Source/WebCore/dom/CustomElementRegistry.cpp:
(WebCore::enqueueUpgradeInShadowIncludingTreeOrder):
* Source/WebCore/xml/parser/XMLDocumentParserLibxml2.cpp:
(WebCore::XMLDocumentParser::startElementNs):

Canonical link: https://commits.webkit.org/253122@main
  • Loading branch information
rniwa committed Aug 4, 2022
1 parent 86b715c commit 6b27b1f
Show file tree
Hide file tree
Showing 11 changed files with 453 additions and 4 deletions.
@@ -0,0 +1,3 @@

PASS XML parser must perform a microtask checkpoint before constructing a custom element

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Custom Elements: create an element for a token must perform a microtask checkpoint</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org" />
<meta name="assert" content="When the HTML parser creates an element for a token, it must perform a microtask checkpoint before invoking the constructor" />
<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token" />
<meta name="help" content="https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint" />
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<script>
<![CDATA[

function create_window_in_test(test, doc) {
return new Promise((resolve) => {
let iframe = document.createElement('iframe');
blob = new Blob([doc], {type: 'application/xml'});
iframe.src = URL.createObjectURL(blob);
iframe.onload = (event) => {
let contentWindow = iframe.contentWindow;
test.add_cleanup(() => iframe.remove());
resolve(contentWindow);
};
document.body.appendChild(iframe);
});
}

async function construct_custom_element_in_parser(test, markup)
{
const window = await create_window_in_test(test, `<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<body><script>
class SomeElement extends HTMLElement {
constructor() {
super();
window.recordsListInConstructor = recordsList.map((records) => records.slice(0));
}
}
customElements.define('some-element', SomeElement);
const recordsList = [];
const observer = new MutationObserver((records) => {
recordsList.push(records);
});
observer.observe(document.body, {childList: true, subtree: true});
window.onload = () => {
window.recordsListInDOMContentLoaded = recordsList.map((records) => records.slice(0));
}
</scr` + `ipt>${markup}</body></html>`);
return window;
}

promise_test(async function () {
const contentWindow = await construct_custom_element_in_parser(this, '<b><some-element></some-element></b>');
const contentDocument = contentWindow.document;

let recordsList = contentWindow.recordsListInConstructor;
assert_true(Array.isArray(recordsList));
assert_equals(recordsList.length, 1);
assert_true(Array.isArray(recordsList[0]));
assert_equals(recordsList[0].length, 1);
let record = recordsList[0][0];
assert_equals(record.type, 'childList');
assert_equals(record.target, contentDocument.body);
assert_equals(record.previousSibling, contentDocument.querySelector('script'));
assert_equals(record.nextSibling, null);
assert_equals(record.removedNodes.length, 0);
assert_equals(record.addedNodes.length, 1);
assert_equals(record.addedNodes[0], contentDocument.querySelector('b'));

recordsList = contentWindow.recordsListInDOMContentLoaded;
assert_true(Array.isArray(recordsList));
assert_equals(recordsList.length, 2);
assert_true(Array.isArray(recordsList[1]));
assert_equals(recordsList[1].length, 1);
record = recordsList[1][0];
assert_equals(record.type, 'childList');
assert_equals(record.target, contentDocument.querySelector('b'));
assert_equals(record.previousSibling, null);
assert_equals(record.nextSibling, null);
assert_equals(record.removedNodes.length, 0);
assert_equals(record.addedNodes.length, 1);
assert_equals(record.addedNodes[0], contentDocument.querySelector('some-element'));
}, 'XML parser must perform a microtask checkpoint before constructing a custom element');

]]>
</script>
</body>
</html>
@@ -0,0 +1,13 @@

PASS document.open() must throw an InvalidStateError when synchronously constructing a custom element
PASS document.open("text/html") must throw an InvalidStateError when synchronously constructing a custom element
PASS document.open(URL) must NOT throw an InvalidStateError when synchronously constructing a custom element
PASS document.close() must throw an InvalidStateError when synchronously constructing a custom element
PASS document.write must throw an InvalidStateError when synchronously constructing a custom element
PASS document.writeln must throw an InvalidStateError when synchronously constructing a custom element
PASS document.open() of another document must not throw an InvalidStateError when synchronously constructing a custom element
PASS document.open("text/html") of another document must not throw an InvalidStateError when synchronously constructing a custom element
PASS document.close() of another document must not throw an InvalidStateError when synchronously constructing a custom element
PASS document.write of another document must not throw an InvalidStateError when synchronously constructing a custom element
PASS document.writeln of another document must not throw an InvalidStateError when synchronously constructing a custom element

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Custom Elements: create an element for a token must increment and decrement document's throw-on-dynamic-markup-insertion counter</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org" />
<meta name="assert" content="Invoking document.open, document.write, document.writeln, and document.write must throw an exception when the HTML parser is creating a custom element for a token" />
<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token" />
<meta name="help" content="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter" />
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<script>
<![CDATA[

function create_window_in_test(test, doc, type) {
return new Promise((resolve) => {
let iframe = document.createElement('iframe');
blob = new Blob([doc], {type});
iframe.src = URL.createObjectURL(blob);
iframe.onload = (event) => {
let contentWindow = iframe.contentWindow;
test.add_cleanup(() => iframe.remove());
resolve(contentWindow);
};
document.body.appendChild(iframe);
});
}

async function construct_custom_element_in_parser(test, code)
{
window.executed = false;
window.exception = false;
const content_window = await create_window_in_test(test, `<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script>
<![CDATA[
let executed = false;
let exception = null;
class CustomElement extends window.HTMLElement {
constructor() {
super();
try {
${code}
} catch (error) {
exception = error;
}
executed = true;
}
}
customElements.define('some-element', CustomElement);
]]` + `>
</` + `script>
</head>
<body>
<some-element></some-element>
<script>
top.executed = executed;
top.exception = exception;
</script>
</body>
</html>`, 'application/xml');
let content_document;
try {
content_document = content_window.document;
} catch (error) { }
assert_true(executed, 'Must synchronously instantiate a custom element');
return {window: content_window, document: content_document, exception};
}

promise_test(async function () {
const result = await construct_custom_element_in_parser(this, `document.open()`);
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
}, 'document.open() must throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
const result = await construct_custom_element_in_parser(this, `document.open('text/html')`);
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
}, 'document.open("text/html") must throw an InvalidStateError when synchronously constructing a custom element');

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open-window
promise_test(async function () {
let load_promise = new Promise((resolve) => window.onmessage = (event) => resolve(event.data));
const url = top.location.href.substring(0, top.location.href.lastIndexOf('/')) + '/resources/navigation-destination.html';
const result = await construct_custom_element_in_parser(this, `document.open('${url}', '_self', '')`);
assert_equals(result.exception, null);
assert_equals(await load_promise, 'didNavigate');
}, 'document.open(URL) must NOT throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
const result = await construct_custom_element_in_parser(this, `document.close()`);
assert_not_equals(result.exception, null);
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
}, 'document.close() must throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
const result = await construct_custom_element_in_parser(this, `document.write('<b>some text</b>')`);
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content');
}, 'document.write must throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
const result = await construct_custom_element_in_parser(this, `document.writeln('<b>some text</b>')`);
assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content');
}, 'document.writeln must throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html');
const result = await construct_custom_element_in_parser(this, `top.another_window.document.open()`);
assert_equals(result.exception, null);
}, 'document.open() of another document must not throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html');
const result = await construct_custom_element_in_parser(this, `top.another_window.document.open('text/html')`);
assert_equals(result.exception, null);
}, 'document.open("text/html") of another document must not throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html');
const result = await construct_custom_element_in_parser(this, `top.another_window.document.close()`);
assert_equals(result.exception, null);
}, 'document.close() of another document must not throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html');
const result = await construct_custom_element_in_parser(this, `top.another_window.document.write('<b>some text</b>')`);
assert_equals(result.exception, null);
assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
}, 'document.write of another document must not throw an InvalidStateError when synchronously constructing a custom element');

promise_test(async function () {
window.another_window = await create_window_in_test(this, '<!DOCTYPE html><html><body>', 'text/html');
const result = await construct_custom_element_in_parser(this, `top.another_window.document.writeln('<b>some text</b>')`);
assert_equals(result.exception, null);
assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
}, 'document.writeln of another document must not throw an InvalidStateError when synchronously constructing a custom element');

]]>
</script>
</body>
</html>
@@ -0,0 +1,13 @@

PASS document.open() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.open("text/html") must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.open(URL) must NOT throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.close() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.write must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.writeln must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.open() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.open("text/html") of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.close() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.write of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element
PASS document.writeln of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element

0 comments on commit 6b27b1f

Please sign in to comment.