LibWeb: Throw pre insertion validity errors from the correct global

This commit is contained in:
Tim Ledbetter 2025-07-25 03:35:01 +01:00 committed by Jelle Raaijmakers
commit 89fb783b42
Notes: github-actions[bot] 2025-07-25 07:09:31 +00:00
7 changed files with 2201 additions and 13 deletions

View file

@ -592,28 +592,28 @@ bool Node::is_browsing_context_connected() const
}
// https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
WebIDL::ExceptionOr<void> Node::ensure_pre_insertion_validity(GC::Ref<Node> node, GC::Ptr<Node> child) const
WebIDL::ExceptionOr<void> Node::ensure_pre_insertion_validity(JS::Realm& realm, GC::Ref<Node> node, GC::Ptr<Node> child) const
{
// 1. If parent is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
if (!is<Document>(this) && !is<DocumentFragment>(this) && !is<Element>(this))
return WebIDL::HierarchyRequestError::create(realm(), "Can only insert into a document, document fragment or element"_string);
return WebIDL::HierarchyRequestError::create(realm, "Can only insert into a document, document fragment or element"_string);
// 2. If node is a host-including inclusive ancestor of parent, then throw a "HierarchyRequestError" DOMException.
if (node->is_host_including_inclusive_ancestor_of(*this))
return WebIDL::HierarchyRequestError::create(realm(), "New node is an ancestor of this node"_string);
return WebIDL::HierarchyRequestError::create(realm, "New node is an ancestor of this node"_string);
// 3. If child is non-null and its parent is not parent, then throw a "NotFoundError" DOMException.
if (child && child->parent() != this)
return WebIDL::NotFoundError::create(realm(), "This node is not the parent of the given child"_string);
return WebIDL::NotFoundError::create(realm, "This node is not the parent of the given child"_string);
// FIXME: All the following "Invalid node type for insertion" messages could be more descriptive.
// 4. If node is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException.
if (!is<DocumentFragment>(*node) && !is<DocumentType>(*node) && !is<Element>(*node) && !is<Text>(*node) && !is<Comment>(*node) && !is<ProcessingInstruction>(*node) && !is<CDATASection>(*node))
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
return WebIDL::HierarchyRequestError::create(realm, "Invalid node type for insertion"_string);
// 5. If either node is a Text node and parent is a document, or node is a doctype and parent is not a document, then throw a "HierarchyRequestError" DOMException.
if ((is<Text>(*node) && is<Document>(this)) || (is<DocumentType>(*node) && !is<Document>(this)))
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
return WebIDL::HierarchyRequestError::create(realm, "Invalid node type for insertion"_string);
// 6. If parent is a document, and any of the statements below, switched on the interface node implements, are true, then throw a "HierarchyRequestError" DOMException.
if (is<Document>(this)) {
@ -624,18 +624,18 @@ WebIDL::ExceptionOr<void> Node::ensure_pre_insertion_validity(GC::Ref<Node> node
auto node_element_child_count = as<DocumentFragment>(*node).child_element_count();
if ((node_element_child_count > 1 || node->has_child_of_type<Text>())
|| (node_element_child_count == 1 && (has_child_of_type<Element>() || is<DocumentType>(child.ptr()) || (child && child->has_following_node_of_type_in_tree_order<DocumentType>())))) {
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
return WebIDL::HierarchyRequestError::create(realm, "Invalid node type for insertion"_string);
}
} else if (is<Element>(*node)) {
// Element
// If parent has an element child, child is a doctype, or child is non-null and a doctype is following child.
if (has_child_of_type<Element>() || is<DocumentType>(child.ptr()) || (child && child->has_following_node_of_type_in_tree_order<DocumentType>()))
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
return WebIDL::HierarchyRequestError::create(realm, "Invalid node type for insertion"_string);
} else if (is<DocumentType>(*node)) {
// DocumentType
// parent has a doctype child, child is non-null and an element is preceding child, or child is null and parent has an element child.
if (has_child_of_type<DocumentType>() || (child && child->has_preceding_node_of_type_in_tree_order<Element>()) || (!child && has_child_of_type<Element>()))
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
return WebIDL::HierarchyRequestError::create(realm, "Invalid node type for insertion"_string);
}
}
@ -811,7 +811,7 @@ void Node::insert_before(GC::Ref<Node> node, GC::Ptr<Node> child, bool suppress_
WebIDL::ExceptionOr<GC::Ref<Node>> Node::pre_insert(GC::Ref<Node> node, GC::Ptr<Node> child)
{
// 1. Ensure pre-insertion validity of node into parent before child.
TRY(ensure_pre_insertion_validity(node, child));
TRY(ensure_pre_insertion_validity(realm(), node, child));
// 2. Let referenceChild be child.
auto reference_child = child;

View file

@ -375,7 +375,7 @@ public:
template<typename T>
bool fast_is() const = delete;
WebIDL::ExceptionOr<void> ensure_pre_insertion_validity(GC::Ref<Node> node, GC::Ptr<Node> child) const;
WebIDL::ExceptionOr<void> ensure_pre_insertion_validity(JS::Realm&, GC::Ref<Node> node, GC::Ptr<Node> child) const;
bool is_host_including_inclusive_ancestor_of(Node const&) const;

View file

@ -237,7 +237,7 @@ WebIDL::ExceptionOr<void> ParentNode::replace_children(Vector<Variant<GC::Root<N
auto node = TRY(convert_nodes_to_single_node(nodes, document()));
// 2. Ensure pre-insertion validity of node into this before null.
TRY(ensure_pre_insertion_validity(node, nullptr));
TRY(ensure_pre_insertion_validity(realm(), node, nullptr));
// 3. Replace all with node within this.
replace_all(*node);

View file

@ -839,7 +839,7 @@ WebIDL::ExceptionOr<void> Range::insert(GC::Ref<Node> node)
parent = reference_node->parent();
// 6. Ensure pre-insertion validity of node into parent before referenceNode.
TRY(parent->ensure_pre_insertion_validity(node, reference_node));
TRY(parent->ensure_pre_insertion_validity(node->realm(), node, reference_node));
// 7. If ranges start node is a Text node, set referenceNode to the result of splitting it with offset ranges start offset.
if (is<Text>(*m_start_container))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,287 @@
<!doctype html>
<title>Range.insertNode() tests</title>
<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
<meta name=timeout content=long>
<p>To debug test failures, add a query parameter "subtest" with the test id (like
"?subtest=5,16"). Only that test will be run. Then you can look at the resulting
iframes in the DOM.
<div id=log></div>
<script src=../../resources/testharness.js></script>
<script src=../../resources/testharnessreport.js></script>
<script src=../common.js></script>
<script>
"use strict";
testDiv.parentNode.removeChild(testDiv);
function restoreIframe(iframe, i, j) {
// Most of this function is designed to work around the fact that Opera
// doesn't let you add a doctype to a document that no longer has one, in
// any way I can figure out. I eventually compromised on something that
// will still let Opera pass most tests that don't actually involve
// doctypes.
while (iframe.contentDocument.firstChild
&& iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
}
while (iframe.contentDocument.lastChild
&& iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
}
if (!iframe.contentDocument.firstChild) {
// This will throw an exception in Opera if we reach here, which is why
// I try to avoid it. It will never happen in a browser that obeys the
// spec, so it's really just insurance. I don't think it actually gets
// hit by anything.
iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
}
iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
iframe.contentWindow.setupRangeTests();
iframe.contentWindow.testRangeInput = testRangesShort[i];
iframe.contentWindow.testNodeInput = testNodesShort[j];
iframe.contentWindow.run();
}
function testInsertNode(i, j) {
var actualRange;
var expectedRange;
var actualNode;
var expectedNode;
var actualRoots = [];
var expectedRoots = [];
var detached = false;
domTests[i][j].step(function() {
restoreIframe(actualIframe, i, j);
restoreIframe(expectedIframe, i, j);
actualRange = actualIframe.contentWindow.testRange;
expectedRange = expectedIframe.contentWindow.testRange;
actualNode = actualIframe.contentWindow.testNode;
expectedNode = expectedIframe.contentWindow.testNode;
try {
actualRange.collapsed;
} catch (e) {
detached = true;
}
assert_equals(actualIframe.contentWindow.unexpectedException, null,
"Unexpected exception thrown when setting up Range for actual insertNode()");
assert_equals(expectedIframe.contentWindow.unexpectedException, null,
"Unexpected exception thrown when setting up Range for simulated insertNode()");
assert_equals(typeof actualRange, "object",
"typeof Range produced in actual iframe");
assert_not_equals(actualRange, null,
"Range produced in actual iframe was null");
assert_equals(typeof expectedRange, "object",
"typeof Range produced in expected iframe");
assert_not_equals(expectedRange, null,
"Range produced in expected iframe was null");
assert_equals(typeof actualNode, "object",
"typeof Node produced in actual iframe");
assert_not_equals(actualNode, null,
"Node produced in actual iframe was null");
assert_equals(typeof expectedNode, "object",
"typeof Node produced in expected iframe");
assert_not_equals(expectedNode, null,
"Node produced in expected iframe was null");
// We want to test that the trees containing the ranges are equal, and
// also the trees containing the moved nodes. These might not be the
// same, if we're inserting a node from a detached tree or a different
// document.
//
// Detached ranges are always in the contentDocument.
if (detached) {
actualRoots.push(actualIframe.contentDocument);
expectedRoots.push(expectedIframe.contentDocument);
} else {
actualRoots.push(furthestAncestor(actualRange.startContainer));
expectedRoots.push(furthestAncestor(expectedRange.startContainer));
}
if (furthestAncestor(actualNode) != actualRoots[0]) {
actualRoots.push(furthestAncestor(actualNode));
}
if (furthestAncestor(expectedNode) != expectedRoots[0]) {
expectedRoots.push(furthestAncestor(expectedNode));
}
assert_equals(actualRoots.length, expectedRoots.length,
"Either the actual node and actual range are in the same tree but the expected are in different trees, or vice versa");
// This doctype stuff is to work around the fact that Opera 11.00 will
// move around doctypes within a document, even to totally invalid
// positions, but it won't allow a new doctype to be added to a
// document in any way I can figure out. So if we try moving a doctype
// to some invalid place, in Opera it will actually succeed, and then
// restoreIframe() will remove the doctype along with the root element,
// and then nothing can re-add the doctype. So instead, we catch it
// during the test itself and move it back to the right place while we
// still can.
//
// I spent *way* too much time debugging and working around this bug.
var actualDoctype = actualIframe.contentDocument.doctype;
var expectedDoctype = expectedIframe.contentDocument.doctype;
var result;
try {
result = myInsertNode(expectedRange, expectedNode);
} catch (e) {
if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
}
throw e;
}
if (typeof result == "string") {
assert_throws_dom(result, actualIframe.contentWindow.DOMException, function() {
try {
actualRange.insertNode(actualNode);
} catch (e) {
if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
}
if (actualDoctype != actualIframe.contentDocument.firstChild) {
actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
}
throw e;
}
}, "A " + result + " DOMException must be thrown in this case");
// Don't return, we still need to test DOM equality
} else {
try {
actualRange.insertNode(actualNode);
} catch (e) {
if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
}
if (actualDoctype != actualIframe.contentDocument.firstChild) {
actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
}
throw e;
}
}
for (var k = 0; k < actualRoots.length; k++) {
assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
}
});
domTests[i][j].done();
positionTests[i][j].step(function() {
assert_equals(actualIframe.contentWindow.unexpectedException, null,
"Unexpected exception thrown when setting up Range for actual insertNode()");
assert_equals(expectedIframe.contentWindow.unexpectedException, null,
"Unexpected exception thrown when setting up Range for simulated insertNode()");
assert_equals(typeof actualRange, "object",
"typeof Range produced in actual iframe");
assert_not_equals(actualRange, null,
"Range produced in actual iframe was null");
assert_equals(typeof expectedRange, "object",
"typeof Range produced in expected iframe");
assert_not_equals(expectedRange, null,
"Range produced in expected iframe was null");
assert_equals(typeof actualNode, "object",
"typeof Node produced in actual iframe");
assert_not_equals(actualNode, null,
"Node produced in actual iframe was null");
assert_equals(typeof expectedNode, "object",
"typeof Node produced in expected iframe");
assert_not_equals(expectedNode, null,
"Node produced in expected iframe was null");
for (var k = 0; k < actualRoots.length; k++) {
assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
}
if (detached) {
// No further tests we can do
return;
}
assert_equals(actualRange.startOffset, expectedRange.startOffset,
"Unexpected startOffset after insertNode()");
assert_equals(actualRange.endOffset, expectedRange.endOffset,
"Unexpected endOffset after insertNode()");
// How do we decide that the two nodes are equal, since they're in
// different trees? Since the DOMs are the same, it's enough to check
// that the index in the parent is the same all the way up the tree.
// But we can first cheat by just checking they're actually equal.
assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
"Unexpected startContainer after insertNode(), expected " +
expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
actualRange.startContainer.nodeName.toLowerCase());
var currentActual = actualRange.startContainer;
var currentExpected = expectedRange.startContainer;
var actual = "";
var expected = "";
while (currentActual && currentExpected) {
actual = indexOf(currentActual) + "-" + actual;
expected = indexOf(currentExpected) + "-" + expected;
currentActual = currentActual.parentNode;
currentExpected = currentExpected.parentNode;
}
actual = actual.substr(0, actual.length - 1);
expected = expected.substr(0, expected.length - 1);
assert_equals(actual, expected,
"startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened");
});
positionTests[i][j].done();
}
testRanges.unshift('"detached"');
var iStart = 0;
var iStop = testRangesShort.length;
var jStart = 0;
var jStop = testNodesShort.length;
if (/subtest=[0-9]+,[0-9]+/.test(location.search)) {
var matches = /subtest=([0-9]+),([0-9]+)/.exec(location.search);
iStart = Number(matches[1]);
iStop = Number(matches[1]) + 1;
jStart = Number(matches[2]) + 0;
jStop = Number(matches[2]) + 1;
}
var domTests = [];
var positionTests = [];
for (var i = iStart; i < iStop; i++) {
domTests[i] = [];
positionTests[i] = [];
for (var j = jStart; j < jStop; j++) {
domTests[i][j] = async_test(i + "," + j + ": resulting DOM for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
positionTests[i][j] = async_test(i + "," + j + ": resulting range position for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
}
}
var actualIframe = document.createElement("iframe");
actualIframe.style.display = "none";
document.body.appendChild(actualIframe);
var expectedIframe = document.createElement("iframe");
expectedIframe.style.display = "none";
document.body.appendChild(expectedIframe);
var referenceDoc = document.implementation.createHTMLDocument("");
referenceDoc.removeChild(referenceDoc.documentElement);
actualIframe.onload = function() {
expectedIframe.onload = function() {
for (var i = iStart; i < iStop; i++) {
for (var j = jStart; j < jStop; j++) {
testInsertNode(i, j);
}
}
}
expectedIframe.src = "resources/Range-test-iframe.html";
referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
}
// FIXME: This location of this file has been modified so that this file is not treated as a test file.
actualIframe.src = "resources/Range-test-iframe.html";
</script>

View file

@ -0,0 +1,56 @@
<!doctype html>
<title>Range test iframe</title>
<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
<meta name=timeout content=long>
<body onload=run()>
<script src=../../common.js></script>
<script>
"use strict";
// This script only exists because we want to evaluate the range endpoints
// in each iframe using that iframe's local variables set up by common.js. It
// just creates the range and does nothing else. The data is returned via
// window.testRange, and if an exception is thrown, it's put in
// window.unexpectedException.
window.unexpectedException = null;
function run() {
try {
window.unexpectedException = null;
if (typeof window.testNodeInput != "undefined") {
window.testNode = eval(window.testNodeInput);
}
var rangeEndpoints;
if (typeof window.testRangeInput == "undefined") {
// Use the hash (old way of doing things, bad because it requires
// navigation)
if (location.hash == "") {
return;
}
rangeEndpoints = eval(location.hash.substr(1));
} else {
// Get the variable directly off the window, faster and can be done
// synchronously
rangeEndpoints = eval(window.testRangeInput);
}
var range;
if (rangeEndpoints == "detached") {
range = document.createRange();
range.detach();
} else {
range = ownerDocument(rangeEndpoints[0]).createRange();
range.setStart(rangeEndpoints[0], rangeEndpoints[1]);
range.setEnd(rangeEndpoints[2], rangeEndpoints[3]);
}
window.testRange = range;
} catch(e) {
window.unexpectedException = e;
}
}
testDiv.style.display = "none";
</script>