diff --git a/moli-core/src/page/mod.rs b/moli-core/src/page/mod.rs index d94f4eea58..5834022b96 100644 --- a/moli-core/src/page/mod.rs +++ b/moli-core/src/page/mod.rs @@ -136,8 +136,8 @@ pub use moli_renderer_v8::{ RendererServiceWorkerVersionStatus, RendererSetDocumentContentResult, RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent, RendererSharedWorkerTargetInfo, RendererSyntheticResponseBody, RendererTextSearchMatch, - RendererTouchEventInput, RendererTouchPoint, RendererVisualStateToken, RendererWindowDocumentSource, - RuntimeConsoleMessageSnapshot, + RendererTouchEventInput, RendererTouchPoint, RendererVisualStateToken, + RendererWindowDocumentSource, RuntimeConsoleMessageSnapshot, }; pub use moli_renderer_v8::{ RendererAppManifest, RendererAppManifestDisplayMode, RendererAppManifestError, diff --git a/moli-protocol/src/conn/browser_context/target_session_owner.rs b/moli-protocol/src/conn/browser_context/target_session_owner.rs index 471b0150e9..c6292bc2a9 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -1407,6 +1407,32 @@ impl CdpConnection { .clear_pending_navigation_history_update() } + pub(crate) fn stop_renderer_location_navigation_for_owner( + &mut self, + owner: &CommandOwnerScope, + source_document: moli_core::RendererDocumentLifecycleIdentity, + ) { + let Some(mut resolved) = self.target_session_owner_mut_for_owner(owner) else { + return; + }; + let target = resolved.target_mut(); + // stop() aborts traversal signals and API promises, but script cannot + // cancel the browser's physical cross-document history traversal. + if target + .owner_state + .navigation_history_state + .is_pending_traversal() + { + return; + } + if target + .runtime_slot + .stop_pending_document_navigation(source_document) + { + target.owner_state.clear_pending_navigation_history_update(); + } + } + pub(crate) async fn mark_target_crashed_for_owner_async( &mut self, owner: &CommandOwnerScope, diff --git a/moli-protocol/src/conn/state/navigation.rs b/moli-protocol/src/conn/state/navigation.rs index 739919f64b..9158a40e21 100644 --- a/moli-protocol/src/conn/state/navigation.rs +++ b/moli-protocol/src/conn/state/navigation.rs @@ -239,6 +239,13 @@ impl TargetNavigationHistoryState { self.pending_update = None; } + pub(crate) fn is_pending_traversal(&self) -> bool { + matches!( + self.pending_update, + Some(PendingNavigationHistoryUpdate::TraverseToEntry(_)) + ) + } + pub(crate) fn is_pending_reload(&self) -> bool { matches!( self.pending_update, @@ -287,8 +294,8 @@ impl TargetNavigationHistoryState { } let mutation = match self.pending_update { Some( - PendingNavigationHistoryUpdate::ReplaceCurrent - | PendingNavigationHistoryUpdate::ReplaceInitialEmptyDocument, + PendingNavigationHistoryUpdate::ReplaceCurrent(_) + | PendingNavigationHistoryUpdate::ReplaceInitialEmptyDocument(_), ) => moli_page_types::NavigationHistoryMutation::Replace, _ => moli_page_types::NavigationHistoryMutation::Push, }; diff --git a/moli-protocol/src/conn/state/page_slot.rs b/moli-protocol/src/conn/state/page_slot.rs index 40795a4fe3..6ca8037db9 100644 --- a/moli-protocol/src/conn/state/page_slot.rs +++ b/moli-protocol/src/conn/state/page_slot.rs @@ -680,6 +680,25 @@ impl TargetPageSlot { } } + pub(crate) fn cancel_uncommitted_document_navigation(&mut self) -> bool { + let Some(token) = self + .pending_navigation_request + .as_ref() + .filter(|request| !request.committed) + .map(|request| request.token.clone()) + else { + return false; + }; + self.cancel_pending_document_navigation(); + if matches!(&self.pending_renderer_page, + Some(PendingRendererPageBinding::DocumentNavigation { navigation, .. }) + if navigation == &token) + { + self.pending_renderer_page = None; + } + true + } + pub(crate) fn document_navigation_cancellation_handle( &self, token: &DocumentNavigationToken, diff --git a/moli-protocol/src/conn/state/runtime_slot.rs b/moli-protocol/src/conn/state/runtime_slot.rs index 174b09c911..0b2d9fbee8 100644 --- a/moli-protocol/src/conn/state/runtime_slot.rs +++ b/moli-protocol/src/conn/state/runtime_slot.rs @@ -665,6 +665,20 @@ impl TargetRuntimeSlot { self.page_slot.has_pending_document_navigation() } + pub(crate) fn stop_pending_document_navigation( + &mut self, + source_document: RendererDocumentLifecycleIdentity, + ) -> bool { + if self + .committed_renderer_document_binding() + .map(CommittedRendererDocumentBinding::renderer_document_identity) + != Some(source_document) + { + return false; + } + self.page_slot.cancel_uncommitted_document_navigation() + } + pub(crate) fn moli_memory_diagnostics(&self) -> Value { json!({ "hasLoadedPage": self.has_loaded_page(), diff --git a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs index 8891730a92..a2054e593a 100644 --- a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs +++ b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs @@ -209,6 +209,14 @@ impl PreparedProtocolOutputs { ) -> Self { let mut prepared = Self::empty(); match action { + RendererOwnerAction::StopTopLevelLocationNavigation { source_document } => { + crate::domains::page::PagePreparedOutputs::from_renderer_location_navigation_stop( + conn, + owner, + source_document, + ) + .append_to_location_navigation_stop_output_sink(&mut prepared); + } RendererOwnerAction::FileChooser(activation) => { crate::domains::input::InputPreparedOutputs::from_renderer_file_chooser_activation( conn, owner, activation, diff --git a/moli-protocol/src/domains/activity/output_slot.rs b/moli-protocol/src/domains/activity/output_slot.rs index 56fb472897..c0f5d9d3b3 100644 --- a/moli-protocol/src/domains/activity/output_slot.rs +++ b/moli-protocol/src/domains/activity/output_slot.rs @@ -32,6 +32,7 @@ pub(in crate::domains) enum ProtocolOutputSlot { WebMcp, PendingSubresourceContinueEvents, TopLevelLocationNavigation, + StopTopLevelLocationNavigation, TopLevelHistoryTraversal, FileChooser, Download, @@ -83,6 +84,7 @@ impl ProtocolOutputSlot { | Self::SessionHistoryUpdate | Self::SameDocumentNavigation | Self::TopLevelLocationNavigation + | Self::StopTopLevelLocationNavigation | Self::TopLevelHistoryTraversal => ProtocolOutputDelivery::OwnerAction, Self::WindowOpen | Self::WebMcp @@ -117,6 +119,7 @@ impl ProtocolOutputSlot { ProtocolOutputResponseOrder::AfterResponse } Self::PendingSubresourceContinueEvents + | Self::StopTopLevelLocationNavigation // Blink's file-input activation probe queues // Page.fileChooserOpened synchronously. A script may continue into // document.open(), but Chromium still flushes the chooser event @@ -246,6 +249,7 @@ impl ProtocolOutputSlot { | Self::SessionHistoryUpdate | Self::SameDocumentNavigation | Self::TopLevelLocationNavigation + | Self::StopTopLevelLocationNavigation | Self::TopLevelHistoryTraversal => { crate::domains::page::project_page_output_async(self, conn, context, payloads) .await; @@ -288,6 +292,7 @@ mod tests { BeforeResponse, ), (TopLevelLocationNavigation, OwnerAction, AfterResponse), + (StopTopLevelLocationNavigation, OwnerAction, BeforeResponse), (TopLevelHistoryTraversal, OwnerAction, AfterResponse), (FileChooser, OwnerAction, BeforeResponse), (Download, OwnerAction, AfterResponse), diff --git a/moli-protocol/src/domains/page.rs b/moli-protocol/src/domains/page.rs index 939b8ec833..6797e35916 100644 --- a/moli-protocol/src/domains/page.rs +++ b/moli-protocol/src/domains/page.rs @@ -553,6 +553,10 @@ pub(crate) struct PagePreparedOutputs { moli_page_types::SessionHistoryUpdate, )>, top_level_location_navigation: Option, + stopped_location_navigations: Vec<( + crate::conn::TargetPageResidenceIdentity, + RendererDocumentLifecycleIdentity, + )>, top_level_history_traversal: Option, } diff --git a/moli-protocol/src/domains/page/navigation.rs b/moli-protocol/src/domains/page/navigation.rs index 9354126741..4ca0aa046f 100644 --- a/moli-protocol/src/domains/page/navigation.rs +++ b/moli-protocol/src/domains/page/navigation.rs @@ -2320,17 +2320,9 @@ pub(super) fn start_session_owner_navigation_from_renderer( conn.mark_next_auxiliary_navigation_history_for_owner(owner, kind); } let result_projection = NavigationResultProjection::Cdp(result_payload); - // Top-level Navigation API traversals hand admission to the browser, - // unlike ordinary renderer navigations whose entry points own the - // source check. Keep this separate from the event's initiator. - let beforeunload = if navigation_history - .as_ref() - .is_some_and(|history| history.navigation_type() == Some("traverse")) - { - BeforeUnloadCheck::Required - } else { - BeforeUnloadCheck::RendererHandled - }; + // Renderer entry points check the exact outgoing document tree before + // handing the request to the browser, including named Window reuse. + let beforeunload = BeforeUnloadCheck::RendererHandled; start_navigate_to_url_command_with_background_policy_and_request( conn, None, @@ -2925,9 +2917,13 @@ fn start_navigate_to_url_command_with_background_policy( allow_background_navigation, request_load_policy, initiator, - // Browser commands and renderer History API handoffs reach this - // entry point before checking the outgoing document tree. - BeforeUnloadCheck::Required, + // Renderer History API handoffs have already checked the outgoing + // tree. Browser commands still own that check here. + if matches!(initiator, NavigationStartInitiator::Renderer) { + BeforeUnloadCheck::RendererHandled + } else { + BeforeUnloadCheck::Required + }, None, None, None, diff --git a/moli-protocol/src/domains/page/navigation_commit.rs b/moli-protocol/src/domains/page/navigation_commit.rs index d0d1876ea8..e4159af69e 100644 --- a/moli-protocol/src/domains/page/navigation_commit.rs +++ b/moli-protocol/src/domains/page/navigation_commit.rs @@ -36,7 +36,7 @@ pub(super) async fn commit_loaded_navigation_async( response_from_cache, main_document_body, initial_runtime_realms, - mut renderer_output_predecessor, + renderer_output_predecessor, main_document_commit, progress_gate, network_error_page, diff --git a/moli-protocol/src/domains/page/output.rs b/moli-protocol/src/domains/page/output.rs index 2f6aa7f56a..46dd76ddd8 100644 --- a/moli-protocol/src/domains/page/output.rs +++ b/moli-protocol/src/domains/page/output.rs @@ -1,6 +1,30 @@ use super::*; impl PagePreparedOutputs { + pub(crate) fn from_renderer_location_navigation_stop( + conn: &CdpConnection, + owner: &CommandOwnerScope, + source_document: RendererDocumentLifecycleIdentity, + ) -> Self { + let Some(residence) = conn.target_page_residence_identity_for_owner(owner) else { + return Self::default(); + }; + Self { + stopped_location_navigations: vec![(residence, source_document)], + ..Self::default() + } + } + + pub(in crate::domains) fn append_to_location_navigation_stop_output_sink( + self, + sink: &mut (impl ProtocolOutputSink + ?Sized), + ) { + if !self.stopped_location_navigations.is_empty() { + sink.push_produced_slot(ProtocolOutputSlot::StopTopLevelLocationNavigation); + sink.push_prepared_payload(PagePreparedOutputSlot::from_outputs(self).into()); + } + } + pub(crate) fn from_renderer_auxiliary_window_navigation( conn: &CdpConnection, owner: &CommandOwnerScope, @@ -382,6 +406,8 @@ impl PagePreparedOutputs { } self.same_document_navigations .extend(other.same_document_navigations); + self.stopped_location_navigations + .extend(other.stopped_location_navigations); if self.top_level_location_navigation.is_none() { self.top_level_location_navigation = other.top_level_location_navigation; } @@ -510,6 +536,7 @@ impl PagePreparedOutputs { child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -539,6 +566,7 @@ impl PagePreparedOutputs { child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -590,6 +618,7 @@ impl PagePreparedOutputs { child_frame_activities: vec![activity], same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -616,6 +645,7 @@ impl PagePreparedOutputs { }) .collect(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -637,6 +667,7 @@ impl PagePreparedOutputs { child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: navigation .map(|navigation| PagePreparedTopLevelLocationNavigation::new(owner, navigation)), top_level_history_traversal: None, @@ -935,6 +966,17 @@ pub(in crate::domains) async fn project_page_output_async( prepared_outputs, ); } + ProtocolOutputSlot::StopTopLevelLocationNavigation => { + if let Some(slot) = prepared_outputs.page_mut() { + for (residence, source_document) in + std::mem::take(&mut slot.outputs.stopped_location_navigations) + { + if conn.target_page_residence_identity_is_current(&residence) { + conn.stop_renderer_location_navigation_for_owner(&owner, source_document); + } + } + } + } ProtocolOutputSlot::TopLevelHistoryTraversal => { let mut events = Vec::new(); emit_top_level_history_traversal_activity_background_events_async( diff --git a/moli-protocol/src/domains/target/popup.rs b/moli-protocol/src/domains/target/popup.rs index 78ecb34c69..1b9835159f 100644 --- a/moli-protocol/src/domains/target/popup.rs +++ b/moli-protocol/src/domains/target/popup.rs @@ -687,6 +687,7 @@ pub(crate) async fn complete_popup_target_navigation_owner_action_async( &[], request_kind, None, + None, auxiliary_navigation, document_response, initial_document_environment, diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs index 5b26f66a0b..00607bf60a 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs @@ -1277,7 +1277,7 @@ async fn rust_cdp_chromium_target_named_reuse_updates_dom_opener_before_returnin &mut ctx, &parent_session, 269_027, - "child.opener=null;Object.getOwnPropertyDescriptor(child,'opener').value===null", + "child.opener=null;child.opener===null && typeof Object.getOwnPropertyDescriptor(child,'opener').get==='function'", ) .await; assert_eq!(disowned["result"]["result"]["value"], true); @@ -1285,7 +1285,7 @@ async fn rust_cdp_chromium_target_named_reuse_updates_dom_opener_before_returnin &mut ctx, &sibling_session, 269_028, - "open('', 'child').opener===null", + "open('', 'child').opener===window", ) .await; assert_eq!(shadowed["result"]["result"]["value"], true); diff --git a/moli-protocol/src/testing.rs b/moli-protocol/src/testing.rs index 8673b9bcb0..3cbb076e11 100644 --- a/moli-protocol/src/testing.rs +++ b/moli-protocol/src/testing.rs @@ -15,7 +15,7 @@ use super::conn::{ }; use crate::automation::{AutomationCommand, AutomationResult, DevToolsError}; use crate::domains::activity::{ - ProtocolSchedulerWork, ProtocolSchedulerWorkKind, RuntimeCommandOutputBarrierCompletion, + ProtocolSchedulerWork, RuntimeCommandOutputBarrierCompletion, RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarriers, }; use moli_core::{ @@ -920,7 +920,30 @@ impl TestContext { // renderer Page build, so composing both futures inline can // exceed Rust's default test-thread stack before the owner // turn gets a chance to yield. - let completed = Box::pin(pending.wait()).await; + let executes_javascript = pending.runtime_command_executes_page_javascript(); + let mut completion = Box::pin(pending.wait()); + let completed = if executes_javascript { + loop { + tokio::select! { + biased; + completed = &mut completion => break completed, + input = Box::pin(self.receive_one_test_scheduler_input()) => { + let (_, input) = input.expect( + "pending command lost its renderer scheduler inputs" + ); + // Receiving is cancellation-safe; projecting + // a publication is not. Finish its actions + // even if the command becomes ready meanwhile. + let mut work = VecDeque::from([input]); + Box::pin(self.route_test_scheduler_work_queue(&mut work)).await; + } + } + } + } else { + // Page construction must install its owner binding + // before admitting the renderer's first publication. + completion.await + }; step = Box::pin(self.conn.complete_pending_command_dispatch_with_context( completed, command_context, @@ -1374,75 +1397,76 @@ impl TestContext { return ready; } - let mut work = VecDeque::new(); + let Some((input_kind, input)) = Box::pin(self.receive_one_test_scheduler_input()).await + else { + return TestSchedulerTurnOutcome::Idle; + }; + let mut work = VecDeque::from([input]); + Box::pin(self.route_test_scheduler_work_queue(&mut work)).await; + TestSchedulerTurnOutcome::Processed(input_kind) + } + + async fn receive_one_test_scheduler_input( + &mut self, + ) -> Option<(TestSchedulerInputKind, TestSchedulerWork)> { let background_navigation_scheduler_enabled = self.background_navigation_scheduler_enabled; - let input_kind = if !self.pending_runtime_deferred_replies.is_empty() { + if !self.pending_runtime_deferred_replies.is_empty() { tokio::select! { biased; maybe_response = self.runtime_inspector_response_ready_rx.recv() => { - let Some(response) = maybe_response else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RuntimeDeferredReplyReady(response)); - TestSchedulerInputKind::RuntimeDeferredReply + maybe_response.map(|response| ( + TestSchedulerInputKind::RuntimeDeferredReply, + TestSchedulerWork::RuntimeDeferredReplyReady(response), + )) } maybe_completion = self.background_navigation_completion_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(completion) = maybe_completion else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(completion)); - TestSchedulerInputKind::BackgroundNavigationCompletion + maybe_completion.map(|completion| ( + TestSchedulerInputKind::BackgroundNavigationCompletion, + TestSchedulerWork::BackgroundNavigationCompletion(completion), + )) } maybe_event = self.background_event_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(event) = maybe_event else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundEvent(event)); - TestSchedulerInputKind::BackgroundEvent + maybe_event.map(|event| ( + TestSchedulerInputKind::BackgroundEvent, + TestSchedulerWork::BackgroundEvent(event), + )) } maybe_publication = self.renderer_publication_rx.recv() => { - let Some(publication) = maybe_publication else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RendererPublication(publication)); - TestSchedulerInputKind::RendererPublication + maybe_publication.map(|publication| ( + TestSchedulerInputKind::RendererPublication, + TestSchedulerWork::RendererPublication(publication), + )) } } } else { tokio::select! { biased; maybe_completion = self.background_navigation_completion_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(completion) = maybe_completion else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(completion)); - TestSchedulerInputKind::BackgroundNavigationCompletion + maybe_completion.map(|completion| ( + TestSchedulerInputKind::BackgroundNavigationCompletion, + TestSchedulerWork::BackgroundNavigationCompletion(completion), + )) } maybe_event = self.background_event_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(event) = maybe_event else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundEvent(event)); - TestSchedulerInputKind::BackgroundEvent + maybe_event.map(|event| ( + TestSchedulerInputKind::BackgroundEvent, + TestSchedulerWork::BackgroundEvent(event), + )) } maybe_publication = self.renderer_publication_rx.recv() => { - let Some(publication) = maybe_publication else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RendererPublication(publication)); - TestSchedulerInputKind::RendererPublication + maybe_publication.map(|publication| ( + TestSchedulerInputKind::RendererPublication, + TestSchedulerWork::RendererPublication(publication), + )) } maybe_response = self.runtime_inspector_response_ready_rx.recv() => { - let Some(response) = maybe_response else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RuntimeDeferredReplyReady(response)); - TestSchedulerInputKind::RuntimeDeferredReply + maybe_response.map(|response| ( + TestSchedulerInputKind::RuntimeDeferredReply, + TestSchedulerWork::RuntimeDeferredReplyReady(response), + )) } } - }; - Box::pin(self.route_test_scheduler_work_queue(&mut work)).await; - TestSchedulerTurnOutcome::Processed(input_kind) + } } async fn route_protocol_events_like_scheduler( @@ -1565,47 +1589,9 @@ impl TestContext { work: &mut VecDeque, ) { loop { - let selected_index = match self.pending_protocol_scheduler_work.front() { - Some(front) if front.is_ready() => 0, - Some(front) - if front.kind() == ProtocolSchedulerWorkKind::MainDocumentLoadOwnerAction => - { - let Some(index) = - self.pending_protocol_scheduler_work - .iter() - .position(|candidate| { - candidate.is_ready() - && candidate.navigation_dependency() - == crate::ProtocolNavigationDependency::ReplacesPendingLoad - }) - else { - return; - }; - // Production checks the pending load observer out of the - // FIFO while it waits for the renderer. An unconstrained - // location owner action may then run and replace that - // exact source Document, which completes the observer as - // Superseded. Keeping the pending observer at the front in - // this protocol-only harness would deadlock the action - // needed to make it terminal. - index - } - Some(_) | None => return, - }; - if !self.background_navigation_scheduler_enabled - && self - .pending_protocol_scheduler_work - .get(selected_index) - .is_some_and(ProtocolSchedulerWork::requires_background_navigation_scheduler) - { - // The default protocol fixture has no owner task lane. Keep - // independent popup navigation resident rather than invoking - // the production function's synchronous fallback while the - // exact renderer cursor is still being projected. Tests that - // assert navigation progress opt into the production-shaped - // background scheduler and drive its typed completions. + let Some(selected_index) = self.next_ready_protocol_work_index() else { return; - } + }; let protocol_work = self .pending_protocol_scheduler_work .remove(selected_index) @@ -1624,6 +1610,34 @@ impl TestContext { } } + fn next_ready_protocol_work_index(&self) -> Option { + // Match production's per-target ordering. An unfinished opener load + // cannot block an independent popup, and a replacement must reach + // the owner that supersedes its pending load. + let mut blocked_targets = Vec::new(); + for (index, candidate) in self.pending_protocol_scheduler_work.iter().enumerate() { + let target = candidate.navigation_gate_target_id(); + if target.is_some_and(|target| blocked_targets.contains(&target)) + && candidate.navigation_dependency() + != crate::ProtocolNavigationDependency::ReplacesPendingLoad + { + continue; + } + if !candidate.is_ready() + || (!self.background_navigation_scheduler_enabled + && candidate.requires_background_navigation_scheduler()) + { + // An unscoped predecessor orders all later work. Fixtures + // that need popup navigation enable the background channels; + // otherwise retain that action on its target's lane. + blocked_targets.push(target?); + continue; + } + return Some(index); + } + None + } + async fn ingest_renderer_publication_like_scheduler( &mut self, publication: RendererOutputTransportMessage, diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 49db58b453..4a6b018c57 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -90,8 +90,8 @@ mod navigation_restore; mod navigation_result; mod navigation_seed; mod navigation_serialize; -pub(crate) use navigation_serialize::current_document_referrer_policy; pub(crate) use navigation_entry::navigation_entry_public_token; +pub(crate) use navigation_serialize::current_document_referrer_policy; pub(crate) use navigation_serialize::{ document_referrer_policy_for_native_document, history_document_id_for_holder, publish_top_level_navigation_history, @@ -183,17 +183,15 @@ pub(crate) use css_fontface_runtime::{ pub(crate) use form_navigation::FormNavigationHistory; pub(crate) use history_mutation::update_history_for_document_open; pub(crate) use location_navigation::{ + HyperlinkNavigationOptions, LocationNavigationKind, blocks_ancestor_location_navigation, dispatch_top_level_navigation_event_with_source_element, - HyperlinkNavigationOptions, LocationNavigationKind, navigate_location_object_for_hyperlink, - is_native_location, + history_entry_seed_for_cross_document_location, is_native_location, meta_refresh_navigation_kind, navigate_borrowed_location, + navigate_location_object_for_element_fragment, navigate_location_object_for_hyperlink, navigate_location_object_with_child_navigate_event, - history_entry_seed_for_cross_document_location, - 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, - navigate_location_object_for_element_fragment, }; pub(crate) use navigation_cancellation::{ NavigationCancellationReason, inform_about_canceled_navigation_for_window, @@ -367,9 +365,8 @@ pub(crate) use self::indexed_db::{ flush_blocked_indexed_db_requests, flush_indexed_db_connection_notification, flush_indexed_db_task_by_id, flush_indexed_db_transaction_starts, flush_next_indexed_db_task, indexed_db_has_pending_tasks, install_worker_indexed_db_runtime_state, - retire_indexed_db_context, set_indexed_db_manager_for_context, - set_worker_indexed_db_task_wake_for_context, - retain_indexed_db_state_in_retired_realm, + retain_indexed_db_state_in_retired_realm, retire_indexed_db_context, + set_indexed_db_manager_for_context, set_worker_indexed_db_task_wake_for_context, }; #[cfg(test)] pub(crate) use self::indexed_db::{ @@ -474,7 +471,6 @@ pub(crate) use self::runtime_state::install_webassembly_runtime_state; #[cfg(feature = "wpt-extensions")] pub(crate) use self::runtime_state::install_wpt_webdriver_runtime_state; pub(crate) use self::runtime_state::set_window_origin_runtime_state; -pub(crate) use self::runtime_state::window_realm_secure_context_available; pub(crate) use self::runtime_state::{ ORIGINAL_WEBASSEMBLY_COMPILE_ERROR_CONSTRUCTOR_SLOT, ORIGINAL_WEBASSEMBLY_GLOBAL_VALUE_GETTER_SLOT, ORIGINAL_WEBASSEMBLY_INSTANCE_CONSTRUCTOR_SLOT, @@ -1199,7 +1195,6 @@ pub(crate) fn live_ranges_detached_text_split<'s>( pub(crate) use self::window_accessors::{ bind_current_child_window_document, current_window_style_viewport, retain_window_document_in_retired_realm, window_parent_or_top, - }; pub(crate) use session_history::{ diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs index 928273ed71..aa3c4a60e2 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs @@ -545,16 +545,6 @@ pub(crate) fn new_dom_exception_value<'s>( scope, "DOMException", ) - .or_else(|| { - // A native exception must materialize its own realm's intrinsic. - // Reading the public Window property can run author code or enter - // an access check again after navigation detaches its global. - crate::context_bootstrap::exposed_interfaces::ensure_intrinsic_interface_prototype( - scope, - "DOMException", - ) - .ok() - }) { let exception = v8::Object::new(scope); initialize_dom_exception(scope, exception, message, name); diff --git a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs index fce43c7e04..62309a717f 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs @@ -27,7 +27,6 @@ use super::navigation_window::{ runtime_window_is_global, window_history_for_holder, window_location_for_holder, window_navigation_for_holder, }; -use super::*; use crate::context_bootstrap::navigation_entry::wrappers as entry_wrappers; use crate::structured_clone::{deserialize_history_state, serialize_history_state}; use crate::webidl; @@ -210,7 +209,7 @@ fn mutate_history_object<'s>( // Admission follows argument conversion, state serialization and URL // validation, but precedes navigation cancellation and event dispatch. // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return; }; let Some(dispatch_scope) = runtime_window_dispatch_scope(scope, owner) else { @@ -411,7 +410,7 @@ fn history_api_base_url<'s>( owner: v8::Local<'s, v8::Object>, current_url: &url::Url, ) -> url::Url { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return current_url.clone(); }; let host = unsafe { &*host_ptr }; @@ -439,7 +438,7 @@ fn history_document_url<'s>( owner: v8::Local<'s, v8::Object>, current_href: &str, ) -> Option { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return url::Url::parse(current_href).ok(); }; let host = unsafe { &*host_ptr }; @@ -466,7 +465,7 @@ fn effective_history_mutation_kind<'s>( if !matches!(kind, HistoryMutationKind::Push) { return kind; } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return kind; }; let host = unsafe { &*host_ptr }; diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs index e2c80fa977..084399a306 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs @@ -1,5 +1,5 @@ use super::super::navigation_window::{navigation_document_is_active, runtime_window_owner}; -use super::super::{context_host_ptr_from_global_bridge, throw_dom_exception_value}; +use super::super::throw_dom_exception_value; /// History API algorithms call this after WebIDL argument conversion. pub(in crate::context_bootstrap) fn require_fully_active_history_owner<'s>( @@ -10,7 +10,7 @@ pub(in crate::context_bootstrap) fn require_fully_active_history_owner<'s>( let is_fully_active = if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { - context_host_ptr_from_global_bridge(scope) + super::super::window_accessors::window_host_ptr(scope, owner) .is_some_and(|host_ptr| unsafe { &*host_ptr }.lightweight_popup_is_open(popup_id)) } else { navigation_document_is_active(scope, owner) diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index 7a541f171a..7c62a7ca3d 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -84,6 +84,7 @@ struct LocationNavigationOptions { dispatch_child_navigate_event_for_all_kinds: bool, force_exact_same_document_navigation: bool, explicit_initiator_url: Option, + explicit_navigation_initiator: Option, user_initiated: bool, source_can_access_target: Option, hyperlink: bool, @@ -93,7 +94,7 @@ struct LocationNavigationOptions { pub(crate) struct HyperlinkNavigationOptions { pub(crate) user_initiated: bool, pub(crate) source_can_access_target: bool, - pub(crate) initiator_url: url::Url, + pub(crate) initiator: crate::runtime::RendererNavigationInitiator, pub(crate) named_popup: Option, } @@ -115,7 +116,8 @@ pub(crate) fn navigate_location_object_for_hyperlink<'s>( source_can_access_target: Some(options.source_can_access_target), hyperlink: true, force_exact_same_document_navigation: true, - explicit_initiator_url: Some(options.initiator_url), + explicit_initiator_url: Some(options.initiator.url().clone()), + explicit_navigation_initiator: Some(options.initiator), named_hyperlink_popup: options.named_popup, ..LocationNavigationOptions::default() }, @@ -237,12 +239,7 @@ pub(crate) fn navigate_top_level_same_document_from_browser( // fragment-only string through Location: Chromium pushes a same-document // history entry and runs the Navigation/popstate surfaces even though the // serialized URL does not change. - navigate_location_object_for_same_document( - scope, - location, - &target, - kind, - ); + navigate_location_object_for_same_document(scope, location, &target, kind); true } @@ -371,6 +368,28 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( source_element: Option>, options: LocationNavigationOptions, ) { + if let Some(context) = location.get_creation_context(scope) + && context != scope.get_current_context() + && is_native_location(scope, location) + && crate::util::context_host_ptr_from_context_slot(context) + != context_host_ptr_from_global_bridge(scope) + { + // URL parsing retains the entry settings before entering the native + // receiver's realm. The incumbent source remains available for the + // initiator, origin and about-document inheritance checks below. + let raw_target = + raw_target.map(|raw| resolve_cross_window_location_target(scope, &raw).unwrap_or(raw)); + let scope = &mut v8::ContextScope::new(scope, context); + navigate_location_object_with_source_element_and_child_navigate_event( + scope, + location, + kind, + raw_target, + source_element, + options, + ); + return; + } // The Location setters and methods return before URL parsing when their // relevant Document is null, including when argument conversion removed it. if !location_has_relevant_document(scope, location) { @@ -393,6 +412,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( dispatch_child_navigate_event_for_all_kinds, force_exact_same_document_navigation, explicit_initiator_url, + explicit_navigation_initiator, user_initiated, source_can_access_target, hyperlink, @@ -425,7 +445,13 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( let navigation_source = if matches!(kind, LocationNavigationKind::Reload) { None } else { - capture_location_navigation_source(scope, owner, &resolved) + let captured = capture_location_navigation_source(scope, owner, &resolved); + match (explicit_navigation_initiator, captured) { + (Some(initiator), captured) => { + Some((initiator, captured.and_then(|(_, environment)| environment))) + } + (None, captured) => captured, + } }; if resolved.scheme() == "javascript" { let source_context = scope @@ -464,6 +490,14 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( }); let owner = runtime_window_owner(scope, location); let source_can_access_target = source_can_access_target.unwrap_or_else(|| { + if runtime_window_is_global(scope, owner) + && let Some(context) = owner.get_creation_context(scope) + { + let source = scope + .get_incumbent_context() + .unwrap_or_else(|| scope.get_current_context()); + return crate::native_bridge::window_contexts_allow_access(source, context); + } let Some(host_ptr) = context_host_ptr_for_navigation_owner(scope, owner) else { return true; }; @@ -477,6 +511,16 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( target, ) }); + // Child navigations capture the source origin and recheck the target when + // their queued task executes. Preserve that check instead of rejecting the + // Location write synchronously as for a top-level related Page. + if resolved.scheme() == "javascript" + && !source_can_access_target + && runtime_window_is_global(scope, owner) + { + crate::native_bridge::throw_cross_origin_location_security_error(scope); + return; + } let event_source_element = source_element.filter(|_| source_can_access_target); if navigation_unload_event_active(scope, owner) || super::navigation_cancellation::window_navigation_is_stopping(scope, owner) @@ -549,14 +593,16 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( let replaces_initial_about_blank = super::navigation_window::navigation_document_is_initial_empty(scope, owner) || child_handle.is_some_and(|handle| { - context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { - unsafe { &*host_ptr }.child_current_document_is_initial_empty(handle) - }) - }) || popup_id.is_some_and(|popup_id| { - context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { - unsafe { &*host_ptr }.lightweight_popup_current_document_is_initial_empty(popup_id) - }) - }); + context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { + unsafe { &*host_ptr }.child_current_document_is_initial_empty(handle) + }) + }) + || popup_id.is_some_and(|popup_id| { + context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { + unsafe { &*host_ptr } + .lightweight_popup_current_document_is_initial_empty(popup_id) + }) + }); let effective_kind = match kind { LocationNavigationKind::Assign if replaces_initial_about_blank => { LocationNavigationKind::Replace @@ -574,6 +620,9 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( }; let mut navigate_outcome = navigation.map(|navigation| { let _ = cancel_active_navigation_event(scope, navigation); + super::navigation_result::cancel_active_cross_document_navigation( + scope, navigation, None, + ); cancel_pending_precommit_same_document_navigation(scope, navigation); cancel_pending_precommit_history_traversal(scope, navigation); cancel_active_intercepted_same_document_navigation(scope, navigation); @@ -794,6 +843,16 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( return; } + let kind = if kind == LocationNavigationKind::Assign + && resolved.scheme() != "javascript" + && runtime_window_is_global(scope, owner) + && context_host_ptr_for_navigation_owner(scope, owner) + .is_some_and(|host| unsafe { &*host }.main_document_is_initial_empty()) + { + LocationNavigationKind::Replace + } else { + kind + }; let child_handle = if runtime_window_is_global(scope, owner) { None } else { @@ -804,11 +863,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( && let Some(navigation) = super::navigation_window::window_navigation_for_holder(scope, owner) { - if popup_id.is_some() { - super::navigation_result::cancel_active_cross_document_navigation( - scope, navigation, None, - ); - } + super::navigation_result::cancel_active_cross_document_navigation(scope, navigation, None); let _ = cancel_active_navigation_event(scope, navigation); cancel_pending_precommit_same_document_navigation(scope, navigation); cancel_pending_precommit_history_traversal(scope, navigation); @@ -863,14 +918,12 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( ); return; } - if popup_id.is_some() { - super::navigation_result::track_cross_document_location_navigation( - scope, - navigation, - outcome.signal, - resolved.as_str(), - ); - } + super::navigation_result::track_cross_document_location_navigation( + scope, + navigation, + outcome.signal, + resolved.as_str(), + ); } if let Some(popup_id) = popup_id { @@ -996,6 +1049,7 @@ pub(crate) fn is_native_location<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>, ) -> bool { + let receiver = moli_webapi_declare::web_api_object_target(scope, receiver).unwrap_or(receiver); crate::util::get_private_value(scope, receiver, WINDOW_LOCATION_HREF_SLOT).is_some() } @@ -1093,7 +1147,7 @@ fn capture_location_navigation_source<'s>( } } -fn blocks_ancestor_location_navigation<'s>( +pub(crate) fn blocks_ancestor_location_navigation<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, destination: &url::Url, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs b/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs index 7ed4313d36..6d178e34c7 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs @@ -16,7 +16,7 @@ use super::{ use crate::native_bridge::{ JsContextHost, OwnerDispatchScope, WindowDocumentTaskTarget, WindowExecutionContextBinding, }; -use crate::util::{context_host_ptr_from_global_bridge, get_private_value, set_private_value}; +use crate::util::{get_private_value, set_private_value}; const WINDOW_STOP_ACTIVE_SLOT: &str = "__lmWindowStopActive"; @@ -133,7 +133,7 @@ pub(crate) fn stop_navigation_for_window_and_descendants<'s>( window: v8::Local<'s, v8::Object>, binding: WindowExecutionContextBinding, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, window) else { return; }; let host = unsafe { &*host_ptr }; @@ -216,7 +216,7 @@ pub(super) fn clear_pending_cross_document_navigation_for_window<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, window) else { return; }; let host = unsafe { &mut *host_ptr }; @@ -238,7 +238,7 @@ pub(super) fn clear_pending_cross_document_navigation_for_window<'s>( } match dispatch_scope { Some(crate::native_bridge::OwnerDispatchScope::Top) => { - host.clear_pending_location_navigation(); + host.cancel_pending_location_navigation(); } Some(crate::native_bridge::OwnerDispatchScope::Child(handle)) => { host.cancel_pending_child_browsing_context_navigation(handle); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs index cd3c2ab78a..b2e31c1c5b 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs @@ -181,7 +181,9 @@ pub(super) fn navigation_document_origin<'s>( ) -> String { super::navigation_window::runtime_window_dispatch_scope(scope, owner) .and_then(|dispatch_scope| { - crate::util::context_host_ptr_from_global_bridge(scope) + owner + .get_creation_context(scope) + .and_then(crate::util::context_host_ptr_from_context_slot) .and_then(|host| unsafe { &*host }.window_document_origin(dispatch_scope)) }) .or_else(|| { diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs b/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs index 925c9747a5..99b403d875 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs @@ -118,6 +118,14 @@ pub(super) fn dispatch_cross_document_navigation_event<'s>( return false; } if !outcome.intercepted { + if !same_document && download_request.is_none() { + super::navigation_result::track_cross_document_location_navigation( + scope, + navigation, + outcome.signal, + href, + ); + } return true; } cancel_pending_same_document_navigation_finishes(scope, navigation); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs index 667dcd8278..a5a5879e56 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs @@ -1,7 +1,5 @@ use super::history_runtime::native; -use super::navigation_window::{ - runtime_window_dispatch_scope, -}; +use super::navigation_window::runtime_window_dispatch_scope; use crate::context_bootstrap::navigation_entry::wrappers as entry_wrappers; use crate::util::{context_host_ptr_from_global_bridge, serialize_v8_iter_array}; use moli_history::HistoryEntryRef; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs index 9b7e2004ff..39ba6010b0 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs @@ -105,7 +105,11 @@ pub(super) fn build_current_navigation_entry_from_seed<'s>( HistoryEntry { url: "about:blank".to_owned(), inherited_origin: None, - document_origin: super::navigation_entry::navigation_document_origin(scope, owner, "about:blank"), + document_origin: super::navigation_entry::navigation_document_origin( + scope, + owner, + "about:blank", + ), referrer_policy: None, history_state: state.clone(), navigation_state: state, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs index 6fa79cc4b0..b8ee65863f 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs @@ -285,9 +285,7 @@ pub(super) fn navigation_reload_callback<'s>( if navigation_can_update_current_entry(scope, args.this()) && let Some(navigation) = window_navigation_for_holder(scope, owner) { - if crate::native_bridge::lightweight_popup_id_from_window(scope, owner).is_some() { - cancel_active_cross_document_navigation(scope, navigation, None); - } + cancel_active_cross_document_navigation(scope, navigation, None); let _ = cancel_active_navigation_event(scope, navigation); cancel_pending_precommit_same_document_navigation(scope, navigation); cancel_pending_precommit_history_traversal(scope, navigation); @@ -397,6 +395,48 @@ pub(super) fn navigation_reload_callback<'s>( rv.set(pending.object.into()); return; } + if super::navigation_window::runtime_window_is_global(scope, owner) + && let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) + && let Some(context) = owner.get_creation_context(scope) + && let Some(entry_seed) = + super::navigation_seed::history_entry_seed_for_reload(scope, owner) + && let Ok(url) = url::Url::parse(¤t_href) + { + let scope = &mut v8::ContextScope::new(scope, context); + if !unsafe { &mut *host_ptr }.dispatch_main_document_tree_beforeunload(scope) { + rv.set( + reload_canceled_after_dispatch_result( + scope, + navigation, + &outcome, + ¤t_href, + ) + .into(), + ); + return; + } + let about_document_state = unsafe { &*host_ptr }.capture_about_document_state( + crate::native_bridge::OwnerDispatchScope::Top, + &url, + None, + ); + unsafe { &mut *host_ptr }.record_pending_location_navigation_with_kind( + url, + Some(entry_seed), + moli_fetch::BrowserNavigationRequestKind::Reload, + about_document_state, + ); + rv.set( + navigation_cross_document_pending_result( + scope, + navigation, + outcome.signal, + ¤t_href, + ) + .into(), + ); + return; + } if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) && let Some(entry_seed) = diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs index f846f5c717..089392b960 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs @@ -5,14 +5,12 @@ use super::navigation_events::{ NavigationDispatchOutcome, dispatch_navigation_traverse_event_with_outcome, }; use super::navigation_result::{ - cancel_active_cross_document_navigation, navigation_dom_exception, navigation_pending_result, + cancel_active_cross_document_navigation, navigation_dom_exception, navigation_rejected_dom_exception_result, track_cross_document_traversal_navigation, }; use super::navigation_seed::history_entry_seed_for_traversal; use super::navigation_window::{navigation_document_is_active, window_navigation_for_holder}; -use super::navigation_window::{ - runtime_window_is_global, window_history_for_holder, window_task_target_for_runtime_owner, -}; +use super::navigation_window::{window_history_for_holder, window_task_target_for_runtime_owner}; use super::*; use crate::native_bridge::{ PendingCrossDocumentTraversal, PendingHistoryTraversalAction, PendingNavigationResult, @@ -45,7 +43,7 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( "SecurityError", )); } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, target.owner) else { return Some(navigation_rejected_dom_exception_result( scope, "Navigation was canceled", @@ -60,7 +58,7 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( "AbortError", )); }; - if let Some((target_url, mut seed)) = history_entry_seed_for_traversal( + if let Some((_target_url, mut seed)) = history_entry_seed_for_traversal( scope, target.owner, target.current_index, @@ -68,10 +66,6 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( ) { seed.session_history.target_step = target.joint_step; super::session_history::capture_for_navigation(scope, target.owner, &mut seed); - if runtime_window_is_global(scope, target.owner) { - host.record_pending_location_navigation(target_url, Some(seed.clone()), None); - return Some(navigation_pending_result(scope)); - } let target_key = traversal_target_entry(scope, &target) .map(|entry| entry.borrow().key.as_str().to_owned()); let receiver_context = target @@ -124,7 +118,7 @@ pub(super) fn queue_history_traversal_by_delta<'s>( delta: i64, ) { let owner = super::navigation_window::runtime_window_owner(scope, history); - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return; }; let host = unsafe { &mut *host_ptr }; @@ -175,6 +169,7 @@ fn apply_history_traversal_by_delta( crate::native_bridge::OwnerDispatchScope::Top, crate::native_bridge::OwnerDispatchScope::Top, ) + && host.dispatch_main_document_tree_beforeunload(scope) { host.record_pending_top_level_history_traversal(delta); } @@ -322,14 +317,18 @@ pub(in crate::context_bootstrap) fn apply_pending_cross_document_traversal<'s, ' } // close() queues retirement of this exact Window. Keep its dispatched // event and API results alive until the close task aborts them together. - let closing_popup = matches!(dispatch_scope, - crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) - if host.lightweight_popup_is_closing(popup_id)) - && host.current_window_document_task_target_for_dispatch_scope(dispatch_scope) - == retiring_document; + let closing_window = match dispatch_scope { + crate::native_bridge::OwnerDispatchScope::Top => host.browsing_context_is_closed(), + crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => { + host.lightweight_popup_is_closing(popup_id) + } + crate::native_bridge::OwnerDispatchScope::Child(_) => false, + } && host + .current_window_document_task_target_for_dispatch_scope(dispatch_scope) + == retiring_document; if outcome.proceed && outcome.abort_error.is_none() - && (is_current(scope, host) || closing_popup) + && (is_current(scope, host) || closing_window) && let Some(navigation) = navigation { track_cross_document_traversal_navigation( @@ -344,7 +343,7 @@ pub(in crate::context_bootstrap) fn apply_pending_cross_document_traversal<'s, ' // cross-document traversals cannot be canceled by script. Only continue // while the original Window and Document are still active. if !is_current(scope, host) { - if !closing_popup { + if !closing_window { reject_cross_document_traversal(scope, &traversal); } return false; @@ -366,7 +365,42 @@ pub(in crate::context_bootstrap) fn apply_pending_cross_document_traversal<'s, ' queued } crate::native_bridge::OwnerDispatchScope::Top => { - unreachable!("top-level traversals are handed off to the browser") + if host.page_script_environment().is_none_or(|environment| { + environment.top_level_navigation_dispatch() + == crate::RendererTopLevelNavigationDispatch::DelegateToBrowser + }) { + let Some(step) = traversal.seed.session_history.target_step else { + reject_cross_document_traversal(scope, &traversal); + return false; + }; + let binding = super::session_history::binding(scope, host, owner); + let Some(delta) = host.session_histories.get_mut(binding.popup).delta_to(step) + else { + reject_cross_document_traversal(scope, &traversal); + return false; + }; + if let Some(key) = traversal.target_key { + host.top_level_navigation_history() + .select_joint_traversal(key, step); + } + host.record_pending_top_level_history_traversal(delta); + return true; + } + let Ok(url) = url::Url::parse(&target_url) else { + reject_cross_document_traversal(scope, &traversal); + return false; + }; + if let Some(key) = traversal.target_key + && let Some(step) = traversal.seed.session_history.target_step + { + host.top_level_navigation_history() + .select_joint_traversal(key, step); + } + // The standalone adapter has no browser history controller to + // consume a delta. Retain its concrete destination and seed after + // the shared navigate/beforeunload dispatch above. + host.record_pending_location_navigation(url, Some(traversal.seed), None); + true } } } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs index 5b61a34a8d..e6fb889706 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs @@ -31,7 +31,7 @@ fn navigation_owner_local_window_id<'s>( owner: v8::Local<'s, v8::Object>, ) -> Option { let dispatch_scope = runtime_window_dispatch_scope(scope, owner)?; - let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host_ptr = super::window_accessors::window_host_ptr(scope, owner)?; let owner = unsafe { &*host_ptr }.current_window_execution_context_owner(dispatch_scope)?; Some(match owner { crate::native_bridge::WindowExecutionContextOwner::Frame(local_window_id) => { @@ -63,7 +63,7 @@ fn navigation_owner_document<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> Option { - let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host_ptr = super::window_accessors::window_host_ptr(scope, owner)?; super::window_accessors::window_document_handle(scope, owner, unsafe { &*host_ptr }) } @@ -247,7 +247,7 @@ pub(super) fn navigation_document_is_initial_empty<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> bool { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; @@ -270,7 +270,7 @@ pub(super) fn navigation_document_has_disabled_entries<'s>( if navigation_document_has_opaque_origin(scope, owner) { return true; } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; @@ -279,6 +279,9 @@ pub(super) fn navigation_document_has_disabled_entries<'s>( if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { return !host.lightweight_popup_has_committed_navigation(popup_id); } + if runtime_window_is_global(scope, owner) { + return !host.main_document_has_committed_navigation(); + } child_browsing_context_handle_for_runtime_owner(scope, owner) .is_some_and(|handle| !host.child_has_committed_navigation(handle)) } @@ -287,7 +290,7 @@ pub(super) fn navigation_document_has_opaque_origin<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> bool { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; @@ -311,7 +314,7 @@ pub(crate) fn navigation_unload_event_active<'s>( ) -> bool { get_private_value(scope, owner, WINDOW_UNLOAD_EVENT_ACTIVE_SLOT) .is_some_and(|value| value.is_true()) - || context_host_ptr_from_global_bridge(scope).is_some_and(|host_ptr| { + || super::window_accessors::window_host_ptr(scope, owner).is_some_and(|host_ptr| { let host = unsafe { &*host_ptr }; super::window_accessors::window_document_handle(scope, owner, host) .is_some_and(|document| host.has_document_unload_counter(document)) @@ -341,7 +344,7 @@ pub(super) fn navigation_document_base_url<'s>( owner: v8::Local<'s, v8::Object>, fallback_href: &str, ) -> Option { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return url::Url::parse(fallback_href).ok(); }; let host = unsafe { &*host_ptr }; @@ -374,16 +377,17 @@ fn navigation_document_is_live<'s>( owner: v8::Local<'s, v8::Object>, ) -> bool { if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { - return context_host_ptr_from_global_bridge(scope) + return super::window_accessors::window_host_ptr(scope, owner) .is_some_and(|host_ptr| unsafe { &*host_ptr }.lightweight_popup_is_open(popup_id)); } if runtime_window_is_global(scope, owner) { - return true; + return super::window_accessors::window_host_ptr(scope, owner) + .is_some_and(|host_ptr| !unsafe { &*host_ptr }.browsing_context_is_closed()); } let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) else { return false; }; - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; diff --git a/moli-renderer-v8/src/context_bootstrap/session_history.rs b/moli-renderer-v8/src/context_bootstrap/session_history.rs index 02820cbb71..fae7be6e8e 100644 --- a/moli-renderer-v8/src/context_bootstrap/session_history.rs +++ b/moli-renderer-v8/src/context_bootstrap/session_history.rs @@ -234,13 +234,15 @@ pub(super) fn length<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> usize { - if super::navigation_window::navigation_document_is_initial_empty(scope, owner) { - return 1; - } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return 1; }; let host = unsafe { &mut *host_ptr }; + if super::navigation_window::runtime_window_is_global(scope, owner) + && !host.main_document_has_committed_navigation() + { + return 0; + } let binding = binding(scope, host, owner); host.session_histories .get_mut(binding.popup) diff --git a/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs b/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs index 00499d6b8b..4feea848d7 100644 --- a/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs @@ -92,17 +92,9 @@ pub(super) fn apply_step( if let Some(index) = root { let target = &plan.targets[index]; if runtime_window_is_global(scope, target.owner) { - let Some(delta) = host.session_histories.get_mut(binding.popup).delta_to(step) else { - return; - }; - if let Some(entry) = history_entries(scope, target.history) - .and_then(|entries| entries.get(target.target_index as usize).cloned()) - { - let key = entry.borrow().key.as_str().to_owned(); - host.top_level_navigation_history() - .select_joint_traversal(key, step); + if let Some(traversal) = cross_document_traversal(scope, host, target, step, method) { + apply_pending_cross_document_traversal(scope, host, traversal); } - host.record_pending_top_level_history_traversal(delta); return; } } @@ -130,38 +122,8 @@ pub(super) fn apply_step( host.retain_active_history_delta_position(binding.popup); let accepted = if let Some(index) = root { let target = &plan.targets[index]; - let pending = window_task_target_for_runtime_owner(scope, host, target.owner).zip( - history_entry_seed_for_traversal( - scope, - target.owner, - target.current_index, - target.target_index, - ), - ); - if let Some((exact, (_url, mut seed))) = pending { - seed.session_history.target_step = Some(step); - let key = history_entries(scope, target.history) - .and_then(|entries| entries.get(target.target_index as usize).cloned()) - .map(|entry| entry.borrow().key.as_str().to_owned()); - let (info, results) = method.map_or_else( - || (None, Vec::new()), - |method| (method.info, method.results), - ); - apply_pending_cross_document_traversal( - scope, - host, - PendingCrossDocumentTraversal { - target: exact, - target_index: target.target_index, - target_key: key, - seed, - info, - results, - }, - ) - } else { - false - } + cross_document_traversal(scope, host, target, step, method) + .is_some_and(|traversal| apply_pending_cross_document_traversal(scope, host, traversal)) } else { let history = window_history_for_holder(scope, owner).expect("admitted traversal has a history"); @@ -263,3 +225,35 @@ pub(crate) fn finish_without_document_commit( finish(scope, host_ptr, root, popup, step); } } + +fn cross_document_traversal<'s>( + scope: &mut v8::PinScope<'s, '_>, + host: &JsContextHost, + target: &super::navigation_traversal_execution::TraversalTarget<'s>, + step: SessionHistoryStepId, + method: Option, +) -> Option { + let exact = window_task_target_for_runtime_owner(scope, host, target.owner)?; + let (_, mut seed) = history_entry_seed_for_traversal( + scope, + target.owner, + target.current_index, + target.target_index, + )?; + seed.session_history.target_step = Some(step); + let key = history_entries(scope, target.history) + .and_then(|entries| entries.get(target.target_index as usize).cloned()) + .map(|entry| entry.borrow().key.as_str().to_owned()); + let (info, results) = method.map_or_else( + || (None, Vec::new()), + |method| (method.info, method.results), + ); + Some(PendingCrossDocumentTraversal { + target: exact, + target_index: target.target_index, + target_key: key, + seed, + info, + results, + }) +} diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs index 26b1187e3f..9997c6d858 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs @@ -41,7 +41,8 @@ pub(super) use interceptors::{ window_named_property_query, }; pub(crate) use surface::{ - window_parent_or_top, window_closed_getter, window_opener_getter, window_parent_getter, window_top_getter, + window_closed_getter, window_opener_getter, window_parent_getter, window_parent_or_top, + window_top_getter, }; pub(super) use surface::{ window_custom_elements_getter, window_device_pixel_ratio_getter, window_frames_getter, @@ -50,5 +51,4 @@ pub(super) use surface::{ window_outer_height_getter, window_outer_width_getter, window_performance_getter, window_screen_getter, window_scroll_x_getter, window_scroll_y_getter, window_self_getter, window_speech_synthesis_getter, window_visual_viewport_getter, window_window_getter, - }; diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs index c3f42de909..1edd589439 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs @@ -1,7 +1,7 @@ use super::helpers::{ - same_origin_window_receiver, - window_child_context_handle, window_document_handle, window_has_discarded_child_browsing_context, - window_hidden_value, window_host_ptr, window_is_closed, window_receiver, + same_origin_window_receiver, window_child_context_handle, window_document_handle, + window_has_discarded_child_browsing_context, window_hidden_value, window_host_ptr, + window_is_closed, window_receiver, }; use super::*; diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs index 85af20d8b7..f9d10b9275 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs @@ -5,8 +5,7 @@ use crate::{ util::{get_private_object, set_private_value}, }; -pub(in crate::context_bootstrap) const WINDOW_DOCUMENT_SLOT: &str = - "__moliWindowAssociatedDocument"; +pub(in crate::context_bootstrap) use super::super::shared::WINDOW_DOCUMENT_SLOT; pub(in crate::context_bootstrap) fn window_document_getter_template<'s>( scope: &mut v8::PinScope<'s, '_, ()>, @@ -19,9 +18,9 @@ pub(in crate::context_bootstrap) fn window_document_getter_template<'s>( None => v8::FunctionTemplate::new(scope, window_document_getter), }; template.remove_prototype(); - // V8 checks the receiver against the actual access Context before - // invoking the callback, including when the getter is borrowed. - template.set_accept_any_receiver(false); + // WindowProxy checks ordinary property reads before using the cache. + // Explicit getter calls must reach our receiver check in the callee's + // realm so a borrowed getter creates errors with the correct intrinsic. template } @@ -30,9 +29,7 @@ pub(crate) fn retain_window_document_in_retired_realm( ) -> Option<()> { let context = scope.get_current_context(); let host_ptr = crate::util::context_host_ptr_from_context_slot(context)?; - let _ = WindowEnvironmentSettings::bind_current_main_document_for_retirement(scope, unsafe { - &*host_ptr - }); + let _ = WindowEnvironmentSettings::bind_current_main_document(scope, unsafe { &*host_ptr }); let window = context.global(scope); if get_private_object(scope, window, WINDOW_DOCUMENT_SLOT).is_some() { return Some(()); @@ -94,6 +91,12 @@ pub(in crate::context_bootstrap) fn window_document_getter<'s>( rv.set(document.into()); return; } + if crate::native_bridge::lightweight_popup_id_from_window(scope, receiver).is_some() { + // Popup bootstrap may precede its Document projection. The opener's + // host Document is never this Window's associated Document. + rv.set_null(); + return; + } let handle = if let Some(settings) = WindowEnvironmentSettings::for_current_realm(scope) { settings.document_handle() } else if let Some(handle) = window_child_context_handle(scope, receiver) { diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs index 321196c24e..fae57b7f14 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs @@ -1,6 +1,6 @@ use super::helpers::{ - window_child_context_handle, window_has_discarded_child_browsing_context, window_hidden_value, window_host_ptr, window_is_closed, - window_receiver, + window_child_context_handle, window_has_discarded_child_browsing_context, window_hidden_value, + window_host_ptr, window_is_closed, window_receiver, }; use super::*; use crate::{ @@ -176,7 +176,6 @@ pub(crate) fn window_parent_or_top<'s>( } pub(crate) fn window_opener_getter<'s>( - scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, @@ -193,17 +192,23 @@ pub(crate) fn window_opener_getter<'s>( return; }; rv.set_null(); - if window_is_closed(scope, receiver) || window_has_discarded_child_browsing_context(scope, receiver) { + if window_is_closed(scope, receiver) + || window_has_discarded_child_browsing_context(scope, receiver) + { return; } if let Some(host_ptr) = window_host_ptr(scope, receiver) { let host = unsafe { &*host_ptr }; if let Some(popup_id) = lightweight_popup_id_from_window(scope, receiver) { - if let Some(opener) = host.lightweight_popup_opener_window(scope, popup_id) { rv.set(opener.into()); } + if let Some(opener) = host.lightweight_popup_opener_window(scope, popup_id) { + rv.set(opener.into()); + } return; } if let Some(handle) = window_child_context_handle(scope, receiver) { - if let Some(opener) = host.child_browsing_context_opener(scope, handle) { rv.set(opener.into()); } + if let Some(opener) = host.child_browsing_context_opener(scope, handle) { + rv.set(opener.into()); + } return; } } diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime.rs index f7bbe43995..8e2ac9370b 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime.rs @@ -16,8 +16,8 @@ pub(super) use base64::{ pub(super) use dialogs::entered_window_api_base_url; pub(super) use dialogs::{window_alert_callback, window_confirm_callback, window_prompt_callback}; pub(crate) use dialogs::{ - window_blur_callback, window_close_callback, window_const_false_callback, window_focus_callback, - window_noop_callback, window_open_callback, window_stop_callback, + window_blur_callback, window_close_callback, window_const_false_callback, + window_focus_callback, window_noop_callback, window_open_callback, window_stop_callback, }; pub(crate) use navigator::{ LegacyStorageQuotaCallbackOutcome, LegacyStorageQuotaCallbackTask, diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs index daabf0577e..2cb203565e 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs @@ -12,7 +12,6 @@ use crate::{ SpecialBrowsingContextTarget, navigate_existing_browsing_context_target, navigate_iframe_target, }, - entered_child_window_handle, }, runtime::{ RendererPendingJavaScriptDialog, RendererPendingPopupActivation, @@ -88,8 +87,15 @@ pub(crate) fn window_close_callback<'s>( }); if let Some(host_ptr) = receiver_context.and_then(context_host_ptr_from_context_slot) { let host = unsafe { &mut *host_ptr }; - if let Some(window) = host.auxiliary_window() { - host.request_auxiliary_window_close(window); + if let Some(window) = host.auxiliary_window() + && host.request_auxiliary_window_close(window) + && let Some(context) = receiver_context + { + // Closing has its own synchronous beforeunload check, including + // close() called from pagehide. The native close transition above + // rejects reentry; the queued close task owns final retirement. + let scope = &mut v8::ContextScope::new(scope, context); + host.dispatch_main_document_tree_beforeunload_for_close(scope); } } } @@ -142,9 +148,15 @@ pub(crate) fn window_stop_callback<'s>( if !crate::context_bootstrap::require_same_origin_window_receiver(scope, args.this(), false) { return; } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::super::window_accessors::window_host_ptr(scope, args.this()) else { return; }; + let Some(context) = args.this().get_creation_context(scope) else { + return; + }; + // The receiver was authorized above. Its native binding belongs to its + // own host, including when stop() is borrowed from another Window. + let scope = &mut v8::ContextScope::new(scope, context); let receiver = match crate::native_bridge::WindowOperationReceiver::capture_and_authorize( scope, args.this(), @@ -462,7 +474,7 @@ pub(crate) fn window_open_callback<'s>( // The entered Window identifies the creator even when `noopener` removes // the opener relationship. Its child marker is more precise than the // ambient dispatch scope while a child callback is running. - let source_child_handle = window_open_receiver_child_handle(scope, entered_window); + let source_child_handle = window_receiver_child_handle(scope, entered_window); let popup_source_scope = source_child_handle .map(OwnerDispatchScope::Child) .unwrap_or(source_scope); @@ -480,7 +492,10 @@ pub(crate) fn window_open_callback<'s>( _ => crate::RendererPopupDisposition::Foreground, }; if host.has_browser_owned_auxiliary_page_factory() - && (!suppress_opener || url.as_deref().is_none_or(|url| Url::parse(url).is_ok_and(|url| moli_url::is_about_blank(&url)))) + && (!suppress_opener + || url + .as_deref() + .is_none_or(|url| Url::parse(url).is_ok_and(|url| moli_url::is_about_blank(&url)))) { let creator_child_handle = window_receiver_child_handle(scope, entered_window); match host.open_renderer_owned_auxiliary_window( @@ -496,6 +511,14 @@ pub(crate) fn window_open_callback<'s>( true, ) { Ok(opened) => { + if !parsed.raw_url.is_empty() && !opened.prepare_navigation_activation(scope) { + if suppress_opener { + rv.set_null(); + } else { + rv.set(opened.window_proxy.into()); + } + return; + } let disposition = if opened.pending_page.is_some() || host.protocol_user_gesture_activation() || host.current_input_event().is_some() @@ -552,7 +575,7 @@ pub(crate) fn window_open_callback<'s>( url.as_deref(), entered_base_url, creator_policy_container, - true, + false, ), _ => host.open_lightweight_popup_window( scope, @@ -582,7 +605,9 @@ pub(crate) fn window_open_callback<'s>( let popup_id = opened_popup.popup_id; if opened_popup.created_new_browsing_context { host.set_lightweight_popup_is_popup(popup_id, parsed_features.is_popup()); - } else if url.is_none() || !opened_popup.allows_navigation_activation(scope, host) { + } else if (url.is_none() && host.lightweight_popup_has_pending_document_load(popup_id)) + || !opened_popup.allows_navigation_activation(scope, host) + { if suppress_opener { rv.set(v8::null(scope).into()); } else { @@ -796,7 +821,8 @@ fn open_dialog( if !allows_modals { return None; } - let source_url = window_open_entered_document_url(scope, host).to_string(); + let source_url = + window_open_document_url(host, window_open_entry_scope(scope, host)).to_string(); let dialog_id = host.allocate_javascript_dialog_id(); host.open_modal_javascript_dialog( target, diff --git a/moli-renderer-v8/src/frame_owner_model/records.rs b/moli-renderer-v8/src/frame_owner_model/records.rs index b0b500508a..597d007636 100644 --- a/moli-renderer-v8/src/frame_owner_model/records.rs +++ b/moli-renderer-v8/src/frame_owner_model/records.rs @@ -926,7 +926,11 @@ impl DocumentLifecycleRecord { self.domcontentloaded_transition_token = None; self.complete_transition_token = None; self.readiness = Some(DocumentReadinessState::Complete); - self.load = DocumentLoadEventProgress::Ready; + self.load = if self.load_delivery_kind == DocumentLoadDeliveryKind::Main { + DocumentLoadEventProgress::Suppressed + } else { + DocumentLoadEventProgress::Ready + }; self.completely_loaded = true; true } @@ -1129,6 +1133,10 @@ impl DocumentLifecycleRecord { Some(finished) } + pub(super) fn unload_has_started(&self) -> bool { + self.unload != DocumentUnloadEventProgress::Pending + } + pub(super) fn begin_unload_dispatch(&mut self) -> bool { if !self.child_load_event_has_started() || self.unload != DocumentUnloadEventProgress::Pending diff --git a/moli-renderer-v8/src/frame_owner_model/store.rs b/moli-renderer-v8/src/frame_owner_model/store.rs index 0b2d44c6bc..747655949e 100644 --- a/moli-renderer-v8/src/frame_owner_model/store.rs +++ b/moli-renderer-v8/src/frame_owner_model/store.rs @@ -179,6 +179,38 @@ impl FrameOwnerStore { realm_id } + pub(crate) fn mark_main_document_initial_empty(&mut self) { + let owner = self + .current_main_owner_snapshot() + .expect("an initial main Document must have an installed owner"); + let document = self + .documents + .get_mut(&owner.document_id) + .expect("the current main Document must retain its owner record"); + document.creation_kind = DocumentCreationKind::InitialEmpty; + document.has_committed_navigation = false; + } + + pub(crate) fn main_document_has_committed_navigation(&self) -> bool { + self.current_main_owner_snapshot() + .and_then(|owner| self.documents.get(&owner.document_id)) + .is_some_and(|document| document.has_committed_navigation) + } + + pub(crate) fn complete_current_main_initial_empty_document(&mut self) -> bool { + let Some(owner) = self.current_main_document_task_owner() else { + return false; + }; + self.documents + .get_mut(&owner.document_id) + .filter(|document| moli_url::is_about_blank(&document.url)) + .is_some_and(|document| { + document + .lifecycle_progress + .complete_initial_empty_document() + }) + } + pub(crate) fn ensure_child_frame( &mut self, child_handle: DomHandle, @@ -397,25 +429,27 @@ impl FrameOwnerStore { .lifecycle_progress .document_open_load_continuation(); let completely_loaded = retired_document.lifecycle_progress.is_completely_loaded(); - let has_committed_navigation = if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { - retired_document.has_committed_navigation - } else { - !creation_kind.is_initial_empty() - }; + let has_committed_navigation = + if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { + retired_document.has_committed_navigation + } else { + !creation_kind.is_initial_empty() + }; retired_document.lifecycle = DocumentLifecycleState::Replaced; retired_document.lifecycle_progress.retire(); retired_document.active_requests.clear(); let document_id = transition.current_owner().document_id; - let lifecycle_progress = if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { - self.new_loading_document_lifecycle_for_document_open( - DocumentLoadDeliveryKind::Main, - load_continuation, - completely_loaded, - ) - } else { - self.new_loading_document_lifecycle(DocumentLoadDeliveryKind::Main) - }; + let lifecycle_progress = + if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { + self.new_loading_document_lifecycle_for_document_open( + DocumentLoadDeliveryKind::Main, + load_continuation, + completely_loaded, + ) + } else { + self.new_loading_document_lifecycle(DocumentLoadDeliveryKind::Main) + }; self.documents.insert( document_id, DocumentRecord { @@ -2085,6 +2119,12 @@ impl FrameOwnerStore { }) } + pub(crate) fn current_main_document_unload_has_started(&self) -> bool { + self.current_main_document_task_owner() + .and_then(|owner| self.documents.get(&owner.document_id)) + .is_some_and(|document| document.lifecycle_progress.unload_has_started()) + } + pub(crate) fn begin_current_main_document_unload(&mut self) -> Option { let owner = self.current_main_document_task_owner()?; self.documents diff --git a/moli-renderer-v8/src/frame_owner_model/store_tests.rs b/moli-renderer-v8/src/frame_owner_model/store_tests.rs index 91119a9c75..6cf017525d 100644 --- a/moli-renderer-v8/src/frame_owner_model/store_tests.rs +++ b/moli-renderer-v8/src/frame_owner_model/store_tests.rs @@ -381,6 +381,53 @@ fn initial_main_document_commit_prepares_owners_before_replacement_without_turn_ ); } +#[test] +fn main_initial_document_stays_provisional_through_open_until_navigation_commits() { + let mut store = FrameOwnerStore::default(); + store.ensure_main_frame( + handle(1), + url("about:blank"), + url("https://creator.test/"), + "https://creator.test".to_owned(), + policy_container(), + policy_context(), + None, + ); + store.mark_main_document_initial_empty(); + assert!(!store.main_document_has_committed_navigation()); + assert!(store.complete_current_main_initial_empty_document()); + assert!(!store.complete_current_main_initial_empty_document()); + let initial = store.current_main_document_task_owner().unwrap(); + assert_eq!( + store.current_document_is_completely_loaded(initial.document_owner()), + Some(true) + ); + assert_eq!( + store.current_main_document_complete_transition_is_ready(initial), + Some(false) + ); + store + .replace_main_document( + handle(1), + url("https://creator.test/written"), + url("https://creator.test/written"), + ) + .unwrap(); + assert!(!store.main_document_has_committed_navigation()); + let transition = store + .prepare_initial_main_document_owner_transition() + .unwrap(); + store + .replace_initial_main_document( + transition, + handle(2), + url("https://creator.test/committed"), + url("https://creator.test/committed"), + ) + .unwrap(); + assert!(store.main_document_has_committed_navigation()); +} + #[test] fn main_document_replacement_rotates_document_owner_without_replacing_window_or_realm() { let mut store = FrameOwnerStore::default(); diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index cd6d31831f..7d5d617dd8 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -232,10 +232,9 @@ pub use runtime::{ CompletedWorkerRuntimeInspectorCommandDispatch, DetachedParserScriptFetchContinuation, DevToolsSessionKey, ExternalRawDocumentBodyStream, JsRuntime, JsRuntimeOwner, PageId, PendingHtmlPage, PendingWorkerRuntimeInspectorSessionResponse, PreparedRendererDocument, - RendererAboutDocumentState, - RendererAccessibilityPayloadsForObjectId, RendererActivityDiagnostics, - RendererAgentAttachmentId, RendererAutofillAddressField, RendererAutofillCreditCard, - RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, + RendererAboutDocumentState, RendererAccessibilityPayloadsForObjectId, + RendererActivityDiagnostics, RendererAgentAttachmentId, RendererAutofillAddressField, + RendererAutofillCreditCard, RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, RendererAuxiliaryDocumentResponse, RendererAuxiliaryNavigationKind, RendererAuxiliaryWindow, RendererBrowserContextRuntime, RendererBrowserContextRuntimeId, RendererBrowserContextRuntimeOwner, RendererBrowserContextRuntimeOwnerAccess, @@ -286,11 +285,12 @@ pub use runtime::{ RendererNativeCommandReadyResponse, RendererNativeCommandResponseGuard, RendererNativeCommandTerminal, RendererNativeOperation, RendererNativeOperationStep, RendererNativeProtocolError, RendererNativeProtocolNotification, - RendererNativeProtocolResponse, RendererNativeProtocolStateUpdate, RendererNavigationInitiator, RendererNavigationHistory, RendererNavigationHistoryRequest, - RendererOutputCursor, RendererOutputFence, RendererOutputFenceLeaseId, RendererOutputItem, - RendererOutputPublication, RendererOutputPublicationOrdering, RendererOutputRecord, - RendererOutputResidenceIdentity, RendererOutputStreamCloseReason, RendererOutputStreamControl, - RendererOutputStreamEpoch, RendererOutputStreamIdentity, RendererOutputTransportDiagnostics, + RendererNativeProtocolResponse, RendererNativeProtocolStateUpdate, RendererNavigationHistory, + RendererNavigationHistoryRequest, RendererNavigationInitiator, RendererOutputCursor, + RendererOutputFence, RendererOutputFenceLeaseId, RendererOutputItem, RendererOutputPublication, + RendererOutputPublicationOrdering, RendererOutputRecord, RendererOutputResidenceIdentity, + RendererOutputStreamCloseReason, RendererOutputStreamControl, RendererOutputStreamEpoch, + RendererOutputStreamIdentity, RendererOutputTransportDiagnostics, RendererOutputTransportMessage, RendererOutputTransportReceiver, RendererOutputTransportSendError, RendererOutputTransportSender, RendererOwnerAction, RendererOwnerCommand, RendererOwnerHandle, RendererOwnerLocalHostId, RendererOwnerReply, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs index 301ca8d828..fc0172a719 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs @@ -1229,6 +1229,7 @@ impl JsContextHost { host_ptr: *mut Self, document_handle: DomHandle, ) { + let _unload = unsafe { &*host_ptr }.enter_document_unload(document_handle); let handles = unsafe { &*host_ptr }.child_browsing_context_handles_in_document_order(); for handle in handles { if unsafe { &*host_ptr } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs index f895dd3be7..7a1a2f72f1 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs @@ -151,7 +151,7 @@ impl JsContextHost { self.retire_window_realm_resources(stale.runtime_observable_context_token); self.retire_window_execution_contexts_for_context_token( stale.runtime_observable_context_token, - config.resource_owner_id, + Some(config.resource_owner_id), ); if self.child_window_proxy_frame_is_current(handle, &stale.frame_id) { stale_context.detach_global(); diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs index 431bc5b33b..b1f9e3557c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs @@ -65,6 +65,7 @@ impl CrossOriginWindowProperty { /// on the Window or reading its private slots from the accessing realm. pub(crate) struct CrossOriginWindowAccessor<'s> { surface: v8::Local<'s, v8::Object>, + receiver: v8::Local<'s, v8::Object>, } impl<'s> CrossOriginWindowAccessor<'s> { @@ -110,26 +111,35 @@ impl<'s> CrossOriginWindowAccessor<'s> { receiver: v8::Local<'s, v8::Object>, ) -> Option { let context = receiver.get_creation_context(scope)?; + let accessing_context = cross_origin_accessing_context(scope); if !receiver.strict_equals(context.global(scope).into()) && crate::native_bridge::lightweight_popup_id_from_window(scope, receiver).is_some() { - let accessing_context = scope.get_current_context(); if synthetic_window_context_can_access(scope, accessing_context, receiver) { return None; } - return window_cross_origin_access_surface(scope, receiver) - .map(|surface| Self { surface }); + return window_cross_origin_backing_surface(scope, receiver) + .map(|surface| Self { surface, receiver }); } if !receiver.strict_equals(context.global(scope).into()) - || super::super::window_contexts_allow_access(scope.get_current_context(), context) + || contexts_can_script_access(scope, accessing_context, context) { return None; } // Only native Window realms install this context slot. A copied bridge // property, inherited Window prototype, or author Proxy cannot supply it. + if context + .get_slot::() + .is_some() + { + return Some(Self { + surface: context.get_extras_binding_object(scope), + receiver, + }); + } context.get_slot::()?; - let surface = window_cross_origin_access_surface(scope, receiver)?; - Some(Self { surface }) + let surface = window_cross_origin_backing_surface(scope, receiver)?; + Some(Self { surface, receiver }) } pub(crate) fn get( @@ -137,6 +147,14 @@ impl<'s> CrossOriginWindowAccessor<'s> { scope: &mut v8::PinScope<'s, '_>, property: CrossOriginWindowProperty, ) -> Option> { + if self + .receiver + .get_creation_context(scope)? + .get_slot::() + .is_some() + { + return main_page::window_property_value(scope, self.receiver, property); + } self.surface .get(scope, v8str(scope, property.name()).into()) } @@ -220,6 +238,23 @@ impl ChildWindowProxyRecords { .map(|window| v8::Local::new(scope, window)) } + fn cross_origin_handler_data<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + handle: DomHandle, + ) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> { + let window = self.live_window(scope, handle)?; + let context = window.get_creation_context(scope)?; + let target_scope = &mut v8::ContextScope::new(scope, context); + let surface = get_private_value( + target_scope, + window, + CROSS_ORIGIN_WINDOW_ACCESS_SURFACE_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok())?; + Some((surface, window)) + } + pub(in crate::native_bridge::context_host) fn has_live_window( &self, handle: DomHandle, @@ -880,6 +915,8 @@ struct CrossOriginLocationProxyHandlerDeclaration { set: (), #[webapi(method, length = 2, callback = cross_origin_location_proxy_descriptor_callback)] get_own_property_descriptor: (), + #[webapi(method, length = 1, callback = cross_origin_location_proxy_own_keys_callback)] + own_keys: (), #[webapi(method, length = 2, callback = cross_origin_window_denied_callback)] delete_property: (), #[webapi(method, length = 3, callback = cross_origin_window_denied_callback)] @@ -1502,6 +1539,18 @@ impl JsContextHost { &self, scope: &mut v8::PinScope<'s, '_>, ) -> Option> { + if self.auxiliary_window().is_some() + && let Some(environment) = self.page_script_environment() + { + // Named reuse changes the related Page's opener synchronously. + // Its script environment also records an explicitly disowned + // opener; an older endpoint must not restore that relationship. + let opener = environment.opener_in_scope(scope)?; + let host = opener + .get_creation_context(scope) + .and_then(crate::util::context_host_ptr_from_context_slot)?; + return (!unsafe { &*host }.browsing_context_is_closed()).then_some(opener); + } let (endpoint, opener) = self.top_window_opener.as_ref()?; let opener = v8::Local::new(scope, opener); self.window_opener_endpoint_is_live(scope, *endpoint, opener) @@ -1559,6 +1608,15 @@ impl JsContextHost { } } + pub(crate) fn child_browsing_context_top_window_for_current_realm<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + handle: DomHandle, + ) -> Option> { + self.child_window_proxy_records + .browsing_context_top(scope, handle) + } + pub(in crate::native_bridge::context_host) fn child_browsing_context_parent_window<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, @@ -2361,6 +2419,7 @@ fn wrap_cross_origin_location_proxy<'s>( get: (), set: (), get_own_property_descriptor: (), + own_keys: (), delete_property: (), define_property: (), } @@ -2518,6 +2577,11 @@ fn window_cross_origin_access_surface<'s>( scope: &mut v8::PinScope<'s, '_>, holder: v8::Local<'s, v8::Object>, ) -> Option> { + if let Some((surface, _)) = child_window_cross_origin_handler_data(scope, holder) + && main_page::is_main_surface(scope, surface) + { + return Some(surface); + } if crate::native_bridge::lightweight_popup_id_from_window(scope, holder).is_some() { let surface = get_private_value(scope, holder, CROSS_ORIGIN_WINDOW_ACCESS_SURFACE_SLOT) .and_then(|value| v8::Local::::try_from(value).ok())?; @@ -2555,6 +2619,23 @@ fn window_cross_origin_access_surface<'s>( } } +// Native getter projection reads the target's backing surface. Public +// descriptors use the caller surface and must validate their actual receiver. +fn window_cross_origin_backing_surface<'s>( + scope: &mut v8::PinScope<'s, '_>, + holder: v8::Local<'s, v8::Object>, +) -> Option> { + let context = holder.get_creation_context(scope)?; + let scope = &mut v8::ContextScope::new(scope, context); + get_private_value(scope, holder, CROSS_ORIGIN_WINDOW_ACCESS_SURFACE_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .or_else(|| { + context + .get_slot::() + .and_then(|surface| surface.0.to_local(scope)) + }) +} + fn window_cross_origin_caller_surface<'s>( scope: &mut v8::PinScope<'s, '_>, storage: v8::Local<'s, v8::Object>, @@ -2635,7 +2716,9 @@ fn child_window_cross_origin_handler_data<'s>( if let Some(identity) = host.window_execution_context_identity_for_access_check(holder_context) && host.window_execution_context_identity_is_current(identity) { - if !host.window_execution_context_identity_is_default_world(identity) { + let (_, default_context) = + host.window_execution_context(scope, identity.owner(), identity.dispatch_scope())?; + if default_context != holder_context { return None; } if identity.dispatch_scope() == super::super::OwnerDispatchScope::Top { @@ -2759,7 +2842,7 @@ fn child_window_cross_origin_named_enumerator<'s>( property_names.index_filter(v8::IndexFilter::SkipIndices); property_names.key_conversion(v8::KeyConversionMode::ConvertToString); let names = window_cross_origin_access_surface(scope, callback_args.holder()) - .and_then(|surface| surface.get_own_property_names(scope, property_names.build())) + .and_then(|surface| surface.get_property_names(scope, property_names.build())) .unwrap_or_else(|| v8::Array::new(scope, 0)); rv.set(names); } @@ -2813,7 +2896,16 @@ fn child_window_cross_origin_named_child_value<'s>( if cross_origin_window_has_discarded_browsing_context(scope, surface) { return None; } - let parent = if let Some(handle) = child_handle_from_object(scope, surface) { + let main_host = main_page::is_main_surface(scope, surface) + .then(|| { + let window = get_private_value(scope, surface, CROSS_ORIGIN_WINDOW_SELF_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + crate::util::context_host_ptr_from_context_slot(window.get_creation_context(scope)?) + }) + .flatten(); + let parent = if main_host.is_some() { + super::super::OwnerDispatchScope::Top + } else if let Some(handle) = child_handle_from_object(scope, surface) { super::super::OwnerDispatchScope::Child(handle) } else if let Some(id) = cross_origin_lightweight_popup_id(scope, surface) { super::super::OwnerDispatchScope::LightweightPopup(id) @@ -2822,7 +2914,10 @@ fn child_window_cross_origin_named_child_value<'s>( } else { return None; }; - let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host_ptr = main_host.or_else(|| context_host_ptr_from_global_bridge(scope))?; + if unsafe { &*host_ptr }.browsing_context_is_closed() { + return None; + } let child_handle = unsafe { &*host_ptr } .child_browsing_context_named_child_handle_for_scope(parent, &key_name)?; unsafe { &mut *host_ptr } @@ -2838,7 +2933,9 @@ fn child_window_cross_origin_indexed_getter<'s>( ) -> v8::Intercepted { if let Some(surface) = window_cross_origin_access_surface(scope, args.holder()) && main_page::is_main_surface(scope, surface) - && !surface.has_own_property(scope, v8_string(scope, &index.to_string()).unwrap().into()).unwrap_or(false) + && !surface + .has_own_property(scope, v8_string(scope, &index.to_string()).unwrap().into()) + .unwrap_or(false) { return v8::Intercepted::kNo; } @@ -2923,9 +3020,7 @@ fn child_window_cross_origin_indexed_enumerator<'s>( ) { let holder = args.holder(); let count = window_cross_origin_access_surface(scope, holder) - .filter(|surface| { - !cross_origin_window_has_discarded_browsing_context(scope, *surface) - }) + .filter(|surface| !cross_origin_window_has_discarded_browsing_context(scope, *surface)) .and_then(|surface| { cross_origin_surface_get(scope, holder, surface, v8str(scope, "length").into()) }) @@ -3052,7 +3147,10 @@ fn cross_origin_location_proxy_get_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if is_cross_origin_location_href_key_value(scope, args.get(1)) { + if !v8::Local::::try_from(args.get(1)) + .is_ok_and(|key| cross_origin_location_key_is_allowed(scope, key)) + || is_cross_origin_location_href_key_value(scope, args.get(1)) + { throw_cross_origin_location_security_error(scope); return; } @@ -3061,10 +3159,9 @@ fn cross_origin_location_proxy_get_callback<'s>( return; }; let receiver = v8::Local::::try_from(args.get(2)).unwrap_or(target); - let surface = main_page::location_access_surface(scope, target).unwrap_or(target); let accessing_context = cross_origin_accessing_context(scope); let scope = &mut v8::ContextScope::new(scope, accessing_context); - match surface.get_with_receiver(scope, args.get(1), receiver) { + match target.get_with_receiver(scope, args.get(1), receiver) { Some(value) => rv.set(value), None => rv.set_undefined(), } @@ -3081,27 +3178,50 @@ fn cross_origin_location_proxy_descriptor_callback<'s>( let Ok(key) = v8::Local::::try_from(args.get(1)) else { return; }; - let main_surface = main_page::location_access_surface(scope, target); - let surface = main_surface.unwrap_or(target); + if !cross_origin_location_key_is_allowed(scope, key) { + throw_cross_origin_location_security_error(scope); + return; + } let accessing_context = cross_origin_accessing_context(scope); let scope = &mut v8::ContextScope::new(scope, accessing_context); - let Some(descriptor) = surface.get_own_property_descriptor(scope, key) else { + let Some(descriptor) = target.get_own_property_descriptor(scope, key) else { return; }; - if main_surface.is_some() - && let Ok(descriptor) = v8::Local::::try_from(descriptor) - { - // Main Location's target holds placeholders. Its descriptors are - // projected into each accessor realm and must remain configurable. - let _ = descriptor.set( - scope, - v8str(scope, "configurable").into(), - v8::Boolean::new(scope, true).into(), - ); - } rv.set(descriptor); } +fn cross_origin_location_key_is_allowed<'s>( + scope: &mut v8::PinScope<'s, '_>, + key: v8::Local<'s, v8::Name>, +) -> bool { + ["href", "replace", "then"] + .into_iter() + .any(|name| key == v8str(scope, name)) + || [ + v8::Symbol::get_to_string_tag(scope), + v8::Symbol::get_has_instance(scope), + v8::Symbol::get_is_concat_spreadable(scope), + ] + .into_iter() + .any(|symbol| key == symbol) +} + +fn cross_origin_location_proxy_own_keys_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let keys = [ + v8str(scope, "href").into(), + v8str(scope, "replace").into(), + v8str(scope, "then").into(), + v8::Symbol::get_to_string_tag(scope).into(), + v8::Symbol::get_has_instance(scope).into(), + v8::Symbol::get_is_concat_spreadable(scope).into(), + ]; + rv.set(v8::Array::new_with_elements(scope, &keys).into()); +} + fn cross_origin_location_proxy_set_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, target: v8::Local<'s, v8::Object>, @@ -3126,16 +3246,7 @@ fn cross_origin_location_proxy_set_callback<'s>( return; }; if !is_cross_origin_location_href_key_value(scope, args.get(1)) { - let receiver = v8::Local::::try_from(args.get(3)).unwrap_or(target); - rv.set( - v8::Boolean::new( - scope, - target - .set_with_receiver(scope, args.get(1), args.get(2), receiver) - .unwrap_or(false), - ) - .into(), - ); + throw_cross_origin_location_security_error(scope); return; } let Some(receiver) = cross_origin_location_proxy_set_receiver(scope, target, args.get(3)) @@ -3255,7 +3366,11 @@ fn cross_origin_window_has_discarded_browsing_context<'s>( get_cross_origin_proxy_private_value(scope, window, CROSS_ORIGIN_WINDOW_SELF_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) .unwrap_or(window); - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = identity + .get_creation_context(scope) + .and_then(crate::util::context_host_ptr_from_context_slot) + .or_else(|| context_host_ptr_from_global_bridge(scope)) + else { return true; }; // DOM handles survive removal/reinsertion, and a WindowProxy survives @@ -3463,14 +3578,6 @@ fn cross_origin_location_navigate<'s>( receiver: v8::Local<'s, v8::Object>, value: v8::Local<'s, v8::Value>, ) -> bool { - if main_page::is_main_location(scope, receiver) { - return main_page::navigate( - scope, - receiver, - value, - crate::context_bootstrap::LocationNavigationKind::Assign, - ); - } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return false; }; @@ -3547,22 +3654,6 @@ fn cross_origin_location_replace_callback<'s>( ); return; } - if main_page::is_main_location(scope, args.this()) { - let Some(parsed) = webidl::parse_args::(scope, &args) - else { - return; - }; - let Some(value) = v8_string(scope, &parsed.url) else { - return; - }; - main_page::navigate( - scope, - args.this(), - value.into(), - crate::context_bootstrap::LocationNavigationKind::Replace, - ); - return; - } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return; }; diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs index 75e5cbef93..140c25d2ce 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs @@ -4,12 +4,49 @@ use crate::context_bootstrap::{ window_opener_getter, window_parent_getter, window_top_getter, }; -const LOCATION: &str = "__moliMainWindowCrossOriginLocationObject"; -const LOCATION_SURFACE: &str = "__moliMainWindowCrossOriginLocationSurface"; -const MAIN_LOCATION: &str = "__moliMainWindowCrossOriginLocation"; const INDEXED_COUNT: &str = "__moliMainWindowCrossOriginIndexedCount"; const WINDOW_LOCATION_SLOT: &str = "__moliWindowLocation"; +pub(super) fn window_property_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: v8::Local<'s, v8::Object>, + property: CrossOriginWindowProperty, +) -> Option> { + if matches!(property, CrossOriginWindowProperty::Location) { + return location_for_window(scope, window).map(Into::into); + } + let context = window.get_creation_context(scope)?; + let host = crate::util::context_host_ptr_from_context_slot(context); + let closed = host.is_none_or(|host| unsafe { &*host }.browsing_context_is_closed()); + Some(match property { + CrossOriginWindowProperty::Closed => v8::Boolean::new(scope, closed).into(), + CrossOriginWindowProperty::Length => { + let length = if closed { + 0 + } else { + unsafe { &*host? }.child_browsing_context_count() + }; + v8::Integer::new_from_unsigned(scope, length as u32).into() + } + CrossOriginWindowProperty::Opener => { + if closed { + v8::null(scope).into() + } else { + unsafe { &*host? } + .top_window_opener(scope) + .map_or_else(|| v8::null(scope).into(), Into::into) + } + } + CrossOriginWindowProperty::Parent | CrossOriginWindowProperty::Top if closed => { + v8::null(scope).into() + } + CrossOriginWindowProperty::Parent + | CrossOriginWindowProperty::Top + | CrossOriginWindowProperty::Window => window.into(), + CrossOriginWindowProperty::Location => unreachable!(), + }) +} + #[derive(WebApiObject)] #[webapi(plain)] struct MainWindowCrossOriginSurface { @@ -51,51 +88,12 @@ fn cached_access_surface<'s>( let accessing_context = cross_origin_accessing_context(scope); cached_cross_origin_window_surface(scope, target_context, accessing_context, |scope| { let window = target_context.global(scope); - let location = shared_cross_origin_location(scope, window) + let location = location_for_window(scope, window) .expect("main Window cross-origin Location should initialize"); build_accessing_surface(scope, window, location) }) } -fn ensure_shared_location<'s>( - scope: &mut v8::PinScope<'s, '_>, - anchor: v8::Local<'s, v8::Object>, - window: v8::Local<'s, v8::Object>, -) -> v8::Local<'s, v8::Object> { - if let Some(location) = get_private_value(scope, anchor, LOCATION) - .and_then(|value| v8::Local::::try_from(value).ok()) - { - return location; - } - let target = new_null_prototype_object(scope); - let marker = v8::Boolean::new(scope, true).into(); - set_private_value(scope, target, CROSS_ORIGIN_LOCATION_PROXY_SLOT, marker); - set_private_value(scope, target, MAIN_LOCATION, window.into()); - // Preserve the target's keys without exposing realm-specific - // functions. Configurable placeholders allow each observer's - // get/descriptor projection to obey Proxy invariants. - let undefined = v8::undefined(scope).into(); - for name in CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES - .iter() - .copied() - .chain(["href", "replace", "then"]) - { - let key = v8str(scope, name).into(); - let _ = target.define_own_property(scope, key, undefined, v8::PropertyAttribute::DONT_ENUM); - } - install_cross_origin_symbol_slots(scope, target, "Location"); - let location = wrap_cross_origin_location_proxy(scope, target) - .expect("main Window cross-origin Location proxy should initialize"); - set_private_value( - scope, - target, - CROSS_ORIGIN_LOCATION_PROXY_SELF_SLOT, - location.into(), - ); - set_private_value(scope, anchor, LOCATION, location.into()); - location -} - fn build_accessing_surface<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, @@ -127,49 +125,12 @@ fn build_accessing_surface<'s>( CROSS_ORIGIN_WINDOW_LOCATION_SLOT, location.into(), ); - install_cross_origin_symbol_slots(scope, surface, "Window"); + set_private_value(scope, surface, CROSS_ORIGIN_WINDOW_SELF_SLOT, window.into()); + install_cross_origin_symbol_slots(scope, surface); - let location_surface = new_null_prototype_object(scope); - install_cross_origin_symbol_slots(scope, location_surface, "Location"); - install_cross_origin_denied_accessors( - scope, - location_surface, - CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES, - ); - let setter = v8::Function::builder(cross_origin_location_navigate_setter_callback) - .length(1) - .constructor_behavior(v8::ConstructorBehavior::Throw) - .build(scope) - .expect("cross-origin Location href setter should initialize"); - setter.set_name(v8str(scope, "set href")); - let href = v8str(scope, "href").into(); - define_get_set_property( - scope, - location_surface, - href, - undefined, - setter.into(), - cross_origin_property_attributes(), - "href", - ) - .expect("cross-origin Location href descriptor should initialize"); - install_cross_origin_location_methods(scope, location_surface); - set_cross_origin_object_slot(scope, location_surface, "then", undefined); - set_private_value(scope, surface, LOCATION_SURFACE, location_surface.into()); surface } -pub(super) fn location_access_surface<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, -) -> Option> { - let window = main_location_window(scope, receiver)?; - let context = window.get_creation_context(scope)?; - let surface = cached_access_surface(scope, context); - get_private_value(scope, surface, LOCATION_SURFACE) - .and_then(|value| v8::Local::::try_from(value).ok()) -} - pub(super) fn is_main_surface<'s>( scope: &mut v8::PinScope<'s, '_>, surface: v8::Local<'s, v8::Object>, @@ -193,9 +154,11 @@ fn refresh_indexed_children<'s>( host.sync_child_browsing_context_subtree(scope, host.document_handle()); let count = host.child_browsing_context_count() as u32; for index in 0..count { - if let Some(handle) = host.child_browsing_context_handle_by_index(index as usize) - && let Some(window) = - host.child_browsing_context_window_proxy_for_top(scope, handle) + if let Some(handle) = host.child_browsing_context_handle_by_index_for_document( + host.document_handle(), + index as usize, + ) && let Some(window) = + host.child_browsing_context_window_proxy_for_current_realm(scope, handle) { let key = v8_string(scope, &index.to_string()).unwrap(); let _ = surface.define_own_property( @@ -215,39 +178,6 @@ fn refresh_indexed_children<'s>( set_private_value(scope, surface, INDEXED_COUNT, count); } -fn main_location_window<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, -) -> Option> { - get_cross_origin_proxy_private_value(scope, receiver, MAIN_LOCATION) - .and_then(|value| v8::Local::::try_from(value).ok()) -} - -pub(super) fn is_main_location<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, -) -> bool { - main_location_window(scope, receiver).is_some() -} - -pub(super) fn navigate<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, - value: v8::Local<'s, v8::Value>, - kind: crate::context_bootstrap::LocationNavigationKind, -) -> bool { - let Some(window) = main_location_window(scope, receiver) else { - return false; - }; - // The stable WindowProxy follows its successor, but a Location belongs to - // the Window realm that created it. An old Location must not gain the new - // Document's navigation authority through that proxy. - if receiver.get_creation_context(scope) != window.get_creation_context(scope) { - return false; - } - navigate_window(scope, window, value, kind, true) -} - fn navigate_window<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, @@ -270,19 +200,29 @@ fn navigate_window<'s>( throw_cross_origin_location_security_error(scope); return false; } - let raw = if kind != crate::context_bootstrap::LocationNavigationKind::Reload { + let Some(context) = window.get_creation_context(scope) else { + return false; + }; + let accessing_context = cross_origin_accessing_context(scope); + let raw = if context != accessing_context + && kind != crate::context_bootstrap::LocationNavigationKind::Reload + { crate::context_bootstrap::resolve_cross_window_location_target(scope, &raw).unwrap_or(raw) } else { raw }; - let Some(context) = window.get_creation_context(scope) else { + if let Ok(destination) = url::Url::parse(&raw) + && crate::context_bootstrap::blocks_ancestor_location_navigation( + scope, + window, + &destination, + ) + { + throw_cross_origin_location_security_error(scope); return false; - }; + } let scope = &mut v8::ContextScope::new(scope, context); - let Some(location) = window - .get(scope, v8str(scope, "location").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { + let Some(location) = crate::context_bootstrap::window_location_for_holder(scope, window) else { return false; }; crate::context_bootstrap::navigate_location_object_with_child_navigate_event( @@ -316,15 +256,14 @@ fn require_window_receiver<'s>( None } -fn shared_cross_origin_location<'s>( +pub(super) fn location_for_window<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, ) -> Option> { let context = window.get_creation_context(scope)?; let scope = &mut v8::ContextScope::new(scope, context); - let anchor = context.get_extras_binding_object(scope); - let window = context.global(scope); - Some(ensure_shared_location(scope, anchor, window)) + get_private_value(scope, window, WINDOW_LOCATION_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) } fn location_getter<'s>( @@ -335,18 +274,7 @@ fn location_getter<'s>( let Some(window) = require_window_receiver(scope, &args, "get", "location") else { return; }; - if caller_can_access_window(scope, window) { - if let Some(location) = get_private_value(scope, window, WINDOW_LOCATION_SLOT) { - rv.set(location); - return; - } - throw_type_error( - scope, - "Failed to get the 'location' property on 'Window': Illegal invocation", - ); - return; - } - if let Some(location) = shared_cross_origin_location(scope, window) { + if let Some(location) = location_for_window(scope, window) { rv.set(location.into()); } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs index b7462e739e..df84b2f8d8 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs @@ -215,6 +215,13 @@ impl JsContextHost { } pub(crate) fn child_browsing_context_host_is_active(&self, handle: DomHandle) -> bool { + if self + .dom_host() + .owner_document_handle(handle) + .is_none_or(|document| !self.document_has_browsing_context(document)) + { + return false; + } if self .dom_host() .owner_document_handle(handle) 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 504966119f..55e4c38694 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 @@ -419,13 +419,16 @@ impl JsContextHost { } }; let root_name_matches = match root { - OwnerDispatchScope::Top => !self.browsing_context_is_closed() - && self.browsing_context_name().get() == name, - OwnerDispatchScope::LightweightPopup(id) => self.lightweight_popup_record(id) + OwnerDispatchScope::Top => { + !self.browsing_context_is_closed() && self.browsing_context_name().get() == name + } + OwnerDispatchScope::LightweightPopup(id) => self + .lightweight_popup_record(id) .is_some_and(|record| record.is_open() && record.name.get() == name), OwnerDispatchScope::Child(_) => false, }; - if root_window.is_some() && root_name_matches + if root_window.is_some() + && root_name_matches && related_to.is_none_or(|source| self.browsing_contexts_are_familiar(source, root)) { return Some(root); diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 983b47487c..79bb1065ee 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -15,6 +15,7 @@ fn slotchange_microtask_callback( impl JsContextHost { pub(crate) fn mark_main_document_initial_empty(&mut self) { assert!(moli_url::is_about_blank(self.document_url())); + self.frame_owner_store.mark_main_document_initial_empty(); self.initial_empty_main_document = true; } @@ -22,6 +23,16 @@ impl JsContextHost { self.initial_empty_main_document } + pub(crate) fn main_document_has_committed_navigation(&self) -> bool { + self.frame_owner_store + .main_document_has_committed_navigation() + } + + pub(crate) fn complete_current_main_initial_empty_document(&mut self) -> bool { + self.frame_owner_store + .complete_current_main_initial_empty_document() + } + pub(crate) fn rebind_initial_main_document_senders( &mut self, resource: RendererResourceCompletionSender, @@ -47,14 +58,18 @@ impl JsContextHost { &self, origin: &str, inherited_opaque_origin_matches: bool, - inherited_document_domain: Option<&str>, + inherited_origin: Option<&super::WindowAccessOrigin>, policy: &crate::document_runtime::DocumentPolicyContainer, ) -> crate::frame_owner_model::FrameDocumentLocalWindowTransition { - let same_origin = self.document_domain_override.get().as_deref() - == inherited_document_domain - && (inherited_opaque_origin_matches - || moli_url::WebOrigin::from_serialized(&self.main_document_security_origin()) - .same_origin(&moli_url::WebOrigin::from_serialized(origin))); + let same_origin = inherited_opaque_origin_matches + || if let Some(origin @ super::WindowAccessOrigin::Tuple { .. }) = inherited_origin { + self.main_window_access_origin() + .is_some_and(|current| current.can_access(origin)) + } else { + self.document_domain_override.get().is_none() + && moli_url::WebOrigin::from_serialized(&self.main_document_security_origin()) + .same_origin(&moli_url::WebOrigin::from_serialized(origin)) + }; crate::frame_owner_model::FrameDocumentLocalWindowTransition::for_document_commit( self.initial_empty_main_document, same_origin, @@ -143,11 +158,14 @@ impl JsContextHost { pub(crate) fn request_auxiliary_window_close( &mut self, window: crate::runtime::RendererAuxiliaryWindow, - ) { + ) -> bool { if window.close() { self.append_live_turn_items(vec![crate::runtime::RendererOutputItem::OwnerAction( crate::runtime::RendererOwnerAction::CloseAuxiliaryWindow(window), )]); + true + } else { + false } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs index 41666ae0c2..95c47612e4 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs @@ -21,6 +21,21 @@ impl JsContextHost { pub(crate) fn dispatch_main_document_tree_beforeunload( &mut self, scope: &mut v8::PinScope<'_, '_>, + ) -> bool { + self.dispatch_main_document_tree_beforeunload_with_close(scope, false) + } + + pub(crate) fn dispatch_main_document_tree_beforeunload_for_close( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + ) -> bool { + self.dispatch_main_document_tree_beforeunload_with_close(scope, true) + } + + fn dispatch_main_document_tree_beforeunload_with_close( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + closing: bool, ) -> bool { let Some(owner) = self.current_main_document_task_owner() else { return false; @@ -30,9 +45,14 @@ impl JsContextHost { }; let scope = &mut v8::ContextScope::new(scope, context); let window = context.global(scope); - if crate::context_bootstrap::navigation_unload_event_active(scope, window) { + if !closing && crate::context_bootstrap::navigation_unload_event_active(scope, window) { return false; } + let was_closed = self.browsing_context_is_closed(); + let is_current = |host: &Self| { + host.main_document_task_owner_is_current(owner) + && host.browsing_context_is_closed() == was_closed + }; let descendants = self.child_document_descendants_unload_snapshot(self.document_handle()); // Keep the navigation target protected while checking its descendants: // a descendant's beforeunload must not erase the target with open/write. @@ -42,11 +62,14 @@ impl JsContextHost { let previous = crate::context_bootstrap::replace_navigation_unload_event_active(scope, window, true); crate::context_bootstrap::dispatch_beforeunload_for_runtime_owner(scope, window); - self.dispatch_child_documents_beforeunload(scope, descendants, None, |host| { - host.main_document_task_owner_is_current(owner) - }); + self.dispatch_child_documents_beforeunload(scope, descendants, None, is_current); crate::context_bootstrap::replace_navigation_unload_event_active(scope, window, previous); - self.main_document_task_owner_is_current(owner) + is_current(self) + } + + pub(crate) fn main_document_unload_has_started(&self) -> bool { + self.frame_owner_store + .current_main_document_unload_has_started() } pub(crate) fn dispatch_main_document_unload_for_navigation_commit( diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 4a72fa7218..aebfe5be85 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -1460,7 +1460,7 @@ impl JsContextHost { self.shutdown_workers(); self.output_journal = None; self.root_document_lifecycle = None; - self.child_default_context_bootstrap = None; + self.window_default_context_bootstrap = None; self.command_turn_output = None; self.lightweight_popup_browsing_contexts.clear(); self.internal_inspector_value_references.clear(); diff --git a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs index e0cb6be2a6..cfa47c81f0 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs @@ -487,6 +487,24 @@ impl JsContextHost { } } + pub(crate) fn cancel_pending_location_navigation(&mut self) { + self.clear_pending_location_navigation(); + // A request already handed to the browser no longer occupies the + // renderer's pending slot. Cancel through the same ordered owner + // channel, retaining the Document that authorized this action. + if let Some(source_document) = self + .root_document_lifecycle + .as_ref() + .map(RendererDocumentLifecycleJournalHandle::identity) + { + self.append_live_turn_owner_action( + crate::runtime::RendererOwnerAction::StopTopLevelLocationNavigation { + source_document, + }, + ); + } + } + pub(crate) fn record_pending_top_level_history_traversal(&mut self, delta: i64) { self.clear_pending_top_level_navigation(); let traversal = RendererPendingTopLevelHistoryTraversal { delta }; diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index 6b3a70cbf6..a164365e50 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -247,6 +247,10 @@ impl PendingLightweightPopupHistory { window: v8::Local<'s, v8::Object>, final_url: &Url, ) { + let Some(context) = window.get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); let mut seed = match self.entry_seed() { Some(seed) => seed.clone(), None => { @@ -525,7 +529,7 @@ enum LightweightPopupLifecycle { } pub(super) struct LightweightPopupBrowsingContextRecord { - name: crate::runtime::RendererBrowsingContextName, + pub(in crate::native_bridge::context_host) name: crate::runtime::RendererBrowsingContextName, auxiliary_window: crate::runtime::RendererAuxiliaryWindow, name_lookup_allowed: bool, window_proxy: v8::Global, @@ -546,7 +550,7 @@ impl LightweightPopupBrowsingContextRecord { matches!(self.lifecycle, LightweightPopupLifecycle::Open(_)) } - fn is_open(&self) -> bool { + pub(in crate::native_bridge::context_host) fn is_open(&self) -> bool { self.is_live() && !self.is_closing && !self.auxiliary_window.is_closed() } } @@ -666,7 +670,7 @@ impl LightweightPopupStorageScope { } impl JsContextHost { - fn lightweight_popup_record( + pub(in crate::native_bridge::context_host) fn lightweight_popup_record( &self, popup_id: u64, ) -> Option<&LightweightPopupBrowsingContextRecord> { @@ -796,7 +800,11 @@ impl JsContextHost { let Some(transition) = self.take_lightweight_popup_close_transition(popup_id) else { return false; }; - let window_handle = self.lightweight_popup_record(popup_id).expect("closed popup record retained").auxiliary_window.clone(); + let window_handle = self + .lightweight_popup_record(popup_id) + .expect("closed popup record retained") + .auxiliary_window + .clone(); self.request_auxiliary_window_close(window_handle); self.popup_nested_histories .remove(&OwnerDispatchScope::LightweightPopup(popup_id)); @@ -902,9 +910,12 @@ impl JsContextHost { pub(crate) fn named_lightweight_popup_id(&self, target_name: &str) -> Option { let name = trackable_lightweight_popup_window_name(target_name)?; - self.lightweight_popup_browsing_contexts.iter().find_map(|(id, record)| { - (record.is_open() && record.name_lookup_allowed && record.name.get() == name).then_some(*id) - }) + self.lightweight_popup_browsing_contexts + .iter() + .find_map(|(id, record)| { + (record.is_open() && record.name_lookup_allowed && record.name.get() == name) + .then_some(*id) + }) } pub(crate) fn open_lightweight_popup_window<'s>( @@ -959,7 +970,7 @@ impl JsContextHost { }) } - fn popup_sandbox_policy_for_source( + pub(in crate::native_bridge::context_host) fn popup_sandbox_policy_for_source( &self, source_scope: OwnerDispatchScope, ) -> Option { @@ -1012,41 +1023,42 @@ impl JsContextHost { return None; } let creator_is_secure_context = self.popup_creator_is_secure_context(scope, creator_scope); - let window = self.lightweight_popup_window(scope, popup_id)?; - if update_existing_opener && let Some(opener) = opener { - set_private_value(scope, window, LIGHTWEIGHT_POPUP_OPENER_SLOT, opener.into()); - let endpoint = lightweight_popup_initiator_endpoint(scope, Some(opener), opener_child_handle); - if let Some(record) = self.lightweight_popup_record_mut(popup_id) { - record.opener = endpoint; - record.opener_window = Some(v8::Global::new(scope, opener)); - } + let window = self.lightweight_popup_window(scope, popup_id)?; + if update_existing_opener && let Some(opener) = opener { + set_private_value(scope, window, LIGHTWEIGHT_POPUP_OPENER_SLOT, opener.into()); + let endpoint = + lightweight_popup_initiator_endpoint(scope, Some(opener), opener_child_handle); + if let Some(record) = self.lightweight_popup_record_mut(popup_id) { + record.opener = endpoint; + record.opener_window = Some(v8::Global::new(scope, opener)); } - let Some(href) = href else { - return Some(OpenedLightweightPopup { - window, - popup_id, - created_new_browsing_context: false, - document_response: None, - }); - }; - let (document_response, receiver) = self.auxiliary_document_response_channel(href); - let window = self.reopen_lightweight_popup_window( - scope, + } + let Some(href) = href else { + return Some(OpenedLightweightPopup { + window, popup_id, - opener, - opener_child_handle, - href, - creator_base_url.clone(), - creator_policy_container.clone(), - creator_is_secure_context, - receiver, - )?; - Some(OpenedLightweightPopup { - window, - popup_id, - created_new_browsing_context: false, - document_response, - }) + created_new_browsing_context: false, + document_response: None, + }); + }; + let (document_response, receiver) = self.auxiliary_document_response_channel(href); + let window = self.reopen_lightweight_popup_window( + scope, + popup_id, + opener, + opener_child_handle, + href, + creator_base_url.clone(), + creator_policy_container.clone(), + creator_is_secure_context, + receiver, + )?; + Some(OpenedLightweightPopup { + window, + popup_id, + created_new_browsing_context: false, + document_response, + }) } pub(crate) fn create_lightweight_popup_window<'s>( @@ -2310,7 +2322,10 @@ impl JsContextHost { &self, popup_id: u64, ) -> Option { - if !self.lightweight_popup_is_open(popup_id) { + if !self + .lightweight_popup_record(popup_id) + .is_some_and(|record| record.is_live()) + { return None; } self.lightweight_popup_document_record(popup_id) @@ -5928,60 +5943,6 @@ fn lightweight_popup_window_name_setter<'s>( } } -fn lightweight_popup_opener_getter<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let open = lightweight_popup_id_from_window(scope, args.this()) - .and_then(|id| { - context_host_ptr_from_global_bridge(scope) - .map(|host| unsafe { &*host }.lightweight_popup_is_open(id)) - }) - .unwrap_or(false); - if open - && let Some(opener) = get_private_value(scope, args.this(), LIGHTWEIGHT_POPUP_OPENER_SLOT) - { - rv.set(opener); - return; - } - rv.set_null(); -} - -fn lightweight_popup_opener_setter<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - _rv: v8::ReturnValue<'_, v8::Value>, -) { - set_private_value( - scope, - args.this(), - LIGHTWEIGHT_POPUP_OPENER_SLOT, - args.get(0), - ); - if args.get(0).is_null() - && let Some(popup_id) = lightweight_popup_id_from_window(scope, args.this()) - && let Some(host) = context_host_ptr_from_global_bridge(scope) - && let Some(record) = unsafe { &mut *host }.lightweight_popup_record_mut(popup_id) - { - record.opener = None; - } -} - -fn lightweight_popup_closed_getter<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let closed = lightweight_popup_id_from_window(scope, args.this()) - .and_then(|id| { - context_host_ptr_from_global_bridge(scope) - .and_then(|host| unsafe { &*host }.lightweight_popup_record(id)) - }) - .is_none_or(|record| !record.is_open()); - rv.set(v8::Boolean::new(scope, closed).into()); -} - fn lightweight_popup_window_opener_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs b/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs index 913f5182c3..73e30726a9 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs @@ -98,7 +98,7 @@ impl JsContextHost { } self.retire_window_execution_contexts_for_context_token( retired.realm_token, - config.resource_owner_id, + Some(config.resource_owner_id), ); context.detach_global(); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs index 38c0128aee..8b45978ab2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs @@ -12,6 +12,30 @@ pub(crate) struct OpenedRendererWindow<'s> { Option, } +impl<'s> OpenedRendererWindow<'s> { + pub(crate) fn prepare_navigation_activation(&self, scope: &mut v8::PinScope<'s, '_>) -> bool { + if self + .window + .as_ref() + .is_some_and(|window| window.is_closed()) + || crate::context_bootstrap::navigation_unload_event_active(scope, self.window_proxy) + { + return false; + } + if self.pending_page.is_some() { + return true; + } + let Some(context) = self.window_proxy.get_creation_context(scope) else { + return false; + }; + let Some(host_ptr) = crate::util::context_host_ptr_from_context_slot(context) else { + return false; + }; + let scope = &mut v8::ContextScope::new(scope, context); + unsafe { &mut *host_ptr }.dispatch_main_document_tree_beforeunload(scope) + } +} + impl JsContextHost { pub(crate) fn has_browser_owned_auxiliary_page_factory(&self) -> bool { self.browser_context_runtime @@ -165,13 +189,29 @@ impl JsContextHost { } else { about_blank_url() }; + policy.document_referrer = Url::parse(&policy.document_referrer) + .ok() + .and_then(|source| { + moli_fetch::referrer_value( + &source, + if exposes_opener { + &source + } else { + &initial_url + }, + None, + policy.referrer_policy.as_deref(), + ) + }) + .unwrap_or_default(); let base_url = if exposes_opener { lightweight_popup_initial_base_url(&initial_url, creator_base_url) } else { initial_url.clone() }; - let sandbox = creator_child_handle - .and_then(|handle| self.child_browsing_context_popup_opener_sandbox_policy(handle)); + let sandbox = creator_child_handle.and_then(|handle| { + self.popup_sandbox_policy_for_source(OwnerDispatchScope::Child(handle)) + }); if policy.sandbox.allows_popups_to_escape { policy.sandbox = DocumentSandboxPolicy::default(); } @@ -202,6 +242,19 @@ impl JsContextHost { let loader = source_loader .request_client() .fork_with_isolated_page_network_policy(); + let about_document_state = self + .capture_about_document_state( + creator_child_handle.map_or(OwnerDispatchScope::Top, OwnerDispatchScope::Child), + &initial_url, + None, + ) + .map(|state| { + crate::runtime::RendererAboutDocumentState::new( + state.origin().clone(), + base_url.clone(), + policy.clone(), + ) + }); let env = crate::runtime::PageVmEnvConfig { root_frame_id: None, main_document_commit: None, @@ -235,6 +288,8 @@ impl JsContextHost { layout_configuration: Default::default(), wpt_extensions_enabled: false, navigation_bootstrap_entry: None, + navigation_history_source: None, + about_document_state, reserved_service_worker_client_id: None, }; let mut dom_host = crate::dom::native::DomHost::from_dom( diff --git a/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs b/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs index 6b4d90fb82..b36c8df3e2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs @@ -162,14 +162,14 @@ impl JsContextHost { .collect::>(); realm_tokens.sort_unstable(); realm_tokens.dedup(); + let resource_owner_id = self + .window_default_context_bootstrap + .as_ref() + .map(|config| config.resource_owner_id); for realm_token in realm_tokens { self.retire_window_realm_resources(realm_token); - let resource_owner_id = self - .child_default_context_bootstrap - .as_ref() - .map(|config| config.resource_owner_id); - self.retire_window_execution_context_registry(realm_token, resource_owner_id); + self.retire_window_execution_contexts_for_context_token(realm_token, resource_owner_id); } for owner in owners { @@ -490,14 +490,6 @@ impl JsContextHost { } pub(crate) fn retire_window_execution_contexts_for_context_token( - &mut self, - context_token: RuntimeObservableContextToken, - resource_owner_id: crate::resource_owner::ResourceOwnerId, - ) -> usize { - self.retire_window_execution_context_registry(context_token, Some(resource_owner_id)) - } - - fn retire_window_execution_context_registry( &mut self, context_token: RuntimeObservableContextToken, resource_owner_id: Option, diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs index d86aca5ea0..9c179b5104 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs @@ -59,7 +59,7 @@ impl WindowEnvironmentSettings { scope.get_current_context().get_slot::() } - pub(crate) fn bind_current_main_document_for_retirement( + pub(crate) fn bind_current_main_document( scope: &mut v8::PinScope<'_, '_>, host: &JsContextHost, ) -> Option<()> { diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs index 3daecda867..e2af3e163c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs @@ -21,8 +21,7 @@ pub(crate) fn window_contexts_allow_access( else { return false; }; - let Some(accessed_host_ptr) = - crate::util::context_host_ptr_from_context_slot(accessed_context) + let Some(accessed_host_ptr) = crate::util::context_host_ptr_from_context_slot(accessed_context) else { return false; }; @@ -236,7 +235,6 @@ impl JsContextHost { return Some(owner.origin); } WindowSecurityOrigin::for_context(context) - } pub(in crate::native_bridge) fn document_has_same_origin_as_entry<'s>( @@ -285,7 +283,6 @@ impl JsContextHost { origin .zip(entry_origin) .is_some_and(|(origin, entry)| origin.has_same_origin(&entry)) - } pub(crate) fn install_window_context_security_origin( @@ -821,7 +818,9 @@ impl JsContextHost { } } - pub(in crate::native_bridge::context_host) fn main_window_access_origin(&self) -> Option { + pub(in crate::native_bridge::context_host) fn main_window_access_origin( + &self, + ) -> Option { if !self .document_policy_container() .sandbox diff --git a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs index 16ef1320f9..dd4677766d 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs @@ -265,12 +265,8 @@ fn navigate_element_popup_target( .or_else(|| creator.policy_container.referrer_policy.clone()) }, ); - creator.policy_container.document_referrer = if relations.suppress_opener { - navigation_initiator - .document_referrer(&url::Url::parse(resolved_url).expect("resolved hyperlink URL")) - } else { - navigation_initiator.outgoing_referrer() - }; + creator.policy_container.document_referrer = navigation_initiator + .document_referrer(&url::Url::parse(resolved_url).expect("resolved hyperlink URL")); let opener = (!relations.suppress_opener).then_some(creator.opener); let source_child_handle = match dispatch_scope { crate::native_bridge::OwnerDispatchScope::Child(handle) => Some(handle), @@ -306,6 +302,9 @@ fn navigate_element_popup_target( return false; } }; + if !opened.prepare_navigation_activation(scope) { + return true; + } let disposition = if opened.pending_page.is_some() || runtime.protocol_user_gesture_activation() || runtime.current_input_event().is_some() @@ -455,6 +454,14 @@ pub(in crate::native_bridge) fn choose_form_navigation_target( return None; } let relations = element_popup_relations(unsafe { &*runtime_ptr }, form, target_name); + if let Some(ExistingHyperlinkTarget::RelatedPage(window)) = + existing_hyperlink_target(scope, runtime_ptr, source, form, Some(target_name)) + && crate::context_bootstrap::navigation_unload_event_active(scope, window) + { + // A related Page being unloaded is still the named target. Do not + // fall through to creating a second legacy popup with the same name. + return None; + } let mut creator = element_popup_creator(scope, runtime_ptr, form)?; creator.policy_container.document_referrer = if relations.suppress_referrer { String::new() @@ -486,6 +493,7 @@ pub(in crate::native_bridge) fn choose_form_navigation_target( None, creator.base_url, creator.policy_container, + false, ), _ => runtime.open_lightweight_popup_window( scope, @@ -496,6 +504,7 @@ pub(in crate::native_bridge) fn choose_form_navigation_target( None, creator.base_url, creator.policy_container, + false, ), }?; if !opened.allows_navigation_activation(scope, runtime) { @@ -641,19 +650,26 @@ pub(crate) fn navigate_existing_browsing_context_target<'s>( if navigated { Some(target_window) } else { None } } +#[derive(Clone, Copy)] +enum ExistingHyperlinkTarget<'s> { + Local(crate::native_bridge::OwnerDispatchScope), + RelatedPage(v8::Local<'s, v8::Object>), +} + /// Resolve an existing target without reading script-visible WindowProxy properties. -fn existing_hyperlink_target( - scope: &mut v8::PinScope<'_, '_>, +fn existing_hyperlink_target<'s>( + scope: &mut v8::PinScope<'s, '_>, runtime_ptr: *mut JsContextHost, source: crate::native_bridge::OwnerDispatchScope, + source_handle: DomHandle, target_name: Option<&str>, -) -> Option { +) -> Option> { use crate::native_bridge::OwnerDispatchScope; - let runtime = unsafe { &*runtime_ptr }; + let runtime = unsafe { &mut *runtime_ptr }; let Some(name) = target_name else { - return Some(source); + return Some(ExistingHyperlinkTarget::Local(source)); }; - match SpecialBrowsingContextTarget::parse(name) { + let local = match SpecialBrowsingContextTarget::parse(name) { Some(SpecialBrowsingContextTarget::Current) => Some(source), Some( special @ (SpecialBrowsingContextTarget::Top | SpecialBrowsingContextTarget::Parent), @@ -668,9 +684,23 @@ fn existing_hyperlink_target( Some(target) } Some(SpecialBrowsingContextTarget::Blank) => None, - None => unsafe { &mut *runtime_ptr } - .browsing_context_target_by_name_for_navigation(scope, name, source), + None => runtime.browsing_context_target_by_name_for_navigation(scope, name, source), + }; + if let Some(local) = local { + return Some(ExistingHyperlinkTarget::Local(local)); } + if SpecialBrowsingContextTarget::parse(name).is_some() + || element_popup_relations(runtime, source_handle, name).suppress_opener + { + return None; + } + let related = runtime + .page_script_environment()? + .named_related_window(name, runtime.browsing_context_name())?; + related + .window_proxy_in_scope(scope) + .ok() + .map(ExistingHyperlinkTarget::RelatedPage) } pub(in crate::native_bridge) fn navigate_element_target_browsing_context( @@ -689,7 +719,9 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( let Some(source) = unsafe { &*runtime_ptr }.owner_dispatch_scope_for_node(source_handle) else { return false; }; - let Some(target) = existing_hyperlink_target(scope, runtime_ptr, source, target_name) else { + let Some(target) = + existing_hyperlink_target(scope, runtime_ptr, source, source_handle, target_name) + else { return target_name.is_some_and(|name| { navigate_element_popup_target( scope, @@ -702,8 +734,10 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( }); }; let runtime = unsafe { &mut *runtime_ptr }; - if url::Url::parse(resolved_url) - .is_ok_and(|destination| runtime.blocks_ancestor_navigation(source, target, &destination)) + if let ExistingHyperlinkTarget::Local(target) = &target + && url::Url::parse(resolved_url).is_ok_and(|destination| { + runtime.blocks_ancestor_navigation(source, *target, &destination) + }) { return true; } @@ -711,29 +745,77 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( return false; }; let initiator_url = runtime.document_url_for_handle(document); - let Some(source_element) = crate::util::node_wrapper_from_handle(scope, source_handle) else { + let initiator = crate::runtime::RendererNavigationInitiator::new( + runtime + .document_referrer_source_url_for_dispatch_scope(source) + .unwrap_or(initiator_url), + runtime + .document_resource_loader_for_dispatch_scope(source) + .map(|loader| loader.fetch_context().request_origin()) + .unwrap_or(moli_url::WebOrigin::Opaque), + if element_popup_relations(runtime, source_handle, target_name.unwrap_or("_self")) + .suppress_referrer + { + Some("no-referrer".to_owned()) + } else { + element_popup_referrer_policy(runtime, source_handle) + .map(str::to_owned) + .or_else(|| { + runtime + .document_policy_container_for_inheritance(source) + .and_then(|policy| policy.referrer_policy) + }) + }, + ); + let Some(source_element) = + runtime + .native_bridge_mut() + .wrap_handle(scope, runtime_ptr, source_handle) + else { return false; }; // Resolve the native target realm before reading its private Location slot. // The source's access check controls event dispatch and sourceElement exposure. - let context = match target { - OwnerDispatchScope::Child(handle) => runtime - .ensure_prebootstrapped_child_default_context(scope, handle) + let context = match &target { + ExistingHyperlinkTarget::Local(OwnerDispatchScope::Child(handle)) => runtime + .ensure_prebootstrapped_child_default_context(scope, *handle) .ok(), - _ => runtime - .current_registered_window_execution_context_identity(target) - .and_then(|identity| runtime.window_execution_context(scope, identity.owner(), target)) + ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(id)) => runtime + .lightweight_popup_window(scope, *id) + .and_then(|window| window.get_creation_context(scope)), + ExistingHyperlinkTarget::Local(target) => runtime + .current_registered_window_execution_context_identity(*target) + .and_then(|identity| runtime.window_execution_context(scope, identity.owner(), *target)) .map(|(_, context)| context), + ExistingHyperlinkTarget::RelatedPage(window) => window.get_creation_context(scope), }; let Some(context) = context else { return false }; + let native_popup_context = matches!(target, ExistingHyperlinkTarget::RelatedPage(_)) + || matches!(target, ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(id)) + if runtime.lightweight_popup_window(scope, id) + .is_some_and(|window| window.strict_equals(context.global(scope).into()))); let can_access = runtime .current_registered_window_execution_context_identity(source) .is_some_and(|identity| { - runtime.window_execution_context_can_access_dispatch_scope(identity, target) + if native_popup_context { + runtime + .window_execution_context(scope, identity.owner(), source) + .is_some_and(|(_, source_context)| { + crate::native_bridge::window_contexts_allow_access(source_context, context) + }) + } else { + let ExistingHyperlinkTarget::Local(target) = target else { + unreachable!("related Pages use native context access checks") + }; + runtime.window_execution_context_can_access_dispatch_scope(identity, target) + } }); let scope = &mut v8::ContextScope::new(scope, context); - let window = match target { - OwnerDispatchScope::LightweightPopup(id) => runtime.lightweight_popup_window(scope, id), + let window = match &target { + ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(id)) => { + runtime.lightweight_popup_window(scope, *id) + } + ExistingHyperlinkTarget::RelatedPage(window) => Some(*window), _ => Some(context.global(scope)), }; let Some(location) = window @@ -749,11 +831,13 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( crate::context_bootstrap::HyperlinkNavigationOptions { user_initiated, source_can_access_target: can_access, - initiator_url, + initiator, named_popup: target_name .filter(|name| { - matches!(target, OwnerDispatchScope::LightweightPopup(_)) - && SpecialBrowsingContextTarget::parse(name).is_none() + matches!( + target, + ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(_)) + ) && SpecialBrowsingContextTarget::parse(name).is_none() }) .map(|name| NamedHyperlinkPopup { source_handle, diff --git a/moli-renderer-v8/src/native_bridge/identity.rs b/moli-renderer-v8/src/native_bridge/identity.rs index a2c5d423d9..050510646a 100644 --- a/moli-renderer-v8/src/native_bridge/identity.rs +++ b/moli-renderer-v8/src/native_bridge/identity.rs @@ -449,6 +449,10 @@ pub(crate) fn contexts_share_wrapper_world( right: v8::Local<'_, v8::Context>, ) -> bool { left == right + // Related native Pages share the isolate's default wrapper world, + // even though each native DOM host owns a separate reflector cache. + || (left.get_slot::().is_some() + && right.get_slot::().is_some()) || match ( left.get_slot::>(), right.get_slot::>(), @@ -587,16 +591,40 @@ pub(crate) fn clear_context_wrapper_cache_for_teardown( .map(|token| token.as_u64()); if let Some(cache) = context.get_slot::>() { let mut entries = cache.borrow_mut(); - for entry in entries.wrappers.values_mut() { - if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm { - entry.wrapper.retain_in_realm(scope); + entries + .retired_wrappers + .retain(|_, wrapper| !wrapper.is_empty()); + entries + .retired_live_collection_wrappers + .retain(|_, wrapper| !wrapper.is_empty()); + // A retained realm must preserve wrappers that JS still reaches without + // making every detached node reachable through the realm itself. + let mut retired = Vec::new(); + entries.wrappers.retain(|id, entry| { + if !all_entries && entry.creation_realm.map(NonZeroU64::get) != realm { + return true; } - } - for entry in entries.live_collection_wrappers.values_mut() { - if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm { - entry.wrapper.retain_in_realm(scope); + if let Some(wrapper) = entry.wrapper.to_local(scope) { + retired.push((id, v8::Weak::new(scope, wrapper))); } - } + false + }); + entries.retired_wrappers.extend(retired); + let mut retired_collections = Vec::new(); + entries + .live_collection_wrappers + .retain(|descriptor, entry| { + if !all_entries && entry.creation_realm.map(NonZeroU64::get) != realm { + return true; + } + if let Some(wrapper) = entry.wrapper.to_local(scope) { + retired_collections.push((descriptor.clone(), v8::Weak::new(scope, wrapper))); + } + false + }); + entries + .retired_live_collection_wrappers + .extend(retired_collections); drop(entries); crate::util::retain_context_v8_handle_state_for_safe_release(context, cache); } @@ -620,6 +648,24 @@ pub(super) struct BridgeIdentityStore { } impl BridgeIdentityStore { + fn current_wrapper_cache( + &self, + scope: &mut v8::PinScope<'_, '_>, + ) -> Rc> { + if scope + .get_current_context() + .get_slot::() + .is_some() + { + // Reflector IDs are local to this native host. A cross-Page call + // in the same default world must use this store's canonical cache, + // not the calling Page's cache with potentially colliding IDs. + self.default_world_wrapper_cache.clone() + } else { + context_wrapper_cache(scope) + } + } + pub(super) fn install_default_world_wrapper_cache(&self, context: v8::Local<'_, v8::Context>) { let _ = context.set_slot(self.default_world_wrapper_cache.clone()); let _ = context.set_slot(Rc::new(SharedDefaultWorldWrapperCache)); @@ -655,13 +701,13 @@ impl BridgeIdentityStore { if matches!(self.bridge_handle(reflector_id), Some(BridgeHandle::Window)) { return context_window_wrapper(scope); } - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .wrappers .get(&reflector_id) .and_then(|entry| entry.wrapper.to_local(scope)) .or_else(|| { - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .retired_wrappers .get(&reflector_id) @@ -679,7 +725,7 @@ impl BridgeIdentityStore { set_context_window_wrapper(scope, wrapper); return; } - let cache = context_wrapper_cache(scope); + let cache = self.current_wrapper_cache(scope); let mut cache = cache.borrow_mut(); if scope .get_current_context() @@ -712,13 +758,13 @@ impl BridgeIdentityStore { scope: &mut v8::PinScope<'s, '_>, descriptor: &LiveCollectionDescriptor, ) -> Option> { - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .live_collection_wrappers .get(descriptor) .and_then(|entry| entry.wrapper.to_local(scope)) .or_else(|| { - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .retired_live_collection_wrappers .get(descriptor) @@ -732,7 +778,7 @@ impl BridgeIdentityStore { descriptor: LiveCollectionDescriptor, wrapper: v8::Local<'_, v8::Object>, ) { - let cache = context_wrapper_cache(scope); + let cache = self.current_wrapper_cache(scope); let mut cache = cache.borrow_mut(); if scope .get_current_context() diff --git a/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs b/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs index ad867a9b03..32973600da 100644 --- a/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs +++ b/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs @@ -113,13 +113,6 @@ impl DenseReflectorMap { pub(super) fn values(&self) -> impl Iterator { self.dense.iter().flatten().chain(self.sparse.values()) } - - pub(super) fn values_mut(&mut self) -> impl Iterator { - self.dense - .iter_mut() - .flatten() - .chain(self.sparse.values_mut()) - } } #[cfg(test)] diff --git a/moli-renderer-v8/src/runtime/owner/document_response.rs b/moli-renderer-v8/src/runtime/owner/document_response.rs index be79011629..cc3fb82b10 100644 --- a/moli-renderer-v8/src/runtime/owner/document_response.rs +++ b/moli-renderer-v8/src/runtime/owner/document_response.rs @@ -24,12 +24,18 @@ impl RendererCreateStreamingRawPageRequest { local_executor: JsLocalExecutor, runtime_hooks: PageVmRuntimeHooks, ) -> Result { - let navigation_bootstrap_entry = self.navigation_history + let navigation_bootstrap_entry = self + .navigation_history .as_ref() .map(|history| history.resolve(&self.final_url)) .transpose()?; - self.bootstrap_with_navigation_seed(page_id, local_executor, runtime_hooks, navigation_bootstrap_entry) - .await + self.bootstrap_with_navigation_seed( + page_id, + local_executor, + runtime_hooks, + navigation_bootstrap_entry, + ) + .await } pub(in crate::runtime) async fn bootstrap_with_navigation_seed( @@ -85,9 +91,12 @@ impl RendererCreateStreamingRawPageRequest { let document_policy_container = about_document_state .as_ref() .map(|state| state.policy_container().clone()) - .unwrap_or_else(|| DocumentPolicyContainer::from_navigation_response_headers( - &response_headers, &final_url, - )) + .unwrap_or_else(|| { + DocumentPolicyContainer::from_navigation_response_headers( + &response_headers, + &final_url, + ) + }) .with_content_security_policy_bypass(document_settings.bypass_content_security_policy); let document_default_language = crate::document_language::document_default_language_from_headers(&response_headers); @@ -119,7 +128,9 @@ impl RendererCreateStreamingRawPageRequest { main_document_commit, top_level_storage_key: None, navigation_bootstrap_entry, - navigation_history_source: navigation_history.as_ref().map(|history| history.source_history()), + navigation_history_source: navigation_history + .as_ref() + .map(|history| history.source_history()), reserved_service_worker_client_id: reserved_service_worker_client .map(RendererReservedServiceWorkerClient::release), }; diff --git a/moli-renderer-v8/src/runtime/owner_local_store/entry.rs b/moli-renderer-v8/src/runtime/owner_local_store/entry.rs index 114b49b6c6..e96d265f9b 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store/entry.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store/entry.rs @@ -342,6 +342,12 @@ impl LivePageEntry { dispatch: RendererTopLevelNavigationDispatch, ) { self.top_level_navigation_dispatch = dispatch; + if let Some(environment) = self + .active_page_vm() + .and_then(PageVm::renderer_page_script_environment) + { + environment.set_top_level_navigation_dispatch(dispatch); + } } pub(in crate::runtime) fn top_level_navigation_dispatch( diff --git a/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs b/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs index b4df9eb557..f28f6ef18a 100644 --- a/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs +++ b/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs @@ -64,7 +64,10 @@ impl PageVm { ); vm.install_page_task_capabilities(capabilities); vm.set_root_document_lifecycle(lifecycle.clone()); - vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)?; + // This Document completed synchronously before window.open returned. + // Complete the native lifecycle too, so parser scheduling cannot emit + // a second load that observers mistake for the requested navigation. + vm.complete_related_initial_empty_document()?; for milestone in [ RendererDocumentLifecycleMilestone::DomContentLoaded, RendererDocumentLifecycleMilestone::Load, diff --git a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs index 33e4aaae85..2db5d5bae6 100644 --- a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs @@ -1133,20 +1133,26 @@ impl PageVm { pub(in crate::runtime) fn validate_prepared_document_response( &mut self, - request: crate::runtime::owner::RendererCreateStreamingRawPageRequest, + mut request: crate::runtime::owner::RendererCreateStreamingRawPageRequest, mut bootstrap: crate::script_vm::RendererDocumentIsolateBootstrap, reservation: crate::runtime::owner_local_store::RendererDocumentIsolateReservation, ) -> Result { request.validate_bootstrap_configuration()?; - let navigation_bootstrap_entry = match request + // Browser requests retain the destination identity while resolving the + // latest source state at commit. A session-history position alone is + // only a fallback for requests without that explicit history handle. + let navigation_bootstrap_entry = if let Some(history) = request.navigation_history.as_ref() + { + Some(history.resolve(&request.final_url)?) + } else if let Some(position) = request .main_document_commit .as_ref() .and_then(|commit| commit.session_history_position) { - Some(position) => self - .vm_mut() - .capture_inherited_history_for_browser_commit(&request.final_url, position)?, - None => None, + self.vm_mut() + .capture_inherited_history_for_browser_commit(&request.final_url, position)? + } else { + None }; if moli_url::is_about_blank(&request.final_url) && let Some(replacement) = request.document_replacement.as_ref() @@ -1167,6 +1173,14 @@ impl PageVm { ); } } + if let Some(state) = bootstrap + .initial_document_environment + .as_ref() + .and_then(|environment| environment.about_document_state.clone()) + && let Some(history) = request.navigation_history.take() + { + request.navigation_history = Some(history.with_about_document_state(Some(state))); + } ensure!( request.lifecycle_decider.is_none(), "a Page replacement cannot use a Page-creation lifecycle decider" diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index 54f89f3801..883c3cedb6 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -1956,6 +1956,12 @@ impl PageVm { let Some(vm) = self.vm.as_mut() else { return; }; + // A native auxiliary Window is closed on the protocol owner's task. + // Settle its Navigation API while the exact realm and callbacks remain + // registered, before clearing the context's resources. + if let Err(error) = vm.cancel_closed_main_window_navigation() { + tracing::warn!(%error, "failed to cancel closed Window navigation before retirement"); + } vm.detach_default_inspector_context_for_context_teardown(); vm.close_page_context_resources_for_context_teardown(); } @@ -4658,11 +4664,10 @@ impl PageVm { mode, AuxiliaryEnvironmentApply::Navigation | AuxiliaryEnvironmentApply::InheritedNavigation ) { - self.vm_mut() - .install_navigation_bootstrap_from_history( - env.navigation_bootstrap_entry.clone(), - env.navigation_history_source.clone(), - ); + self.vm_mut().install_navigation_bootstrap_from_history( + env.navigation_bootstrap_entry.clone(), + env.navigation_history_source.clone(), + ); if env.navigation_bootstrap_entry.is_none() && let Some(position) = env .main_document_commit diff --git a/moli-renderer-v8/src/runtime/protocol_output/item.rs b/moli-renderer-v8/src/runtime/protocol_output/item.rs index a5b10a09aa..436019e10a 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/item.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/item.rs @@ -64,6 +64,9 @@ pub enum RendererOwnerAction { update: moli_page_types::SessionHistoryUpdate, }, TopLevelLocationNavigation(RendererDocumentSourcedTopLevelLocationNavigation), + StopTopLevelLocationNavigation { + source_document: RendererDocumentLifecycleIdentity, + }, TopLevelHistoryTraversal(RendererPendingTopLevelHistoryTraversal), SubresourceFetchPause { source_document: RendererDocumentLifecycleIdentity, diff --git a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs index ef8040a777..8930ca0bd9 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs @@ -313,7 +313,8 @@ fn owner_action_transport_charge_bytes(action: &RendererOwnerAction) -> usize { .unwrap_or_default(), ) .saturating_add(headers_charge(event.request_headers())), - RendererOwnerAction::TopLevelHistoryTraversal(_) => 0, + RendererOwnerAction::TopLevelHistoryTraversal(_) + | RendererOwnerAction::StopTopLevelLocationNavigation { .. } => 0, RendererOwnerAction::SubresourceFetchPause { info, .. } => { info.renderer_transport_charge_bytes() } diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 3dc580b3c3..a36e89986c 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -809,9 +809,9 @@ mod inspector; mod promise_rejection_task; mod script_preparation_error; pub(crate) use inspector::{dispatch_inspector_io_owner_wake, dispatch_inspector_main_owner_wake}; +mod canvas_blob_serialization; mod document_context_transition; mod initial_window_document; -mod canvas_blob_serialization; mod isolated_worlds; mod main_document_lifecycle; mod main_document_lifecycle_body; @@ -2019,6 +2019,7 @@ pub(crate) struct ScriptVmInitialDocumentEnvironment { pub(crate) policy_container: crate::document_runtime::DocumentPolicyContainer, fallback_base_url: Option, storage_key: Option, + pub(crate) about_document_state: Option, } pub(crate) use document_environment::ScriptVmCapturedDocumentEnvironment; @@ -2050,6 +2051,9 @@ impl ScriptVmPageRealmBootstrap { .initial_document_environment .take() }); + let about_document_state = initial_environment + .as_ref() + .and_then(|environment| environment.about_document_state.clone()); let document_handle = dom_host.document_handle(); if let Some(base) = initial_environment .as_ref() @@ -2182,6 +2186,9 @@ impl ScriptVmPageRealmBootstrap { .bind_deferred_context_host_release_queue( renderer_document_isolate.deferred_context_host_release_queue(), ); + context_host + .borrow_mut() + .set_main_about_document_state(about_document_state); if let Some(root_frame_id) = root_frame_id.as_deref() { let name = context_host .borrow() @@ -2485,10 +2492,32 @@ impl ScriptVmDefaultWorldBootstrap { >, about_document_state: Option, ) -> std::result::Result { + let about_document_state = renderer_document_isolate_bootstrap + .initial_document_environment + .as_ref() + .and_then(|environment| environment.about_document_state.clone()) + .or(about_document_state); let top_level_storage_key = about_document_state .as_ref() .and_then(crate::runtime::RendererAboutDocumentState::storage_key) .or(top_level_storage_key); + let initial_environment = if renderer_document_isolate_bootstrap + .initial_document_environment + .is_none() + { + about_document_state + .as_ref() + .map(|state| ScriptVmInitialDocumentEnvironment { + security_token: None, + origin: state.origin().serialized_origin(), + policy_container: state.policy_container().clone(), + fallback_base_url: Some(state.base_url().clone()), + storage_key: state.storage_key(), + about_document_state: Some(state.clone()), + }) + } else { + None + }; let mut bootstrap = ScriptVmPageRealmBootstrap::new_from_dom_host( bootstrap_dom_host, bypass_content_security_policy, @@ -2505,7 +2534,7 @@ impl ScriptVmDefaultWorldBootstrap { main_document_commit, top_level_storage_key, reserved_service_worker_client_id, - None, + initial_environment, )?; if let Some(state) = &about_document_state { bootstrap diff --git a/moli-renderer-v8/src/script_vm/document_environment.rs b/moli-renderer-v8/src/script_vm/document_environment.rs index c7f22ab4b8..636278ef9d 100644 --- a/moli-renderer-v8/src/script_vm/document_environment.rs +++ b/moli-renderer-v8/src/script_vm/document_environment.rs @@ -85,6 +85,23 @@ impl ScriptVmInitialDocumentEnvironment { host.inherited_document_domain(child) }) .unwrap_or_default(); + let about_document_state = crate::util::context_host_ptr_from_context_slot(context) + .and_then(|host| { + let host = unsafe { &*host }; + let owner = host.window_dispatch_scope_for_context(scope, context)?; + host.capture_about_document_state( + owner, + &Url::parse("about:blank").expect("valid about:blank URL"), + None, + ) + }) + .map(|state| { + crate::runtime::RendererAboutDocumentState::new( + state.origin().clone(), + base_url.clone(), + policy_container.clone(), + ) + }); Ok(Self { security_token, document_domain_override, @@ -92,6 +109,7 @@ impl ScriptVmInitialDocumentEnvironment { policy_container, fallback_base_url: Some(base_url), storage_key: Some(storage_key), + about_document_state, }) } } @@ -423,23 +441,47 @@ impl ScriptVm { Some(host.document_base_url_for_handle(host.document_handle())) }; let mut policy_container = host.document_policy_container().clone(); - let source = if preserve_navigation_referrer { - url::Url::parse(&policy_container.document_referrer).ok() - } else { - Some(host.document_url().clone()) - }; - policy_container.document_referrer = source - .as_ref() - .and_then(|source| { - moli_fetch::referrer_value( - source, - host.document_url(), - None, - policy_container.referrer_policy.as_deref(), - ) - }) + if preserve_navigation_referrer && host.main_document_is_initial_empty() { + // The synchronous initial Document may expose the creator's + // full URL. A browser reload reapplies the navigation policy + // to that retained source for its actual about:blank target. + policy_container.document_referrer = + Url::parse(&policy_container.document_referrer) + .ok() + .and_then(|source| { + moli_fetch::referrer_value( + &source, + host.document_url(), + None, + policy_container.referrer_policy.as_deref(), + ) + }) + .unwrap_or_default(); + } else if !preserve_navigation_referrer { + policy_container.document_referrer = moli_fetch::referrer_value( + host.document_url(), + host.document_url(), + None, + policy_container.referrer_policy.as_deref(), + ) .unwrap_or_default(); + } let token = scope.get_current_context().get_security_token(scope); + let about_document_state = host + .capture_about_document_state( + crate::native_bridge::OwnerDispatchScope::Top, + &Url::parse("about:blank").expect("valid about:blank URL"), + None, + ) + .map(|state| { + crate::runtime::RendererAboutDocumentState::new( + state.origin().clone(), + fallback_base_url + .clone() + .unwrap_or_else(|| host.document_url().clone()), + policy_container.clone(), + ) + }); Ok(ScriptVmInitialDocumentEnvironment { security_token: Some(v8::Global::new(scope, token)), document_domain_override: host.inherited_document_domain(None), @@ -447,6 +489,7 @@ impl ScriptVm { policy_container, fallback_base_url, storage_key: Some(host.top_web_storage_scope().storage_key().clone()), + about_document_state, }) }) } diff --git a/moli-renderer-v8/src/script_vm/document_isolate.rs b/moli-renderer-v8/src/script_vm/document_isolate.rs index f6673bf811..a8b5e7af57 100644 --- a/moli-renderer-v8/src/script_vm/document_isolate.rs +++ b/moli-renderer-v8/src/script_vm/document_isolate.rs @@ -277,6 +277,7 @@ struct RendererPageScriptEnvironmentInner { auxiliary_window: OnceCell, opener: RefCell>>, document_host: RefCell>>, + top_level_navigation_dispatch: Cell, closed: Cell, } @@ -360,6 +361,9 @@ impl RendererPageScriptEnvironment { auxiliary_window: OnceCell::new(), opener: RefCell::new(None), document_host: RefCell::new(Weak::new()), + top_level_navigation_dispatch: Cell::new( + crate::RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter, + ), closed: Cell::new(false), }), }; @@ -380,6 +384,19 @@ impl RendererPageScriptEnvironment { *self.inner.document_host.borrow_mut() = Rc::downgrade(host); } + pub(crate) fn set_top_level_navigation_dispatch( + &self, + dispatch: crate::RendererTopLevelNavigationDispatch, + ) { + self.inner.top_level_navigation_dispatch.set(dispatch); + } + + pub(crate) fn top_level_navigation_dispatch( + &self, + ) -> crate::RendererTopLevelNavigationDispatch { + self.inner.top_level_navigation_dispatch.get() + } + pub(crate) fn enqueue_related_page_turn_completion(&self) { let host = self.inner.document_host.borrow().clone(); self.inner diff --git a/moli-renderer-v8/src/script_vm/execution_contexts.rs b/moli-renderer-v8/src/script_vm/execution_contexts.rs index b5561c858c..c59029a1e2 100644 --- a/moli-renderer-v8/src/script_vm/execution_contexts.rs +++ b/moli-renderer-v8/src/script_vm/execution_contexts.rs @@ -424,7 +424,7 @@ impl ScriptVm { host.retire_window_realm_resources(context.runtime_observable_context_token); host.retire_window_execution_contexts_for_context_token( context.runtime_observable_context_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } let reuses_window_proxy = self @@ -562,7 +562,6 @@ impl ScriptVm { context.local_window_id, ), ); - self.clear_context_wrapper_cache_for_context_ptr(&context.context, false); } { let mut host = self._context_host.borrow_mut(); @@ -570,7 +569,7 @@ impl ScriptVm { host.retire_window_realm_resources(context.runtime_observable_context_token); host.retire_window_execution_contexts_for_context_token( context.runtime_observable_context_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } } @@ -646,7 +645,7 @@ impl ScriptVm { host.retire_window_realm_resources(context.runtime_observable_context_token); host.retire_window_execution_contexts_for_context_token( context.runtime_observable_context_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } assert!( diff --git a/moli-renderer-v8/src/script_vm/initial_window_document.rs b/moli-renderer-v8/src/script_vm/initial_window_document.rs index 1f754b4cf1..a74a1e9d61 100644 --- a/moli-renderer-v8/src/script_vm/initial_window_document.rs +++ b/moli-renderer-v8/src/script_vm/initial_window_document.rs @@ -25,17 +25,23 @@ impl ScriptVm { } else { false }; - let inherited_domain = - inherited.map(|environment| environment.document_domain_override.get()); + let inherited_origin = inherited.and_then(|environment| { + let mut origin = environment.about_document_state.as_ref()?.origin().clone(); + if let crate::native_bridge::WindowAccessOrigin::Tuple { + document_domain, .. + } = &mut origin + { + *document_domain = environment.document_domain_override.get(); + } + Some(origin) + }); Ok(self ._context_host .borrow() .main_document_local_window_transition_for_commit( origin, inherited_opaque_origin_matches, - inherited_domain - .as_ref() - .and_then(|domain| domain.as_deref()), + inherited_origin.as_ref(), policy, )) } @@ -121,6 +127,12 @@ impl ScriptVm { environment.document_domain_override.clone(), ); } + host.set_main_about_document_state( + inherited + .as_ref() + .and_then(|environment| environment.about_document_state.clone()) + .or_else(|| env.about_document_state.clone()), + ); host.rebind_initial_main_document_senders( resource_completion, page_task_tx.top_level_navigation_handoff_sender(), @@ -161,19 +173,27 @@ impl ScriptVm { let scope = &mut scope.init(); let context = v8::Local::new(scope, context); let scope = &mut v8::ContextScope::new(scope, context); - crate::context_bootstrap::sync_global_location_runtime_state( - scope, - document_url.as_str(), - ); - let global = context.global(scope); - if let Some(document) = global - .get(scope, crate::util::v8str(scope, "document").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) + let window = context.global(scope); { - crate::context_bootstrap::sync_document_location_runtime_state_from_window( - scope, document, global, + let mut host = vm._context_host.borrow_mut(); + crate::native_bridge::WindowEnvironmentSettings::bind_current_main_document(scope, &host) + .ok_or_else(|| anyhow!("retained Window has no current Document settings"))?; + let host_ptr = &mut *host as *mut JsContextHost; + let document = host.native_bridge_mut() + .wrap_handle(scope, host_ptr, document_handle) + .ok_or_else(|| anyhow!("retained Window Document could not be wrapped"))?; + crate::util::set_private_value( + scope, + window, + crate::context_bootstrap::WINDOW_DOCUMENT_SLOT, + document.into(), ); } + crate::context_bootstrap::reset_window_location_history_navigation_runtime_state( + scope, + window, + document_url.as_str(), + )?; Ok(()) })?; Ok(()) diff --git a/moli-renderer-v8/src/script_vm/popup_close.rs b/moli-renderer-v8/src/script_vm/popup_close.rs index 8fc4abc753..85bee67e6b 100644 --- a/moli-renderer-v8/src/script_vm/popup_close.rs +++ b/moli-renderer-v8/src/script_vm/popup_close.rs @@ -11,6 +11,28 @@ use crate::{ }; impl ScriptVm { + pub(crate) fn cancel_closed_main_window_navigation(&mut self) -> Result<()> { + if !self._context_host.borrow().browsing_context_is_closed() { + return Ok(()); + } + self.with_default_context_scope(|scope, host_ptr| { + let window = scope.get_current_context().global(scope); + // Closing from pagehide/unload must not settle the outgoing + // Document's Navigation API promises after its unload has begun. + if !unsafe { &*host_ptr }.main_document_unload_has_started() { + inform_about_canceled_navigation_for_window( + scope, + window, + NavigationCancellationReason::WindowClose, + ); + } + crate::native_bridge::JsContextHost::dispatch_main_document_unload_for_navigation_commit( + scope, host_ptr, + ); + Ok(()) + }) + } + pub(crate) fn current_popup_close_owner( &self, popup_id: u64, diff --git a/moli-renderer-v8/src/script_vm/post_parse.rs b/moli-renderer-v8/src/script_vm/post_parse.rs index 6afaf3beaf..08980ffa77 100644 --- a/moli-renderer-v8/src/script_vm/post_parse.rs +++ b/moli-renderer-v8/src/script_vm/post_parse.rs @@ -570,7 +570,8 @@ impl PendingWindowRealmBootstrapRegistration { realm_token: crate::native_bridge::RuntimeObservableContextToken, resource_owner_id: ResourceOwnerId, ) -> Result> { - let Some((owner, dispatch_scope, access_policy)) = mode.registration() else { + let Some((owner, dispatch_scope, access_policy)) = mode.registration(unsafe { &*host }) + else { return Ok(None); }; if !unsafe { &mut *host }.register_window_execution_context_realm( @@ -599,7 +600,7 @@ impl Drop for PendingWindowRealmBootstrapRegistration { if !self.committed { unsafe { &mut *self.host }.retire_window_execution_contexts_for_context_token( self.realm_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } } @@ -608,13 +609,20 @@ impl Drop for PendingWindowRealmBootstrapRegistration { impl WindowContextBootstrapMode { fn registration( self, + host: &JsContextHost, ) -> Option<( crate::native_bridge::WindowExecutionContextOwner, crate::native_bridge::OwnerDispatchScope, crate::native_bridge::WindowExecutionContextAccessPolicy, )> { match self { - Self::MainDefault => None, + Self::MainDefault => Some(( + host.current_window_execution_context_owner( + crate::native_bridge::OwnerDispatchScope::Top, + )?, + crate::native_bridge::OwnerDispatchScope::Top, + crate::native_bridge::WindowExecutionContextAccessPolicy::EnforceWebOrigin, + )), Self::PopupDefault { popup_id, local_window_id, diff --git a/moli-renderer-v8/src/script_vm/related_page.rs b/moli-renderer-v8/src/script_vm/related_page.rs index 6acaaf6278..e92432e1e6 100644 --- a/moli-renderer-v8/src/script_vm/related_page.rs +++ b/moli-renderer-v8/src/script_vm/related_page.rs @@ -1,5 +1,18 @@ use super::*; +impl ScriptVm { + pub(crate) fn complete_related_initial_empty_document(&mut self) -> Result<()> { + anyhow::ensure!( + self._context_host + .borrow_mut() + .complete_current_main_initial_empty_document(), + "related initial Document rejected synchronous completion" + ); + self.document_runtime.note_dom_content_loaded_dispatched(); + self.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete) + } +} + impl ScriptVmDefaultWorldBootstrap { #[allow(clippy::too_many_arguments)] pub(crate) fn related_page_in_scope( @@ -43,6 +56,7 @@ impl ScriptVmDefaultWorldBootstrap { storage_key: None, origin: inherited_origin.clone(), policy_container: env.document_policy_container.clone(), + about_document_state: env.about_document_state.clone(), }), )?; // Noopener blank pages commit a navigation; only pages exposing an @@ -107,6 +121,7 @@ impl ScriptVmPageRealmBootstrap { root_frame_id, context_host, prebootstrapped_child_default_contexts, + popup_default_contexts, page_context_cancel_tx, post_domcontentloaded_page_task_tx, page_runtime_wake_tx, @@ -193,6 +208,7 @@ impl ScriptVmPageRealmBootstrap { root_frame_id, context_host, prebootstrapped_child_default_contexts, + popup_default_contexts, page_context_cancel_tx, post_domcontentloaded_page_task_tx, page_runtime_wake_tx, diff --git a/moli-renderer-v8/src/script_vm/runtime_bindings.rs b/moli-renderer-v8/src/script_vm/runtime_bindings.rs index 2fa3b08599..345e63a205 100644 --- a/moli-renderer-v8/src/script_vm/runtime_bindings.rs +++ b/moli-renderer-v8/src/script_vm/runtime_bindings.rs @@ -71,10 +71,7 @@ fn queue_agent_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) -> u .get_slot_mut::() .map(|pending| std::mem::take(&mut pending.0)) .unwrap_or_default(); - pending - .into_iter() - .map(queue_promise_rejection_state) - .sum() + pending.into_iter().map(queue_promise_rejection_state).sum() } pub(super) fn promise_reject_dispatch_slot( diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index e152ea9d33..e3cd1024f5 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -115,8 +115,6 @@ mod window_open_named_targets; mod window_open_referrer; mod window_open_special_targets; mod worker_listener_invocation; - - mod error_event_init; mod message_event_init; mod window_current_event_private; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs index 426463f4f7..504531b9a4 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs @@ -132,7 +132,14 @@ setTimeout(() => { "#, ) .unwrap(); - assert!(vm.take_pending_popup_activations().is_empty(), "{kind}"); + // Named reuse may publish a focus/activation record. Its browser + // handoff must explicitly prohibit navigation and another load. + assert!( + vm.take_pending_popup_activations() + .iter() + .all(|activation| !activation.navigation_requested()), + "{kind}" + ); advance_page_task_executor_until_eval_equals( &mut vm, &loader, diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs index 29c8293b8c..16c74ccd83 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs @@ -3122,7 +3122,9 @@ async fn borrowed_cross_origin_window_accessors_follow_the_receiver() { } catch (error) { plain = error instanceof TypeError ? "type-error" : String(error && error.name); } - setLocation.call(window, "#pr957-receiver"); + // A relative fragment on srcdoc resolves against the inherited API base URL. + // Use its explicit URL to isolate which Window the borrowed setter navigates. + setLocation.call(window, "about:srcdoc#pr957-receiver"); return JSON.stringify({ plain, normalParentIsSelf: w.parent === w, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 2e2aa4b274..f52d907f78 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2252,7 +2252,6 @@ mod domrect_receiver_consolidation; mod dynamic_inline_scripts; mod element_click; mod eme_owner_interface_exposure_interfaces; -mod encoded_video_chunk_shell; mod encoded_video_chunk; mod event_receivers; mod extracted; diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index fd8d0a55a4..071da10237 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -917,10 +917,14 @@ pub(crate) fn window_post_message_callback<'s>( .as_ref() .map(|(identity, _, _)| *identity) .or_else(|| { - active_lightweight_popup_id(scope) - .map(PendingWindowMessageEndpoint::LightweightPopup) - .and_then(|endpoint| current_window_message_source_identity(scope, host, endpoint)) - .or_else(|| incumbent_window_message_source_identity(scope, host)) + incumbent_window_message_source_identity(scope, host) + .or_else(|| { + active_lightweight_popup_id(scope) + .map(PendingWindowMessageEndpoint::LightweightPopup) + .and_then(|endpoint| { + current_window_message_source_identity(scope, host, endpoint) + }) + }) .or_else(|| { host.current_window_message_source().and_then(|endpoint| { current_window_message_source_identity(scope, host, endpoint) @@ -1889,6 +1893,31 @@ fn dispatch_window_message_event<'s>( } } +fn top_window_message_source_for_target<'s>( + scope: &mut v8::PinScope<'s, '_>, + host: &JsContextHost, + target: PendingWindowMessageEndpoint, +) -> Option> { + let PendingWindowMessageEndpoint::ChildWindow(handle) = target else { + return host + .page_default_context(scope) + .map(|context| context.global(scope).into()); + }; + let top = host.child_browsing_context_top_window_for_current_realm(scope, handle); + if moli_trace::window_message_trace_enabled() { + tracing::info!( + target: "moli_window_message_trace", + handle = handle.index(), + relation_found = top.is_some(), + top_is_target_global = top.is_some_and(|top| { + top.strict_equals(scope.get_current_context().global(scope).into()) + }), + stage = "top_window_message_source_resolved", + ); + } + top.map(Into::into) +} + fn child_window_message_source<'s>( scope: &mut v8::PinScope<'s, '_>, host: &mut JsContextHost, diff --git a/moli-window-features/src/lib.rs b/moli-window-features/src/lib.rs index 686bd18392..4e8c18e8cb 100644 --- a/moli-window-features/src/lib.rs +++ b/moli-window-features/src/lib.rs @@ -144,11 +144,6 @@ impl WindowOpenFeatures { self.noreferrer } - /// Returns whether the features request an independent popup window. - pub fn is_popup(&self) -> bool { - self.is_popup - } - /// Returns the normalized enabled features used by the popup runtime. pub fn enabled_feature_strings(&self) -> Vec { let mut enabled = Vec::new();