fix(forms): resolve navigation keywords from the form owner

This commit is contained in:
ldm0
2026-10-04 15:37:27 +08:00
committed by Donough Liu
parent 5b7984973f
commit d6d96c8a55
5 changed files with 188 additions and 30 deletions
@@ -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,
@@ -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,
@@ -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<crate::document_runtime::DomHandle> {
// 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>,
@@ -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<DomHandle>,
source_document: Option<DomHandle>,
target_document: DomHandle,
resolved_url: Url,
body: Vec<u8>,
content_type: String,
form_data_entries: &[(String, v8::Global<v8::Value>)],
) -> 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;
@@ -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",
"<!doctype html><body></body>",
);
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='<body></body>';
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:?}"),
}
}
}
}
}
}
}