fix(dom): bind DOMParser to its relevant document

This commit is contained in:
ldm0
2026-10-02 03:28:07 +08:00
parent 76325f2161
commit 4f79a50468
4 changed files with 263 additions and 20 deletions
@@ -111,6 +111,14 @@ impl DocumentRuntime {
host_ptr: *mut JsContextHost,
roots: &[DomHandle],
) {
let iframe_roots = roots
.iter()
.copied()
.filter(|root| self.dom_host.is_html_element_named(*root, "iframe"))
.collect::<Vec<_>>();
if iframe_roots.is_empty() {
return;
}
let Some(event_ctor) = scope
.get_current_context()
.global(scope)
@@ -119,10 +127,7 @@ impl DocumentRuntime {
else {
return;
};
for &root in roots {
if !self.dom_host.is_html_element_named(root, "iframe") {
continue;
}
for root in iframe_roots {
let Some(event) = event_ctor.new_instance(scope, &[v8str(scope, "load").into()]) else {
continue;
};
+97 -13
View File
@@ -4,20 +4,28 @@ use moli_webapi_declare::WebApiFunctionTemplate;
use url::Url;
use crate::{
document_runtime::DomHandle,
dom::native::{DomHost, NativeDom, NativeNodeId},
parser::{HtmlParser, XmlParser},
webidl,
};
use super::{
native_bridge::document::{
build_detached_document_object_from_dom_host,
build_detached_document_object_from_dom_host_with_content_type,
native_bridge::{
OwnerDispatchScope,
document::{
build_detached_document_object_from_dom_host,
build_detached_document_object_from_dom_host_with_content_type,
},
},
util::{
context_host_ptr_from_global_bridge, get_private_object, get_private_value,
set_private_value, throw_type_error,
},
util::{context_host_ptr_from_global_bridge, get_private_object, throw_type_error},
};
pub(crate) const DOM_PARSER_FOREIGN_NODE_SLOT: &str = "__moliDomParserForeignNode";
const DOM_PARSER_DOCUMENT_HANDLE_SLOT: &str = "__moliDomParserDocumentHandle";
const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
const PARSER_ERROR_STYLE: &str = "display: block; white-space: pre; border: 2px solid #c77; padding: 0 1em 0 1em; margin: 1em; background-color: #fdd; color: black";
const PARSER_ERROR_DETAIL_STYLE: &str = "font-family:monospace;font-size:12px";
@@ -118,7 +126,21 @@ pub(super) fn dom_parser_constructor_callback(
throw_type_error(scope, "DOMParser constructor must be called with new");
return;
}
rv.set(args.this().into());
let parser = args.this();
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
throw_type_error(scope, "DOMParser constructor has no associated Document");
return;
};
let runtime = unsafe { &*host_ptr };
let document_handle = dom_parser_constructor_document_handle(scope, runtime);
let handle_value = v8::BigInt::new_from_u64(scope, document_handle.index() as u64);
set_private_value(
scope,
parser,
DOM_PARSER_DOCUMENT_HANDLE_SLOT,
handle_value.into(),
);
rv.set(parser.into());
}
pub(super) fn dom_parser_parse_from_string_callback<'s>(
@@ -126,6 +148,13 @@ pub(super) fn dom_parser_parse_from_string_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(document_handle) = dom_parser_document_handle(scope, args.this()) else {
throw_type_error(
scope,
"Failed to execute 'parseFromString' on 'DOMParser': Illegal invocation.",
);
return;
};
let Some(parsed) = webidl::parse_args::<DomParserParseFromStringArgs>(scope, &args) else {
return;
};
@@ -150,14 +179,53 @@ pub(super) fn dom_parser_parse_from_string_callback<'s>(
source
}
};
let Some(obj) = parse_detached_document_from_string(scope, &source, parsed.mime.as_mime())
else {
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
rv.set(v8::null(scope).into());
return;
};
let document_url = unsafe { &*host_ptr }.document_url_for_handle(document_handle);
let Some(obj) = parse_detached_document_from_string_with_url(
scope,
document_url,
&source,
parsed.mime.as_mime(),
) else {
rv.set(v8::null(scope).into());
return;
};
rv.set(obj.into());
}
fn dom_parser_constructor_document_handle(
scope: &mut v8::PinScope<'_, '_>,
runtime: &super::native_bridge::JsContextHost,
) -> DomHandle {
let context = scope.get_current_context();
let Some(identity) = runtime.window_execution_context_identity_for_v8_context(scope, context)
else {
return runtime.document_handle();
};
match identity.dispatch_scope() {
OwnerDispatchScope::Top => runtime.document_handle(),
OwnerDispatchScope::Child(handle) => runtime
.child_browsing_context_document_handle(handle)
.unwrap_or_else(|| runtime.document_handle()),
OwnerDispatchScope::LightweightPopup(popup_id) => runtime
.lightweight_popup_document_handle(popup_id)
.unwrap_or_else(|| runtime.document_handle()),
}
}
fn dom_parser_document_handle<'s>(
scope: &mut v8::PinScope<'s, '_>,
parser: v8::Local<'s, v8::Object>,
) -> Option<DomHandle> {
let value = get_private_value(scope, parser, DOM_PARSER_DOCUMENT_HANDLE_SLOT)?;
let value = v8::Local::<v8::BigInt>::try_from(value).ok()?;
let (index, lossless) = value.u64_value();
lossless.then(|| DomHandle::new(index as usize))
}
pub(crate) fn install_dom_parser_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -185,16 +253,32 @@ pub(super) fn parse_detached_document_from_string<'s>(
let host_ptr = context_host_ptr_from_global_bridge(scope)?;
let runtime = unsafe { &*host_ptr };
parse_detached_document_from_string_with_url(
scope,
runtime.document_url().clone(),
source,
mime,
)
}
fn parse_detached_document_from_string_with_url<'s>(
scope: &mut v8::PinScope<'s, '_>,
document_url: Url,
source: &str,
mime: &str,
) -> Option<v8::Local<'s, v8::Object>> {
let is_html = is_html_document_mime(mime);
let is_xml = is_dom_parser_xml_mime(mime);
if !is_html && !is_xml {
return None;
}
if is_html {
return parse_detached_html_document_from_source(
scope,
runtime.document_url().clone(),
source,
);
return parse_detached_html_document_from_source(scope, document_url, source);
}
let parser = XmlParser;
let parsed = parser.parse(runtime.document_url().clone(), source.to_owned());
let parsed = parser.parse(document_url, source.to_owned());
let parsed = if parsed.parse_errors().is_empty() && native_document_has_element_child(&parsed) {
parsed
} else {
@@ -141,9 +141,7 @@ fn define_detached_attribute_maps_for_live_element<'s>(
namespace_attributes.into(),
);
if let Some(snapshot) =
live_native_attribute_snapshot(scope, element).filter(|snapshot| !snapshot.is_empty())
{
if let Some(snapshot) = live_native_attribute_snapshot(scope, element) {
for attribute in snapshot {
detached_map_set(scope, attributes, &attribute.name, &attribute.value);
if attribute.namespace_uri.is_some()
@@ -96,6 +96,162 @@ fn dom_parser_prototype_parse_from_string_is_declared_operation() {
);
}
#[test]
fn dom_parser_uses_its_constructor_realm_associated_document() {
let mut vm = new_storage_test_vm("https://dom-parser-realm.test/top.html");
let result = vm
.eval(
r#"
(() => {
const frame = document.createElement('iframe');
(document.body || document.documentElement || document).appendChild(frame);
const child = frame.contentWindow;
child.history.replaceState(null, '', '/child-v1.html');
const topParser = new DOMParser();
const childParser = new child.DOMParser();
const childParserWithTopNewTarget = Reflect.construct(
child.DOMParser,
[],
DOMParser
);
const topParserWithChildNewTarget = Reflect.construct(
DOMParser,
[],
child.DOMParser
);
const parseUrl = parser =>
DOMParser.prototype.parseFromString.call(
parser,
'<html></html>',
'text/html'
).URL;
const beforeSameDocumentUpdate = parseUrl(childParser);
child.history.replaceState(null, '', '/child-v2.html');
let invalidReceiver;
try {
DOMParser.prototype.parseFromString.call({}, '<html></html>', 'text/html');
invalidReceiver = 'no-throw';
} catch (error) {
invalidReceiver = error.name;
}
return JSON.stringify({
top: parseUrl(topParser),
child: parseUrl(childParser),
childWithTopNewTarget: parseUrl(childParserWithTopNewTarget),
topWithChildNewTarget: child.DOMParser.prototype.parseFromString.call(
topParserWithChildNewTarget,
'<html></html>',
'text/html'
).URL,
beforeSameDocumentUpdate,
invalidReceiver
});
})()
"#,
)
.expect("DOMParser relevant realm probe should evaluate");
assert_eq!(
result,
r#"{"top":"https://dom-parser-realm.test/top.html","child":"https://dom-parser-realm.test/child-v2.html","childWithTopNewTarget":"https://dom-parser-realm.test/child-v2.html","topWithChildNewTarget":"https://dom-parser-realm.test/top.html","beforeSameDocumentUpdate":"https://dom-parser-realm.test/child-v1.html","invalidReceiver":"TypeError"}"#
);
}
#[tokio::test(flavor = "current_thread")]
async fn dom_parser_retained_across_child_navigation_uses_original_document() {
const HOST: &str = "dom-parser-retained.test";
let server = StaticHttpServer::spawn(2).await;
let top_url = server.url_for_host(HOST, "/top.html");
let loader = static_http_loader([server.resolve_entry(HOST)]);
let mut vm = new_storage_page_task_executor_test_vm_with_loader(top_url.as_str(), &loader);
vm.eval(
r#"
(() => {
const frame = document.createElement('iframe');
globalThis.__domParserRetainedLoadCount = 0;
frame.onload = () => { globalThis.__domParserRetainedLoadCount += 1; };
frame.src = '/child.html?1';
(document.body || document.documentElement || document).appendChild(frame);
globalThis.__domParserRetainedFrame = frame;
})()
"#,
)
.expect("retained DOMParser initial navigation should evaluate");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(globalThis.__domParserRetainedLoadCount)",
"1",
"initial same-origin DOMParser child should load",
)
.await;
vm.eval(
r#"
(() => {
const frame = globalThis.__domParserRetainedFrame;
const child = frame.contentWindow;
const parser = new child.DOMParser();
globalThis.__domParserRetainedParser = parser;
globalThis.__domParserRetainedMethod = child.DOMParser.prototype.parseFromString;
globalThis.__domParserRetainedBoundMethod = parser.parseFromString.bind(parser);
globalThis.__domParserRetainedUrlBeforeNavigation = child.document.URL;
frame.src = '/child.html?2';
})()
"#,
)
.expect("retained DOMParser navigation setup should evaluate");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(globalThis.__domParserRetainedLoadCount)",
"2",
"replacement same-origin DOMParser child should load",
)
.await;
let result = vm
.eval(
r#"
(() => {
const probe = callback => {
try {
return callback();
} catch (error) {
return `error:${error.name}:${error.message}`;
}
};
const parser = globalThis.__domParserRetainedParser;
const source = '<html></html>';
const oldUrl = globalThis.__domParserRetainedUrlBeforeNavigation;
return JSON.stringify({
property: probe(() => typeof parser.parseFromString),
topCallUsesOldUrl: probe(() => DOMParser.prototype.parseFromString.call(parser, source, 'text/html').URL === oldUrl),
savedCallUsesOldUrl: probe(() => __domParserRetainedMethod.call(parser, source, 'text/html').URL === oldUrl),
boundCallUsesOldUrl: probe(() => __domParserRetainedBoundMethod(source, 'text/html').URL === oldUrl),
usesReplacementPrototype: probe(() => Object.getPrototypeOf(parser) === DOMParser.prototype)
});
})()
"#,
)
.expect("retained DOMParser probe should evaluate");
assert_eq!(
result,
r#"{"property":"function","topCallUsesOldUrl":true,"savedCallUsesOldUrl":true,"boundCallUsesOldUrl":true,"usesReplacementPrototype":false}"#
);
assert_eq!(
server.finish_targets().await,
vec!["/child.html?1", "/child.html?2"]
);
}
#[test]
fn dom_parser_xml_text_content_excludes_processing_instruction_descendants() {
let mut vm = new_storage_test_vm("https://dom-parser-xml-text-content.test/");