mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 16:01:01 +00:00
fix(dom): bind DOMParser to its relevant document
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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/");
|
||||
|
||||
Reference in New Issue
Block a user