fix(html): honor base targets across frame realms

Source-commit: 02e4964a24
This commit is contained in:
ldm0
2026-09-28 01:54:16 +08:00
parent a47b30ae5e
commit daa121bb53
4 changed files with 141 additions and 10 deletions
@@ -172,13 +172,28 @@ fn bind_frame_element<'s>(
handle: crate::document_runtime::DomHandle,
) {
let host_ptr = host as *mut JsContextHost;
if let Some(frame_element) = host
.native_bridge_mut()
.wrap_handle(scope, host_ptr, handle)
.map(Into::into)
{
set_private_value(scope, global, "__moliWindowFrameElement", frame_element);
}
let Some(owner_context) =
crate::native_bridge::node::node_owner_document_relevant_context(scope, host_ptr, handle)
else {
return;
};
let frame_element = {
let owner_scope = &mut v8::ContextScope::new(scope, owner_context);
let Some(frame_element) =
host.native_bridge_mut()
.wrap_handle(owner_scope, host_ptr, handle)
else {
return;
};
v8::Global::new(owner_scope, frame_element)
};
let frame_element = v8::Local::new(scope, &frame_element);
set_private_value(
scope,
global,
"__moliWindowFrameElement",
frame_element.into(),
);
}
fn bind_navigation<'s>(
@@ -1890,14 +1890,22 @@ fn anchor_click_default_action(
None,
);
}
let declared_target_name =
element_attribute(runtime, handle, "target").filter(|value| !value.is_empty());
let effective_target_name = element_attribute(runtime, handle, "target")
.filter(|value| !value.is_empty())
.or_else(|| {
let document = runtime.dom_host().owner_document_handle(handle)?;
runtime
.dom_host()
.document_base_target_for_handle(document)
.filter(|target| !target.is_empty())
.map(str::to_owned)
});
let (target_name, popup_disposition) = match navigation_policy {
HyperlinkNavigationPolicy::Auxiliary(disposition) => {
(Some("_blank".to_owned()), disposition)
}
HyperlinkNavigationPolicy::Current => {
(declared_target_name, RendererPopupDisposition::Foreground)
(effective_target_name, RendererPopupDisposition::Foreground)
}
HyperlinkNavigationPolicy::Download => {
unreachable!("download hyperlink policy returned before target selection")
@@ -1346,6 +1346,9 @@ pub(super) fn node_owner_document_relevant_context<'s>(
let document_handle = unsafe { &*runtime_ptr }
.dom_host()
.owner_document_handle(handle)?;
if document_handle == unsafe { &*runtime_ptr }.document_handle() {
return unsafe { &*runtime_ptr }.page_default_context(scope);
}
if let Some(child_handle) =
unsafe { &*runtime_ptr }.child_browsing_context_host_for_document_handle(document_handle)
&& let Some(context) =
@@ -675,6 +675,111 @@ fn child_webassembly_constructors_use_newtarget_child_realm_default_prototype()
r#"{"namespaceIsSeparate":true,"constructorFunctionPrototype":true,"moduleDirect":true,"moduleBound":true,"moduleProxy":true,"moduleProxyBound":true,"memoryProxy":true,"compileErrorDirect":true,"moduleTopRealm":true,"moduleIntrinsicSurvivesNamespaceReplacement":true}"#
);
}
#[tokio::test(flavor = "current_thread")]
async fn base_target_navigation_exposes_replacement_document_before_iframe_load() {
const HOST: &str = "anchor-base-target.test";
let server = StaticHttpServer::spawn(3).await;
let top_url = server.url_for_host(HOST, "/path/page.html");
let replacement_url = server.url_for_host(HOST, "/replacement.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#"
(() => {
globalThis.__targetLoadCount = 0;
globalThis.__targetLoadAccess = "pending";
globalThis.__frameLoadCount = 0;
globalThis.__baseTargetOwnerRealm = "pending";
const root = document.documentElement || document.appendChild(document.createElement('html'));
const body = document.body || root.appendChild(document.createElement('body'));
document.addEventListener('load', () => { __frameLoadCount += 1; }, true);
body.innerHTML = `
<iframe id="base-target-source" name="sourceFrame" src="/source.html"></iframe>
<iframe id="base-target-target" name="targetFrame" src="/target.html"></iframe>
`;
})()
"#,
)
.expect("base-target network child setup should evaluate");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__frameLoadCount)",
"2",
"initial parser-created base-target child documents should load",
)
.await;
vm.eval(
r#"
(() => {
const source = document.getElementById('base-target-source');
const target = document.getElementById('base-target-target');
__baseTargetOwnerRealm = [
source instanceof HTMLIFrameElement,
target instanceof HTMLIFrameElement,
target instanceof target.contentWindow.HTMLIFrameElement,
target.contentWindow.frameElement === target
].join('|');
const doc = source.contentDocument;
const firstBase = doc.createElement('base');
firstBase.target = 'targetFrame';
const secondBase = doc.createElement('base');
secondBase.target = '_self';
doc.head.append(firstBase, secondBase);
const link = doc.createElement('a');
link.href = '/replacement.html';
link.setAttribute('target', '');
doc.body.appendChild(link);
target.addEventListener('load', () => {
__targetLoadCount += 1;
try {
__targetLoadAccess = target.contentDocument.location.href;
} catch (error) {
__targetLoadAccess = `${error && error.name}:${error && error.message}`;
}
}, true);
link.click();
})()
"#,
)
.expect("base-target replacement navigation should start");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__targetLoadCount)",
"1",
"base-target replacement document should load",
)
.await;
assert_eq!(
vm.eval("__baseTargetOwnerRealm")
.expect("base-target frame owner realm result should evaluate"),
"true|true|false|true",
"parser-created frame elements and window.frameElement must use the owner Document realm"
);
assert_eq!(
vm.eval("__targetLoadAccess")
.expect("target load callback access result should evaluate"),
replacement_url.as_str(),
"the replacement Document must be same-origin accessible during the iframe load callback"
);
let mut targets = server.finish_targets().await;
targets.sort();
assert_eq!(
targets,
["/replacement.html", "/source.html", "/target.html"]
);
}
#[test]
fn targeted_anchor_click_reports_same_document_hash_change_for_child_window() {
let mut vm = new_storage_test_vm("https://targeted-child-hash.test/page.html");