fix(location): use executing document as navigation referrer

This commit is contained in:
ldm0
2026-09-27 07:56:11 +08:00
parent 549c4bfd60
commit e1c5cb07c8
11 changed files with 223 additions and 8 deletions
@@ -177,6 +177,7 @@ pub(in crate::context_bootstrap) fn dispatch_history_entry_post_commit_events<'s
unsafe { &mut *host_ptr }.queue_child_browsing_context_navigation_without_seed_update(
child_handle,
&applied.url,
None,
);
}
} else {
@@ -596,15 +596,18 @@ 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);
if matches!(kind, LocationNavigationKind::Reload) {
host.queue_child_browsing_context_reload_from_existing_seed(
handle,
resolved.as_str(),
initiator_url,
);
} else {
host.queue_child_browsing_context_navigation_without_seed_update(
handle,
resolved.as_str(),
initiator_url,
);
}
}
@@ -877,6 +880,33 @@ fn context_host_ptr_for_navigation_owner(
})
}
fn location_navigation_initiator_url(
scope: &mut v8::PinScope<'_, '_>,
host: &JsContextHost,
) -> Option<url::Url> {
if let Some(popup_id) = crate::native_bridge::active_lightweight_popup_id(scope) {
return host.lightweight_popup_document_url(popup_id);
}
let dispatch_scope = scope
.get_incumbent_context()
.and_then(|context| host.window_execution_context_identity_for_access_check(context))
.map(|identity| identity.dispatch_scope())
.or_else(|| {
crate::context_bootstrap::current_child_browsing_context_handle_for_runtime_scope(scope)
.map(crate::native_bridge::OwnerDispatchScope::Child)
})
.unwrap_or(crate::native_bridge::OwnerDispatchScope::Top);
match dispatch_scope {
crate::native_bridge::OwnerDispatchScope::Top => Some(host.document_url().clone()),
crate::native_bridge::OwnerDispatchScope::Child(handle) => {
host.child_browsing_context_current_url(handle)
}
crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => {
host.lightweight_popup_document_url(popup_id)
}
}
}
#[cfg(test)]
mod meta_refresh_tests {
use super::*;
@@ -45,6 +45,7 @@ pub(super) struct PendingChildDocumentNavigation {
pub(super) admitted_history_entry: Option<moli_session_history::SessionHistoryEntry>,
pub(super) target: ChildDocumentNavigationFetchTarget,
pub(super) target_url: Url,
pub(super) referrer_source_url: Url,
pub(super) resource_loader: crate::network::navigation::NavigationResourceLoader,
pub(super) reserved_service_worker_client_id:
Option<crate::service_worker_runtime::ServiceWorkerClientId>,
@@ -14,6 +14,7 @@ impl JsContextHost {
bootstrap: ChildBrowsingContextBootstrap,
navigation_load: FrameDocumentNavigationLoadBinding,
initiator: ChildDocumentNavigationInitiator,
initiator_url: Option<Url>,
) -> Option<ChildDocumentCommitResult> {
if !self.child_browsing_contexts.contains_key(&handle) {
return None;
@@ -71,7 +72,13 @@ impl JsContextHost {
if requires_async_load {
if self
.start_child_document_load(handle, bootstrap.clone(), navigation_load, initiator)
.start_child_document_load(
handle,
bootstrap.clone(),
navigation_load,
initiator,
initiator_url,
)
.is_some()
{
return Some(ChildDocumentCommitResult::pending());
@@ -116,6 +116,7 @@ impl JsContextHost {
bootstrap: ChildBrowsingContextBootstrap,
navigation_load: crate::frame_owner_model::FrameDocumentNavigationLoadBinding,
initiator: ChildDocumentNavigationInitiator,
initiator_url: Option<url::Url>,
) -> Option<u64> {
let target_url = Self::child_browsing_context_bootstrap_url(&bootstrap)?;
let initiating_loader = self.document_resource_loader_for_owner(navigation_load.owner())?;
@@ -179,7 +180,8 @@ impl JsContextHost {
.and_then(|entry| entry.pending_service_worker_client_id());
let frame_owner_resource_timing =
self.pending_frame_owner_resource_timing(handle, &target_url, initiator);
let initiator_url = self.document_url_for_child_context(handle);
let initiator_url =
initiator_url.unwrap_or_else(|| self.document_url_for_child_context(handle));
let browser_context = self.host_document().cookie_browser_context();
self.pending_child_document_navigations.insert(
load_id,
@@ -190,6 +192,7 @@ impl JsContextHost {
.and_then(|entry| entry.navigation_entry_seed().session_history.admitted_entry),
target,
target_url: target_url.clone(),
referrer_source_url: initiator_url.clone(),
resource_loader: resource_loader.clone(),
reserved_service_worker_client_id: service_worker_client_id,
document_credentialless,
@@ -581,6 +584,13 @@ impl JsContextHost {
loaded.final_url.clone(),
&pending.target_url,
);
let document_referrer = moli_fetch::referrer_header_value(
&pending.referrer_source_url,
&final_url,
None,
None,
)
.unwrap_or_default();
self.clear_child_browsing_context_pending_navigation(handle);
let Some(entry) = self.child_browsing_contexts.get_mut(&handle) else {
self.clear_pending_service_worker_child_client_if_matches(
@@ -602,6 +612,7 @@ impl JsContextHost {
};
entry.commit_pending_child_document_load(
&final_url,
&document_referrer,
&loaded.policy_container,
sandbox,
document_credentialless,
@@ -31,7 +31,7 @@ impl JsContextHost {
}
}
self.sync_existing_child_browsing_context_runtime_surface_from_seed(scope, handle);
self.queue_child_browsing_context_navigation_to_url(handle, &url)
self.queue_child_browsing_context_navigation_to_url(handle, &url, None)
}
pub(crate) fn navigate_child_browsing_context_with_request(
@@ -74,13 +74,14 @@ impl JsContextHost {
if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) {
entry.apply_queued_navigation_to_entry_seed(&url, replace_current);
}
self.queue_child_browsing_context_navigation_to_url(handle, &url)
self.queue_child_browsing_context_navigation_to_url(handle, &url, None)
}
pub(crate) fn queue_child_browsing_context_navigation_without_seed_update(
&mut self,
handle: DomHandle,
resolved_url: &str,
initiator_url: Option<Url>,
) -> bool {
if !self.child_browsing_contexts.contains_key(&handle) {
return false;
@@ -88,7 +89,7 @@ impl JsContextHost {
let Some(url) = Url::parse(resolved_url).ok() else {
return false;
};
self.queue_child_browsing_context_navigation_to_url(handle, &url)
self.queue_child_browsing_context_navigation_to_url(handle, &url, initiator_url)
}
pub(crate) fn queue_deferred_child_browsing_context_navigation_from_entry_seed(
@@ -110,6 +111,7 @@ impl JsContextHost {
.set_child_browsing_context_pending_navigation(
handle,
ChildBrowsingContextBootstrap::Url(url),
None,
false,
)
.is_none()
@@ -156,6 +158,7 @@ impl JsContextHost {
.set_child_browsing_context_pending_navigation(
handle,
ChildBrowsingContextBootstrap::Url(url),
None,
false,
)
.is_none()
@@ -184,6 +187,7 @@ impl JsContextHost {
.set_child_browsing_context_pending_navigation(
handle,
ChildBrowsingContextBootstrap::Request(request),
None,
false,
)
.is_none()
@@ -197,10 +201,11 @@ impl JsContextHost {
&mut self,
handle: DomHandle,
resolved_url: &str,
initiator_url: Option<Url>,
) -> bool {
let Some(url) = Url::parse(resolved_url).ok() else {
return false;
};
self.queue_child_browsing_context_navigation_to_url(handle, &url)
self.queue_child_browsing_context_navigation_to_url(handle, &url, initiator_url)
}
}
@@ -239,6 +239,7 @@ impl JsContextHost {
&mut self,
handle: DomHandle,
url: &Url,
initiator_url: Option<Url>,
) -> bool {
if !self.child_browsing_contexts.contains_key(&handle) {
return false;
@@ -251,6 +252,7 @@ impl JsContextHost {
.set_child_browsing_context_pending_navigation(
handle,
ChildBrowsingContextBootstrap::Url(url.clone()),
initiator_url,
true,
)
.is_none()
@@ -278,6 +280,7 @@ impl JsContextHost {
.set_child_browsing_context_pending_navigation(
handle,
ChildBrowsingContextBootstrap::Request(request),
None,
true,
)
.is_none()
@@ -354,7 +357,11 @@ impl JsContextHost {
let dispatch_load_on_no_string_completion =
entry_snapshot.navigation_seed_is_initial_about_blank_commit();
let entry = self.child_browsing_contexts.get_mut(&handle)?;
entry.set_pending_navigation(ChildBrowsingContextBootstrap::Url(url.clone()), true);
entry.set_pending_navigation(
ChildBrowsingContextBootstrap::Url(url.clone()),
None,
true,
);
self.queue_child_browsing_context_javascript_url_execution(
handle,
url.clone(),
@@ -381,6 +388,7 @@ impl JsContextHost {
attribute_bootstrap,
navigation_load,
ChildDocumentNavigationInitiator::FrameOwnerElement,
None,
);
self.sync_existing_child_browsing_context_window_state(scope, handle);
commit_result
@@ -394,6 +402,7 @@ impl JsContextHost {
) -> Option<ChildDocumentCommitResult> {
let entry_snapshot = self.child_browsing_contexts.get(&handle).cloned()?;
let pending_bootstrap = entry_snapshot.pending_live_navigation()?;
let initiator_url = entry_snapshot.pending_live_navigation_initiator_url();
if self.current_child_navigation_load(handle) != Some(navigation_load) {
tracing::debug!(
?handle,
@@ -423,6 +432,7 @@ impl JsContextHost {
pending_bootstrap,
navigation_load,
ChildDocumentNavigationInitiator::BrowsingContext,
initiator_url,
);
self.sync_existing_child_browsing_context_window_state(scope, handle);
commit_result
@@ -47,6 +47,7 @@ pub(super) struct ChildBrowsingContextEntry {
attribute_bootstrap: ChildBrowsingContextBootstrap,
pending_attribute_bootstrap_commit: bool,
pending_live_navigation: Option<ChildBrowsingContextBootstrap>,
pending_live_navigation_initiator_url: Option<Url>,
pending_live_navigation_reflects_window_state: bool,
live_bootstrap: ChildBrowsingContextBootstrap,
navigation_entry_seed: NavigationHistoryEntrySeed,
@@ -441,6 +442,7 @@ impl ChildBrowsingContextEntry {
pub(super) fn commit_pending_child_document_load(
&mut self,
final_url: &Url,
document_referrer: &str,
policy_container: &ChildDocumentPolicyContainer,
sandbox: DocumentSandboxPolicy,
credentialless: bool,
@@ -450,6 +452,7 @@ impl ChildBrowsingContextEntry {
self.reset_performance_time_origin();
self.clear_document_runtime_state();
self.rewrite_current_navigation_url_after_load(final_url);
self.document_policy_container.document_referrer = document_referrer.to_owned();
self.apply_loaded_referrer_policy_to_current_document(
policy_container.referrer_policy.clone(),
);
@@ -967,6 +970,7 @@ impl ChildBrowsingContextEntry {
pub(super) fn clear_pending_form_submission_navigation(&mut self) {
self.pending_live_navigation = None;
self.pending_live_navigation_initiator_url = None;
}
pub(super) fn has_pending_live_navigation(&self) -> bool {
@@ -1031,6 +1035,19 @@ impl ChildBrowsingContextEntry {
.flatten()
}
pub(super) fn pending_live_navigation_initiator_url(&self) -> Option<Url> {
self.pending_live_navigation_initiator_url.clone()
}
pub(super) fn pending_live_navigation_initiator_url_for_refresh(
&self,
attribute_bootstrap_changed: bool,
) -> Option<Url> {
(!attribute_bootstrap_changed)
.then_some(self.pending_live_navigation_initiator_url())
.flatten()
}
pub(super) fn pending_live_navigation_reflects_window_state(&self) -> bool {
self.pending_live_navigation_reflects_window_state
}
@@ -1051,16 +1068,19 @@ impl ChildBrowsingContextEntry {
pub(super) fn set_pending_navigation(
&mut self,
bootstrap: ChildBrowsingContextBootstrap,
initiator_url: Option<Url>,
reflects_window_state: bool,
) {
self.pending_attribute_bootstrap_commit = false;
self.pending_live_navigation = Some(bootstrap);
self.pending_live_navigation_initiator_url = initiator_url;
self.pending_live_navigation_reflects_window_state = reflects_window_state;
}
pub(super) fn clear_pending_navigation(&mut self) {
self.pending_attribute_bootstrap_commit = false;
self.pending_live_navigation = None;
self.pending_live_navigation_initiator_url = None;
self.pending_live_navigation_reflects_window_state = false;
}
@@ -471,6 +471,7 @@ impl JsContextHost {
&mut self,
handle: DomHandle,
bootstrap: ChildBrowsingContextBootstrap,
initiator_url: Option<Url>,
reflects_window_state: bool,
) -> Option<crate::frame_owner_model::FrameDocumentNavigationLoadBinding> {
if !self.child_browsing_contexts.contains_key(&handle) {
@@ -490,7 +491,7 @@ impl JsContextHost {
let entry = self.child_browsing_contexts.get_mut(&handle)?;
entry.set_document_permissions_policy(permissions_policy);
entry.set_ancestor_origins_referrer_policy_snapshot(ancestor_origins_referrer_policy);
entry.set_pending_navigation(bootstrap, reflects_window_state);
entry.set_pending_navigation(bootstrap, initiator_url, reflects_window_state);
self.note_child_frame_load_started_for_parent(handle);
Some(navigation)
}
@@ -424,6 +424,13 @@ impl JsContextHost {
pending_live_navigation: existing.as_ref().and_then(|entry| {
entry.pending_live_navigation_for_refresh(attribute_bootstrap_changed)
}),
pending_live_navigation_initiator_url: existing.as_ref().and_then(
|entry| {
entry.pending_live_navigation_initiator_url_for_refresh(
attribute_bootstrap_changed,
)
},
),
pending_live_navigation_reflects_window_state: existing
.as_ref()
.is_some_and(|entry| {
@@ -3439,3 +3439,125 @@ fn initial_empty_child_history_mutation_preserves_pending_srcdoc_navigation() {
"about:srcdoc|true"
);
}
#[tokio::test(flavor = "current_thread")]
async fn child_location_function_uses_executing_document_as_navigation_referrer() {
const HOST: &str = "child-location-referrer.test";
let server = StaticHttpServer::spawn(2).await;
let top_url = server.url_for_host(HOST, "/source/navigate-child-function.html");
let child_url = server.url_for_host(HOST, "/source/support/location-set.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.__childLocationReferrerLoadCount = 0;
const frame = document.createElement('iframe');
frame.src = {};
frame.onload = () => {{
globalThis.__childLocationReferrerLoadCount++;
}};
(document.body || document.documentElement || document).appendChild(frame);
}})()
"#,
serde_json::to_string(child_url.as_str()).expect("serialize child URL")
))
.expect("child Location referrer setup should evaluate");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__childLocationReferrerLoadCount)",
"1",
"initial child document should load",
)
.await;
let child_context_id = vm
.live_child_default_runtime_realm_inventory()
.into_iter()
.map(|realm| realm.context_id)
.next()
.expect("loaded child realm should exist");
vm.eval_in_child_default_context(
child_context_id,
"globalThis.go = function() { location.href = 'support/dummy.html'; }",
)
.expect("child navigation function should install");
vm.eval("document.querySelector('iframe').contentWindow.go()")
.expect("parent should call the child navigation function");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__childLocationReferrerLoadCount)",
"2",
"child Location navigation should load",
)
.await;
let document_referrer = vm
.eval("document.querySelector('iframe').contentDocument.referrer")
.expect("child navigation referrer should evaluate");
let requests = server.finish().await;
assert_eq!(requests[0].target, "/source/support/location-set.html");
assert_eq!(
requests[1].header_value("referer"),
Some(child_url.as_str())
);
assert_eq!(document_referrer, 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";
let server = StaticHttpServer::spawn(2).await;
let top_url = server.url_for_host(HOST, "/source/navigate-child-function-parent.html");
let child_url = server.url_for_host(HOST, "/source/initial.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.__parentLocationReferrerLoadCount = 0;
const frame = document.createElement('iframe');
frame.src = {};
frame.onload = () => {{
globalThis.__parentLocationReferrerLoadCount++;
}};
(document.body || document.documentElement || document).appendChild(frame);
}})()
"#,
serde_json::to_string(child_url.as_str()).expect("serialize child URL")
))
.expect("parent Location referrer setup should evaluate");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__parentLocationReferrerLoadCount)",
"1",
"initial child document should load",
)
.await;
vm.eval("document.querySelector('iframe').contentWindow.location = 'support/dummy.html'")
.expect("parent should navigate the child Location");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__parentLocationReferrerLoadCount)",
"2",
"parent-initiated child Location navigation should load",
)
.await;
let document_referrer = vm
.eval("document.querySelector('iframe').contentDocument.referrer")
.expect("parent-initiated child navigation referrer should evaluate");
let requests = server.finish().await;
assert_eq!(requests[0].target, "/source/initial.html");
assert_eq!(requests[1].header_value("referer"), Some(top_url.as_str()));
assert_eq!(document_referrer, top_url.as_str());
}