diff --git a/moli-renderer-v8/src/document_runtime/meta_refresh.rs b/moli-renderer-v8/src/document_runtime/meta_refresh.rs index 4b9bf7b78b..44d5da5856 100644 --- a/moli-renderer-v8/src/document_runtime/meta_refresh.rs +++ b/moli-renderer-v8/src/document_runtime/meta_refresh.rs @@ -7,6 +7,11 @@ pub(crate) struct MetaRefreshNavigation { pub(crate) url: Url, } +pub(super) struct MetaRefreshMutationCandidate { + pub(super) document: DomHandle, + pub(super) navigation: MetaRefreshNavigation, +} + #[derive(Debug)] pub(crate) struct ScheduledMetaRefreshNavigation { pub(crate) owner: FrameDocumentTaskOwner, @@ -231,7 +236,7 @@ pub(super) fn meta_refresh_navigations_from_mutation( dom_host: &DomHost, effects: &DomMutationEffects, document_url: &Url, -) -> Vec { +) -> Vec { let document_handle = dom_host.document_handle(); let mut handles = Vec::new(); for &root in effects.tree().connected_roots() { @@ -264,17 +269,31 @@ pub(super) fn meta_refresh_navigations_from_mutation( if handles.is_empty() { return Vec::new(); } - let document_base_url = dom_host - .document_base_url() - .unwrap_or_else(|| document_url.clone()); handles .into_iter() - .filter(|&handle| { - dom_host.owner_document_handle(handle) == Some(document_handle) - && dom_host.is_connected(handle) - }) .filter_map(|handle| { - meta_refresh_navigation_for_element(dom_host, handle, document_url, &document_base_url) + let document = dom_host.owner_document_handle(handle)?; + if !dom_host.is_connected(handle) || dom_host.root_node_handle(handle) != Some(document) + { + return None; + } + let (owner_url, owner_base_url) = if document == document_handle { + (document_url, dom_host.document_base_url()) + } else { + ( + dom_host + .document_url_for_handle(document) + .unwrap_or(document_url), + dom_host.document_base_url_for_handle(document), + ) + }; + let owner_base_url = owner_base_url.unwrap_or_else(|| owner_url.clone()); + let navigation = + meta_refresh_navigation_for_element(dom_host, handle, owner_url, &owner_base_url)?; + Some(MetaRefreshMutationCandidate { + document, + navigation, + }) }) .collect() } diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands.rs b/moli-renderer-v8/src/document_runtime/mutation_commands.rs index 077ef4b340..f1f3548ea1 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands.rs @@ -1720,7 +1720,7 @@ impl DocumentRuntime { pub(crate) struct RuntimeMutationApplyResult { changed: bool, - meta_refresh_candidates: Vec, + meta_refresh_candidates: Vec, devtools_dom_mutations: Vec, runtime_script_start_candidates: Vec, removed_open_popovers: Vec, @@ -1768,8 +1768,16 @@ pub(super) fn finish_runtime_mutation_effects( runtime.queue_devtools_dom_mutations(devtools_dom_mutations); - if let Some(scheduled) = runtime.note_top_level_meta_refresh_candidates(meta_refresh_candidates) - { + let mut top_level_refreshes = Vec::new(); + let mut child_refreshes = Vec::new(); + for candidate in meta_refresh_candidates { + if candidate.document == runtime.document_handle() { + top_level_refreshes.push(candidate.navigation); + } else { + child_refreshes.push(candidate); + } + } + if let Some(scheduled) = runtime.note_top_level_meta_refresh_candidates(top_level_refreshes) { let delay_ms = scheduled.navigation.delay_ms; let url = scheduled.navigation.url.clone(); let owner = scheduled.owner; @@ -1790,6 +1798,15 @@ pub(super) fn finish_runtime_mutation_effects( } } + for candidate in child_refreshes { + unsafe { &mut *host_ptr }.note_child_meta_refresh_after_mutation( + scope, + runtime, + candidate.document, + candidate.navigation, + ); + } + for candidate in runtime_script_start_candidates { finish_runtime_script_start_candidate(runtime, scope, host_ptr, candidate); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs index 448adc5ec9..b0ef5112d9 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs @@ -4,7 +4,8 @@ use crate::{ LocationNavigationKind, construct_original_event, construct_original_page_transition_event, meta_refresh_navigation_kind, navigate_location_object_with_child_navigate_event, }, - document_runtime::{DomHandle, EventTargetHandle, MetaRefreshNavigation}, + document_runtime::{DocumentRuntime, DomHandle, EventTargetHandle, MetaRefreshNavigation}, + dom::native::DocumentReadyState, frame_owner_model::{ FrameDocumentLoadDeliveryAction, FrameDocumentLoadDeliveryPhase, FrameDocumentLoadDeliveryProgress, @@ -20,6 +21,7 @@ pub(in crate::native_bridge::context_host) struct ChildMetaRefreshNavigationTask owner: crate::frame_owner_model::FrameDocumentTaskOwner, target_url: url::Url, navigation_kind: LocationNavigationKind, + delay_ms: u32, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] @@ -616,7 +618,6 @@ impl JsContextHost { scope: &mut v8::PinScope<'_, '_>, handle: DomHandle, ) { - self.cancel_child_meta_refresh_navigation(handle); if self .child_browsing_contexts .get(&handle) @@ -641,11 +642,71 @@ impl JsContextHost { ) else { return; }; + let runtime = unsafe { &mut *self.runtime }; + self.schedule_child_meta_refresh_navigation(scope, runtime, handle, navigation); + } + + pub(crate) fn note_child_meta_refresh_after_mutation( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + runtime: &mut DocumentRuntime, + document: DomHandle, + navigation: MetaRefreshNavigation, + ) { + if runtime.dom_host().document_ready_state_for_handle(document) + != Some(DocumentReadyState::Complete) + { + return; + } + let Some(handle) = self.child_browsing_context_host_for_document_handle(document) else { + return; + }; + // Mutation application already owns the runtime borrow; use it directly + // rather than borrowing it again through the host's runtime pointer. + self.schedule_child_meta_refresh_navigation(scope, runtime, handle, navigation); + } + + fn schedule_child_meta_refresh_navigation( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + runtime: &mut DocumentRuntime, + handle: DomHandle, + navigation: MetaRefreshNavigation, + ) { + if self + .child_browsing_contexts + .get(&handle) + .is_none_or(|entry| !entry.document_sandbox_allows_scripts()) + { + return; + } + let Some(document_url) = self.child_browsing_context_current_url(handle) else { + return; + }; let Some(owner) = self.current_child_document_task_owner(handle) else { return; }; let navigation_kind = meta_refresh_navigation_kind(&document_url, &navigation.url, navigation.delay_ms); + if self + .child_meta_refresh_navigations + .get(&handle) + .is_some_and(|task| { + task.owner == owner + && task.target_url == navigation.url + && task.navigation_kind == navigation_kind + && task.delay_ms == navigation.delay_ms + }) + { + return; + } + let Some(window) = self.child_window_proxy_records.live_window(scope, handle) else { + return; + }; + let Some(context) = window.get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); let data = v8::BigInt::new_from_u64(scope, handle.index() as u64); let Some(callback) = v8::Function::builder(child_meta_refresh_callback) .data(data.into()) @@ -653,7 +714,7 @@ impl JsContextHost { else { return; }; - let timer_id = self.queue_timeout( + let timer_id = runtime.queue_timeout( scope, callback, u64::from(navigation.delay_ms), @@ -663,15 +724,18 @@ impl JsContextHost { if timer_id == 0 { return; } - self.child_meta_refresh_navigations.insert( + if let Some(previous) = self.child_meta_refresh_navigations.insert( handle, ChildMetaRefreshNavigationTask { timer_id, owner, target_url: navigation.url, navigation_kind, + delay_ms: navigation.delay_ms, }, - ); + ) { + runtime.cancel_timer(previous.timer_id); + } } pub(in crate::native_bridge::context_host) fn cancel_child_meta_refresh_navigation( 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 3885668360..98ed91c5ad 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 @@ -1413,9 +1413,14 @@ async fn child_meta_refresh_can_be_inserted_in_owner_load_callback() { assert_child_meta_refresh_can_be_inserted_during_load(false).await; } -async fn assert_child_meta_refresh_survives_adoption_during_load(source_loads_first: bool) { +async fn assert_child_meta_refresh_survives_adoption_during_load( + source_loads_first: bool, + source_sandboxed: bool, +) { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); let mut vm = new_storage_test_vm_with_loader("https://child-meta-refresh.test/", &loader); + vm.eval(&format!("globalThis.sourceSandboxed = {source_sandboxed};")) + .unwrap(); vm.eval( r#" globalThis.sourceLoads = 0; @@ -1423,7 +1428,7 @@ globalThis.destinationLoads = 0; const root = document.documentElement || document.appendChild(document.createElement('html')); const source = document.createElement('iframe'); const destination = document.createElement('iframe'); -destination.setAttribute('sandbox', 'allow-same-origin'); +(sourceSandboxed ? source : destination).setAttribute('sandbox', 'allow-same-origin'); function adoptMetaWhenBothFramesHaveLoaded() { if (sourceLoads === 1 && destinationLoads === 1) { destination.contentDocument.body.appendChild(source.contentDocument.querySelector('meta')); @@ -1452,7 +1457,7 @@ destination.srcdoc = '

destination

'; vm.eval("String(destination.contentDocument.querySelectorAll('meta').length)") .unwrap(), "1", - "the load callback should adopt the refresh element into the sandboxed document", + "the load callback should adopt the refresh element into the destination document", ); vm.advance_timers_until_deadline_for_test(&loader) .await @@ -1460,19 +1465,86 @@ destination.srcdoc = '

destination

'; assert_eq!( vm.eval("JSON.stringify([source.contentWindow.location.href, destination.contentWindow.location.href, sourceLoads, destinationLoads])") .unwrap(), - r#"["about:srcdoc#refreshed","about:srcdoc",1,1]"#, - "adoption must preserve the original refresh and must not refresh the sandboxed destination", + if source_sandboxed { + r#"["about:srcdoc","about:srcdoc#refreshed",1,1]"# + } else { + r#"["about:srcdoc#refreshed","about:srcdoc",1,1]"# + }, + "refreshes belong to each document's sandbox policy when the meta is processed", ); } #[tokio::test] async fn child_meta_refresh_survives_adoption_when_source_loads_last() { - assert_child_meta_refresh_survives_adoption_during_load(false).await; + assert_child_meta_refresh_survives_adoption_during_load(false, false).await; } #[tokio::test] async fn child_meta_refresh_survives_adoption_when_destination_loads_last() { - assert_child_meta_refresh_survives_adoption_during_load(true).await; + assert_child_meta_refresh_survives_adoption_during_load(true, false).await; +} + +#[tokio::test] +async fn child_meta_refresh_adoption_into_loaded_unsandboxed_document_when_source_loads_last() { + assert_child_meta_refresh_survives_adoption_during_load(false, true).await; +} + +#[tokio::test] +async fn child_meta_refresh_adoption_into_unsandboxed_document_when_destination_loads_last() { + assert_child_meta_refresh_survives_adoption_during_load(true, true).await; +} + +async fn assert_child_meta_refresh_insertion_after_load(in_shadow_tree: bool) { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_storage_test_vm_with_loader("https://child-meta-refresh.test/", &loader); + vm.eval( + r#" +const root = document.documentElement || document.appendChild(document.createElement('html')); +const frame = document.createElement('iframe'); +frame.srcdoc = '

loaded

'; +root.appendChild(frame); +'queued' +"#, + ) + .unwrap(); + vm.drain_pending_child_frame_work_for_test(); + vm.eval(&format!( + "globalThis.insertInShadowTree = {in_shadow_tree};" + )) + .unwrap(); + vm.eval( + r#" +const container = frame.contentDocument.createElement('div'); +frame.contentDocument.body.appendChild(container); +const target = insertInShadowTree ? container.attachShadow({mode:'open'}) : container; +target.innerHTML = ''; +String(target.querySelectorAll('meta').length) +"#, + ) + .map(|count| assert_eq!(count, "1")) + .unwrap(); + vm.advance_timers_until_deadline_for_test(&loader) + .await + .unwrap(); + assert_eq!( + vm.eval("frame.contentWindow.location.href").unwrap(), + if in_shadow_tree { + "about:srcdoc" + } else { + "about:srcdoc#inserted" + }, + "a post-load refresh must apply only in the document's light tree", + ); +} + +#[tokio::test] +async fn child_meta_refresh_applies_to_insertion_after_load() { + assert_child_meta_refresh_insertion_after_load(false).await; +} + +#[tokio::test] +async fn child_meta_refresh_ignores_insertion_into_shadow_tree_after_load() { + assert_child_meta_refresh_insertion_after_load(true).await; } #[tokio::test]