diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs index d271cf6c5d..2ee1e65580 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs @@ -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>( diff --git a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs index 7f84666dfc..b647cb6cd6 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/default_action.rs @@ -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") diff --git a/moli-renderer-v8/src/native_bridge/node.rs b/moli-renderer-v8/src/native_bridge/node.rs index 396c93df74..3432e300d8 100644 --- a/moli-renderer-v8/src/native_bridge/node.rs +++ b/moli-renderer-v8/src/native_bridge/node.rs @@ -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) = diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs index 54993a9add..c2f170fa36 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs @@ -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 = ` + + + `; +})() +"#, + ) + .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");