fix(dom): enforce template content host hierarchy

This commit is contained in:
ldm0
2026-09-16 22:06:41 +08:00
parent b6fc53e2af
commit a7bb7ddfcb
10 changed files with 196 additions and 36 deletions
+17 -1
View File
@@ -314,4 +314,20 @@ impl DocumentType {
}
#[derive(Debug, Clone, Default)]
pub struct DocumentFragment;
pub struct DocumentFragment {
host: Option<NativeNodeId>,
}
impl DocumentFragment {
pub fn new(host: Option<NativeNodeId>) -> Self {
Self { host }
}
pub fn host(&self) -> Option<NativeNodeId> {
self.host
}
pub(crate) fn set_host(&mut self, host: NativeNodeId) {
self.host = Some(host);
}
}
+2
View File
@@ -252,6 +252,7 @@ impl DomHost {
available_to_element_internals,
} => {
self.dom.register_stylesheet_candidate_tree_scope(clone);
self.dom.set_document_fragment_host(clone, host);
self.shadow_roots_by_host.borrow_mut().insert(
host,
ShadowRootState {
@@ -399,6 +400,7 @@ impl DomHost {
available_to_element_internals,
} => {
self.dom.register_stylesheet_candidate_tree_scope(clone);
self.dom.set_document_fragment_host(clone, host);
self.shadow_roots_by_host.borrow_mut().insert(
host,
ShadowRootState {
+2
View File
@@ -1698,6 +1698,7 @@ impl DomHost {
return None;
}
let root = self.create_document_fragment();
self.dom.set_document_fragment_host(root, host);
let owner_document = self.node(host).and_then(Node::owner_document);
let connected = self.is_connected(host);
self.dom.register_stylesheet_candidate_tree_scope(root);
@@ -2265,6 +2266,7 @@ impl DomHost {
continue;
}
self.dom.register_stylesheet_candidate_tree_scope(root);
self.dom.set_document_fragment_host(root, host);
self.shadow_roots_by_host.borrow_mut().insert(
host,
ShadowRootState {
@@ -72,7 +72,9 @@ impl NativeDom {
mut reference_child: Option<NativeNodeId>,
commit_candidate_registration: bool,
) -> Option<OwnerLifecycleChanges> {
if !self.can_have_children(parent) || parent == child || self.is_ancestor(child, parent) {
if !self.can_have_children(parent)
|| self.is_host_including_inclusive_ancestor(child, parent)
{
return None;
}
let parent_type = self.node(parent)?.node_type();
@@ -97,8 +99,7 @@ impl NativeDom {
{
let fragment_children = self.child_ids(child).collect::<Vec<_>>();
let all_children_are_insertable = fragment_children.iter().all(|fragment_child| {
parent != *fragment_child
&& !self.is_ancestor(*fragment_child, parent)
!self.is_host_including_inclusive_ancestor(*fragment_child, parent)
&& self.node(*fragment_child).is_some_and(|node| {
!node.data().is_document_fragment()
&& Self::can_insert_child_type(parent_type, node.node_type())
+1 -1
View File
@@ -86,7 +86,7 @@ impl DomHost {
if is_template {
let template_contents = self
.dom
.create_template_contents_fragment_for_document(document_handle);
.create_template_contents_fragment_for_document(document_handle, Some(node_id));
if let Some(element) = self
.node_mut(node_id)
.and_then(|node| node.data_mut().as_element_mut())
+94 -7
View File
@@ -353,7 +353,7 @@ impl NativeDom {
false,
);
if local_name.eq_ignore_ascii_case("template") {
let fragment = self.create_template_contents_fragment();
let fragment = self.create_template_contents_fragment(handle);
if let Some(element) = self
.node_mut(handle)
.and_then(|node| node.data_mut().as_element_mut())
@@ -392,7 +392,7 @@ impl NativeDom {
false,
);
if local_name.eq_ignore_ascii_case("template") {
let fragment = self.create_template_contents_fragment();
let fragment = self.create_template_contents_fragment(handle);
if let Some(element) = self
.node_mut(handle)
.and_then(|node| node.data_mut().as_element_mut())
@@ -496,15 +496,15 @@ impl NativeDom {
owner_document: NativeNodeId,
) -> NativeNodeId {
self.create_node(
NodeData::DocumentFragment(DocumentFragment),
NodeData::DocumentFragment(DocumentFragment::default()),
Some(owner_document),
false,
false,
)
}
pub fn create_template_contents_fragment(&mut self) -> NativeNodeId {
self.create_template_contents_fragment_for_document(self.document_node_id)
pub fn create_template_contents_fragment(&mut self, host: NativeNodeId) -> NativeNodeId {
self.create_template_contents_fragment_for_document(self.document_node_id, Some(host))
}
pub fn is_inert_template_document(&self, document_handle: NativeNodeId) -> bool {
@@ -514,9 +514,33 @@ impl NativeDom {
pub fn create_template_contents_fragment_for_document(
&mut self,
document_handle: NativeNodeId,
host: Option<NativeNodeId>,
) -> NativeNodeId {
let owner_document = self.appropriate_template_contents_owner_document(document_handle);
self.create_document_fragment_for_document(owner_document)
self.create_node(
NodeData::DocumentFragment(DocumentFragment::new(host)),
Some(owner_document),
false,
false,
)
}
pub fn set_document_fragment_host(
&mut self,
fragment: NativeNodeId,
host: NativeNodeId,
) -> bool {
let Some(fragment) = self
.node_mut(fragment)
.and_then(|node| node.data_mut().as_document_fragment_mut())
else {
return false;
};
if fragment.host() == Some(host) {
return false;
}
fragment.set_host(host);
true
}
pub(crate) fn appropriate_template_contents_owner_document(
@@ -600,6 +624,28 @@ impl NativeDom {
false
}
pub fn is_host_including_inclusive_ancestor(
&self,
candidate_ancestor: NativeNodeId,
node_id: NativeNodeId,
) -> bool {
let mut current = Some(node_id);
while let Some(handle) = current {
if handle == candidate_ancestor {
return true;
}
let Some(node) = self.node(handle) else {
return false;
};
current = node.parent_node().or_else(|| {
node.data()
.as_document_fragment()
.and_then(DocumentFragment::host)
});
}
false
}
pub fn detach_from_parent(&mut self, child: NativeNodeId) {
let _ = self.detach_from_parent_with_stylesheet_candidate_changes(child);
}
@@ -2646,7 +2692,8 @@ mod tests {
.expect("second template content owner document");
assert_eq!(second_content_owner, content_owner);
let nested_content = dom.create_template_contents_fragment_for_document(content_owner);
let nested_content =
dom.create_template_contents_fragment_for_document(content_owner, None);
assert_eq!(
dom.node(nested_content).and_then(Node::owner_document),
Some(content_owner)
@@ -2673,6 +2720,46 @@ mod tests {
);
}
#[test]
fn template_content_host_participates_in_hierarchy_checks() {
let mut dom = NativeDom::new_html(test_url());
let document = dom.document_node_id();
let parent = dom.create_element("div");
let template = dom.create_element("template");
let content = dom
.node(template)
.and_then(Node::as_element)
.and_then(Element::template_contents)
.expect("template content");
let span = dom.create_element("span");
assert!(dom.append_child(document, parent));
assert!(dom.append_child(parent, template));
assert!(dom.append_child(content, span));
assert_eq!(
dom.node(content)
.map(Node::data)
.and_then(NodeData::as_document_fragment)
.and_then(DocumentFragment::host),
Some(template)
);
assert!(dom.is_host_including_inclusive_ancestor(template, span));
assert!(dom.is_host_including_inclusive_ancestor(parent, span));
for (insertion_parent, child) in [
(content, parent),
(content, template),
(span, parent),
(span, template),
] {
assert!(!dom.append_child(insertion_parent, child));
}
assert_eq!(dom.parent_node(parent), Some(document));
assert_eq!(dom.parent_node(template), Some(parent));
assert_eq!(dom.parent_node(span), Some(content));
}
#[test]
fn cloned_template_content_keeps_template_owner_document() {
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
+7
View File
@@ -146,4 +146,11 @@ impl NodeData {
_ => None,
}
}
pub fn as_document_fragment_mut(&mut self) -> Option<&mut DocumentFragment> {
match self {
Self::DocumentFragment(fragment) => Some(fragment),
_ => None,
}
}
}
@@ -272,7 +272,7 @@ fn validate_pre_insert_parent_and_ancestor(
child: DomHandle,
) -> bool {
if !node_can_contain_children(runtime, parent)
|| node_move_before_shadow_including_contains(runtime, child, parent)
|| node_is_host_including_inclusive_ancestor(runtime, child, parent)
{
throw_dom_exception(scope, "HierarchyRequestError", 3, "Hierarchy Error");
return false;
@@ -415,7 +415,7 @@ fn node_move_before_after_argument_validation(
let runtime = unsafe { &*runtime_ptr };
if !node_move_before_is_valid_parent(runtime, parent)
|| !node_move_before_is_valid_child(runtime, child)
|| node_move_before_shadow_including_contains(runtime, child, parent)
|| node_is_host_including_inclusive_ancestor(runtime, child, parent)
|| node_move_before_shadow_including_root(runtime, parent)
!= node_move_before_shadow_including_root(runtime, child)
{
@@ -486,29 +486,14 @@ fn node_move_before_is_valid_child(runtime: &JsContextHost, handle: DomHandle) -
})
}
pub(super) fn node_move_before_shadow_including_contains(
pub(super) fn node_is_host_including_inclusive_ancestor(
runtime: &JsContextHost,
ancestor: DomHandle,
node: DomHandle,
) -> bool {
let mut current = Some(node);
while let Some(handle) = current {
if handle == ancestor {
return true;
}
current = runtime
.dom_host()
.node(handle)
.and_then(Node::parent_node)
.or_else(|| {
runtime
.dom_host()
.is_shadow_root(handle)
.then(|| runtime.dom_host().shadow_root_host(handle))
.flatten()
});
}
false
runtime
.dom_host()
.is_host_including_inclusive_ancestor(ancestor, node)
}
fn node_move_before_shadow_including_root(
@@ -1,5 +1,5 @@
use super::core::{
node_can_contain_children, node_move_before_shadow_including_contains, node_type_is_insertable,
node_can_contain_children, node_is_host_including_inclusive_ancestor, node_type_is_insertable,
pre_insert_validation_handles,
};
use super::fragment::{
@@ -148,7 +148,7 @@ fn validate_parent_node_insertion(
return false;
}
for child in inserted {
if node_move_before_shadow_including_contains(runtime, *child, parent) {
if node_is_host_including_inclusive_ancestor(runtime, *child, parent) {
throw_dom_exception(scope, "HierarchyRequestError", 3, "Hierarchy Error");
return false;
}
@@ -256,7 +256,7 @@ fn validate_parent_node_replace_children(
return false;
}
for child in inserted {
if node_move_before_shadow_including_contains(runtime, *child, parent) {
if node_is_host_including_inclusive_ancestor(runtime, *child, parent) {
throw_dom_exception(scope, "HierarchyRequestError", 3, "Hierarchy Error");
return false;
}
@@ -6737,6 +6737,66 @@ fn detached_templates_share_their_inert_owner_document() {
assert_eq!(result, "true|true|true|true|true|true|true|true|true");
}
#[test]
fn template_content_host_rejects_insertion_cycles_after_adoption() {
let mut vm = new_storage_test_vm("https://template-content-host.test/");
let result = vm
.eval(
r#"
(() => {
if (!document.documentElement) {
document.appendChild(document.createElement('html'));
}
if (!document.body) {
document.documentElement.appendChild(document.createElement('body'));
}
const parent = document.createElement('div');
const template = document.createElement('template');
template.innerHTML = '<span>content</span>';
parent.appendChild(template);
document.body.appendChild(parent);
const content = template.content;
const span = content.firstChild;
const errorName = callback => {
try {
callback();
return 'none';
} catch (error) {
return error.name;
}
};
const attempts = () => [
errorName(() => content.appendChild(parent)),
errorName(() => content.appendChild(template)),
errorName(() => span.appendChild(parent)),
errorName(() => span.appendChild(template))
];
const beforeAdoption = attempts();
const newDocument = document.implementation.createHTMLDocument('');
const adopted = newDocument.adoptNode(content);
const afterAdoption = attempts();
newDocument.body.appendChild(content);
return [
...beforeAdoption,
adopted === content,
content.ownerDocument === newDocument,
...afterAdoption,
content.firstChild === null,
span.parentNode === newDocument.body,
template.parentNode === parent
].join('|');
})()
"#,
)
.expect("template content host hierarchy probe should evaluate");
assert_eq!(
result,
"HierarchyRequestError|HierarchyRequestError|HierarchyRequestError|HierarchyRequestError|true|true|HierarchyRequestError|HierarchyRequestError|HierarchyRequestError|HierarchyRequestError|true|true|true"
);
}
#[test]
fn detached_html_image_decode_uses_prototype_method() {
let mut vm = new_storage_test_vm("https://detached-image-decode-surface.test/");