Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub(in crate::context_bootstrap) const WORKER_SHARED_INTERFACE_NAMES: &[&str] =
];

const SECURE_CONTEXT_ONLY_INTERFACE_NAMES: &[&str] = &[
"MediaDevices",
"SubtleCrypto",
"CryptoKey",
"IdleDetector",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod clipboard;
mod collections;
mod geolocation;
mod media_capabilities;
mod media_devices;
mod navigator;
mod navigator_subobjects;
mod screen;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use super::super::window_runtime::{
MEDIA_DEVICES_BRAND_SLOT, navigator_media_devices_get_user_media_callback,
};
use super::super::*;
use crate::util::{get_private_value, set_private_value, throw_type_error};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};

const MEDIA_DEVICES_LISTENERS_SLOT: &str = "__moliMediaDevicesListeners";
const MEDIA_DEVICES_ONDEVICECHANGE_SLOT: &str = "__moliMediaDevicesOndevicechange";

#[derive(Default, WebApiObject)]
#[webapi(interface = "MediaDevices")]
struct MediaDevicesObjectDeclaration {
#[webapi(slot = MEDIA_DEVICES_BRAND_SLOT, init = true)]
brand: (),

#[webapi(slot = SIMPLE_EVENT_TARGET_SLOT, value = MEDIA_DEVICES_LISTENERS_SLOT)]
event_target_slot: (),

#[webapi(slot = SIMPLE_EVENT_TARGET_ORDERED_HANDLERS_SLOT, init = true)]
ordered_handlers: (),

#[webapi(slot = MEDIA_DEVICES_ONDEVICECHANGE_SLOT, init = "null")]
ondevicechange: (),
}

#[derive(WebApiFunctionTemplate)]
#[webapi(name = "MediaDevices", enumerable)]
struct MediaDevicesPrototypeDeclaration {
#[webapi(method, length = 0, callback = enumerate_devices_callback)]
enumerate_devices: (),

#[webapi(method, length = 1, callback = navigator_media_devices_get_user_media_callback)]
get_user_media: (),

#[webapi(accessor_property, getter = ondevicechange_getter, setter = ondevicechange_setter)]
ondevicechange: (),
}

pub(super) fn build_media_devices_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> Option<v8::Local<'s, v8::Object>> {
MediaDevicesObjectDeclaration::default().bind(scope).ok()
}

pub(super) fn install_media_devices_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
let prototype = template.prototype_template(scope);
MediaDevicesPrototypeDeclaration::initialize_prototype_template(scope, prototype);
}

fn receiver_is_media_devices<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, receiver, MEDIA_DEVICES_BRAND_SLOT)
.is_some_and(|value| value.boolean_value(scope))
}

fn enumerate_devices_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
rv.set(resolver.get_promise(scope).into());
if !receiver_is_media_devices(scope, args.this()) {
let message = v8str(scope, "Illegal invocation");
let error = v8::Exception::type_error(scope, message);
let _ = resolver.reject(scope, error);
return;
}

// Enumeration waits while its associated document is not fully active.
// In particular, retaining a MediaDevices object must not make a discarded
// iframe's enumeration resolve against the caller's active document.
let Some(context) = args.this().get_creation_context(scope) else {
return;
};
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return;
};
// SAFETY: all Window realms use the host owned by this isolate's bridge.
let host = unsafe { &*host_ptr };
let Some(identity) = host.window_execution_context_identity_for_v8_context(scope, context)
else {
return;
};
if !host.window_execution_context_identity_is_current(identity) {
return;
}

// The headless media backend currently has no input or output devices.
let devices = v8::Array::new(scope, 0);
let _ = resolver.resolve(scope, devices.into());
}

fn ondevicechange_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if !receiver_is_media_devices(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let value = get_private_value(scope, args.this(), MEDIA_DEVICES_ONDEVICECHANGE_SLOT)
.unwrap_or_else(|| v8::null(scope).into());
rv.set(value);
}

fn ondevicechange_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s, v8::Value>,
) {
if !receiver_is_media_devices(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let value = args.get(0);
let stored = if value.is_function() {
value
} else {
v8::null(scope).into()
};
set_private_value(
scope,
args.this(),
MEDIA_DEVICES_ONDEVICECHANGE_SLOT,
stored,
);
simple_object_event_set_ordered_handler(
scope,
args.this(),
MEDIA_DEVICES_LISTENERS_SLOT,
"devicechange",
MEDIA_DEVICES_ONDEVICECHANGE_SLOT,
stored.is_function(),
);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use super::super::window_runtime::{
MEDIA_DEVICES_BRAND_SLOT, PERMISSIONS_BRAND_SLOT, build_legacy_storage_quota_object,
build_navigator_ua_data_object, install_initial_service_worker_ready_promise,
navigator_get_battery_callback, navigator_java_enabled_callback,
navigator_media_devices_enumerate_devices_callback,
navigator_media_devices_get_user_media_callback, navigator_permissions_query_callback,
PERMISSIONS_BRAND_SLOT, build_legacy_storage_quota_object, build_navigator_ua_data_object,
install_initial_service_worker_ready_promise, navigator_get_battery_callback,
navigator_java_enabled_callback, navigator_permissions_query_callback,
navigator_send_beacon_callback, navigator_service_worker_controller_getter_callback,
navigator_service_worker_controllerchange_handler_getter_callback,
navigator_service_worker_controllerchange_handler_setter_callback,
Expand All @@ -29,6 +27,7 @@ use super::geolocation::{build_geolocation_object, install_geolocation_template_
use super::media_capabilities::{
build_media_capabilities_object, install_media_capabilities_template_bindings,
};
use super::media_devices::{build_media_devices_object, install_media_devices_template_bindings};
use super::navigator_subobjects::{NavigatorSubobject, ensure_navigator_subobject};
use crate::document_runtime::DomHandle;
use crate::native_bridge::OwnerDispatchScope;
Expand Down Expand Up @@ -409,19 +408,6 @@ struct NavigatorUaDataPrototypeMethodsDeclaration {
get_high_entropy_values: (),
}

#[derive(Default, WebApiObject)]
#[webapi(interface = "MediaDevices")]
struct MediaDevicesObjectDeclaration {
#[webapi(slot = MEDIA_DEVICES_BRAND_SLOT, init = true)]
brand: (),

#[webapi(method, enumerable, length = 0, callback = navigator_media_devices_enumerate_devices_callback)]
enumerate_devices: (),

#[webapi(method, enumerable, length = 1, callback = navigator_media_devices_get_user_media_callback)]
get_user_media: (),
}

#[derive(Default, WebApiObject)]
#[webapi(interface = "Object")]
struct ServiceWorkerContainerDeclaration {
Expand Down Expand Up @@ -821,6 +807,7 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
install_media_capabilities_template_bindings(scope, template, interface_name);
let prototype = template.prototype_template(scope);
match interface_name {
"MediaDevices" => install_media_devices_template_bindings(scope, template),
"Navigator" => {
NavigatorRuntimeDataPrototypeDeclaration::initialize_prototype_template(
scope, prototype,
Expand Down Expand Up @@ -854,6 +841,7 @@ fn filter_navigator_secure_context_exposure<'s>(
) -> Result<()> {
if !secure_context {
delete_object_property(scope, prototype, "clipboard")?;
delete_object_property(scope, prototype, "mediaDevices")?;
delete_object_property(scope, prototype, "storage")?;
delete_object_property(scope, prototype, "storageBuckets")?;
delete_object_property(scope, prototype, "serviceWorker")?;
Expand Down Expand Up @@ -998,9 +986,8 @@ pub(super) fn build_lazy_navigator_subobject_in_current_realm<'s>(
| NavigatorSubobject::WebkitPersistentStorage => {
build_legacy_storage_quota_object(scope)?.into()
}
NavigatorSubobject::MediaDevices => MediaDevicesObjectDeclaration::default()
.bind(scope)
.map_err(|error| anyhow!("failed to bind MediaDevices object: {error}"))?
NavigatorSubobject::MediaDevices => build_media_devices_object(scope)
.ok_or_else(|| anyhow!("failed to bind MediaDevices object"))?
.into(),
NavigatorSubobject::ServiceWorker => {
build_service_worker_container(scope, owner_child, owner_popup)?.into()
Expand Down
1 change: 0 additions & 1 deletion moli-renderer-v8/src/context_bootstrap/window_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ pub(super) use navigator::{
MEDIA_DEVICES_BRAND_SLOT, PERMISSIONS_BRAND_SLOT, build_legacy_storage_info_object,
build_legacy_storage_quota_object, build_navigator_ua_data_object,
global_caches_getter_callback, navigator_get_battery_callback, navigator_java_enabled_callback,
navigator_media_devices_enumerate_devices_callback,
navigator_media_devices_get_user_media_callback, navigator_permissions_query_callback,
navigator_send_beacon_callback, navigator_storage_estimate_callback,
navigator_storage_get_directory_callback, navigator_storage_persist_callback,
Expand Down
18 changes: 0 additions & 18 deletions moli-renderer-v8/src/context_bootstrap/window_runtime/navigator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3564,24 +3564,6 @@ fn build_storage_usage_details_object<'s>(
.expect("StorageUsageDetails declaration should bind")
}

pub(in crate::context_bootstrap) fn navigator_media_devices_enumerate_devices_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
if !media_devices_receiver_branded(scope, args.this()) {
reject_type_error(scope, resolver, "Illegal invocation");
rv.set(resolver.get_promise(scope).into());
return;
}
let devices = v8::Array::new(scope, 0);
let _ = resolver.resolve(scope, devices.into());
rv.set(resolver.get_promise(scope).into());
}

pub(in crate::context_bootstrap) fn navigator_media_devices_get_user_media_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
Expand Down
2 changes: 1 addition & 1 deletion moli-renderer-v8/src/script_vm/tests/browser_api/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2143,7 +2143,7 @@ fn zhihu_probe_media_devices_surface_exposes_promise_methods() {
const proto = Object.getPrototypeOf(devices);
const fakeDevices = Object.create(MediaDevices.prototype);
const summarizeMethodDescriptor = name => {
const descriptor = Object.getOwnPropertyDescriptor(devices, name);
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
return [
!!descriptor,
typeof descriptor?.value,
Expand Down
112 changes: 112 additions & 0 deletions moli-renderer-v8/src/script_vm/tests/browser_api/media_devices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use super::*;

#[test]
fn media_devices_enumeration_and_devicechange_live_on_a_branded_event_target_prototype() {
let mut vm = new_storage_test_vm("https://media-devices.test/");
let result = vm.eval(r#"
(() => {
const devices = navigator.mediaDevices;
const prototype = MediaDevices.prototype;
const method = Object.getOwnPropertyDescriptor(prototype, 'enumerateDevices');
const handler = Object.getOwnPropertyDescriptor(prototype, 'ondevicechange');
const checks = [
devices instanceof MediaDevices, devices instanceof EventTarget,
devices === navigator.mediaDevices,
Object.getOwnPropertyNames(devices).length === 0,
method.value.name === 'enumerateDevices', method.value.length === 0,
method.enumerable, method.writable, method.configurable,
handler.get.name === 'get ondevicechange', handler.get.length === 0,
handler.set.name === 'set ondevicechange', handler.set.length === 1,
handler.enumerable, handler.configurable, devices.ondevicechange === null
];
const events = [];
devices.addEventListener('devicechange', () => events.push('first'));
devices.ondevicechange = event => events.push('old');
devices.addEventListener('devicechange', () => events.push('last'));
devices.ondevicechange = event => events.push(event.target === devices ? 'handler' : 'bad target');
devices.dispatchEvent(new Event('devicechange'));
checks.push(events.join(',') === 'first,handler,last');
devices.ondevicechange = {};
checks.push(devices.ondevicechange === null);
for (const fake of [{}, prototype, Object.create(devices)]) {
for (const callback of [() => handler.get.call(fake), () => handler.set.call(fake, null)]) {
try { callback(); checks.push(false); } catch (error) { checks.push(error instanceof TypeError); }
}
}
return checks.every(Boolean);
})()
"#).expect("MediaDevices prototype and event dispatch should evaluate");
assert_eq!(result, "true");
}

#[test]
fn media_devices_enumeration_resolves_fresh_empty_lists_and_rejects_fake_receivers() {
let mut vm = new_storage_test_vm("https://media-devices-enumeration.test/");
vm.exec(r#"
const devices = navigator.mediaDevices;
const fake = Object.create(devices);
fake.__moliMediaDevicesBrand = true;
const calls = [devices.enumerateDevices(), devices.enumerateDevices(),
...[null, undefined, {}, fake].map(receiver => devices.enumerateDevices.call(receiver))];
globalThis.__enumerationPromises = calls.every(value => value instanceof Promise);
Promise.allSettled(calls).then(results => {
globalThis.__enumerationResult = [
...results.slice(0, 2).map(result => result.status === 'fulfilled' &&
Array.isArray(result.value) && result.value.length === 0),
results[0].value !== results[1].value,
...results.slice(2).map(result => result.status === 'rejected' && result.reason instanceof TypeError)
].every(Boolean);
});
"#, None).expect("enumeration promises should be created without synchronous throws");
assert_eq!(
vm.eval("__enumerationPromises && __enumerationResult")
.unwrap(),
"true"
);
}

#[test]
fn media_devices_enumeration_keeps_discarded_receivers_pending_even_with_borrowed_methods() {
let mut vm = new_parsed_test_vm(
"https://media-devices-discard.test/",
"<!doctype html><body></body>",
);
vm.exec(
r#"
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const devices = iframe.contentWindow.navigator.mediaDevices;
const foreignEnumerate = devices.enumerateDevices;
iframe.remove();
globalThis.__discardedSettled = false;
globalThis.__activeSettled = false;
const markDiscarded = () => { __discardedSettled = true; };
devices.enumerateDevices().then(markDiscarded, markDiscarded);
navigator.mediaDevices.enumerateDevices.call(devices).then(markDiscarded, markDiscarded);
foreignEnumerate.call(navigator.mediaDevices).then(result => {
__activeSettled = Array.isArray(result) && result.length === 0;
});
"#,
None,
)
.expect("enumeration should retain its receiver's document activity");
assert_eq!(
vm.eval("!__discardedSettled && __activeSettled").unwrap(),
"true"
);
}

#[test]
fn media_devices_is_only_exposed_in_secure_windows() {
for (url, expected) in [
("https://media-devices.test/", "true|true"),
("http://media-devices.test/", "false|false"),
] {
let mut vm = new_storage_test_vm(url);
assert_eq!(
vm.eval("['MediaDevices' in globalThis, 'mediaDevices' in navigator].join('|')")
.unwrap(),
expected
);
}
}
Loading
Loading