mirror of
https://github.com/lexmount/moli.git
synced 2026-10-31 00:06:19 +00:00
fix(dom): validate Range DocumentType contents before mutation
This commit is contained in:
@@ -175,6 +175,31 @@ fn process_contents_from_snapshot<'s>(
|
||||
highest_ancestor_under_common_root(scope, snapshot.start_container, common_root);
|
||||
let partial_end =
|
||||
highest_ancestor_under_common_root(scope, snapshot.end_container, common_root);
|
||||
let mut process_start = child_of_common_root_before_offset(
|
||||
scope,
|
||||
snapshot.start_container,
|
||||
snapshot.start_offset,
|
||||
common_root,
|
||||
)?;
|
||||
if snapshot.start_container != common_root {
|
||||
process_start = process_start.and_then(|node| next_sibling_handle(scope, node));
|
||||
}
|
||||
let process_end = child_of_common_root_before_offset(
|
||||
scope,
|
||||
snapshot.end_container,
|
||||
snapshot.end_offset,
|
||||
common_root,
|
||||
)?;
|
||||
|
||||
let contained_children = match process_start {
|
||||
Some(start) => collect_following_siblings_until(scope, start, process_end),
|
||||
None => Vec::new(),
|
||||
};
|
||||
if action.produces_fragment() && contains_document_type(scope, &contained_children) {
|
||||
throw_contents_hierarchy_request_error(scope);
|
||||
return None;
|
||||
}
|
||||
|
||||
let mutation_collapse_target = if action.mutates_range_contents() {
|
||||
Some(contents_mutation_collapse_target(
|
||||
scope,
|
||||
@@ -229,22 +254,6 @@ fn process_contents_from_snapshot<'s>(
|
||||
)?
|
||||
};
|
||||
|
||||
let mut process_start = child_of_common_root_before_offset(
|
||||
scope,
|
||||
snapshot.start_container,
|
||||
snapshot.start_offset,
|
||||
common_root,
|
||||
)?;
|
||||
if snapshot.start_container != common_root {
|
||||
process_start = process_start.and_then(|node| next_sibling_handle(scope, node));
|
||||
}
|
||||
let process_end = child_of_common_root_before_offset(
|
||||
scope,
|
||||
snapshot.end_container,
|
||||
snapshot.end_offset,
|
||||
common_root,
|
||||
)?;
|
||||
|
||||
if action.mutates_range_contents() {
|
||||
collapse_range_after_contents_mutation(
|
||||
scope,
|
||||
@@ -260,10 +269,7 @@ fn process_contents_from_snapshot<'s>(
|
||||
let _ = append_child_internal_handle(scope, fragment, left_contents);
|
||||
}
|
||||
|
||||
if let Some(process_start) = process_start {
|
||||
let nodes = collect_following_siblings_until(scope, process_start, process_end);
|
||||
process_nodes(scope, action, nodes, common_root, fragment)?;
|
||||
}
|
||||
process_nodes(scope, action, contained_children, common_root, fragment)?;
|
||||
|
||||
if let (Some(fragment), Some(right_contents)) = (fragment, right_contents) {
|
||||
let _ = append_child_internal_handle(scope, fragment, right_contents);
|
||||
|
||||
+61
@@ -1012,3 +1012,64 @@ fn svg_svg_element_deselect_all_clears_the_owner_document_selection() {
|
||||
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_document_type_preflight_preserves_partial_contents_and_live_boundaries() {
|
||||
let mut vm = new_storage_test_vm("https://range-doctype-preflight.test/");
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
let cases = 0;
|
||||
for (const method of ['extractContents', 'cloneContents']) {
|
||||
for (const kind of ['document-start', 'partial-comment', 'same-container']) {
|
||||
const doc = document.implementation.createDocument(null, 'root',
|
||||
document.implementation.createDocumentType('root', '', ''));
|
||||
const root = doc.documentElement;
|
||||
root.appendChild(doc.createTextNode('abcd'));
|
||||
const text = root.firstChild;
|
||||
const comment = doc.createComment('before');
|
||||
doc.insertBefore(comment, doc.doctype);
|
||||
const doctype = doc.doctype;
|
||||
const children = Array.from(doc.childNodes);
|
||||
const range = doc.createRange();
|
||||
if (kind === 'partial-comment') range.setStart(comment, 2);
|
||||
else range.setStart(doc, 0);
|
||||
if (kind === 'same-container') range.setEnd(doc, 2);
|
||||
else range.setEnd(text, 2);
|
||||
const other = range.cloneRange();
|
||||
const boundaries = [range.startContainer, range.startOffset,
|
||||
range.endContainer, range.endOffset];
|
||||
const observer = new MutationObserver(() => {});
|
||||
observer.observe(doc, {subtree: true, childList: true, characterData: true});
|
||||
let error;
|
||||
try { range[method](); } catch (caught) { error = caught; }
|
||||
const label = method + '/' + kind;
|
||||
const check = (condition, detail) => {
|
||||
if (!condition) throw new Error(label + ': ' + detail);
|
||||
};
|
||||
check(error && error.name === 'HierarchyRequestError', 'expected hierarchy error');
|
||||
check(text.data === 'abcd' && comment.data === 'before', 'partial data changed');
|
||||
check(doc.doctype === doctype && doctype.parentNode === doc, 'doctype moved');
|
||||
check(root.parentNode === doc && text.parentNode === root, 'tree ownership changed');
|
||||
check(children.length === doc.childNodes.length &&
|
||||
children.every((node, index) => doc.childNodes[index] === node), 'children changed');
|
||||
for (const candidate of [range, other]) {
|
||||
check(candidate.startContainer === boundaries[0] &&
|
||||
candidate.startOffset === boundaries[1] &&
|
||||
candidate.endContainer === boundaries[2] &&
|
||||
candidate.endOffset === boundaries[3], 'live boundaries changed');
|
||||
}
|
||||
check(observer.takeRecords().length === 0, 'observable mutation before rejection');
|
||||
observer.disconnect();
|
||||
++cases;
|
||||
}
|
||||
}
|
||||
return String(cases);
|
||||
})()
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
"6"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user