From dc1368b6b4c62d0bec1ed8015836d051b68b7f08 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 8 Sep 2026 06:14:30 +0800 Subject: [PATCH] fix(cdp): synchronize Navigator emulation through native state --- moli-core/src/page/settings_support.rs | 33 +++ moli-core/src/runtime/navigation_engine.rs | 2 + moli-page-types/src/lib.rs | 2 + moli-page-types/src/navigator_overrides.rs | 24 ++ .../browser_context/target_session_owner.rs | 5 + .../src/conn/page_state/page_targets.rs | 11 + moli-protocol/src/conn/page_state/surfaces.rs | 188 ++++----------- moli-protocol/src/conn/runtime_load.rs | 1 + moli-protocol/src/domains/emulation.rs | 124 +++++++--- moli-protocol/src/domains/emulation/tests.rs | 47 +++- .../emulation/tests/native_navigator.rs | 221 ++++++++++++++++++ .../emulation_input_storage.rs | 9 +- moli-renderer-v8/src/context_bootstrap.rs | 1 + .../context_bootstrap/navigator_runtime.rs | 2 + .../navigator_runtime/geolocation.rs | 90 +++++-- .../navigator_runtime/geolocation/position.rs | 213 +++++++++++++++++ .../navigator_runtime/geolocation/watches.rs | 110 +++++++++ .../navigator_runtime/navigator.rs | 14 ++ .../src/document_runtime/events.rs | 8 +- moli-renderer-v8/src/host/timers.rs | 8 +- moli-renderer-v8/src/host/window_callbacks.rs | 16 +- .../src/native_bridge/context_host/core.rs | 2 + .../src/native_bridge/context_host/mod.rs | 3 + .../context_host/navigator_overrides.rs | 47 ++++ moli-renderer-v8/src/runtime/owner.rs | 9 + .../src/runtime/owner_local_store/mod.rs | 1 + moli-renderer-v8/src/runtime/page_commands.rs | 4 + moli-renderer-v8/src/runtime/page_network.rs | 10 + moli-renderer-v8/src/runtime/page_surface.rs | 1 + .../runtime/page_vm/followed_navigation.rs | 1 + moli-renderer-v8/src/runtime/page_vm/mod.rs | 6 + .../src/runtime/page_vm/test_support.rs | 1 + .../src/runtime/page_vm/tests/mod.rs | 2 + moli-renderer-v8/src/runtime/phase_one/mod.rs | 12 + .../src/runtime/phase_one/streaming.rs | 1 + moli-renderer-v8/src/runtime/tests.rs | 3 + moli-renderer-v8/src/script_vm.rs | 27 +++ 37 files changed, 1039 insertions(+), 220 deletions(-) create mode 100644 moli-page-types/src/navigator_overrides.rs create mode 100644 moli-protocol/src/domains/emulation/tests/native_navigator.rs create mode 100644 moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/position.rs create mode 100644 moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/watches.rs create mode 100644 moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs diff --git a/moli-core/src/page/settings_support.rs b/moli-core/src/page/settings_support.rs index 90355f652..919846f4d 100644 --- a/moli-core/src/page/settings_support.rs +++ b/moli-core/src/page/settings_support.rs @@ -543,6 +543,39 @@ impl Page { .await } + pub async fn set_navigator_overrides_async( + &mut self, + overrides: &moli_page_types::NavigatorOverrides, + ) -> Result<()> { + self.dispatch_unit_page_command_async( + RendererPageCommand::SetNavigatorOverrides(overrides.clone()), + "set navigator overrides", + ) + .await + } + + pub fn start_set_navigator_overrides( + &self, + overrides: &moli_page_types::NavigatorOverrides, + ) -> Result { + self.start_page_command(RendererPageCommand::SetNavigatorOverrides( + overrides.clone(), + )) + } + + pub fn finish_set_navigator_overrides( + &mut self, + completion: CompletedPageCommand, + ) -> Result<()> { + let reply = self.finish_page_command(completion); + expect_page_reply!( + reply, + "set navigator overrides", + "a unit reply", + RendererPageReply::Unit => Ok(()), + ) + } + pub fn start_set_network_offline(&self, offline: bool) -> Result { self.start_page_command(RendererPageCommand::SetNetworkOffline(offline)) } diff --git a/moli-core/src/runtime/navigation_engine.rs b/moli-core/src/runtime/navigation_engine.rs index 9c446526a..055e2bb24 100644 --- a/moli-core/src/runtime/navigation_engine.rs +++ b/moli-core/src/runtime/navigation_engine.rs @@ -209,6 +209,7 @@ pub struct PreparedDocumentPageCommitConfiguration { pub cpu_throttling_rate: f64, pub emulated_media: EmulatedMediaOverrides, pub idle_override: Option, + pub navigator_overrides: moli_page_types::NavigatorOverrides, pub viewport_surface: Option, pub browser_resource_runtime: BrowserResourceRuntime, pub navigator_identity: moli_browser_profile::BrowserIdentityProfile, @@ -271,6 +272,7 @@ impl PreparedDocumentPage { cpu_throttling_rate: configuration.cpu_throttling_rate, emulated_media: configuration.emulated_media, idle_override: configuration.idle_override, + navigator_overrides: configuration.navigator_overrides, viewport_surface: configuration.viewport_surface, browser_resource_runtime: configuration.browser_resource_runtime, navigator_identity: configuration.navigator_identity, diff --git a/moli-page-types/src/lib.rs b/moli-page-types/src/lib.rs index 687fe3a75..b89a0f950 100644 --- a/moli-page-types/src/lib.rs +++ b/moli-page-types/src/lib.rs @@ -8,6 +8,7 @@ mod inspector_identity; mod inspector_state; mod layout; mod navigation_history; +mod navigator_overrides; mod renderer_transport_memory; use std::{ @@ -49,6 +50,7 @@ pub use inspector_identity::{ RendererInspectorResponseDelivery, }; pub use layout::LayoutPolicy; +pub use navigator_overrides::{GeolocationPositionOverride, NavigatorOverrides}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct DocumentNodeAttributeSnapshot { diff --git a/moli-page-types/src/navigator_overrides.rs b/moli-page-types/src/navigator_overrides.rs new file mode 100644 index 000000000..379a93e43 --- /dev/null +++ b/moli-page-types/src/navigator_overrides.rs @@ -0,0 +1,24 @@ +//! Browser-owned emulation inputs consumed by native Navigator/Geolocation APIs. +//! +//! These values are state, not document-start JavaScript. Updating or clearing +//! an override must not replace Web IDL descriptors or expose a second object. + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct NavigatorOverrides { + /// None uses the renderer's actual network state. + pub online: Option, + pub max_touch_points: u32, + /// None means that no emulated coordinate source is available. + pub geolocation: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct GeolocationPositionOverride { + pub latitude: f64, + pub longitude: f64, + pub accuracy: f64, + pub altitude: Option, + pub altitude_accuracy: Option, + pub heading: Option, + pub speed: Option, +} 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 b4c48ce44..2b45e5cf4 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -172,6 +172,7 @@ pub(crate) struct TargetNavigationLoadInputs { pub(crate) emulated_media: moli_core::page::EmulatedMediaOverrides, pub(crate) viewport_surface: Option, pub(crate) network_offline: bool, + pub(crate) navigator_overrides: moli_page_types::NavigatorOverrides, pub(crate) bypass_service_worker: bool, pub(crate) cache_disabled: bool, pub(crate) blocked_url_patterns: Vec, @@ -336,6 +337,9 @@ impl TargetNavigationLoadInputs { network_offline: page_state.network_policy.network_offline() || effective_network_conditions .is_some_and(|conditions| !conditions.navigator_online()), + navigator_overrides: browser_context + .navigator_overrides_for_target(target_id) + .expect("resolved Page target retains its navigator state"), bypass_service_worker: effective_policy.bypass_service_worker(), cache_disabled: effective_policy.cache_disabled(), blocked_url_patterns: effective_policy.blocked_url_patterns().to_vec(), @@ -403,6 +407,7 @@ impl TargetNavigationLoadInputs { emulated_media: Default::default(), viewport_surface: None, network_offline: false, + navigator_overrides: Default::default(), bypass_service_worker: false, cache_disabled: false, blocked_url_patterns: Vec::new(), diff --git a/moli-protocol/src/conn/page_state/page_targets.rs b/moli-protocol/src/conn/page_state/page_targets.rs index 3e71277ea..b80d33159 100644 --- a/moli-protocol/src/conn/page_state/page_targets.rs +++ b/moli-protocol/src/conn/page_state/page_targets.rs @@ -453,6 +453,9 @@ impl BrowserContext { return Ok(false); } target.reset_primary_session_target_state_fields(); + let navigator_overrides = self + .navigator_overrides_for_target(target_id) + .expect("disposing target retains navigator state"); let effective_headers = self.effective_extra_headers_for_target(target_id); let target = self @@ -516,6 +519,14 @@ impl BrowserContext { anyhow::anyhow!("failed to restore page timezone: {error}") }); } + if let Err(error) = page + .set_navigator_overrides_async(&navigator_overrides) + .await + { + first_error.get_or_insert_with(|| { + anyhow::anyhow!("failed to restore native navigator state: {error}") + }); + } if let Some(surface_script) = surface_script && let Err(error) = page .run_page_surface_override_script_async(&surface_script.source) diff --git a/moli-protocol/src/conn/page_state/surfaces.rs b/moli-protocol/src/conn/page_state/surfaces.rs index 08f0035db..63fc640f4 100644 --- a/moli-protocol/src/conn/page_state/surfaces.rs +++ b/moli-protocol/src/conn/page_state/surfaces.rs @@ -88,9 +88,26 @@ impl SurfaceOverrideInputs { } } - fn navigator_online(&self) -> bool { - self.network_conditions - .is_none_or(|conditions| conditions.navigator_online()) + fn navigator_overrides(&self) -> moli_page_types::NavigatorOverrides { + moli_page_types::NavigatorOverrides { + online: self + .network_conditions + .map(|conditions| conditions.navigator_online()), + max_touch_points: self.max_touch_points(), + geolocation: self + .geolocation_override + .as_ref() + .and_then(EmulatedGeolocationOverrideState::position) + .map(|position| moli_page_types::GeolocationPositionOverride { + latitude: position.latitude, + longitude: position.longitude, + accuracy: position.accuracy, + altitude: position.altitude, + altitude_accuracy: position.altitude_accuracy, + heading: position.heading, + speed: position.speed, + }), + } } fn document_is_visible(&self) -> bool { @@ -108,6 +125,28 @@ impl SurfaceOverrideInputs { } impl BrowserContext { + pub(crate) fn navigator_overrides_for_target( + &self, + target_id: &str, + ) -> Option { + let target = self.page_target(target_id)?; + Some( + SurfaceOverrideInputs::from_background( + target, + self.default_network_conditions + .or(self.global_network_conditions), + self.default_geolocation_override + .clone() + .or_else(|| self.global_geolocation_override.clone()), + self.default_emulated_device_metrics.clone(), + ) + .navigator_overrides(), + ) + } + + pub(crate) fn active_navigator_overrides(&self) -> moli_page_types::NavigatorOverrides { + SurfaceOverrideInputs::from_active(self).navigator_overrides() + } #[cfg(test)] async fn mutate_document_cookie_manager_surface_async( &mut self, @@ -390,12 +429,16 @@ impl BrowserContext { else { return Ok(false); }; + let overrides = self + .navigator_overrides_for_target(target_id) + .expect("resolved background target retains navigator state"); let Some(page) = self .background_target_mut(target_id) .and_then(|target| target.runtime_slot.loaded_page_mut()) else { return Ok(false); }; + page.set_navigator_overrides_async(&overrides).await?; page.run_page_surface_override_script_async(&script.source) .await .map_err(|error| anyhow::anyhow!("failed to hide background page surface: {error}"))?; @@ -411,8 +454,6 @@ impl BrowserContext { fn generated_surface_override_script_from_inputs( inputs: &SurfaceOverrideInputs, ) -> Option { - let geolocation_override = inputs.geolocation_override.as_ref(); - let navigator_online = inputs.navigator_online(); // Preserve the renderer's native Window/Screen descriptors unless a // client explicitly enabled device emulation. Installing the default // profile as JS getters makes otherwise native attributes observable @@ -424,7 +465,6 @@ impl BrowserContext { .as_ref() .map(|metrics| viewport_surface_install_script(&metrics.viewport_surface(), true)) .unwrap_or_default(); - let max_touch_points = inputs.max_touch_points(); let document_has_focus = inputs.document_has_focus(); let document_hidden = inputs.document_hidden(); let document_visibility_state = inputs.document_visibility_state(); @@ -437,125 +477,7 @@ impl BrowserContext { Object.defineProperty(obj, key, {{ configurable: true, get: getter }}); }} catch (_error) {{}} }}; - const geolocationOverride = {geolocation_override}; - const navigatorOnline = {navigator_online}; - const maxTouchPoints = {max_touch_points}; {viewport_surface_script} - try {{ - globalThis.__moliNavigatorOnline = navigatorOnline; - }} catch (_error) {{}} - const currentNavigatorOnline = () => {{ - try {{ - return globalThis.__moliNavigatorOnline !== false; - }} catch (_error) {{ - return navigatorOnline; - }} - }}; - try {{ - const geoState = globalThis.__moliGeolocationState || {{ - nextWatchId: 1, - watchers: new Map(), - object: null - }}; - globalThis.__moliGeolocationState = geoState; - const previousOverrideKey = geoState.overrideKey || null; - geoState.override = geolocationOverride && typeof geolocationOverride === 'object' - ? geolocationOverride - : null; - geoState.overrideKey = JSON.stringify(geoState.override); - if (!(geoState.watchers instanceof Map)) {{ - geoState.watchers = new Map(); - }} - const queue = typeof queueMicrotask === 'function' - ? queueMicrotask - : (callback) => Promise.resolve().then(callback); - const makeError = (code, message) => {{ - const error = {{ code, message }}; - try {{ - Object.defineProperty(error, 'PERMISSION_DENIED', {{ value: 1 }}); - Object.defineProperty(error, 'POSITION_UNAVAILABLE', {{ value: 2 }}); - Object.defineProperty(error, 'TIMEOUT', {{ value: 3 }}); - }} catch (_error) {{}} - return error; - }}; - const makePosition = () => {{ - const override = geoState.override; - return {{ - coords: {{ - latitude: override.latitude, - longitude: override.longitude, - accuracy: override.accuracy, - altitude: override.altitude ?? null, - altitudeAccuracy: override.altitudeAccuracy ?? null, - heading: override.heading ?? null, - speed: override.speed ?? null - }}, - timestamp: Date.now() - }}; - }}; - const deliverGeolocation = (success, error) => {{ - queue(() => {{ - const fail = (code, message) => {{ - if (typeof error === 'function') {{ - error.call(geoState.object, makeError(code, message)); - }} - }}; - const succeed = () => {{ - if (typeof success === 'function') {{ - success.call(geoState.object, makePosition()); - }} - }}; - const finish = () => {{ - if (!geoState.override) {{ - fail(2, 'Position unavailable'); - }} else {{ - succeed(); - }} - }}; - try {{ - const permissions = navigator && navigator.permissions; - if (permissions && typeof permissions.query === 'function') {{ - const queried = permissions.query({{ name: 'geolocation' }}); - if (queried && typeof queried.then === 'function') {{ - queried.then((status) => {{ - if (status && status.state === 'denied') {{ - fail(1, 'User denied Geolocation'); - }} else {{ - finish(); - }} - }}, finish); - return; - }} - }} - }} catch (_error) {{}} - finish(); - }}); - }}; - if (!geoState.object) {{ - geoState.object = {{ - getCurrentPosition(success, error, _options) {{ - deliverGeolocation(success, error); - }}, - watchPosition(success, error, _options) {{ - const id = geoState.nextWatchId++; - geoState.watchers.set(id, {{ success, error }}); - deliverGeolocation(success, error); - return id; - }}, - clearWatch(id) {{ - geoState.watchers.delete(id); - }} - }}; - }} - if (previousOverrideKey !== null && previousOverrideKey !== geoState.overrideKey) {{ - for (const watcher of geoState.watchers.values()) {{ - deliverGeolocation(watcher.success, watcher.error); - }} - }} - defineGetter(navigator, 'geolocation', () => geoState.object); - }} catch (_error) {{}} - defineGetter(navigator, 'onLine', () => currentNavigatorOnline()); - defineGetter(navigator, 'maxTouchPoints', () => maxTouchPoints); if (document) {{ // The renderer's Document bridge currently installs these // surfaces as own accessors, so CDP emulation must shadow @@ -571,23 +493,7 @@ impl BrowserContext { }} catch (_error) {{}} }} }})();", - geolocation_override = geolocation_override - .and_then(EmulatedGeolocationOverrideState::position) - .map(|position| { - json!({ - "latitude": position.latitude, - "longitude": position.longitude, - "accuracy": position.accuracy, - "altitude": position.altitude, - "altitudeAccuracy": position.altitude_accuracy, - "heading": position.heading, - "speed": position.speed, - }) - .to_string() - }) - .unwrap_or_else(|| "null".to_owned()), viewport_surface_script = viewport_surface_script, - max_touch_points = max_touch_points, document_hidden = document_hidden, document_visibility_state = json!(document_visibility_state), document_has_focus = document_has_focus, @@ -609,9 +515,11 @@ impl BrowserContext { let Some(script) = self.generated_surface_override_script() else { return Ok(()); }; + let overrides = self.active_navigator_overrides(); let Some(page) = self.active_page_target_mut().runtime_slot.loaded_page_mut() else { return Ok(()); }; + page.set_navigator_overrides_async(&overrides).await?; page.run_page_surface_override_script_async(&script.source) .await .map_err(|error| anyhow::anyhow!("failed to apply page surface overrides: {error}")) diff --git a/moli-protocol/src/conn/runtime_load.rs b/moli-protocol/src/conn/runtime_load.rs index cdc72beb7..c2af0b434 100644 --- a/moli-protocol/src/conn/runtime_load.rs +++ b/moli-protocol/src/conn/runtime_load.rs @@ -1566,6 +1566,7 @@ impl CdpConnection { cpu_throttling_rate: load_inputs.cpu_throttling_rate, emulated_media: load_inputs.emulated_media, idle_override, + navigator_overrides: load_inputs.navigator_overrides, viewport_surface: load_inputs.viewport_surface, browser_resource_runtime, navigator_identity, diff --git a/moli-protocol/src/domains/emulation.rs b/moli-protocol/src/domains/emulation.rs index 6a324da23..b185a07bf 100644 --- a/moli-protocol/src/domains/emulation.rs +++ b/moli-protocol/src/domains/emulation.rs @@ -98,6 +98,7 @@ enum PendingEmulationPageOperation { SetNetworkConditions, SetCpuThrottlingRate, SetIdleOverride, + SetNavigatorOverrides, SetTimezoneOverride, SetEmulatedMedia, SetViewportSurface, @@ -110,6 +111,7 @@ impl PendingEmulationPageOperation { fn has_authoritative_replay_state(&self) -> bool { match self { Self::SetExtraHttpHeaders + | Self::SetNavigatorOverrides | Self::SetLocaleOverride | Self::SetNetworkConditions | Self::SetCpuThrottlingRate @@ -209,9 +211,7 @@ pub(crate) fn try_start_emulation_command_dispatch( Some(start_cpu_throttling_rate_command(conn, cmd)) } Some(EmulationAction::SetTouchEmulationEnabled) => { - Some(EmulationCommandTaskStep::Complete( - touch_emulation_enabled_command_output_plan(conn, cmd), - )) + Some(start_touch_emulation_enabled_command(conn, cmd)) } Some(EmulationAction::SetEmitTouchEventsForMouse) => { Some(EmulationCommandTaskStep::Complete( @@ -267,26 +267,57 @@ fn focus_emulation_enabled_command_output_plan( } } -fn touch_emulation_enabled_command_output_plan( +fn start_touch_emulation_enabled_command( conn: &mut CdpConnection, cmd: &Cmd<'_>, -) -> CommandOutputPlan { +) -> EmulationCommandTaskStep { let params: params::SetTouchEmulationEnabledParams = match cmd.get_params() { Ok(Some(params)) => params, - _ => return CommandOutputPlan::error(-32602, "InvalidParams"), + _ => { + return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( + -32602, + "InvalidParams", + )); + } }; if conn.browser_context.is_none() { - return CommandOutputPlan::result(json!({})); + return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); } - match page_session::update_page_emulation_state(conn, cmd.session_id, |mut state| { - state.set_touch_emulation_enabled(params.enabled); - }) { - Ok(()) => CommandOutputPlan::result(json!({})), - Err(message) if message == "BrowserContextNotLoaded" => { - CommandOutputPlan::error(-31998, "BrowserContextNotLoaded") - } - Err(message) => CommandOutputPlan::error(-32000, message), + if let Err(message) = + page_session::update_page_emulation_state(conn, cmd.session_id, |mut state| { + state.set_touch_emulation_enabled(params.enabled); + }) + { + let code = if message == "BrowserContextNotLoaded" { + -31998 + } else { + -32000 + }; + return EmulationCommandTaskStep::Complete(CommandOutputPlan::error(code, message)); } + let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id); + let overrides = conn + .navigation_load_inputs_for_owner(&owner_scope) + .navigator_overrides; + let Some(page) = loaded_page_mut_for_target_configuration(conn, cmd.session_id) else { + return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); + }; + let pending = match page.start_set_navigator_overrides(&overrides) { + Ok(pending) => pending, + Err(error) => { + return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + error.to_string(), + )); + } + }; + EmulationCommandTaskStep::Pending(single_pending_emulation_dispatch( + cmd.id, + owner_scope, + PendingEmulationPageOperation::SetNavigatorOverrides, + pending, + None, + )) } fn start_cpu_throttling_rate_command( @@ -2826,6 +2857,7 @@ fn start_geolocation_surface_override_page_commands( let Some(script) = browser_context.generated_surface_override_script_for_active_target() else { return Ok(Vec::new()); }; + let navigator_overrides = browser_context.active_navigator_overrides(); let browser_context_id = browser_context.id.clone(); let Some(target_id) = browser_context.active_target_id_owned() else { return Ok(Vec::new()); @@ -2844,9 +2876,9 @@ fn start_geolocation_surface_override_page_commands( }, page, script, + navigator_overrides, runtime_call_id, ) - .map(|pending| vec![pending]) } fn start_session_surface_override_page_command( @@ -2861,7 +2893,7 @@ fn start_session_surface_override_page_command_for_owner( conn: &mut CdpConnection, owner_scope: &CommandOwnerScope, ) -> Result, String> { - let script = { + let (script, navigator_overrides) = { let Some((browser_context_id, target_id)) = conn.target_owner_identity_for_owner(owner_scope) else { @@ -2873,9 +2905,17 @@ fn start_session_surface_override_page_command_for_owner( if let Some(target_id) = target_id.as_deref() && browser_context.background_target(target_id).is_some() { - browser_context.generated_surface_override_script_for_background_target(target_id) + ( + browser_context.generated_surface_override_script_for_background_target(target_id), + browser_context + .navigator_overrides_for_target(target_id) + .expect("resolved target"), + ) } else { - browser_context.generated_surface_override_script_for_active_target() + ( + browser_context.generated_surface_override_script_for_active_target(), + browser_context.active_navigator_overrides(), + ) } }; let Some(script) = script else { @@ -2894,9 +2934,9 @@ fn start_session_surface_override_page_command_for_owner( }, page, script, + navigator_overrides, runtime_call_id, ) - .map(|pending| vec![pending]) } fn start_surface_override_for_route( @@ -2904,7 +2944,7 @@ fn start_surface_override_for_route( target: PendingEmulationPageTarget, route: &CdpSessionRoute, ) -> Result, String> { - let script = match &target { + let (script, navigator_overrides) = match &target { PendingEmulationPageTarget::BrowserContextTarget { browser_context_id, target_id, @@ -2912,11 +2952,17 @@ fn start_surface_override_for_route( let Some(browser_context) = conn.browser_context_by_id(browser_context_id) else { return Err("BrowserContextNotLoaded".to_owned()); }; - if browser_context.is_active_target(target_id) { + let script = if browser_context.is_active_target(target_id) { browser_context.generated_surface_override_script_for_active_target() } else { browser_context.generated_surface_override_script_for_background_target(target_id) - } + }; + ( + script, + browser_context + .navigator_overrides_for_target(target_id) + .unwrap_or_default(), + ) } PendingEmulationPageTarget::SessionOwner { owner_scope } => { return start_session_surface_override_page_command_for_owner(conn, owner_scope); @@ -2933,24 +2979,35 @@ fn start_surface_override_for_route( else { return Ok(Vec::new()); }; - start_surface_override_page_command(target, page, script, runtime_call_id) - .map(|pending| vec![pending]) + start_surface_override_page_command(target, page, script, navigator_overrides, runtime_call_id) } fn start_surface_override_page_command( target: PendingEmulationPageTarget, page: &moli_core::page::Page, script: crate::conn::DocumentStartScript, + navigator_overrides: moli_page_types::NavigatorOverrides, runtime_call_id: u64, -) -> Result { +) -> Result, String> { + let native_update = page + .start_set_navigator_overrides(&navigator_overrides) + .map_err(|error| error.to_string())?; let (pending, runtime_response_rx) = start_runtime_emulation_protocol_message(page, runtime_call_id, script.source)?; - Ok(PendingEmulationPageCommand { - target, - operation: PendingEmulationPageOperation::RuntimeProtocolMessage, - pending, - runtime_response_rx, - }) + Ok(vec![ + PendingEmulationPageCommand { + target: target.clone(), + operation: PendingEmulationPageOperation::SetNavigatorOverrides, + pending: native_update, + runtime_response_rx: None, + }, + PendingEmulationPageCommand { + target, + operation: PendingEmulationPageOperation::RuntimeProtocolMessage, + pending, + runtime_response_rx, + }, + ]) } fn start_locale_override_page_command( @@ -3067,6 +3124,9 @@ fn finish_emulation_page_operation( PendingEmulationPageOperation::SetIdleOverride => page .finish_set_idle_override(completion) .map_err(|error| error.to_string()), + PendingEmulationPageOperation::SetNavigatorOverrides => page + .finish_set_navigator_overrides(completion) + .map_err(|error| error.to_string()), PendingEmulationPageOperation::SetTimezoneOverride => page .finish_set_timezone_override(completion) .map_err(|error| error.to_string()), diff --git a/moli-protocol/src/domains/emulation/tests.rs b/moli-protocol/src/domains/emulation/tests.rs index 87f4ce3b9..2ae558aea 100644 --- a/moli-protocol/src/domains/emulation/tests.rs +++ b/moli-protocol/src/domains/emulation/tests.rs @@ -19,6 +19,18 @@ use tokio::{ time::{Duration, timeout}, }; +mod native_navigator; + +async fn install_geolocation_page_for_test(ctx: &mut TestContext, bc: BrowserContext) { + ctx.conn.install_browser_context_fixture_for_test(bc); + ctx.install_buffered_navigation_fixture_for_session_owner( + url::Url::parse("https://geolocation.example/page").unwrap(), + "geolocation".into(), + Some("SID-1"), + ) + .await; +} + async fn complete_pending_command_task_for_test( ctx: &mut TestContext, pending: PendingCdpCommandDispatch, @@ -2393,6 +2405,10 @@ async fn evaluate_geolocation_once_for_session( } })) .await; + crate::testing::wait_until_scheduler_message(ctx, "native Geolocation callback", |message| { + message["id"] == json!(id) + }) + .await; ctx.take_response_by_id(id)["result"]["result"]["value"].clone() } @@ -2402,7 +2418,7 @@ async fn set_geolocation_override_updates_loaded_page_geolocation_surface() { let mut bc = BrowserContext::new("BID-1".into()); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); - install_session_page_for_emulation_test(&mut ctx, bc, "data:text/html,ok").await; + install_geolocation_page_for_test(&mut ctx, bc).await; ctx.process_async(json!({ "id": 87, @@ -2426,6 +2442,19 @@ async fn set_geolocation_override_updates_loaded_page_geolocation_surface() { #[tokio::test(flavor = "multi_thread")] async fn set_geolocation_override_applies_to_subsequent_navigation_surface() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve( + listener, + Router::new().route( + "/geo", + get(|| async { axum::response::Html("geo") }), + ), + ) + .await + .unwrap(); + }); let mut ctx = TestContext::new(); let mut bc = BrowserContext::new("BID-1".into()); bc.set_active_target_id("TID-1"); @@ -2445,7 +2474,7 @@ async fn set_geolocation_override_applies_to_subsequent_navigation_surface() { "id": 90, "method": "Page.navigate", "sessionId": "SID-1", - "params": { "url": "data:text/html,geo" } + "params": { "url": format!("http://{addr}/geo") } })) .await; let _ = ctx.take_all(); @@ -2457,6 +2486,7 @@ async fn set_geolocation_override_applies_to_subsequent_navigation_surface() { assert_eq!(payload["latitude"], json!(35.658581)); assert_eq!(payload["longitude"], json!(139.745433)); assert_eq!(payload["accuracy"], json!(3)); + server.abort(); } #[tokio::test(flavor = "multi_thread")] @@ -2465,7 +2495,7 @@ async fn set_geolocation_override_missing_position_reports_unavailable() { let mut bc = BrowserContext::new("BID-1".into()); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); - install_session_page_for_emulation_test(&mut ctx, bc, "data:text/html,ok").await; + install_geolocation_page_for_test(&mut ctx, bc).await; ctx.process_async(json!({ "id": 92, @@ -2497,7 +2527,7 @@ async fn clear_geolocation_override_restores_default_after_explicit_unavailable( speed: None, }, )); - install_session_page_for_emulation_test(&mut ctx, bc, "data:text/html,geo").await; + install_geolocation_page_for_test(&mut ctx, bc).await; ctx.process_async(json!({ "id": 97, @@ -2556,7 +2586,7 @@ async fn set_geolocation_override_respects_denied_permission() { let mut bc = BrowserContext::new("BID-1".into()); bc.set_active_target_id("TID-1"); bc.attach_active_session("SID-1"); - install_session_page_for_emulation_test(&mut ctx, bc, "data:text/html,ok").await; + install_geolocation_page_for_test(&mut ctx, bc).await; ctx.process_async(json!({ "id": 94, @@ -2580,7 +2610,7 @@ async fn set_geolocation_override_respects_denied_permission() { ctx.expect_result(95, json!({}), None); let value = evaluate_geolocation_once(&mut ctx, 96).await; - assert_eq!(value, json!("error:1:User denied Geolocation")); + assert_eq!(value, json!("error:1:Geolocation permission denied")); } #[tokio::test(flavor = "multi_thread")] @@ -3089,8 +3119,9 @@ async fn session_emulation_routes_to_loaded_background_owner_without_activation( bc.attach_active_session("SID-active"); bc.insert_page_target_host(background); ctx.conn.install_browser_context_fixture_for_test(bc); - ctx.install_navigation_fixture_for_session_owner( - "data:text/html,background", + ctx.install_buffered_navigation_fixture_for_session_owner( + url::Url::parse("https://geolocation.example/background").unwrap(), + "background".into(), Some("SID-background"), ) .await; diff --git a/moli-protocol/src/domains/emulation/tests/native_navigator.rs b/moli-protocol/src/domains/emulation/tests/native_navigator.rs new file mode 100644 index 000000000..8e7ec2f60 --- /dev/null +++ b/moli-protocol/src/domains/emulation/tests/native_navigator.rs @@ -0,0 +1,221 @@ +use super::*; + +async fn evaluate(ctx: &mut TestContext, expression: &str) -> serde_json::Value { + ctx.process_async(json!({ + "id": 88000, "sessionId": "SID-1", "method": "Runtime.evaluate", + "params": {"expression": expression, "returnByValue": true, "awaitPromise": true} + })) + .await; + crate::testing::wait_until_scheduler_message(ctx, "native Navigator evaluation", |message| { + message["id"] == json!(88000) + }) + .await; + let response = ctx.take_response_by_id(88000); + assert!( + response["result"]["exceptionDetails"].is_null(), + "{response}" + ); + assert!(response["error"].is_null(), "{response}"); + response["result"]["result"]["value"].clone() +} + +async fn setup() -> TestContext { + let mut ctx = TestContext::new(); + let mut bc = BrowserContext::new("BID-1".into()); + bc.set_active_target_id("TID-1"); + bc.attach_active_session("SID-1"); + install_geolocation_page_for_test(&mut ctx, bc).await; + ctx +} + +async fn set_position(ctx: &mut TestContext, latitude: f64) { + expect_session_command_result(ctx, 88001, "SID-1", "Emulation.setGeolocationOverride", + json!({"latitude": latitude, "longitude": 2, "accuracy": 3, "altitude": 4, "altitudeAccuracy": 5, "heading": 6, "speed": 7})).await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_navigator_descriptors_survive_cdp_override_and_clear() { + let mut ctx = setup().await; + assert_eq!(evaluate(&mut ctx, r#" + globalThis.navKeys = ['onLine', 'maxTouchPoints', 'geolocation']; + globalThis.navGetters = navKeys.map(k => Object.getOwnPropertyDescriptor(Navigator.prototype, k).get); + globalThis.geo = navigator.geolocation; + globalThis.checkDescriptors = () => navKeys.every((k, i) => + !Object.hasOwn(navigator, k) && + navGetters[i] === Object.getOwnPropertyDescriptor(Navigator.prototype, k).get && + Function.prototype.toString.call(navGetters[i]).includes('[native code]')) && + navigator.geolocation === geo && geo instanceof Geolocation && + !('__moliGeolocationState' in globalThis) && !('__moliNavigatorOnline' in globalThis); + checkDescriptors() + "#).await, json!(true)); + set_position(&mut ctx, 1.0).await; + expect_session_command_result( + &mut ctx, + 88002, + "SID-1", + "Emulation.setTouchEmulationEnabled", + json!({"enabled": true}), + ) + .await; + expect_session_command_result( + &mut ctx, + 88003, + "SID-1", + "Network.emulateNetworkConditions", + json!({"offline": true, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1}), + ) + .await; + assert_eq!( + evaluate( + &mut ctx, + "[checkDescriptors(), navigator.onLine, navigator.maxTouchPoints]" + ) + .await, + json!([true, false, 1]) + ); + expect_session_command_result( + &mut ctx, + 88004, + "SID-1", + "Network.emulateNetworkConditions", + json!({"offline": false, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1}), + ) + .await; + expect_session_command_result( + &mut ctx, + 88005, + "SID-1", + "Emulation.setTouchEmulationEnabled", + json!({"enabled": false}), + ) + .await; + expect_session_command_result( + &mut ctx, + 88006, + "SID-1", + "Emulation.clearGeolocationOverride", + json!({}), + ) + .await; + assert_eq!( + evaluate( + &mut ctx, + "[checkDescriptors(), navigator.onLine, navigator.maxTouchPoints]" + ) + .await, + json!([true, true, 0]) + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_geolocation_position_has_branded_prototype_attributes() { + let mut ctx = setup().await; + set_position(&mut ctx, 1.0).await; + assert_eq!(evaluate(&mut ctx, r#" + new Promise((resolve, reject) => navigator.geolocation.getCurrentPosition(function(p) { + 'use strict'; + const getters = [ + [GeolocationPosition.prototype, 'coords'], [GeolocationPosition.prototype, 'timestamp'], + ...['latitude','longitude','altitude','accuracy','altitudeAccuracy','heading','speed'].map(k => [GeolocationCoordinates.prototype, k]) + ]; + const branded = getters.every(([prototype, key]) => { + try { Object.getOwnPropertyDescriptor(prototype, key).get.call(Object.create(prototype)); return false; } + catch (e) { return e instanceof TypeError; } + }); + resolve([ + this === undefined, p instanceof GeolocationPosition, p.coords instanceof GeolocationCoordinates, + Object.keys(p).length, Object.keys(p.coords).length, branded, + p.coords.toJSON(), Number.isInteger(p.timestamp), p.toJSON().coords.latitude + ]); + }, reject)) + "#).await, json!([true, true, true, 0, 0, true, + {"latitude":1,"longitude":2,"altitude":4,"accuracy":3,"altitudeAccuracy":5,"heading":6,"speed":7},true,1])); +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_geolocation_watch_updates_and_clear_cancels_delivery() { + let mut ctx = setup().await; + set_position(&mut ctx, 1.0).await; + assert_eq!( + evaluate( + &mut ctx, + r#" + globalThis.seen = []; + globalThis.nextPosition = new Promise(resolve => globalThis.nextResolve = resolve); + globalThis.watchId = navigator.geolocation.watchPosition(p => { + seen.push(p.coords.latitude); nextResolve(p.coords.latitude); + }); + nextPosition + "# + ) + .await, + json!(1) + ); + evaluate(&mut ctx, "globalThis.nextPosition = new Promise(resolve => globalThis.nextResolve = resolve); undefined").await; + set_position(&mut ctx, 8.0).await; + assert_eq!(evaluate(&mut ctx, "nextPosition").await, json!(8)); + evaluate(&mut ctx, "navigator.geolocation.clearWatch(watchId)").await; + set_position(&mut ctx, 9.0).await; + // A task checkpoint after the update is a deterministic cancellation fence. + assert_eq!( + evaluate( + &mut ctx, + "new Promise(resolve => setTimeout(() => resolve(seen), 0))" + ) + .await, + json!([1, 8]) + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_navigator_overrides_are_installed_before_author_script() { + let mut ctx = setup().await; + set_position(&mut ctx, 1.0).await; + expect_session_command_result( + &mut ctx, + 88002, + "SID-1", + "Emulation.setTouchEmulationEnabled", + json!({"enabled": true}), + ) + .await; + ctx.install_buffered_navigation_fixture_for_session_owner( + url::Url::parse("https://geolocation.example/next").unwrap(), + r#""# + .into(), + Some("SID-1"), + ) + .await; + assert_eq!(evaluate(&mut ctx, "initialTouch").await, json!(1)); + assert_eq!(evaluate(&mut ctx, "initialPosition").await, json!(1)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_geolocation_override_does_not_bypass_insecure_context() { + let mut ctx = TestContext::new(); + load_session_page_for_pending_emulation_test(&mut ctx).await; + set_position(&mut ctx, 1.0).await; + assert_eq!(evaluate(&mut ctx, "new Promise(resolve => navigator.geolocation.getCurrentPosition(() => resolve('unexpected position'), e => resolve(e.code)))").await, json!(1)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_geolocation_result_uses_receiver_realm() { + let mut ctx = setup().await; + set_position(&mut ctx, 1.0).await; + assert_eq!(evaluate(&mut ctx, r#" + (async () => { + const frame = document.createElement('iframe'); + frame.srcdoc = 'child'; + await new Promise(resolve => { frame.onload = resolve; document.body.append(frame); }); + const child = frame.contentWindow; + return new Promise((resolve, reject) => Geolocation.prototype.getCurrentPosition.call( + child.navigator.geolocation, + p => resolve([p instanceof child.GeolocationPosition, p instanceof GeolocationPosition, + p.coords instanceof child.GeolocationCoordinates, p.coords.latitude]), reject)); + })() + "#).await, json!([true, false, true, 1])); +} diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/emulation_input_storage.rs b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/emulation_input_storage.rs index 109048d65..ca8792afc 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/emulation_input_storage.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/emulation_input_storage.rs @@ -350,13 +350,14 @@ async fn rust_cdp_chromium_import_emulation_device_metrics_hot_apply_window_surf // Capability source: docs/WEB_CAPABILITIES.md browser/device APIs exposed to pages. #[tokio::test(flavor = "multi_thread")] async fn rust_cdp_capability_emulation_geolocation_override_runtime_surface() { + let fixture = SmokeFixtureServer::start().await; let mut ctx = TestContext::new_with_target_discovery(false); let attached = attached_smoke_session(&mut ctx, 118_000).await; navigate_and_take_response( &mut ctx, &attached.session_id, 118_005, - "data:text/html,geo".to_owned(), + fixture.url("/plain"), ) .await; ctx.process_async(json!({ @@ -390,6 +391,12 @@ async fn rust_cdp_capability_emulation_geolocation_override_runtime_surface() { } })) .await; + crate::testing::wait_until_scheduler_message( + &mut ctx, + "native Geolocation result", + |message| message["id"] == json!(118_007), + ) + .await; let response = take_response_by_id(&mut ctx, 118_007); let payload: Value = serde_json::from_str( response["result"]["result"]["value"] diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index ebac85fd2..61ef4d1a2 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -335,6 +335,7 @@ pub(crate) use self::navigation_restore::{ }; pub(crate) use self::navigation_traversal::queue_top_level_history_traversal_by_delta; pub(crate) use self::navigator_runtime::install_worker_navigator_runtime_state; +pub(crate) use self::navigator_runtime::notify_geolocation_override_changed; pub(crate) use self::navigator_runtime::{ bind_window_navigator_identity_seed, set_window_navigator_identity, update_cached_window_visual_viewport_dimensions, diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs index 84732e140..6331b9dbd 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs @@ -9,6 +9,8 @@ mod screen; mod visual_viewport; mod window_state; +pub(crate) use geolocation::notify_geolocation_override_changed; + pub(in crate::context_bootstrap) use self::clipboard::clipboard_item_constructor_callback; pub(crate) use self::navigator::build_lightweight_popup_window_navigator_object; pub(super) use self::navigator::install_navigator_template_bindings; diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation.rs index 3f3698513..ad7442089 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation.rs @@ -9,6 +9,9 @@ use crate::{ }; use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; +mod position; +mod watches; + const GEOLOCATION_BRAND_SLOT: &str = "__moliGeolocationBrand"; const GEOLOCATION_SECURE_CONTEXT_SLOT: &str = "__moliGeolocationSecureContext"; const GEOLOCATION_NEXT_WATCH_ID_SLOT: &str = "__moliGeolocationNextWatchId"; @@ -134,6 +137,7 @@ pub(super) fn install_geolocation_template_bindings<'s>( template: v8::Local<'s, v8::FunctionTemplate>, interface_name: &str, ) { + position::install(scope, template, interface_name); let prototype = template.prototype_template(scope); match interface_name { "Geolocation" => { @@ -169,6 +173,10 @@ pub(super) fn build_geolocation_object<'s>( GEOLOCATION_CHILD_HANDLE_SLOT, child_handle, ); + watches::initialize(scope, geolocation); + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + unsafe { &mut *host_ptr }.register_geolocation_object(scope, geolocation); + } Ok(geolocation) } @@ -184,17 +192,14 @@ fn geolocation_get_current_position_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - // Moli currently has no coordinate acquisition source. Conversion - // still validates and captures the required PositionCallback according to - // Web IDL, but no success task is manufactured in unavailable-only mode. - let _ = parsed.success_callback; let _ = ( parsed.options.enable_high_accuracy, parsed.options.maximum_age, ); - queue_geolocation_error( + queue_geolocation_result( scope, args.this(), + parsed.success_callback, parsed.error_callback, parsed.options.timeout, None, @@ -214,20 +219,18 @@ fn geolocation_watch_position_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - // As above, the success callback is a valid Web IDL callback value but - // unavailable-only mode has no position update source that can invoke it. - let _ = parsed.success_callback; let _ = ( parsed.options.enable_high_accuracy, parsed.options.maximum_age, ); let watch_id = take_next_watch_id(scope, args.this()); - queue_geolocation_error( + watches::insert( scope, args.this(), + watch_id, + parsed.success_callback, parsed.error_callback, parsed.options.timeout, - Some(watch_id), ); rv.set_int32(watch_id); } @@ -244,6 +247,7 @@ fn geolocation_clear_watch_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + watches::remove(scope, args.this(), parsed.watch_id); if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { let _ = unsafe { &mut *host_ptr }.cancel_geolocation_watch(scope, args.this(), parsed.watch_id); @@ -329,46 +333,86 @@ fn take_next_watch_id<'s>( watch_id } -fn queue_geolocation_error<'s>( +fn queue_geolocation_result<'s>( scope: &mut v8::PinScope<'s, '_>, geolocation: v8::Local<'s, v8::Object>, + success_callback: webidl::WebIdlCallbackFunction, error_callback: Option, timeout: u32, watch_id: Option, ) { - let Some(error_callback) = error_callback else { + let (code, message) = geolocation_error(scope, geolocation, timeout); + if code == PERMISSION_DENIED + && let Some(watch_id) = watch_id + { + // A permission denial is terminal, unlike a temporarily unavailable + // position. Do not retain the callbacks for future override updates. + watches::remove(scope, geolocation, watch_id); + } + let position = context_host_ptr_from_global_bridge(scope) + .and_then(|host_ptr| { + unsafe { &*host_ptr } + .navigator_overrides() + .geolocation + .clone() + }) + .filter(|_| code == POSITION_UNAVAILABLE); + let callback = if position.is_some() { + success_callback + } else if let Some(callback) = error_callback { + callback + } else { return; }; - let (code, message) = geolocation_error(scope, geolocation, timeout); let Some(geolocation_context) = geolocation.get_creation_context(scope) else { return; }; - // GeolocationPositionError belongs to the Geolocation object's relevant + // Both position and error belong to the Geolocation object's relevant // Realm, independently of the callback's relevant Realm. - let error = { + let result = { let scope = &mut v8::ContextScope::new(scope, geolocation_context); - let Ok(error) = - GeolocationPositionErrorObjectDeclaration::new(code, message.to_owned()).bind(scope) - else { - return; + let result = if let Some(position) = position { + position::build(scope, &position) + } else { + let Ok(error) = + GeolocationPositionErrorObjectDeclaration::new(code, message.to_owned()) + .bind(scope) + else { + return; + }; + error }; - v8::Global::new(scope, v8::Local::::from(error)) + v8::Global::new(scope, v8::Local::::from(result)) }; let owner = geolocation_child_handle(scope, geolocation) .map(HostTimerOwner::ChildWindow) .unwrap_or(HostTimerOwner::Window); if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { - let _ = unsafe { &mut *host_ptr }.queue_window_geolocation_error_callback( + let _ = unsafe { &mut *host_ptr }.queue_window_geolocation_callback( scope, - error_callback, + callback, geolocation, - error, + result, owner, watch_id, ); } } +pub(crate) fn notify_geolocation_override_changed(scope: &mut v8::PinScope<'_, '_>) { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let geolocations = unsafe { &mut *host_ptr }.live_geolocation_objects(scope); + for geolocation in geolocations { + let Some(context) = geolocation.get_creation_context(scope) else { + continue; + }; + let scope = &mut v8::ContextScope::new(scope, context); + watches::notify(scope, geolocation); + } +} + fn geolocation_error<'s>( scope: &mut v8::PinScope<'s, '_>, geolocation: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/position.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/position.rs new file mode 100644 index 000000000..555a293f3 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/position.rs @@ -0,0 +1,213 @@ +use moli_page_types::GeolocationPositionOverride; +use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; + +use crate::util::{ + callback_data_index_value, callback_data_item, get_private_value, throw_type_error, v8str, +}; + +const COORDINATES_SLOT: &str = "__moliGeolocationCoordinatesValues"; +const POSITION_COORDS_SLOT: &str = "__moliGeolocationPositionCoords"; +const POSITION_TIME_SLOT: &str = "__moliGeolocationPositionTime"; +const COORDINATE_NAMES: &[&str] = &[ + "latitude", + "longitude", + "altitude", + "accuracy", + "altitudeAccuracy", + "heading", + "speed", +]; + +#[derive(WebApiObject)] +#[webapi(interface = "GeolocationCoordinates")] +struct CoordinatesDeclaration<'s> { + #[webapi(slot = COORDINATES_SLOT)] + values: v8::Local<'s, v8::Array>, +} + +#[derive(WebApiObject)] +#[webapi(interface = "GeolocationPosition")] +struct PositionDeclaration<'s> { + #[webapi(slot = POSITION_COORDS_SLOT)] + coords: v8::Local<'s, v8::Object>, + #[webapi(slot = POSITION_TIME_SLOT)] + timestamp: f64, +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "GeolocationCoordinates", enumerable)] +struct CoordinatesPrototype { + #[webapi(accessor_property, getter = coordinate_getter, data = callback_data_index_value(scope, 0))] + latitude: (), + #[webapi(accessor_property, getter = coordinate_getter, data = callback_data_index_value(scope, 1))] + longitude: (), + #[webapi(accessor_property, getter = coordinate_getter, data = callback_data_index_value(scope, 2))] + altitude: (), + #[webapi(accessor_property, getter = coordinate_getter, data = callback_data_index_value(scope, 3))] + accuracy: (), + #[webapi(accessor_property = "altitudeAccuracy", getter = coordinate_getter, data = callback_data_index_value(scope, 4))] + altitude_accuracy: (), + #[webapi(accessor_property, getter = coordinate_getter, data = callback_data_index_value(scope, 5))] + heading: (), + #[webapi(accessor_property, getter = coordinate_getter, data = callback_data_index_value(scope, 6))] + speed: (), + #[webapi(method, name = "toJSON", length = 0, callback = coordinates_to_json)] + to_json: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(name = "GeolocationPosition", enumerable)] +struct PositionPrototype { + #[webapi(accessor_property, getter = position_getter, data = callback_data_index_value(scope, 0))] + coords: (), + #[webapi(accessor_property, getter = position_getter, data = callback_data_index_value(scope, 1))] + timestamp: (), + #[webapi(method, name = "toJSON", length = 0, callback = position_to_json)] + to_json: (), +} + +pub(super) fn install<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, + interface_name: &str, +) { + let prototype = template.prototype_template(scope); + match interface_name { + "GeolocationCoordinates" => { + CoordinatesPrototype::initialize_prototype_template(scope, prototype) + } + "GeolocationPosition" => PositionPrototype::initialize_prototype_template(scope, prototype), + _ => {} + } +} + +pub(super) fn build<'s>( + scope: &mut v8::PinScope<'s, '_>, + position: &GeolocationPositionOverride, +) -> v8::Local<'s, v8::Object> { + let values = [ + Some(position.latitude), + Some(position.longitude), + position.altitude, + Some(position.accuracy), + position.altitude_accuracy, + position.heading, + position.speed, + ] + .map(|value| { + value + .map(|value| v8::Number::new(scope, value).into()) + .unwrap_or_else(|| v8::null(scope).into()) + }); + let values = v8::Array::new_with_elements(scope, &values); + let coords = CoordinatesDeclaration::new(values) + .bind(scope) + .expect("native coordinates"); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_millis() as f64) + .unwrap_or_default(); + PositionDeclaration::new(coords, timestamp) + .bind(scope) + .expect("native position") +} + +fn require_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + slot: &str, +) -> Option> { + let value = get_private_value(scope, receiver, slot); + if value.is_none() { + throw_type_error(scope, "Illegal invocation"); + } + value +} + +fn coordinate_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(values) = require_slot(scope, args.this(), COORDINATES_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(index) = callback_data_item( + scope, + &args, + &[0, 1, 2, 3, 4, 5, 6], + "GeolocationCoordinates", + ) else { + return; + }; + if let Some(value) = values.get_index(scope, index) { + rv.set(value); + } +} + +fn position_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(slot) = callback_data_item( + scope, + &args, + &[POSITION_COORDS_SLOT, POSITION_TIME_SLOT], + "GeolocationPosition", + ) else { + return; + }; + if let Some(value) = require_slot(scope, args.this(), slot) { + rv.set(value); + } +} + +fn coordinate_json<'s>( + scope: &mut v8::PinScope<'s, '_>, + coords: v8::Local<'s, v8::Object>, +) -> Option> { + let values = + v8::Local::::try_from(require_slot(scope, coords, COORDINATES_SLOT)?).ok()?; + let result = v8::Object::new(scope); + for index in [3, 0, 1, 2, 4, 5, 6] { + let name = COORDINATE_NAMES[index]; + let value = values.get_index(scope, index as u32)?; + let _ = result.create_data_property(scope, v8str(scope, name).into(), value); + } + Some(result) +} + +fn coordinates_to_json<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + if let Some(result) = coordinate_json(scope, args.this()) { + rv.set(result.into()); + } +} + +fn position_to_json<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(coords) = require_slot(scope, args.this(), POSITION_COORDS_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(coords) = coordinate_json(scope, coords) else { + return; + }; + let Some(timestamp) = require_slot(scope, args.this(), POSITION_TIME_SLOT) else { + return; + }; + let result = v8::Object::new(scope); + let _ = result.create_data_property(scope, v8str(scope, "coords").into(), coords.into()); + let _ = result.create_data_property(scope, v8str(scope, "timestamp").into(), timestamp); + rv.set(result.into()); +} diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/watches.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/watches.rs new file mode 100644 index 000000000..c371e9a79 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/geolocation/watches.rs @@ -0,0 +1,110 @@ +//! Watch callbacks are traced from the Geolocation object, not independent +//! Rust roots. A callback capturing its Navigator/Window cannot create a +//! permanent Rust-to-JavaScript reference cycle. + +use crate::{ + util::{get_private_value, set_private_value}, + v8_traced_webidl_callback::V8TracedWebIdlCallbackFunction, +}; +use moli_webidl_callback::WebIdlCallbackFunction; + +const WATCHES_SLOT: &str = "__moliGeolocationWatches"; +const SUCCESS_SLOT: &str = "__moliGeolocationWatchSuccess"; +const ERROR_SLOT: &str = "__moliGeolocationWatchError"; +const TIMEOUT_SLOT: &str = "__moliGeolocationWatchTimeout"; + +pub(super) fn initialize<'s>( + scope: &mut v8::PinScope<'s, '_>, + geolocation: v8::Local<'s, v8::Object>, +) { + set_private_value(scope, geolocation, WATCHES_SLOT, v8::Map::new(scope).into()); +} + +pub(super) fn insert<'s>( + scope: &mut v8::PinScope<'s, '_>, + geolocation: v8::Local<'s, v8::Object>, + watch_id: i32, + success: WebIdlCallbackFunction, + error: Option, + timeout: u32, +) { + let watch = v8::Object::new(scope); + let success = V8TracedWebIdlCallbackFunction::new(scope, success).into_object(); + set_private_value(scope, watch, SUCCESS_SLOT, success.into()); + if let Some(error) = error { + let error = V8TracedWebIdlCallbackFunction::new(scope, error).into_object(); + set_private_value(scope, watch, ERROR_SLOT, error.into()); + } + set_private_value( + scope, + watch, + TIMEOUT_SLOT, + v8::Integer::new_from_unsigned(scope, timeout).into(), + ); + let _ = map(scope, geolocation).set( + scope, + v8::Integer::new(scope, watch_id).into(), + watch.into(), + ); + queue(scope, geolocation, watch_id, watch); +} + +pub(super) fn remove<'s>( + scope: &mut v8::PinScope<'s, '_>, + geolocation: v8::Local<'s, v8::Object>, + watch_id: i32, +) { + let _ = map(scope, geolocation).delete(scope, v8::Integer::new(scope, watch_id).into()); +} + +pub(super) fn notify<'s>(scope: &mut v8::PinScope<'s, '_>, geolocation: v8::Local<'s, v8::Object>) { + let entries = map(scope, geolocation).as_array(scope); + for index in (0..entries.length()).step_by(2) { + let id = entries + .get_index(scope, index) + .and_then(|value| value.int32_value(scope)); + let watch = entries + .get_index(scope, index + 1) + .and_then(|value| v8::Local::::try_from(value).ok()); + if let (Some(id), Some(watch)) = (id, watch) { + queue(scope, geolocation, id, watch); + } + } +} + +fn map<'s>( + scope: &mut v8::PinScope<'s, '_>, + geolocation: v8::Local<'s, v8::Object>, +) -> v8::Local<'s, v8::Map> { + get_private_value(scope, geolocation, WATCHES_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .expect("branded Geolocation retains its watch map") +} + +fn callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + watch: v8::Local<'s, v8::Object>, + slot: &str, +) -> Option { + let carrier = get_private_value(scope, watch, slot) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let prepared = V8TracedWebIdlCallbackFunction::from_object(carrier).prepare(scope); + let callback = prepared.callback(scope); + let relevant_context = prepared.relevant_context(scope); + let incumbent_context = prepared.incumbent_context(scope); + WebIdlCallbackFunction::try_new(scope, callback, relevant_context, incumbent_context) +} + +fn queue<'s>( + scope: &mut v8::PinScope<'s, '_>, + geolocation: v8::Local<'s, v8::Object>, + id: i32, + watch: v8::Local<'s, v8::Object>, +) { + let success = callback(scope, watch, SUCCESS_SLOT).expect("watch success callback"); + let error = callback(scope, watch, ERROR_SLOT); + let timeout = get_private_value(scope, watch, TIMEOUT_SLOT) + .and_then(|value| value.uint32_value(scope)) + .expect("watch timeout"); + super::queue_geolocation_result(scope, geolocation, success, error, timeout, Some(id)); +} diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs index 14c8f8e2a..583f6015f 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs @@ -677,6 +677,20 @@ fn navigator_runtime_data_getter_callback<'s>( } return; } + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + let host = unsafe { &*host_ptr }; + match key { + "onLine" => { + rv.set_bool(host.navigator_online()); + return; + } + "maxTouchPoints" => { + rv.set_uint32(host.navigator_overrides().max_touch_points); + return; + } + _ => {} + } + } match backing.get(scope, v8str(scope, key).into()) { Some(value) => rv.set(value), None => rv.set(v8::undefined(scope).into()), diff --git a/moli-renderer-v8/src/document_runtime/events.rs b/moli-renderer-v8/src/document_runtime/events.rs index 9ee3afa07..292ac3ef4 100644 --- a/moli-renderer-v8/src/document_runtime/events.rs +++ b/moli-renderer-v8/src/document_runtime/events.rs @@ -168,20 +168,20 @@ impl DocumentRuntime { ) } - pub(crate) fn queue_window_geolocation_error_callback<'s>( + pub(crate) fn queue_window_geolocation_callback<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, callback: moli_webidl_callback::WebIdlCallbackFunction, geolocation: v8::Local<'s, v8::Object>, - error: v8::Global, + result: v8::Global, owner: HostTimerOwner, watch_id: Option, ) -> u32 { - self.timeouts.queue_window_geolocation_error_callback( + self.timeouts.queue_window_geolocation_callback( scope, callback, geolocation, - error, + result, owner, watch_id, ) diff --git a/moli-renderer-v8/src/host/timers.rs b/moli-renderer-v8/src/host/timers.rs index 4e50b8350..18bf88510 100644 --- a/moli-renderer-v8/src/host/timers.rs +++ b/moli-renderer-v8/src/host/timers.rs @@ -416,12 +416,12 @@ impl HostTimeoutScheduler { ) } - pub(crate) fn queue_window_geolocation_error_callback<'s>( + pub(crate) fn queue_window_geolocation_callback<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, callback: moli_webidl_callback::WebIdlCallbackFunction, geolocation: v8::Local<'s, v8::Object>, - error: v8::Global, + result: v8::Global, owner: HostTimerOwner, watch_id: Option, ) -> u32 { @@ -429,11 +429,11 @@ impl HostTimeoutScheduler { scope, callback, geolocation, - WindowWebIdlCallbackTaskKind::GeolocationError { watch_id }, + WindowWebIdlCallbackTaskKind::Geolocation { watch_id }, 0, owner, false, - vec![error], + vec![result], ) } diff --git a/moli-renderer-v8/src/host/window_callbacks.rs b/moli-renderer-v8/src/host/window_callbacks.rs index e7c2b8cc6..6d256597a 100644 --- a/moli-renderer-v8/src/host/window_callbacks.rs +++ b/moli-renderer-v8/src/host/window_callbacks.rs @@ -29,7 +29,7 @@ pub(super) enum WindowWebIdlCallbackTaskKind { Timer, AnimationFrame { timestamp: f64 }, Idle { timeout_deadline_ms: f64 }, - GeolocationError { watch_id: Option }, + Geolocation { watch_id: Option }, } pub(super) struct ScheduledWindowWebIdlCallback { @@ -78,7 +78,7 @@ impl ScheduledWindowWebIdlCallback { } => timeout_deadline_ms < 0.0 || now_ms < timeout_deadline_ms, WindowWebIdlCallbackTaskKind::Timer | WindowWebIdlCallbackTaskKind::AnimationFrame { .. } - | WindowWebIdlCallbackTaskKind::GeolocationError { .. } => false, + | WindowWebIdlCallbackTaskKind::Geolocation { .. } => false, } } @@ -90,7 +90,7 @@ impl ScheduledWindowWebIdlCallback { ) -> bool { matches!( self.kind, - WindowWebIdlCallbackTaskKind::GeolocationError { + WindowWebIdlCallbackTaskKind::Geolocation { watch_id: Some(candidate) } if candidate == watch_id ) && v8::Local::new(scope, &self.target_receiver).strict_equals(geolocation.into()) @@ -131,11 +131,11 @@ impl ScheduledWindowWebIdlCallback { arguments.push(deadline.into()); v8::undefined(scope).into() } - WindowWebIdlCallbackTaskKind::GeolocationError { .. } => { + WindowWebIdlCallbackTaskKind::Geolocation { .. } => { assert_eq!( extra_args.len(), 1, - "a Geolocation error task must retain exactly one error argument" + "a Geolocation task must retain exactly one result argument" ); arguments.push(v8::Local::new(scope, &extra_args[0])); v8::undefined(scope).into() @@ -155,10 +155,10 @@ impl ScheduledWindowWebIdlCallback { "host callback threw", "requestIdleCallback callback", ), - WindowWebIdlCallbackTaskKind::GeolocationError { .. } => ( - "PositionErrorCallback", + WindowWebIdlCallbackTaskKind::Geolocation { .. } => ( + "Geolocation callback", "Geolocation callback threw", - "Geolocation error callback", + "Geolocation callback", ), }; invoke_window_webidl_callback_function( 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 ef2392bf7..e64247c19 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -273,6 +273,8 @@ impl JsContextHost { viewport_surface: None, wpt_extensions_enabled: false, network_offline: false, + navigator_overrides: Default::default(), + geolocation_objects: Vec::new(), blocked_url_patterns: Vec::new(), service_worker_client_id, service_worker_control: None, 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 4cff7952f..7624aa16b 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -126,6 +126,7 @@ mod message_ports; mod messages; mod module_owner_tasks; mod navigation; +mod navigator_overrides; mod opfs_tasks; mod permissions; mod pointer_capture; @@ -876,6 +877,8 @@ pub(crate) struct JsContextHost { viewport_surface: Option, wpt_extensions_enabled: bool, network_offline: bool, + navigator_overrides: moli_page_types::NavigatorOverrides, + geolocation_objects: Vec>, blocked_url_patterns: Vec, service_worker_client_id: ServiceWorkerClientId, service_worker_control: Option, diff --git a/moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs b/moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs new file mode 100644 index 000000000..aeca48c59 --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/navigator_overrides.rs @@ -0,0 +1,47 @@ +use super::JsContextHost; +use moli_page_types::NavigatorOverrides; + +impl JsContextHost { + pub(crate) fn navigator_overrides(&self) -> &NavigatorOverrides { + &self.navigator_overrides + } + + pub(crate) fn navigator_online(&self) -> bool { + self.navigator_overrides + .online + .unwrap_or(!self.network_offline) + } + + pub(crate) fn set_navigator_overrides(&mut self, overrides: &NavigatorOverrides) -> bool { + let geolocation_changed = self.navigator_overrides.geolocation != overrides.geolocation; + self.navigator_overrides = overrides.clone(); + geolocation_changed + } + + pub(crate) fn register_geolocation_object<'s>( + &mut self, + scope: &mut v8::PinScope<'s, '_>, + geolocation: v8::Local<'s, v8::Object>, + ) { + self.geolocation_objects + .retain(|object| object.to_local(scope).is_some()); + self.geolocation_objects + .push(v8::Weak::new(scope, geolocation)); + } + + pub(crate) fn live_geolocation_objects<'s>( + &mut self, + scope: &mut v8::PinScope<'s, '_>, + ) -> Vec> { + let mut live = Vec::new(); + self.geolocation_objects.retain(|object| { + if let Some(object) = object.to_local(scope) { + live.push(object); + true + } else { + false + } + }); + live + } +} diff --git a/moli-renderer-v8/src/runtime/owner.rs b/moli-renderer-v8/src/runtime/owner.rs index c00326f90..78ad3292f 100644 --- a/moli-renderer-v8/src/runtime/owner.rs +++ b/moli-renderer-v8/src/runtime/owner.rs @@ -113,6 +113,7 @@ pub struct RendererPreparedDocumentCommitConfiguration { pub cpu_throttling_rate: f64, pub emulated_media: crate::protocol_types::EmulatedMediaOverrides, pub idle_override: Option, + pub navigator_overrides: moli_page_types::NavigatorOverrides, pub viewport_surface: Option, pub browser_resource_runtime: crate::network::BrowserResourceRuntime, pub navigator_identity: moli_browser_profile::BrowserIdentityProfile, @@ -163,6 +164,7 @@ pub struct RendererCreateHtmlPageRequest { pub storage_bucket_store: Option, pub emulated_media: crate::protocol_types::EmulatedMediaOverrides, pub idle_override: Option, + pub navigator_overrides: moli_page_types::NavigatorOverrides, pub viewport_surface: Option, pub fetch_subresource_interception_enabled: bool, pub fetch_subresource_interception_resource_type: Option, @@ -212,6 +214,7 @@ pub struct RendererCreateStreamingRawPageRequest { pub storage_bucket_store: Option, pub emulated_media: crate::protocol_types::EmulatedMediaOverrides, pub idle_override: Option, + pub navigator_overrides: moli_page_types::NavigatorOverrides, pub viewport_surface: Option, pub fetch_subresource_interception_enabled: bool, pub fetch_subresource_interception_resource_type: Option, @@ -2164,6 +2167,7 @@ impl RendererOwnerHandle { storage_bucket_store: None, emulated_media, idle_override: None, + navigator_overrides: Default::default(), viewport_surface, fetch_subresource_interception_enabled, fetch_subresource_interception_resource_type, @@ -2236,6 +2240,7 @@ impl RendererOwnerHandle { cpu_throttling_rate, emulated_media, idle_override: None, + navigator_overrides: Default::default(), viewport_surface, network_offline, blocked_url_patterns, @@ -6842,6 +6847,7 @@ impl RendererOwnerHandle { storage_bucket_store, emulated_media, idle_override, + navigator_overrides, viewport_surface, fetch_subresource_interception_enabled, fetch_subresource_interception_resource_type, @@ -6928,6 +6934,7 @@ impl RendererOwnerHandle { cpu_throttling_rate, emulated_media, idle_override, + navigator_overrides, viewport_surface, network_offline, blocked_url_patterns, @@ -7155,6 +7162,7 @@ impl RendererOwnerHandle { storage_bucket_store, emulated_media, idle_override, + navigator_overrides, viewport_surface, fetch_subresource_interception_enabled, fetch_subresource_interception_resource_type, @@ -7229,6 +7237,7 @@ impl RendererOwnerHandle { cpu_throttling_rate, emulated_media, idle_override, + navigator_overrides, viewport_surface, network_offline, blocked_url_patterns, diff --git a/moli-renderer-v8/src/runtime/owner_local_store/mod.rs b/moli-renderer-v8/src/runtime/owner_local_store/mod.rs index 55f3463c5..15202f87a 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store/mod.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store/mod.rs @@ -918,6 +918,7 @@ impl RendererOwnerLocalStore { request.cpu_throttling_rate = configuration.cpu_throttling_rate; request.emulated_media = configuration.emulated_media; request.idle_override = configuration.idle_override; + request.navigator_overrides = configuration.navigator_overrides; request.viewport_surface = configuration.viewport_surface; request .loader diff --git a/moli-renderer-v8/src/runtime/page_commands.rs b/moli-renderer-v8/src/runtime/page_commands.rs index a2098e473..75dfb37c1 100644 --- a/moli-renderer-v8/src/runtime/page_commands.rs +++ b/moli-renderer-v8/src/runtime/page_commands.rs @@ -1112,6 +1112,10 @@ impl PageVm { self.set_idle_override(idle_override)?; Ok(RendererPageReply::Unit) } + RendererPageCommand::SetNavigatorOverrides(overrides) => { + self.set_navigator_overrides(&overrides)?; + Ok(RendererPageReply::Unit) + } RendererPageCommand::SetLocaleOverride(locale) => { self.set_locale_override(locale.as_deref())?; Ok(RendererPageReply::Unit) diff --git a/moli-renderer-v8/src/runtime/page_network.rs b/moli-renderer-v8/src/runtime/page_network.rs index 50343f436..108b985a7 100644 --- a/moli-renderer-v8/src/runtime/page_network.rs +++ b/moli-renderer-v8/src/runtime/page_network.rs @@ -198,6 +198,16 @@ impl PageVm { Ok(()) } + pub(crate) fn set_navigator_overrides( + &mut self, + overrides: &moli_page_types::NavigatorOverrides, + ) -> anyhow::Result<()> { + self.vm_mut() + .set_navigator_overrides_and_sync_surface(overrides)?; + self.navigator_overrides = overrides.clone(); + Ok(()) + } + pub(crate) fn set_network_offline(&mut self, offline: bool) { self.network_offline = offline; self.vm_mut().set_network_offline(offline); diff --git a/moli-renderer-v8/src/runtime/page_surface.rs b/moli-renderer-v8/src/runtime/page_surface.rs index 0b67916b3..184f22aea 100644 --- a/moli-renderer-v8/src/runtime/page_surface.rs +++ b/moli-renderer-v8/src/runtime/page_surface.rs @@ -5307,6 +5307,7 @@ pub enum RendererPageCommand { }, SetPermissionOverrides(Vec), SetIdleOverride(Option), + SetNavigatorOverrides(moli_page_types::NavigatorOverrides), SetLocaleOverride(Option), SetTimezoneOverride(Option), SetScriptExecutionDisabled(bool), 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 44af06057..2f008bbea 100644 --- a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs @@ -1196,6 +1196,7 @@ impl PageVm { cpu_throttling_rate: self.cpu_throttling_rate, emulated_media: self.emulated_media.clone(), idle_override: self.idle_override, + navigator_overrides: self.navigator_overrides.clone(), viewport_surface: self.viewport_surface, network_offline: self.network_offline, blocked_url_patterns: self.blocked_url_patterns.clone(), diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index f4502186e..4df4e0cb3 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -1088,6 +1088,7 @@ pub(crate) struct PageVmEnvConfig { pub(crate) cpu_throttling_rate: f64, pub(crate) emulated_media: crate::protocol_types::EmulatedMediaOverrides, pub(crate) idle_override: Option, + pub(crate) navigator_overrides: moli_page_types::NavigatorOverrides, pub(crate) viewport_surface: Option, pub(crate) network_offline: bool, pub(crate) blocked_url_patterns: Vec, @@ -1610,6 +1611,7 @@ pub(crate) struct PageVm { pub(super) cpu_throttling_rate: f64, pub(super) emulated_media: crate::protocol_types::EmulatedMediaOverrides, pub(super) idle_override: Option, + pub(super) navigator_overrides: moli_page_types::NavigatorOverrides, pub(super) viewport_surface: Option, pub(super) network_offline: bool, pub(super) blocked_url_patterns: Vec, @@ -4329,6 +4331,7 @@ impl PageVm { cpu_throttling_rate: env.cpu_throttling_rate, emulated_media: env.emulated_media.clone(), idle_override: env.idle_override, + navigator_overrides: env.navigator_overrides.clone(), viewport_surface: env.viewport_surface, network_offline: env.network_offline, blocked_url_patterns: env.blocked_url_patterns.clone(), @@ -4392,6 +4395,9 @@ impl PageVm { .set_timezone_override(env.timezone_override.as_deref()); page_vm.vm_mut().set_emulated_media(&env.emulated_media); page_vm.vm_mut().set_idle_override(env.idle_override); + page_vm + .vm_mut() + .set_navigator_overrides(&env.navigator_overrides); page_vm .vm_mut() .set_viewport_surface_for_bootstrap(env.viewport_surface); diff --git a/moli-renderer-v8/src/runtime/page_vm/test_support.rs b/moli-renderer-v8/src/runtime/page_vm/test_support.rs index 97f086914..efbad25f3 100644 --- a/moli-renderer-v8/src/runtime/page_vm/test_support.rs +++ b/moli-renderer-v8/src/runtime/page_vm/test_support.rs @@ -673,6 +673,7 @@ fn minimal_test_page_vm_env_config() -> PageVmEnvConfig { cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index 77ceb8fa2..7f77920c7 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -1812,6 +1812,7 @@ fn test_page_vm_with_loader_dom_host_hooks_and_response_referrer_policy( cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -3200,6 +3201,7 @@ fn default_runtime_hooks_reject_direct_no_owner_page_vm_construction() { cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/phase_one/mod.rs b/moli-renderer-v8/src/runtime/phase_one/mod.rs index 4ad3a1908..df9143508 100644 --- a/moli-renderer-v8/src/runtime/phase_one/mod.rs +++ b/moli-renderer-v8/src/runtime/phase_one/mod.rs @@ -2443,6 +2443,7 @@ document.body.setAttribute('data-error-state', [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -6176,6 +6177,7 @@ globalThis.__outerDocumentWriteScriptContinued = true; cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -14581,6 +14583,7 @@ document.body.setAttribute('data-result', [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -14762,6 +14765,7 @@ document.body.setAttribute('data-result', [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -14992,6 +14996,7 @@ document.body.setAttribute('data-result', [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -16991,6 +16996,7 @@ document.body.setAttribute("data-range", [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -17120,6 +17126,7 @@ document.body.setAttribute("data-range", [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -17259,6 +17266,7 @@ document.body.setAttribute("data-range", [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -17433,6 +17441,7 @@ document.body.setAttribute("data-range", [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -17757,6 +17766,7 @@ document.body.setAttribute("data-range", [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -17928,6 +17938,7 @@ document.body.setAttribute("data-range", [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), @@ -18017,6 +18028,7 @@ document.body.setAttribute("data-range", [ cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/phase_one/streaming.rs b/moli-renderer-v8/src/runtime/phase_one/streaming.rs index 35744a6aa..cb81dd458 100644 --- a/moli-renderer-v8/src/runtime/phase_one/streaming.rs +++ b/moli-renderer-v8/src/runtime/phase_one/streaming.rs @@ -951,6 +951,7 @@ mod tests { cpu_throttling_rate: 1.0, emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/tests.rs b/moli-renderer-v8/src/runtime/tests.rs index 2fcdafb94..a0a37a0c9 100644 --- a/moli-renderer-v8/src/runtime/tests.rs +++ b/moli-renderer-v8/src/runtime/tests.rs @@ -1510,6 +1510,7 @@ async fn streaming_unstyled_xml_converts_live_document_before_domcontentloaded() cpu_throttling_rate: 1.0, emulated_media: Default::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, browser_resource_runtime: loader.browser_resource_runtime(), navigator_identity: loader.browser_identity().clone(), @@ -1647,6 +1648,7 @@ async fn prepared_streaming_xml_document_waits_for_permit_and_uses_latest_config cpu_throttling_rate: 1.0, emulated_media: Default::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, browser_resource_runtime: loader.browser_resource_runtime(), navigator_identity: loader.browser_identity().clone(), @@ -2624,6 +2626,7 @@ globalThis.__preparedCommitObserved = JSON.stringify([ cpu_throttling_rate: 1.0, emulated_media: Default::default(), idle_override: None, + navigator_overrides: Default::default(), viewport_surface: None, browser_resource_runtime: loader.browser_resource_runtime(), navigator_identity: loader.browser_identity().clone(), diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 346dc071a..91fb75deb 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -5195,6 +5195,33 @@ impl ScriptVm { self._context_host.borrow_mut().set_network_offline(offline); } + pub(super) fn set_navigator_overrides( + &mut self, + overrides: &moli_page_types::NavigatorOverrides, + ) { + self._context_host + .borrow_mut() + .set_navigator_overrides(overrides); + } + + pub(super) fn set_navigator_overrides_and_sync_surface( + &mut self, + overrides: &moli_page_types::NavigatorOverrides, + ) -> Result<()> { + let changed = self + ._context_host + .borrow_mut() + .set_navigator_overrides(overrides); + if changed { + let context_ptr: *const v8::Global = &self.page_default_context; + self.with_context_scope_by_ptr(context_ptr, |scope, _| { + crate::context_bootstrap::notify_geolocation_override_changed(scope); + Ok(()) + })?; + } + Ok(()) + } + pub(super) fn set_bypass_service_worker(&mut self, bypass: bool) { self._context_host .borrow_mut()