mirror of
https://github.com/lexmount/moli.git
synced 2026-11-03 00:06:37 +00:00
feat(wake-lock): expose secure native interfaces with explicit platform denial
This commit is contained in:
@@ -16,6 +16,7 @@ enum RealmDependentFinalizer {
|
||||
GlobalEventHandlersSecureContextSurface,
|
||||
CryptoSecureContextSurface,
|
||||
BaseAudioContextSecureContextSurface,
|
||||
NavigatorSecureContextSurface,
|
||||
XmlHttpRequestEventTargetState,
|
||||
NotificationPermission,
|
||||
PointerEventSecureContextSurface,
|
||||
@@ -46,6 +47,10 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] =
|
||||
"BaseAudioContext",
|
||||
RealmDependentFinalizer::BaseAudioContextSecureContextSurface,
|
||||
),
|
||||
(
|
||||
"Navigator",
|
||||
RealmDependentFinalizer::NavigatorSecureContextSurface,
|
||||
),
|
||||
(
|
||||
"Crypto",
|
||||
RealmDependentFinalizer::CryptoSecureContextSurface,
|
||||
@@ -123,7 +128,12 @@ pub(super) fn finalize_materialized_interface<'s>(
|
||||
RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface => {
|
||||
crate::context_bootstrap::window_events::finalize_secure_global_event_handler_realm_bindings(
|
||||
scope, prototype,
|
||||
)
|
||||
)?;
|
||||
}
|
||||
RealmDependentFinalizer::NavigatorSecureContextSurface => {
|
||||
crate::context_bootstrap::navigator_runtime::finalize_navigator_realm_bindings(
|
||||
scope, prototype,
|
||||
)?;
|
||||
}
|
||||
RealmDependentFinalizer::CryptoSecureContextSurface => {
|
||||
finalize_crypto_realm_bindings(scope, prototype)
|
||||
@@ -193,6 +203,7 @@ mod tests {
|
||||
"DocumentType",
|
||||
"CharacterData",
|
||||
"BaseAudioContext",
|
||||
"Navigator",
|
||||
"Crypto",
|
||||
"XMLHttpRequestEventTarget",
|
||||
"Notification",
|
||||
|
||||
@@ -201,8 +201,8 @@ const SECURE_CONTEXT_ONLY_INTERFACE_NAMES: &[&str] = &[
|
||||
"MediaDeviceInfo",
|
||||
"InputDeviceInfo",
|
||||
"MediaDevices",
|
||||
"MediaDeviceInfo",
|
||||
"InputDeviceInfo",
|
||||
"WakeLock",
|
||||
"WakeLockSentinel",
|
||||
"SubtleCrypto",
|
||||
"CryptoKey",
|
||||
"PaymentResponse",
|
||||
|
||||
@@ -10,12 +10,14 @@ mod navigator_subobjects;
|
||||
mod screen;
|
||||
mod user_activation;
|
||||
mod visual_viewport;
|
||||
mod wake_lock;
|
||||
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::finalize_navigator_realm_bindings;
|
||||
pub(super) use self::navigator::install_navigator_template_bindings;
|
||||
pub(crate) use self::navigator::install_worker_navigator_runtime_state;
|
||||
pub(in crate::context_bootstrap) use self::navigator::{
|
||||
|
||||
@@ -75,6 +75,7 @@ const NAVIGATOR_RUNTIME_DATA_KEYS: &[&str] = &[
|
||||
"storageBuckets",
|
||||
"geolocation",
|
||||
"mediaCapabilities",
|
||||
"wakeLock",
|
||||
];
|
||||
const WORKER_NAVIGATOR_INSTALLED_SLOT: &str = "__moliWorkerNavigatorInstalled";
|
||||
const WORKER_NAVIGATOR_MATERIALIZING_SLOT: &str = "__moliWorkerNavigatorMaterializing";
|
||||
@@ -194,6 +195,8 @@ struct NavigatorRuntimeDataPrototypeDeclaration {
|
||||
geolocation: (),
|
||||
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 32))]
|
||||
media_capabilities: (),
|
||||
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 33))]
|
||||
wake_lock: (),
|
||||
#[webapi(accessor_property, getter = navigator_cookie_enabled_getter_callback)]
|
||||
cookie_enabled: (),
|
||||
}
|
||||
@@ -585,6 +588,9 @@ struct WindowNavigatorBackingDeclaration<'scope, 'profile> {
|
||||
|
||||
#[webapi(data_property, enumerable)]
|
||||
media_capabilities: v8::Local<'scope, v8::Value>,
|
||||
|
||||
#[webapi(data_property, enumerable)]
|
||||
wake_lock: v8::Local<'scope, v8::Value>,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
@@ -877,6 +883,7 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
|
||||
super::gamepad::install(scope, template, interface_name);
|
||||
install_media_capabilities_template_bindings(scope, template, interface_name);
|
||||
super::credentials::install(scope, template, interface_name);
|
||||
super::wake_lock::install(scope, template, interface_name);
|
||||
super::user_activation::install_user_activation_template_bindings(
|
||||
scope,
|
||||
template,
|
||||
@@ -918,14 +925,17 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_navigator_secure_context_exposure<'s>(
|
||||
pub(in crate::context_bootstrap) fn finalize_navigator_realm_bindings<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
secure_context: bool,
|
||||
) -> Result<()> {
|
||||
let window = scope.get_current_context().global(scope);
|
||||
let secure_context =
|
||||
super::super::runtime_state::window_realm_secure_context_available(scope, window);
|
||||
if !secure_context {
|
||||
delete_object_property(scope, prototype, "clipboard")?;
|
||||
delete_object_property(scope, prototype, "mediaDevices")?;
|
||||
delete_object_property(scope, prototype, "wakeLock")?;
|
||||
delete_object_property(scope, prototype, "storage")?;
|
||||
delete_object_property(scope, prototype, "storageBuckets")?;
|
||||
delete_object_property(scope, prototype, "serviceWorker")?;
|
||||
@@ -1088,6 +1098,7 @@ pub(super) fn build_lazy_navigator_subobject_in_current_realm<'s>(
|
||||
|| navigator_geolocation_secure_context_available(scope, owner_child, owner_popup);
|
||||
build_media_capabilities_object(scope, secure_context, worker)?.into()
|
||||
}
|
||||
NavigatorSubobject::WakeLock => super::wake_lock::build(scope)?.into(),
|
||||
};
|
||||
Ok(value)
|
||||
}
|
||||
@@ -1261,6 +1272,7 @@ fn build_window_navigator_backing_for_owner<'s>(
|
||||
storage_buckets: v8::undefined(scope).into(),
|
||||
geolocation: v8::undefined(scope).into(),
|
||||
media_capabilities: v8::undefined(scope).into(),
|
||||
wake_lock: v8::undefined(scope).into(),
|
||||
}
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to bind Navigator backing object: {error}"))
|
||||
@@ -1289,15 +1301,19 @@ pub(super) fn build_window_navigator_object_for_owner<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner_child: Option<DomHandle>,
|
||||
identity: Option<&BrowserIdentityProfile>,
|
||||
storage_apis_available: bool,
|
||||
) -> Result<v8::Local<'s, v8::Object>> {
|
||||
let backing = build_window_navigator_backing_for_owner(scope, owner_child, None, identity)?;
|
||||
bind_window_navigator(scope, backing)
|
||||
}
|
||||
|
||||
fn bind_window_navigator<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
backing: v8::Local<'s, v8::Object>,
|
||||
) -> Result<v8::Local<'s, v8::Object>> {
|
||||
let _ = ensure_intrinsic_interface_prototype(scope, "Navigator")?;
|
||||
let navigator = NavigatorObjectDeclaration::new(backing)
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to bind Navigator object: {error}"))?;
|
||||
if let Some(prototype) = global_constructor_prototype(scope, "Navigator") {
|
||||
filter_navigator_secure_context_exposure(scope, prototype, storage_apis_available)?;
|
||||
}
|
||||
Ok(navigator)
|
||||
}
|
||||
|
||||
@@ -1353,9 +1369,7 @@ pub(crate) fn build_lightweight_popup_window_navigator_object<'s>(
|
||||
owner_popup: u64,
|
||||
) -> Result<v8::Local<'s, v8::Object>> {
|
||||
let backing = build_window_navigator_backing_for_owner(scope, None, Some(owner_popup), None)?;
|
||||
NavigatorObjectDeclaration::new(backing)
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to bind lightweight popup Navigator object: {error}"))
|
||||
bind_window_navigator(scope, backing)
|
||||
}
|
||||
|
||||
fn build_worker_navigator_backing<'s>(
|
||||
|
||||
@@ -22,11 +22,12 @@ pub(super) enum NavigatorSubobject {
|
||||
StorageBuckets,
|
||||
Geolocation,
|
||||
MediaCapabilities,
|
||||
WakeLock,
|
||||
}
|
||||
|
||||
impl NavigatorSubobject {
|
||||
#[cfg(test)]
|
||||
pub(super) const ALL: [Self; 16] = [
|
||||
pub(super) const ALL: [Self; 17] = [
|
||||
Self::Languages,
|
||||
Self::MimeTypes,
|
||||
Self::Plugins,
|
||||
@@ -43,6 +44,7 @@ impl NavigatorSubobject {
|
||||
Self::StorageBuckets,
|
||||
Self::Geolocation,
|
||||
Self::MediaCapabilities,
|
||||
Self::WakeLock,
|
||||
];
|
||||
|
||||
pub(super) fn from_key(key: &str) -> Option<Self> {
|
||||
@@ -63,6 +65,7 @@ impl NavigatorSubobject {
|
||||
"storageBuckets" => Some(Self::StorageBuckets),
|
||||
"geolocation" => Some(Self::Geolocation),
|
||||
"mediaCapabilities" => Some(Self::MediaCapabilities),
|
||||
"wakeLock" => Some(Self::WakeLock),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -85,6 +88,7 @@ impl NavigatorSubobject {
|
||||
Self::StorageBuckets => "storageBuckets",
|
||||
Self::Geolocation => "geolocation",
|
||||
Self::MediaCapabilities => "mediaCapabilities",
|
||||
Self::WakeLock => "wakeLock",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Secure Window wake-lock bindings for a UA without a platform wake-lock backend.
|
||||
//!
|
||||
//! The specification permits implementation-specific denial. Convert arguments
|
||||
//! before rejecting valid requests; never manufacture an acquired sentinel.
|
||||
|
||||
use anyhow::Result;
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
||||
|
||||
use crate::{native_bridge::throw_dom_exception, web_api_interfaces, webidl};
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::WakeLock)]
|
||||
struct WakeLockObjectDeclaration<'s> {
|
||||
#[webapi(prototype)]
|
||||
prototype: v8::Local<'s, v8::Object>,
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::WakeLock, enumerable, receiver)]
|
||||
struct WakeLockPrototypeDeclaration {
|
||||
#[webapi(method, returns_promise, length = 0, callback = request)]
|
||||
request: (),
|
||||
}
|
||||
|
||||
// request() cannot return a sentinel until platform acquisition is implemented.
|
||||
// Keep the native interface surface and receiver validation without inventing
|
||||
// acquired/released state or dispatching a synthetic release event.
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::WakeLockSentinel, enumerable, receiver)]
|
||||
struct WakeLockSentinelPrototypeDeclaration {
|
||||
#[webapi(accessor_property, getter = sentinel_backend_unavailable)]
|
||||
released: (),
|
||||
#[webapi(accessor_property = "type", getter = sentinel_backend_unavailable)]
|
||||
lock_type: (),
|
||||
#[webapi(method, returns_promise, length = 0, callback = sentinel_backend_unavailable)]
|
||||
release: (),
|
||||
#[webapi(accessor_property, getter = sentinel_backend_unavailable, setter = sentinel_backend_unavailable)]
|
||||
onrelease: (),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, webidl::WebIdlEnum)]
|
||||
#[webidl(name = "WakeLockType")]
|
||||
enum WakeLockType {
|
||||
Screen,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "WakeLock.request")]
|
||||
struct RequestArgs {
|
||||
#[webidl(converter = "enum", default = WakeLockType::Screen)]
|
||||
lock_type: WakeLockType,
|
||||
}
|
||||
|
||||
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 {
|
||||
"WakeLock" => {
|
||||
WakeLockPrototypeDeclaration::initialize_prototype_template(scope, prototype);
|
||||
}
|
||||
"WakeLockSentinel" => {
|
||||
WakeLockSentinelPrototypeDeclaration::initialize_prototype_template(scope, prototype);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build<'s>(scope: &mut v8::PinScope<'s, '_>) -> Result<v8::Local<'s, v8::Object>> {
|
||||
let prototype =
|
||||
crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "WakeLock")?;
|
||||
WakeLockObjectDeclaration::new(prototype)
|
||||
.bind(scope)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn request<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<RequestArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
match parsed.lock_type {
|
||||
WakeLockType::Screen => throw_dom_exception(
|
||||
scope,
|
||||
"NotAllowedError",
|
||||
0,
|
||||
"Screen wake locks are unavailable on this platform.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn sentinel_backend_unavailable<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
throw_dom_exception(
|
||||
scope,
|
||||
"NotSupportedError",
|
||||
9,
|
||||
"Platform wake-lock sentinels are not implemented.",
|
||||
);
|
||||
}
|
||||
@@ -12,8 +12,6 @@ use crate::{
|
||||
use anyhow::{Result, anyhow};
|
||||
use moli_browser_profile::BrowserIdentityProfile;
|
||||
|
||||
const WINDOW_NAVIGATOR_STORAGE_APIS_AVAILABLE_SLOT: &str =
|
||||
"__moliWindowNavigatorStorageApisAvailable";
|
||||
const WINDOW_NAVIGATOR_USER_AGENT_SLOT: &str = "__moliWindowNavigatorUserAgent";
|
||||
const WINDOW_NAVIGATOR_ACCEPT_LANGUAGE_SLOT: &str = "__moliWindowNavigatorAcceptLanguage";
|
||||
|
||||
@@ -32,18 +30,7 @@ pub(in crate::context_bootstrap) fn build_window_navigator_for_receiver<'s>(
|
||||
let identity = navigator_identity_profile(scope, receiver).or_else(|| {
|
||||
user_agent.map(|user_agent| BrowserIdentityProfile::new(user_agent, accept_language))
|
||||
});
|
||||
let storage_apis_available = get_private_value(
|
||||
scope,
|
||||
receiver,
|
||||
WINDOW_NAVIGATOR_STORAGE_APIS_AVAILABLE_SLOT,
|
||||
)
|
||||
.is_none_or(|value| value.boolean_value(scope));
|
||||
build_window_navigator_object_for_owner(
|
||||
scope,
|
||||
owner_child,
|
||||
identity.as_ref(),
|
||||
storage_apis_available,
|
||||
)
|
||||
build_window_navigator_object_for_owner(scope, owner_child, identity.as_ref())
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn install_navigator_runtime_state<'s>(
|
||||
@@ -52,13 +39,6 @@ pub(in crate::context_bootstrap) fn install_navigator_runtime_state<'s>(
|
||||
storage_apis_available: bool,
|
||||
) -> Result<()> {
|
||||
super::user_activation::initialize_window_user_activation(scope, global)?;
|
||||
let storage_apis_available_value = v8::Boolean::new(scope, storage_apis_available);
|
||||
set_private_value(
|
||||
scope,
|
||||
global,
|
||||
WINDOW_NAVIGATOR_STORAGE_APIS_AVAILABLE_SLOT,
|
||||
storage_apis_available_value.into(),
|
||||
);
|
||||
if storage_apis_available {
|
||||
GlobalCachesAccessorDeclaration::new(true)
|
||||
.initialize(scope, global)
|
||||
|
||||
@@ -856,6 +856,14 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
|
||||
interface: web_api_interfaces::MediaDevices::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::WakeLock::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::WakeLockSentinel::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
},
|
||||
ConstructorSpec {
|
||||
interface: web_api_interfaces::Clipboard::DESCRIPTOR,
|
||||
kind: ConstructorKind::Illegal,
|
||||
|
||||
@@ -2320,6 +2320,7 @@ mod time_ranges;
|
||||
mod url_components;
|
||||
mod video_codecs_shell;
|
||||
mod web_lock_interface_exposure_interfaces;
|
||||
mod wake_lock_interfaces;
|
||||
mod webgl_interfaces;
|
||||
mod webidl_collections;
|
||||
mod webidl_fetch;
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
(async () => {
|
||||
const rows = [], platformRequests = [];
|
||||
const assert = (condition, message) => { if (!condition) throw Error(message); };
|
||||
const check = async (name, callback) => {
|
||||
try { await callback(); rows.push({name, pass: true}); }
|
||||
catch (error) { rows.push({name, pass: false, message: String(error)}); }
|
||||
};
|
||||
const thrown = callback => {
|
||||
try { callback(); } catch (error) { return error; }
|
||||
throw Error('expected synchronous exception');
|
||||
};
|
||||
const rejected = async (w, callback) => {
|
||||
const promise = callback();
|
||||
assert(promise instanceof w.Promise, 'Promise belongs to the callee realm');
|
||||
try { await promise; } catch (error) { return error; }
|
||||
throw Error('expected rejected Promise');
|
||||
};
|
||||
const popup = open();
|
||||
const realms = [['main', window], ['child', document.getElementById('child').contentWindow], ['popup', popup]];
|
||||
const mainGetter = isSecureContext && Object.getOwnPropertyDescriptor(Navigator.prototype, 'wakeLock')?.get;
|
||||
try {
|
||||
for (const [label, w] of realms) {
|
||||
if (!w.isSecureContext) {
|
||||
await check(label + '/navigator/insecure prototype before materialization', () => {
|
||||
assert(!Object.hasOwn(w.Navigator.prototype, 'wakeLock'), 'secure prototype member hidden before Navigator getter');
|
||||
});
|
||||
for (const name of ['WakeLock', 'WakeLockSentinel']) {
|
||||
await check(label + '/' + name + '/insecure', () => assert(!(name in w), 'secure interface hidden'));
|
||||
}
|
||||
await check(label + '/navigator/insecure', () => assert(!('wakeLock' in w.navigator), 'secure Navigator member hidden'));
|
||||
await check(label + '/navigator/author properties after finalization', () => {
|
||||
Object.defineProperty(w.Navigator.prototype, 'wakeLock', {configurable: true, value: 42});
|
||||
try { assert(w.navigator.wakeLock === 42, 'author property remains observable'); }
|
||||
finally { delete w.Navigator.prototype.wakeLock; }
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const name of ['WakeLock', 'WakeLockSentinel']) {
|
||||
await check(label + '/' + name + '/interface', () => {
|
||||
const C = w[name], parent = name === 'WakeLock' ? w.Object : w.EventTarget;
|
||||
const d = Object.getOwnPropertyDescriptor(w, name);
|
||||
assert(typeof C === 'function' && C.name === name && C.length === 0, 'interface constructor metadata');
|
||||
assert(d.writable && d.configurable && !d.enumerable, 'global descriptor');
|
||||
assert(Object.getPrototypeOf(C.prototype) === parent.prototype, 'prototype inheritance');
|
||||
assert(Object.getPrototypeOf(C) === (name === 'WakeLock' ? w.Function.prototype : parent), 'constructor inheritance');
|
||||
assert(C.prototype.constructor === C, 'prototype constructor');
|
||||
const tag = Object.getOwnPropertyDescriptor(C.prototype, Symbol.toStringTag);
|
||||
assert(tag.value === name && !tag.writable && !tag.enumerable && tag.configurable, 'interface tag');
|
||||
assert(thrown(() => C()) instanceof w.TypeError && thrown(() => new C()) instanceof w.TypeError, 'illegal constructor realm');
|
||||
});
|
||||
}
|
||||
let lock;
|
||||
await check(label + '/navigator/SameObject intrinsic and relevant realm', () => {
|
||||
const C = w.WakeLock, original = Object.getOwnPropertyDescriptor(w, 'WakeLock');
|
||||
const inherited = Object.getOwnPropertyDescriptor(w.Object.prototype, 'wakeLock');
|
||||
let constructorReads = 0, inheritedReads = 0;
|
||||
Object.defineProperty(w, 'WakeLock', {configurable: true, get() { constructorReads++; throw Error('author constructor'); }});
|
||||
Object.defineProperty(w.Object.prototype, 'wakeLock', {configurable: true, get() { inheritedReads++; throw Error('inherited cache hook'); }});
|
||||
try { lock = mainGetter.call(w.navigator); }
|
||||
finally {
|
||||
if (original) Object.defineProperty(w, 'WakeLock', original); else delete w.WakeLock;
|
||||
if (inherited) Object.defineProperty(w.Object.prototype, 'wakeLock', inherited); else delete w.Object.prototype.wakeLock;
|
||||
}
|
||||
assert(constructorReads === 0 && inheritedReads === 0, 'native materialization ignores public hooks');
|
||||
assert(lock === w.navigator.wakeLock && Object.getPrototypeOf(lock) === C.prototype, 'SameObject in Navigator relevant realm');
|
||||
assert(lock instanceof C && Object.prototype.toString.call(lock) === '[object WakeLock]', 'native WakeLock identity');
|
||||
assert(!Object.hasOwn(w.navigator, 'wakeLock') && !Object.hasOwn(lock, 'request'), 'shared prototype surface');
|
||||
const d = Object.getOwnPropertyDescriptor(w.Navigator.prototype, 'wakeLock');
|
||||
assert(d.enumerable && d.configurable && d.set === undefined && d.get.length === 0 && d.get.name === 'get wakeLock', 'readonly Navigator accessor');
|
||||
assert(Reflect.set(w.navigator, 'wakeLock', {}) === false && w.navigator.wakeLock === lock, 'readonly SameObject');
|
||||
});
|
||||
await check(label + '/navigator/receiver brands', () => {
|
||||
const get = Object.getOwnPropertyDescriptor(w.Navigator.prototype, 'wakeLock').get;
|
||||
let traps = 0; const trap = () => { traps++; throw Error('receiver hook'); };
|
||||
const revoked = Proxy.revocable(w.navigator, {}); revoked.revoke();
|
||||
for (const value of [null, {}, w.Navigator.prototype, Object.create(w.navigator), new Proxy(w.navigator, {get: trap, getPrototypeOf: trap}), revoked.proxy]) {
|
||||
assert(thrown(() => get.call(value)) instanceof w.TypeError, 'callee realm rejects unbranded Navigator');
|
||||
}
|
||||
assert(traps === 0, 'receiver validation does not run author hooks');
|
||||
});
|
||||
await check(label + '/request/descriptor', () => {
|
||||
const d = Object.getOwnPropertyDescriptor(w.WakeLock.prototype, 'request');
|
||||
assert(d.value.length === 0 && d.value.name === 'request' && d.writable && d.enumerable && d.configurable, 'request operation metadata');
|
||||
});
|
||||
await check(label + '/request/brand before conversion', async () => {
|
||||
const request = w.WakeLock.prototype.request;
|
||||
let conversions = 0, traps = 0;
|
||||
const argument = {toString() { conversions++; throw Error('argument conversion'); }};
|
||||
const trap = () => { traps++; throw Error('receiver hook'); };
|
||||
const revoked = Proxy.revocable(lock, {}); revoked.revoke();
|
||||
for (const receiver of [null, {}, w.WakeLock.prototype, Object.create(lock), new Proxy(lock, {get: trap, getPrototypeOf: trap}), revoked.proxy]) {
|
||||
assert(await rejected(w, () => request.call(receiver, argument)) instanceof w.TypeError, 'reject forged and author proxy receivers');
|
||||
}
|
||||
assert(conversions === 0 && traps === 0, 'native receiver checks precede argument conversion');
|
||||
});
|
||||
await check(label + '/request/enum rejection', async () => {
|
||||
for (const value of [null, '', 'system', 'SCREEN', 0, true, Symbol(), {}]) {
|
||||
assert(await rejected(w, () => lock.request(value)) instanceof w.TypeError, 'invalid WakeLockType rejects Promise');
|
||||
}
|
||||
});
|
||||
await check(label + '/request/conversion exceptions and order', async () => {
|
||||
const sentinel = {}, order = [];
|
||||
const argument = {
|
||||
get [Symbol.toPrimitive]() { order.push('get primitive'); return () => { order.push('call primitive'); throw sentinel; }; },
|
||||
toString() { throw Error('unexpected string fallback'); }
|
||||
};
|
||||
assert(await rejected(w, () => lock.request(argument)) === sentinel, 'preserve author conversion exception identity');
|
||||
assert(order.join() === 'get primitive,call primitive', 'enum string conversion runs exactly once');
|
||||
});
|
||||
await check(label + '/request/valid default and platform outcome', async () => {
|
||||
const request = w.WakeLock.prototype.request;
|
||||
const promises = [request.call(lock), request.call(lock, undefined), request.call(lock, 'screen')];
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
const promise = promises[i];
|
||||
assert(promise instanceof w.Promise && promises.filter(value => value === promise).length === 1, 'fresh Promise per request');
|
||||
try {
|
||||
const sentinel = await promise;
|
||||
assert(sentinel instanceof w.WakeLockSentinel && sentinel instanceof w.EventTarget && sentinel.type === 'screen', 'native sentinel if platform permits request');
|
||||
platformRequests.push({realm: label, call: i, result: 'acquired'});
|
||||
await sentinel.release();
|
||||
} catch (error) {
|
||||
assert(error instanceof w.DOMException && error.name === 'NotAllowedError' && error.code === 0, 'platform denial uses NotAllowedError');
|
||||
platformRequests.push({realm: label, call: i, result: 'denied', name: error.name, code: error.code});
|
||||
}
|
||||
}
|
||||
});
|
||||
await check(label + '/request/native identity independent of public prototype', async () => {
|
||||
const request = w.WakeLock.prototype.request, prototype = Object.getPrototypeOf(lock);
|
||||
Object.setPrototypeOf(lock, null);
|
||||
try {
|
||||
assert(await rejected(w, () => request.call(lock, 'invalid')) instanceof w.TypeError, 'native brand retained after prototype removal');
|
||||
} finally { Object.setPrototypeOf(lock, prototype); }
|
||||
});
|
||||
const entries = [['released', false], ['type', false], ['onrelease', true]];
|
||||
for (const [name, writable] of entries) {
|
||||
await check(label + '/sentinel/' + name, () => {
|
||||
const d = Object.getOwnPropertyDescriptor(w.WakeLockSentinel.prototype, name);
|
||||
assert(d.enumerable && d.configurable && d.get.name === 'get ' + name && d.get.length === 0, 'shared sentinel getter metadata');
|
||||
assert(writable ? d.set.name === 'set ' + name && d.set.length === 1 : d.set === undefined, 'sentinel attribute writability');
|
||||
let conversions = 0, traps = 0;
|
||||
const value = {toString() { conversions++; throw Error('unexpected conversion'); }};
|
||||
const trap = () => { traps++; throw Error('receiver hook'); };
|
||||
const fake = Object.create(w.WakeLockSentinel.prototype), revoked = Proxy.revocable(fake, {}); revoked.revoke();
|
||||
for (const receiver of [null, {}, w.WakeLockSentinel.prototype, fake, new Proxy(fake, {get: trap, getPrototypeOf: trap}), revoked.proxy, lock]) {
|
||||
assert(thrown(() => d.get.call(receiver)) instanceof w.TypeError, 'sentinel getter rejects unbranded receiver');
|
||||
if (writable) assert(thrown(() => d.set.call(receiver, value)) instanceof w.TypeError, 'sentinel setter checks receiver first');
|
||||
}
|
||||
assert(conversions === 0 && traps === 0, 'sentinel validation ignores author hooks');
|
||||
});
|
||||
}
|
||||
await check(label + '/sentinel/release', async () => {
|
||||
const d = Object.getOwnPropertyDescriptor(w.WakeLockSentinel.prototype, 'release');
|
||||
assert(d.value.name === 'release' && d.value.length === 0 && d.enumerable && d.configurable && d.writable, 'release operation metadata');
|
||||
const fake = Object.create(w.WakeLockSentinel.prototype), revoked = Proxy.revocable(fake, {}); revoked.revoke();
|
||||
for (const receiver of [null, {}, fake, new Proxy(fake, {}), revoked.proxy, lock]) {
|
||||
assert(await rejected(w, () => d.value.call(receiver)) instanceof w.TypeError, 'release rejects Promise for illegal receiver');
|
||||
}
|
||||
});
|
||||
await check(label + '/request/cross-realm callee Promise and errors', async () => {
|
||||
const other = w === window ? document.getElementById('child').contentWindow : window;
|
||||
const error = await rejected(w, () => w.WakeLock.prototype.request.call(other.navigator.wakeLock, 'invalid'));
|
||||
assert(error instanceof w.TypeError && !(error instanceof other.TypeError), 'TypeError belongs to binding realm');
|
||||
});
|
||||
await check(label + '/request/immutable Promise intrinsic', async () => {
|
||||
const original = w.Promise;
|
||||
w.Promise = function() { throw Error('author Promise constructor'); };
|
||||
let promise;
|
||||
try { promise = w.WakeLock.prototype.request.call(lock, 'invalid'); }
|
||||
finally { w.Promise = original; }
|
||||
assert(promise instanceof original && await rejected(w, () => promise) instanceof w.TypeError, 'Promise wrapper ignores public constructor');
|
||||
});
|
||||
}
|
||||
} finally { popup.close(); }
|
||||
globalThis.__nodeReplacementResults = {rows, platformRequests, failures: rows.filter(row => !row.pass), passed: rows.filter(row => row.pass).length, total: rows.length};
|
||||
return rows.every(row => row.pass);
|
||||
})()
|
||||
@@ -0,0 +1,30 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wake_lock_interfaces_validate_receivers_and_secure_realm_exposure() {
|
||||
for url in ["https://wake-lock.test/", "http://wake-lock.test/"] {
|
||||
let mut vm = new_storage_page_task_executor_test_vm(url);
|
||||
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'")
|
||||
.unwrap();
|
||||
vm.eval(&format!(
|
||||
"({}).then(value => globalThis.__wakeLockDone = value, error => globalThis.__wakeLockDone = String(error));",
|
||||
include_str!("wake_lock_interfaces.js")
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.eval_after_selected_page_tasks("JSON.stringify(__nodeReplacementResults.failures)")
|
||||
.unwrap(),
|
||||
"[]",
|
||||
"{url}"
|
||||
);
|
||||
assert_eq!(vm.eval("__wakeLockDone").unwrap(), "true", "{url}");
|
||||
if url.starts_with("https:") {
|
||||
assert_eq!(
|
||||
vm.eval("__nodeReplacementResults.platformRequests.length === 9 && __nodeReplacementResults.platformRequests.every(row => row.result === 'denied' && row.name === 'NotAllowedError' && row.code === 0)")
|
||||
.unwrap(),
|
||||
"true",
|
||||
"No platform backend must not produce an acquired sentinel"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +342,8 @@ interfaces! {
|
||||
MediaStreamTrack: EventTarget;
|
||||
CanvasCaptureMediaStreamTrack: MediaStreamTrack;
|
||||
MediaDevices: EventTarget;
|
||||
WakeLock;
|
||||
WakeLockSentinel: EventTarget;
|
||||
MediaError;
|
||||
MediaKeySession: EventTarget;
|
||||
MediaKeyStatusMap;
|
||||
|
||||
Reference in New Issue
Block a user