fix(refresh): use child document as navigation referrer

This commit is contained in:
ldm0
2026-09-14 14:40:24 +08:00
parent 4606ce81f8
commit 5aeb6c3277
5 changed files with 150 additions and 18 deletions
+1 -1
View File
@@ -132,7 +132,7 @@ pub(crate) use css_fontface_runtime::{load_font_faces_for_family, rebuild_font_f
pub(crate) use location_navigation::{
LocationNavigationKind, dispatch_top_level_form_navigation_event,
dispatch_top_level_navigation_event_with_source_element, meta_refresh_navigation_kind,
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,
};
@@ -67,6 +67,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>,
@@ -74,7 +81,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(),
);
}
@@ -118,8 +130,10 @@ pub(crate) fn navigate_top_level_same_document_from_browser(
LocationNavigationKind::Assign,
Some(target),
None,
false,
true,
LocationNavigationOptions {
force_exact_same_document_navigation: true,
..LocationNavigationOptions::default()
},
);
true
}
@@ -173,8 +187,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())
@@ -195,8 +211,7 @@ pub(crate) fn navigate_location_object_with_source_element<'s>(
kind,
raw_target,
source_element,
false,
false,
LocationNavigationOptions::default(),
);
}
@@ -207,7 +222,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()
},
);
}
@@ -217,9 +261,13 @@ 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 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
@@ -628,7 +676,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::Assign) && !is_javascript_url {
host.mark_child_browsing_context_top_level_history_increment(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,
);
}
@@ -11384,6 +11384,79 @@ async fn child_location_function_uses_executing_document_as_navigation_referrer(
assert_eq!(document_referrer, child_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())
);
}
#[tokio::test(flavor = "current_thread")]
async fn parent_location_assignment_uses_parent_document_as_navigation_referrer() {
const HOST: &str = "parent-location-referrer.test";
@@ -36,15 +36,21 @@ pub(super) struct StaticHttpServer {
impl StaticHttpServer {
pub(super) async fn spawn(expected_requests: usize) -> Self {
const BODY: &str = "<!doctype html><body>child fixture</body>";
Self::spawn_with_bodies(vec![BODY.to_owned(); expected_requests]).await
}
pub(super) async fn spawn_with_bodies(response_bodies: Vec<String>) -> Self {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind static HTTP test server");
let address = listener
.local_addr()
.expect("read static HTTP test server address");
let expected_requests = response_bodies.len();
let task = tokio::spawn(async move {
let mut requests = Vec::with_capacity(expected_requests);
for _ in 0..expected_requests {
for body in response_bodies {
let (mut socket, _) = listener
.accept()
.await
@@ -119,12 +125,11 @@ impl StaticHttpServer {
headers,
});
const BODY: &str = "<!doctype html><body>child fixture</body>";
socket
.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{BODY}",
BODY.len()
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.as_bytes(),
)