diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs index a379277ae1..a21c19c68b 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs @@ -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 { diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index bd0ac60161..ec46b651b5 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -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 { + 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::*; diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents.rs index d3a1f6609b..fb190f9270 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents.rs @@ -45,6 +45,7 @@ pub(super) struct PendingChildDocumentNavigation { pub(super) admitted_history_entry: Option, 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, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs index 352c364fb7..741940f2a1 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs @@ -14,6 +14,7 @@ impl JsContextHost { bootstrap: ChildBrowsingContextBootstrap, navigation_load: FrameDocumentNavigationLoadBinding, initiator: ChildDocumentNavigationInitiator, + initiator_url: Option, ) -> Option { 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()); diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs index 2e06d8a50a..978ecc6152 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs @@ -116,6 +116,7 @@ impl JsContextHost { bootstrap: ChildBrowsingContextBootstrap, navigation_load: crate::frame_owner_model::FrameDocumentNavigationLoadBinding, initiator: ChildDocumentNavigationInitiator, + initiator_url: Option, ) -> Option { 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, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs index 9af3dd82fd..9c158322f7 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs @@ -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, ) -> 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, ) -> 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) } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs index 926236274b..e353720443 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs @@ -239,6 +239,7 @@ impl JsContextHost { &mut self, handle: DomHandle, url: &Url, + initiator_url: Option, ) -> 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 { 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 diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs index 38de2dc797..ffe1ae58a4 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs @@ -47,6 +47,7 @@ pub(super) struct ChildBrowsingContextEntry { attribute_bootstrap: ChildBrowsingContextBootstrap, pending_attribute_bootstrap_commit: bool, pending_live_navigation: Option, + pending_live_navigation_initiator_url: Option, 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 { + self.pending_live_navigation_initiator_url.clone() + } + + pub(super) fn pending_live_navigation_initiator_url_for_refresh( + &self, + attribute_bootstrap_changed: bool, + ) -> Option { + (!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, 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; } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs index 6a781834d9..0e84827432 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs @@ -471,6 +471,7 @@ impl JsContextHost { &mut self, handle: DomHandle, bootstrap: ChildBrowsingContextBootstrap, + initiator_url: Option, reflects_window_state: bool, ) -> Option { 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) } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs index 7730935952..82b036e86a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs @@ -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| { diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs index 1f9e31404d..bdf6382f4f 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs @@ -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()); +}