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 9a3d0242de..bd330176ed 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 @@ -36,9 +36,9 @@ use super::super::{ update_focus, }; use super::targets::{ - SpecialBrowsingContextTarget, named_iframe_target_handle_for_navigation, - navigate_hyperlink_source_browsing_context, navigate_hyperlink_target_browsing_context, - navigate_target_browsing_context, + SpecialBrowsingContextTarget, form_navigation_target_document, + named_iframe_target_handle_for_navigation, navigate_hyperlink_source_browsing_context, + navigate_hyperlink_target_browsing_context, navigate_target_browsing_context, }; fn array_like_length(scope: &mut v8::PinScope<'_, '_>, object: v8::Local<'_, v8::Object>) -> u32 { @@ -2339,17 +2339,21 @@ pub(in crate::native_bridge) fn navigate_form_target_browsing_context( && !has_noopener && (has_opener || special_target != Some(SpecialBrowsingContextTarget::Blank)) }; - if target_name.is_none() || special_target == Some(SpecialBrowsingContextTarget::Current) { + let target_document = { let runtime = unsafe { &*runtime_ptr }; - let document_handle = runtime + runtime .dom_host() - .node(form_handle) - .and_then(Node::owner_document); - if let Some(document_handle) = document_handle - && document_handle != runtime.document_handle() - && let Some(child_handle) = + .owner_document_handle(form_handle) + .and_then(|source| form_navigation_target_document(runtime, source, target_name)) + }; + if let Some(document_handle) = target_document { + let runtime = unsafe { &*runtime_ptr }; + if document_handle != runtime.document_handle() { + let Some(child_handle) = runtime.child_browsing_context_handle_by_document_handle(scope, document_handle) - { + else { + return false; + }; let runtime = unsafe { &mut *runtime_ptr }; return runtime.navigate_child_browsing_context_to_url( scope, diff --git a/moli-renderer-v8/src/native_bridge/element/activation/mod.rs b/moli-renderer-v8/src/native_bridge/element/activation/mod.rs index 0219bdd595..cda4f3337e 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/mod.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/mod.rs @@ -13,11 +13,13 @@ pub(crate) use default_action::{ prepare_legacy_activation_for_dispatched_click, replace_contenteditable_selection, scroll_to_url_fragment_or_top, select_contenteditable_contents, }; -pub(in crate::native_bridge) use targets::named_iframe_target_handle_for_navigation; pub(crate) use targets::{ SpecialBrowsingContextTarget, navigate_existing_browsing_context_target, navigate_named_iframe_target, }; +pub(in crate::native_bridge) use targets::{ + form_navigation_target_document, named_iframe_target_handle_for_navigation, +}; pub(in crate::native_bridge::element) use targets::{ queue_deferred_named_iframe_target_navigation_from_document, queue_deferred_named_iframe_target_request, diff --git a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs index f9af6e32d5..8277a58b42 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs @@ -37,6 +37,35 @@ impl SpecialBrowsingContextTarget { } } +// Resolve keywords from the native form Document, independently of the realm +// from which its submission method was called. Both GET and POST use this. +pub(in crate::native_bridge) fn form_navigation_target_document( + runtime: &JsContextHost, + source: crate::document_runtime::DomHandle, + target_name: Option<&str>, +) -> Option { + // Lightweight popups retain their existing navigation routing. + if source != runtime.document_handle() + && runtime + .child_browsing_context_host_for_document_handle(source) + .is_none() + { + return None; + } + let target = target_name.and_then(SpecialBrowsingContextTarget::parse); + match (target_name, target) { + (None, _) | (_, Some(SpecialBrowsingContextTarget::Current)) => Some(source), + (_, Some(SpecialBrowsingContextTarget::Top)) => Some(runtime.document_handle()), + (_, Some(SpecialBrowsingContextTarget::Parent)) => Some( + runtime + .child_browsing_context_host_for_document_handle(source) + .and_then(|child| runtime.dom_host().owner_document_handle(child)) + .unwrap_or(source), + ), + _ => None, + } +} + fn navigate_target_window_location( scope: &mut v8::PinScope<'_, '_>, window: v8::Local<'_, v8::Object>, diff --git a/moli-renderer-v8/src/native_bridge/element/forms/submission.rs b/moli-renderer-v8/src/native_bridge/element/forms/submission.rs index ccab81d4d0..7919cbb02d 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms/submission.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms/submission.rs @@ -2,7 +2,8 @@ use super::*; use crate::blob; use crate::native_bridge::context_host::ChildBrowsingContextNavigationRequest; use crate::native_bridge::element::activation::{ - SpecialBrowsingContextTarget, named_iframe_target_handle_for_navigation, + SpecialBrowsingContextTarget, form_navigation_target_document, + named_iframe_target_handle_for_navigation, }; use crate::native_bridge::element::{ NodePublicEventDispatchOutcome, TextEditInputType, activate_default_submit_button_via_keyboard, @@ -707,21 +708,27 @@ pub(in crate::native_bridge) fn submit_form_default_action( content_type, form_data_entries, } => { - if (target_name.is_none() - || special_target == Some(SpecialBrowsingContextTarget::Current)) - && submit_post_form_to_child_self_browsing_context( + let target_document = source_document.and_then(|source| { + form_navigation_target_document( + unsafe { &*runtime_ptr }, + source, + target_name.as_deref(), + ) + }); + if let Some(target_document) = target_document + && target_document != unsafe { &*runtime_ptr }.document_handle() + { + return submit_post_form_to_child_browsing_context( scope, runtime_ptr, form_handle, submitter, - source_document, - resolved_url.clone(), - body.clone(), - content_type.clone(), + target_document, + resolved_url, + body, + content_type, &form_data_entries, - ) - { - return true; + ); } submit_post_form_to_top_level_browsing_context( scope, @@ -960,26 +967,23 @@ fn submit_post_form_to_top_level_browsing_context( true } -fn submit_post_form_to_child_self_browsing_context( +fn submit_post_form_to_child_browsing_context( scope: &mut v8::PinScope<'_, '_>, runtime_ptr: *mut JsContextHost, form_handle: DomHandle, submitter: Option, - source_document: Option, + target_document: DomHandle, resolved_url: Url, body: Vec, content_type: String, form_data_entries: &[(String, v8::Global)], ) -> bool { - let Some(source_document) = source_document else { - return false; - }; let Some(child_handle) = ({ let runtime = unsafe { &mut *runtime_ptr }; - if source_document == runtime.document_handle() { + if target_document == runtime.document_handle() { None } else { - runtime.child_browsing_context_host_for_document_handle(source_document) + runtime.child_browsing_context_host_for_document_handle(target_document) } }) else { return false; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs index 67f48a5838..01c8a45ffa 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs @@ -6008,3 +6008,122 @@ fn disconnected_radio_groups_follow_tree_roots_and_form_owners() { "true|true:false|true|true:false|true|true|true:true|false:true" ); } + +#[test] +fn native_form_keywords_resolve_from_owner_for_get_and_post() { + for depth in 0usize..=2 { + for target in ["_self", "_PARENT", "_top"] { + for method in ["get", "post"] { + for api in ["submit", "requestSubmit"] { + let mut vm = new_parsed_test_vm( + "https://form-target.test/source", + "", + ); + vm.eval(&format!(r#" + (() => {{ + let source=document; + for(let i=0;i<{depth};i++) {{ + const frame=source.createElement('iframe'); + (source.body||source.documentElement||source).appendChild(frame); + frame.srcdoc=''; + source=frame.contentDocument; + }} + const form=source.createElement('form'); + form.method={method:?};form.target={target:?}; + form.action='https://form-target.test/submitted'; + const input=source.createElement('input');input.name='value';input.value='b'; + form.appendChild(input);source.body.appendChild(form); + HTMLFormElement.prototype[{api:?}].call(form); + }})() + "#)).expect("cross-realm native form submission"); + let expected_depth = match target { + "_top" => 0, + "_PARENT" => depth.saturating_sub(1), + _ => depth, + }; + let expected_url = if method == "get" { + "https://form-target.test/submitted?value=b" + } else { + "https://form-target.test/submitted" + }; + let description = + format!("depth={depth} target={target} method={method} api={api}"); + let root_pending = vm.take_pending_location_navigation_with_seed(); + if expected_depth == 0 { + let pending = root_pending.expect(&description); + assert_eq!(pending.url.as_str(), expected_url, "{description}"); + assert_eq!( + pending.request_method, + method.to_ascii_uppercase(), + "{description}" + ); + assert_eq!( + pending.request_body.as_deref(), + (method == "post").then_some(&b"value=b"[..]), + "{description}" + ); + } else { + assert!( + root_pending.is_none(), + "{description}: must retain top document" + ); + } + let mut parent_handle = None; + for index in 0..depth { + let host = vm._context_host.borrow(); + let handle = parent_handle + .map_or_else( + || host.child_browsing_context_handle_by_index(0), + |parent| { + host.child_browsing_context_child_frame_handle_by_index( + parent, 0, + ) + }, + ) + .expect(&description); + parent_handle = Some(handle); + let pending = + host.child_browsing_context_pending_live_navigation_for_test(handle); + if expected_depth != index + 1 { + assert!( + pending.is_none(), + "{description}: unexpected frame {index} navigation: {pending:?}" + ); + continue; + } + use crate::native_bridge::ChildBrowsingContextBootstrap; + match pending.expect(&description) { + ChildBrowsingContextBootstrap::Url(url) if method == "get" => { + assert_eq!(url.as_str(), expected_url, "{description}") + } + ChildBrowsingContextBootstrap::Request(request) => { + assert_eq!(request.url.as_str(), expected_url, "{description}"); + assert_eq!( + request.method, + method.to_ascii_uppercase(), + "{description}" + ); + assert_eq!( + request.body.as_deref(), + (method == "post").then_some(&b"value=b"[..]), + "{description}" + ); + if method == "post" { + assert_eq!( + request.request_headers, + vec![( + "Content-Type".into(), + "application/x-www-form-urlencoded".into() + )], + "{description}" + ); + } + } + other => panic!("{description}: unexpected request {other:?}"), + } + } + } + } + } + } +}