mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 08:00:45 +00:00
feat(gamepad): expose policy-aware empty gamepad enumeration
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
mod clipboard;
|
||||
mod collections;
|
||||
mod gamepad;
|
||||
mod geolocation;
|
||||
mod media_capabilities;
|
||||
mod media_devices;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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: (),
|
||||
|
||||
|
||||
@@ -687,6 +687,7 @@ pub(crate) struct DocumentPolicyContainer {
|
||||
pub(crate) credentialless: bool,
|
||||
pub(crate) credentialless_storage_nonce: Option<moli_storage_key::OpaqueOriginNonce>,
|
||||
pub(crate) sandbox: DocumentSandboxPolicy,
|
||||
pub(crate) permissions_policy: crate::permissions_policy::DocumentPermissionsPolicy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -162,6 +162,11 @@ impl DocumentPolicyContainer {
|
||||
headers,
|
||||
final_url,
|
||||
),
|
||||
permissions_policy:
|
||||
crate::permissions_policy::DocumentPermissionsPolicy::from_navigation_response_headers(
|
||||
headers,
|
||||
final_url,
|
||||
),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ChildInitialEmptyDocumentInit> = 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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -124,6 +124,48 @@ impl JsContextHost {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn document_permissions_policy_for_owner(
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
) -> Option<crate::permissions_policy::DocumentPermissionsPolicy> {
|
||||
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<crate::permissions_policy::DocumentPermissionsPolicy> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<bool> {
|
||||
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::<Vec<_>>();
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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/",
|
||||
"<!doctype html><body></body>",
|
||||
);
|
||||
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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user