fix(refresh): use child document as navigation referrer

This commit is contained in:
ldm0
2026-10-11 18:04:10 +08:00
parent c9c69bbab6
commit c0d5e8d01a
4 changed files with 141 additions and 13 deletions
@@ -157,6 +157,7 @@ pub(crate) use location_navigation::{
dispatch_top_level_navigation_event_with_source_element, is_native_location,
meta_refresh_navigation_kind, navigate_borrowed_location,
navigate_location_object_with_child_navigate_event,
navigate_location_object_with_child_navigate_event_and_initiator_url,
navigate_location_object_with_source_element, navigate_top_level_meta_refresh,
navigate_top_level_same_document_from_browser, resolve_cross_window_location_target,
sandbox_blocks_ancestor_or_top_navigation_from_source,
@@ -77,6 +77,13 @@ pub(super) enum NavigationNavigateHistoryKind {
Replace,
}
#[derive(Default)]
struct LocationNavigationOptions {
dispatch_child_navigate_event_for_all_kinds: bool,
force_exact_same_document_navigation: bool,
explicit_initiator_url: Option<url::Url>,
}
pub(crate) fn navigate_location_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
location: v8::Local<'s, v8::Object>,
@@ -84,7 +91,12 @@ pub(crate) fn navigate_location_object<'s>(
raw_target: Option<String>,
) {
navigate_location_object_with_source_element_and_child_navigate_event(
scope, location, kind, raw_target, None, false, false,
scope,
location,
kind,
raw_target,
None,
LocationNavigationOptions::default(),
);
}
@@ -131,8 +143,10 @@ pub(crate) fn navigate_top_level_same_document_from_browser(
kind,
Some(target),
None,
false,
true,
LocationNavigationOptions {
force_exact_same_document_navigation: true,
..LocationNavigationOptions::default()
},
);
true
}
@@ -186,8 +200,10 @@ pub(crate) fn navigate_top_level_meta_refresh(
kind,
Some(target.to_string()),
None,
false,
same_document_fragment,
LocationNavigationOptions {
force_exact_same_document_navigation: same_document_fragment,
..LocationNavigationOptions::default()
},
);
context_host_ptr_from_global_bridge(scope)
.is_some_and(|host_ptr| unsafe { &*host_ptr }.has_pending_location_navigation())
@@ -208,8 +224,7 @@ pub(crate) fn navigate_location_object_with_source_element<'s>(
kind,
raw_target,
source_element,
false,
false,
LocationNavigationOptions::default(),
);
}
@@ -220,7 +235,36 @@ pub(crate) fn navigate_location_object_with_child_navigate_event<'s>(
raw_target: Option<String>,
) {
navigate_location_object_with_source_element_and_child_navigate_event(
scope, location, kind, raw_target, None, true, false,
scope,
location,
kind,
raw_target,
None,
LocationNavigationOptions {
dispatch_child_navigate_event_for_all_kinds: true,
..LocationNavigationOptions::default()
},
);
}
pub(crate) fn navigate_location_object_with_child_navigate_event_and_initiator_url<'s>(
scope: &mut v8::PinScope<'s, '_>,
location: v8::Local<'s, v8::Object>,
kind: LocationNavigationKind,
raw_target: Option<String>,
initiator_url: url::Url,
) {
navigate_location_object_with_source_element_and_child_navigate_event(
scope,
location,
kind,
raw_target,
None,
LocationNavigationOptions {
dispatch_child_navigate_event_for_all_kinds: true,
explicit_initiator_url: Some(initiator_url),
..LocationNavigationOptions::default()
},
);
}
@@ -230,8 +274,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
kind: LocationNavigationKind,
raw_target: Option<String>,
source_element: Option<v8::Local<'s, v8::Object>>,
dispatch_child_navigate_event_for_all_kinds: bool,
force_exact_same_document_navigation: bool,
options: LocationNavigationOptions,
) {
let owner = runtime_window_owner(scope, location);
if let Some(context) = location.get_creation_context(scope)
@@ -246,6 +289,11 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
}
}
let location = window_location_for_holder(scope, owner).unwrap_or(location);
let LocationNavigationOptions {
dispatch_child_navigate_event_for_all_kinds,
force_exact_same_document_navigation,
explicit_initiator_url,
} = options;
let current_href = location_href_slot(scope, location).unwrap_or_default();
let current_url = url::Url::parse(&current_href).ok();
let raw_target_is_fragment_only = raw_target
@@ -619,7 +667,8 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>(
}
if let Some(host_ptr) = host_ptr {
let host = unsafe { &mut *host_ptr };
let initiator_url = location_navigation_initiator_url(scope, host);
let initiator_url =
explicit_initiator_url.or_else(|| location_navigation_initiator_url(scope, host));
if matches!(kind, LocationNavigationKind::Reload) {
host.queue_child_browsing_context_reload_from_existing_seed(
handle,
@@ -2,7 +2,8 @@ use super::{FrameDocumentLoadDeliveryTask, JsContextHost};
use crate::{
context_bootstrap::{
LocationNavigationKind, construct_original_event, construct_original_page_transition_event,
meta_refresh_navigation_kind, navigate_location_object_with_child_navigate_event,
meta_refresh_navigation_kind,
navigate_location_object_with_child_navigate_event_and_initiator_url,
record_performance_load_event_end_for_window,
record_performance_load_event_start_for_window,
},
@@ -794,6 +795,9 @@ fn child_meta_refresh_callback<'s>(
if host.current_child_document_task_owner(handle) != Some(task.owner) {
return;
}
let Some(initiator_url) = host.child_browsing_context_current_url(handle) else {
return;
};
let Some(window) = host.child_browsing_context_window_wrapper(scope, handle) else {
return;
};
@@ -803,11 +807,12 @@ fn child_meta_refresh_callback<'s>(
else {
return;
};
navigate_location_object_with_child_navigate_event(
navigate_location_object_with_child_navigate_event_and_initiator_url(
scope,
location,
task.navigation_kind,
Some(task.target_url.to_string()),
initiator_url,
);
}
@@ -3697,3 +3697,76 @@ async fn parent_location_assignment_uses_parent_document_as_navigation_referrer(
assert_eq!(requests[1].header_value("referer"), Some(top_url.as_str()));
assert_eq!(document_referrer, top_url.as_str());
}
#[tokio::test(flavor = "current_thread")]
async fn child_meta_refresh_after_same_document_navigation_uses_child_referrer() {
const HOST: &str = "child-meta-refresh-referrer.test";
let server = StaticHttpServer::spawn_with_bodies(vec![
r#"<!doctype html>
<script>location.hash = '#section'</script>
<meta http-equiv="refresh" content="0; url=/refresh/target.html">"#
.to_owned(),
"<!doctype html><body>refresh target</body>".to_owned(),
])
.await;
let top_url = server.url_for_host(HOST, "/refresh/parent.html");
let child_url = server.url_for_host(HOST, "/refresh/source.html");
let target_url = server.url_for_host(HOST, "/refresh/target.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(&format!(
r#"
(() => {{
globalThis.__childMetaRefreshReferrerLoadCount = 0;
const frame = document.createElement('iframe');
frame.src = {};
frame.onload = () => {{
globalThis.__childMetaRefreshReferrerLoadCount++;
}};
(document.body || document.documentElement || document).appendChild(frame);
}})()
"#,
serde_json::to_string(child_url.as_str()).expect("serialize meta refresh child URL")
))
.expect("child meta refresh referrer setup should evaluate");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__childMetaRefreshReferrerLoadCount)",
"1",
"initial meta refresh child document should load",
)
.await;
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__childMetaRefreshReferrerLoadCount)",
"2",
"child meta refresh navigation should load",
)
.await;
let state = vm
.eval(
r#"
(() => {
const frame = document.querySelector('iframe');
return [frame.contentWindow.location.href, frame.contentDocument.referrer].join('|');
})()
"#,
)
.expect("child meta refresh referrer state should evaluate");
assert_eq!(state, format!("{target_url}|{child_url}"));
let requests = server.finish().await;
assert_eq!(requests[0].target, "/refresh/source.html");
assert_eq!(requests[0].header_value("referer"), Some(top_url.as_str()));
assert_eq!(requests[1].target, "/refresh/target.html");
assert_eq!(
requests[1].header_value("referer"),
Some(child_url.as_str())
);
}