diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs index 84732e140..422f2981d 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime.rs @@ -1,5 +1,6 @@ mod clipboard; mod collections; +mod gamepad; mod geolocation; mod media_capabilities; mod media_devices; diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/gamepad.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/gamepad.rs new file mode 100644 index 000000000..d6f89a5fa --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/gamepad.rs @@ -0,0 +1,40 @@ +use super::super::context_host_ptr_from_global_bridge; +use super::navigator::navigator_receiver_branded; +use crate::{util::throw_type_error, webidl}; + +pub(super) fn navigator_get_gamepads_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + if !navigator_receiver_branded(scope, args.this()) { + throw_type_error(scope, "Illegal invocation"); + return; + } + + // The Gamepad algorithm uses the current global's document, including + // when a method from one Window is borrowed by another Navigator. + let context = scope.get_current_context(); + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + // SAFETY: the bridge owns the host for the lifetime of this callback. + let host = unsafe { &*host_ptr }; + if let Some(identity) = + host.window_execution_context_identity_for_v8_context(scope, context) + && host.window_execution_context_identity_is_current(identity) + && host + .document_permissions_policy_for_owner(identity.dispatch_scope()) + .is_some_and(|policy| !policy.gamepad_enabled()) + { + webidl::throw_dom_exception( + scope, + "SecurityError", + "Access to gamepads is disallowed by permissions policy.", + ); + return; + } + } + + // No gamepad backend is connected. An inactive document also returns an + // empty sequence before checking policy. Each call produces a new Array. + rv.set(v8::Array::new(scope, 0).into()); +} 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..fc0a2f296 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/navigator.rs @@ -23,6 +23,7 @@ use super::clipboard::{build_clipboard_object, install_clipboard_template_bindin use super::collections::{ build_navigator_plugin_collections, install_navigator_collection_template_bindings, }; +use super::gamepad::navigator_get_gamepads_callback; use super::geolocation::{build_geolocation_object, install_geolocation_template_bindings}; use super::media_capabilities::{ build_media_capabilities_object, install_media_capabilities_template_bindings, @@ -211,6 +212,9 @@ struct NavigatorRuntimeDataPrototypeDeclaration { #[derive(Default, WebApiFunctionTemplate)] #[webapi(name = "Navigator")] struct NavigatorPrototypeMethodsDeclaration { + #[webapi(method, enumerable, length = 0, callback = navigator_get_gamepads_callback)] + get_gamepads: (), + #[webapi(method, enumerable, length = 0, callback = navigator_java_enabled_callback)] java_enabled: (), diff --git a/moli-renderer-v8/src/document_runtime.rs b/moli-renderer-v8/src/document_runtime.rs index 3814c5353..eb86c9d25 100644 --- a/moli-renderer-v8/src/document_runtime.rs +++ b/moli-renderer-v8/src/document_runtime.rs @@ -687,6 +687,7 @@ pub(crate) struct DocumentPolicyContainer { pub(crate) credentialless: bool, pub(crate) credentialless_storage_nonce: Option, pub(crate) sandbox: DocumentSandboxPolicy, + pub(crate) permissions_policy: crate::permissions_policy::DocumentPermissionsPolicy, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/moli-renderer-v8/src/document_runtime/security_policy.rs b/moli-renderer-v8/src/document_runtime/security_policy.rs index 18feab26e..e25f451fd 100644 --- a/moli-renderer-v8/src/document_runtime/security_policy.rs +++ b/moli-renderer-v8/src/document_runtime/security_policy.rs @@ -162,6 +162,11 @@ impl DocumentPolicyContainer { headers, final_url, ), + permissions_policy: + crate::permissions_policy::DocumentPermissionsPolicy::from_navigation_response_headers( + headers, + final_url, + ), ..Self::default() } } diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index baf9e6989..d46ca79f5 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -79,6 +79,7 @@ mod page_task_queue; mod parser_module_evaluation; mod parser_module_pending; mod parser_script; +mod permissions_policy; mod queue_microtask; mod range_boundary; mod referrer_policy; diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs index ba4df2058..1a87b8609 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs @@ -36,6 +36,15 @@ impl JsContextHost { .map(|entry| entry.document_policy_container_snapshot()) }) .unwrap_or_else(|| self.document_policy_container().clone()); + if let Some(parent_document) = self + .dom_host() + .node(handle) + .and_then(crate::dom::native::Node::owner_document) + && let Some(permissions_policy) = + self.document_permissions_policy_for_document_handle(parent_document) + { + policy_container.permissions_policy = permissions_policy; + } policy_container.document_referrer = self.document_url_for_child_context(handle).to_string(); policy_container diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs index 7959820aa..5cb2034e7 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs @@ -224,6 +224,13 @@ impl ChildBrowsingContextEntry { self.document_policy_container.clone() } + pub(super) fn set_document_permissions_policy( + &mut self, + policy: crate::permissions_policy::DocumentPermissionsPolicy, + ) { + self.document_policy_container.permissions_policy = policy; + } + pub(super) fn owner_credentialless(&self) -> bool { self.credentialless } @@ -329,6 +336,10 @@ impl ChildBrowsingContextEntry { .content_security_reporting_endpoints = policy_container .content_security_reporting_endpoints .clone(); + self.document_policy_container.permissions_policy = self + .document_policy_container + .permissions_policy + .intersect(policy_container.permissions_policy); self.set_document_credentialless_state(credentialless, credentialless_storage_nonce); self.set_document_sandbox_policy(sandbox); } @@ -367,6 +378,10 @@ impl ChildBrowsingContextEntry { .policy_container .content_security_reporting_endpoints .clone(); + self.document_policy_container.permissions_policy = self + .document_policy_container + .permissions_policy + .intersect(snapshot.policy_container.permissions_policy); self.set_document_credentialless_state(credentialless, credentialless_storage_nonce); self.set_document_sandbox_policy(sandbox); } 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 dcb7400b0..b8a11c904 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 @@ -436,6 +436,8 @@ impl JsContextHost { if !self.child_browsing_contexts.contains_key(&handle) { return None; }; + let permissions_policy = + self.child_browsing_context_permissions_policy_for_navigation(handle, &bootstrap); let Some(navigation) = self.replace_child_navigation_load(handle) else { tracing::warn!( ?handle, @@ -444,6 +446,7 @@ impl JsContextHost { return None; }; let entry = self.child_browsing_contexts.get_mut(&handle)?; + entry.set_document_permissions_policy(permissions_policy); entry.set_pending_navigation(bootstrap, reflects_window_state); self.note_child_frame_load_started_for_parent(handle); Some(navigation) diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs index c1ce4f35f..5db5916d5 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs @@ -262,6 +262,20 @@ impl JsContextHost { && child_browsing_context_bootstrap_uses_initial_empty_load( &attribute_bootstrap, ); + let pending_attribute_bootstrap_commit = + ChildBrowsingContextEntry::pending_attribute_bootstrap_commit_for_refresh( + existing.as_ref(), + is_new, + attribute_bootstrap_changed, + initial_about_blank_document_is_complete, + ); + let pending_attribute_permissions_policy = + pending_attribute_bootstrap_commit.then(|| { + self.child_browsing_context_permissions_policy_for_navigation( + handle, + &attribute_bootstrap, + ) + }); if is_new || attribute_bootstrap_changed || frame_identity_changed { self.note_child_frame_load_started_for_parent(handle); } @@ -324,6 +338,20 @@ impl JsContextHost { content_security_reporting_endpoints: refresh_policy_source .map(|policy| policy.content_security_reporting_endpoints.clone()) .unwrap_or_default(), + permissions_policy: if is_new { + // The synchronous initial about:blank Document is + // already subject to the iframe's container policy. + // Use its live inherited origin, not a pending target + // navigation's origin, when applying the allowlist. + self.child_browsing_context_permissions_policy_for_navigation( + handle, + &live_bootstrap, + ) + } else { + refresh_policy_source + .map(|policy| policy.permissions_policy) + .unwrap_or_default() + }, }; let initial_empty_document_init: Option = is_new .then(|| { @@ -342,13 +370,7 @@ impl JsContextHost { name, id: id.filter(|value| !value.is_empty()), attribute_bootstrap, - pending_attribute_bootstrap_commit: - ChildBrowsingContextEntry::pending_attribute_bootstrap_commit_for_refresh( - existing.as_ref(), - is_new, - attribute_bootstrap_changed, - initial_about_blank_document_is_complete, - ), + pending_attribute_bootstrap_commit, pending_live_navigation: existing.as_ref().and_then(|entry| { entry.pending_live_navigation_for_refresh(attribute_bootstrap_changed) }), @@ -365,7 +387,8 @@ impl JsContextHost { cached_snapshot, document_policy_container, completed_document_network: existing.as_ref().and_then(|entry| { - entry.completed_document_network_for_refresh(attribute_bootstrap_changed) + entry + .completed_document_network_for_refresh(attribute_bootstrap_changed) }), completed_frame_owner_resource_timing: existing.as_ref().and_then( |entry| { @@ -468,6 +491,11 @@ impl JsContextHost { self.register_or_update_service_worker_child_client(handle); } } + if let Some(policy) = pending_attribute_permissions_policy + && let Some(entry) = self.child_browsing_contexts.get_mut(&handle) + { + entry.set_document_permissions_policy(policy); + } if attribute_bootstrap_changed { self.cancel_child_meta_refresh_navigation(handle); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs index e6b9200f8..1a4a12cfa 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs @@ -683,6 +683,34 @@ impl JsContextHost { credentialless.then(|| self.ensure_top_document_credentialless_storage_nonce()) } + pub(in crate::native_bridge::context_host) fn child_browsing_context_permissions_policy_for_navigation( + &self, + handle: DomHandle, + bootstrap: &ChildBrowsingContextBootstrap, + ) -> crate::permissions_policy::DocumentPermissionsPolicy { + let parent_document = self + .dom_host() + .node(handle) + .and_then(crate::dom::native::Node::owner_document) + .unwrap_or_else(|| self.document_handle()); + let parent_url = self.document_url_for_handle(parent_document); + let parent_policy = self + .document_permissions_policy_for_document_handle(parent_document) + .unwrap_or_else(|| self.document_policy_container().permissions_policy); + let child_url = Self::child_browsing_context_bootstrap_url(bootstrap) + .unwrap_or_else(|| parent_url.clone()); + let is_iframe = self.dom_host().is_html_element_named(handle, "iframe"); + let allow = is_iframe + .then(|| self.dom_host().get_attribute(handle, "allow")) + .flatten(); + parent_policy.delegated_to_child( + &parent_url, + &child_url, + bootstrap.security_origin_inherited(), + allow.as_deref(), + ) + } + pub(crate) fn child_browsing_context_is_same_origin_with_top(&self, handle: DomHandle) -> bool { self.top_window_can_access_child(handle) } diff --git a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs index 054124668..ddd9f4e24 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs @@ -124,6 +124,48 @@ impl JsContextHost { ) } + pub(crate) fn document_permissions_policy_for_owner( + &self, + owner: OwnerDispatchScope, + ) -> Option { + match owner { + OwnerDispatchScope::Top => Some(self.document_policy_container().permissions_policy), + OwnerDispatchScope::Child(handle) => self + .frame_owner_current_child_snapshot(handle) + .map(|snapshot| { + snapshot + .settings + .document_policy_container + .permissions_policy + }), + OwnerDispatchScope::LightweightPopup(popup_id) => self + .lightweight_popup_policy_container(popup_id) + .map(|policy| policy.permissions_policy), + } + } + + pub(crate) fn document_permissions_policy_for_document_handle( + &self, + document: DomHandle, + ) -> Option { + if document == self.document_handle() { + return Some(self.document_policy_container().permissions_policy); + } + if let Some(popup_id) = self.lightweight_popup_id_for_document_handle(document) { + return self + .lightweight_popup_policy_container(popup_id) + .map(|policy| policy.permissions_policy); + } + let child_handle = self.child_browsing_context_host_for_document_handle(document)?; + let snapshot = self.frame_owner_current_child_snapshot(child_handle)?; + (snapshot.document_handle == document).then_some( + snapshot + .settings + .document_policy_container + .permissions_policy, + ) + } + pub(crate) fn trusted_types_for_script_requirements_for_owner( &self, owner: OwnerDispatchScope, diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 3d2101ed9..e2042fe41 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -3896,6 +3896,13 @@ struct HtmlIFrameElementPrototypeDeclaration { setter = iframe_srcdoc_setter_function )] srcdoc: (), + #[webapi( + accessor_property, + getter = dom_string_reflection_getter_function, + setter = dom_string_reflection_setter_function, + data = DomStringReflection::IframeAllow + )] + allow: (), #[webapi( accessor_property, getter = dom_string_reflection_getter_function, diff --git a/moli-renderer-v8/src/native_bridge/element/reflection.rs b/moli-renderer-v8/src/native_bridge/element/reflection.rs index 554bf77e8..2468922a9 100644 --- a/moli-renderer-v8/src/native_bridge/element/reflection.rs +++ b/moli-renderer-v8/src/native_bridge/element/reflection.rs @@ -188,6 +188,7 @@ pub(super) enum DomStringReflection { HrWidth, HtmlTimeDateTime, HtmlVersion, + IframeAllow, IframeCsp, IframeFrameBorder, IframeHeight, @@ -412,6 +413,10 @@ const DOM_STRING_REFLECTION_DESCRIPTORS: &[(DomStringReflection, DomStringReflec DomStringReflection::HtmlVersion, DomStringReflectionDescriptor::new("HTMLHtmlElement", "version", "version"), ), + ( + DomStringReflection::IframeAllow, + DomStringReflectionDescriptor::new("HTMLIFrameElement", "allow", "allow"), + ), ( DomStringReflection::IframeCsp, DomStringReflectionDescriptor::new_html_element( diff --git a/moli-renderer-v8/src/permissions_policy.rs b/moli-renderer-v8/src/permissions_policy.rs new file mode 100644 index 000000000..4f6afd87e --- /dev/null +++ b/moli-renderer-v8/src/permissions_policy.rs @@ -0,0 +1,200 @@ +use url::Url; + +/// Policy-controlled features that currently have observable renderer behavior. +/// +/// This intentionally stores the effective policy for one committed Document, +/// rather than the raw header/container allowlists. Extend the record when a +/// newly implemented feature needs enforcement at its API boundary. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct DocumentPermissionsPolicy { + gamepad: bool, +} + +impl Default for DocumentPermissionsPolicy { + fn default() -> Self { + Self { gamepad: true } + } +} + +impl DocumentPermissionsPolicy { + pub(crate) const fn gamepad_enabled(self) -> bool { + self.gamepad + } + + pub(crate) const fn intersect(self, other: Self) -> Self { + Self { + gamepad: self.gamepad && other.gamepad, + } + } + + pub(crate) fn from_navigation_response_headers( + headers: &[(String, String)], + document_url: &Url, + ) -> Self { + let mut policy = Self::default(); + for (_, value) in headers + .iter() + .filter(|(name, _)| name.eq_ignore_ascii_case("permissions-policy")) + { + for directive in value.split(',') { + let Some((feature, allowlist)) = directive.split_once('=') else { + continue; + }; + if feature.trim().eq_ignore_ascii_case("gamepad") { + policy.gamepad = response_allowlist_allows_document(allowlist, document_url); + } + } + } + policy + } + + pub(crate) fn delegated_to_child( + self, + parent_url: &Url, + child_url: &Url, + child_inherits_origin: bool, + allow_attribute: Option<&str>, + ) -> Self { + let same_origin = child_inherits_origin || moli_url::same_origin(parent_url, child_url); + let gamepad = iframe_allow_feature( + allow_attribute, + "gamepad", + parent_url, + child_url, + same_origin, + ) + .unwrap_or(true); + Self { + gamepad: self.gamepad && gamepad, + } + } +} + +fn response_allowlist_allows_document(value: &str, document_url: &Url) -> bool { + let value = value.trim(); + if value == "*" { + return true; + } + let Some(value) = value + .strip_prefix('(') + .and_then(|value| value.strip_suffix(')')) + else { + return false; + }; + value.split_ascii_whitespace().any(|token| { + token.eq_ignore_ascii_case("self") + || token == "*" + || token_origin_matches(token, document_url) + }) +} + +fn iframe_allow_feature( + allow_attribute: Option<&str>, + feature: &str, + parent_url: &Url, + child_url: &Url, + same_origin: bool, +) -> Option { + let allow_attribute = allow_attribute?; + allow_attribute.split(';').find_map(|directive| { + let mut tokens = directive.split_ascii_whitespace(); + let name = tokens.next()?; + if !name.eq_ignore_ascii_case(feature) { + return None; + } + let allowlist = tokens.collect::>(); + if allowlist.is_empty() { + return Some(true); + } + if allowlist + .iter() + .any(|token| token.eq_ignore_ascii_case("'none'") || *token == "()") + { + return Some(false); + } + Some(allowlist.into_iter().any(|token| { + token == "*" + || token.eq_ignore_ascii_case("'src'") + || (token.eq_ignore_ascii_case("'self'") && same_origin) + || token_origin_matches(token, child_url) + || token_origin_matches(token, parent_url) && same_origin + })) + }) +} + +fn token_origin_matches(token: &str, url: &Url) -> bool { + let token = token.trim_matches(['\'', '"']); + Url::parse(token) + .ok() + .is_some_and(|origin| moli_url::same_origin(&origin, url)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn url(value: &str) -> Url { + Url::parse(value).unwrap() + } + + #[test] + fn permissions_policy_header_takes_precedence_over_legacy_feature_policy() { + let policy = DocumentPermissionsPolicy::from_navigation_response_headers( + &[ + ("Permissions-Policy".to_owned(), "gamepad=*".to_owned()), + ("Feature-Policy".to_owned(), "gamepad 'none'".to_owned()), + ], + &url("https://example.test/document"), + ); + assert!(policy.gamepad_enabled()); + } + + #[test] + fn permissions_policy_response_none_disables_recognized_features() { + let policy = DocumentPermissionsPolicy::from_navigation_response_headers( + &[("permissions-policy".to_owned(), "gamepad=()".to_owned())], + &url("https://example.test/document"), + ); + assert!(!policy.gamepad_enabled()); + } + + #[test] + fn iframe_gamepad_policy_uses_default_wildcard_allowlist_and_explicit_delegation() { + let parent = url("https://parent.test/page"); + let same_origin = url("https://parent.test/child"); + let cross_origin = url("data:text/html,child"); + let policy = DocumentPermissionsPolicy::default(); + + let same = policy.delegated_to_child(&parent, &same_origin, false, None); + assert!(same.gamepad_enabled()); + + let cross = policy.delegated_to_child(&parent, &cross_origin, false, None); + assert!(cross.gamepad_enabled()); + + let delegated = + policy.delegated_to_child(&parent, &cross_origin, false, Some("payment; gamepad *")); + assert!(delegated.gamepad_enabled()); + } + + #[test] + fn iframe_none_and_parent_policy_cannot_be_overridden() { + let parent = url("https://parent.test/page"); + let child = url("https://parent.test/child"); + let denied = DocumentPermissionsPolicy::default().delegated_to_child( + &parent, + &child, + false, + Some("gamepad 'none'"), + ); + assert!(!denied.gamepad_enabled()); + + let parent_denied = DocumentPermissionsPolicy { gamepad: false }; + let delegated = parent_denied.delegated_to_child( + &parent, + &url("https://other.test/child"), + false, + Some("gamepad *"), + ); + assert!(!delegated.gamepad_enabled()); + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/gamepad.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/gamepad.rs new file mode 100644 index 000000000..e5c7f9b88 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/gamepad.rs @@ -0,0 +1,68 @@ +use super::*; + +#[test] +fn navigator_get_gamepads_returns_fresh_empty_sequences_and_checks_its_receiver() { + let mut vm = new_storage_test_vm("http://gamepad.test/"); + let result = vm + .eval( + r#" + (() => { + const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'getGamepads'); + const first = navigator.getGamepads(); + const second = navigator.getGamepads(); + first.push('local mutation'); + const rejects = receiver => { + try { navigator.getGamepads.call(receiver); return false; } + catch (error) { return error instanceof TypeError; } + }; + return [ + descriptor.value.name === 'getGamepads', descriptor.value.length === 0, + descriptor.writable, descriptor.enumerable, descriptor.configurable, + !Object.hasOwn(navigator, 'getGamepads'), Array.isArray(first), + Array.isArray(second), first !== second, second.length === 0, + navigator.getGamepads().length === 0, + [null, undefined, {}, Navigator.prototype, Object.create(navigator)].every(rejects) + ].every(Boolean); + })() + "#, + ) + .expect("getGamepads surface should evaluate"); + assert_eq!(result, "true"); +} + +#[test] +fn navigator_get_gamepads_checks_the_current_global_policy_and_activity() { + let mut vm = new_parsed_test_vm( + "https://gamepad-policy.test/", + "", + ); + let result = vm + .eval( + r#" + (() => { + const frame = document.createElement('iframe'); + frame.allow = "gamepad 'none'"; + document.body.appendChild(frame); + const child = frame.contentWindow; + const childNavigator = child.navigator; + const childMethod = childNavigator.getGamepads; + const denied = callback => { + try { callback(); return false; } + catch (error) { return error.name === 'SecurityError'; } + }; + const results = [ + navigator.getGamepads().length === 0, + denied(() => childMethod.call(childNavigator)), + navigator.getGamepads.call(childNavigator).length === 0, + denied(() => childMethod.call(navigator)) + ]; + frame.remove(); + const inactive = childMethod.call(childNavigator); + results.push(Array.isArray(inactive), inactive.length === 0); + return results.join('|'); + })() + "#, + ) + .expect("gamepad policy and activity should evaluate"); + assert_eq!(result, "true|true|true|true|true|true"); +} 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 b3cf44da7..db12bf53c 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 @@ -13,6 +13,7 @@ mod crypto_subtle_x25519; mod details; mod event_handlers; mod events_selection_storage; +mod gamepad; mod ice_candidate; mod idle_callbacks; mod idle_detection;