mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 16:01:01 +00:00
fix(dom): enforce template content host hierarchy
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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/");
|
||||
|
||||
Reference in New Issue
Block a user