mirror of
https://github.com/lexmount/moli.git
synced 2026-10-31 00:06:19 +00:00
fix(navigator): validate protocol handler registrations
This commit is contained in:
@@ -7,6 +7,7 @@ mod media_capabilities;
|
||||
mod media_devices;
|
||||
mod navigator;
|
||||
mod navigator_subobjects;
|
||||
mod protocol_handlers;
|
||||
mod screen;
|
||||
mod user_activation;
|
||||
mod visual_viewport;
|
||||
|
||||
@@ -124,6 +124,12 @@ struct PermissionsPrototypeMethodsDeclaration {
|
||||
struct NavigatorObjectDeclaration<'scope> {
|
||||
#[webapi(slot = NAVIGATOR_RUNTIME_DATA_SLOT)]
|
||||
runtime_data: v8::Local<'scope, v8::Object>,
|
||||
|
||||
#[webapi(slot = super::protocol_handlers::DOCUMENT_SLOT)]
|
||||
document: Option<v8::Local<'scope, v8::Object>>,
|
||||
|
||||
#[webapi(slot = super::protocol_handlers::ORIGIN_SLOT)]
|
||||
origin: String,
|
||||
}
|
||||
|
||||
#[derive(Default, WebApiFunctionTemplate)]
|
||||
@@ -900,6 +906,7 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
|
||||
scope, prototype,
|
||||
);
|
||||
NavigatorPrototypeMethodsDeclaration::initialize_prototype_template(scope, prototype);
|
||||
super::protocol_handlers::install(scope, prototype);
|
||||
}
|
||||
"WorkerNavigator" => {
|
||||
WorkerNavigatorRuntimeDataPrototypeDeclaration::initialize_prototype_template(
|
||||
@@ -934,6 +941,8 @@ pub(in crate::context_bootstrap) fn finalize_navigator_realm_bindings<'s>(
|
||||
let secure_context =
|
||||
super::super::runtime_state::window_realm_secure_context_available(scope, window);
|
||||
if !secure_context {
|
||||
delete_object_property(scope, prototype, "registerProtocolHandler")?;
|
||||
delete_object_property(scope, prototype, "unregisterProtocolHandler")?;
|
||||
delete_object_property(scope, prototype, "clipboard")?;
|
||||
delete_object_property(scope, prototype, "mediaDevices")?;
|
||||
delete_object_property(scope, prototype, "wakeLock")?;
|
||||
@@ -1312,7 +1321,34 @@ fn bind_window_navigator<'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)
|
||||
let (child, popup) = navigator_backing_owner(scope, backing);
|
||||
let owner = child
|
||||
.map(OwnerDispatchScope::Child)
|
||||
.or_else(|| popup.map(OwnerDispatchScope::LightweightPopup))
|
||||
.unwrap_or(OwnerDispatchScope::Top);
|
||||
let (document, origin) = context_host_ptr_from_global_bridge(scope)
|
||||
.map(|host_ptr| {
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
let handle = match owner {
|
||||
OwnerDispatchScope::Top => Some(host.document_handle()),
|
||||
OwnerDispatchScope::Child(handle) => {
|
||||
host.child_browsing_context_document_handle(handle)
|
||||
}
|
||||
OwnerDispatchScope::LightweightPopup(id) => {
|
||||
host.lightweight_popup_document_handle(id)
|
||||
}
|
||||
};
|
||||
let origin = host
|
||||
.window_document_origin(owner)
|
||||
.unwrap_or_else(|| "null".to_owned());
|
||||
let document = handle.and_then(|handle| {
|
||||
host.native_bridge_mut()
|
||||
.wrap_handle(scope, host_ptr, handle)
|
||||
});
|
||||
(document, origin)
|
||||
})
|
||||
.unwrap_or_else(|| (None, "null".to_owned()));
|
||||
let navigator = NavigatorObjectDeclaration::new(backing, document, origin)
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to bind Navigator object: {error}"))?;
|
||||
Ok(navigator)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! Protocol-handler validation. The browser currently declines registration
|
||||
//! requests; no external application or handler navigation is installed.
|
||||
|
||||
use crate::{
|
||||
native_bridge::{
|
||||
element::parse_url_with_document_query_encoding,
|
||||
node_runtime_and_handle_from_object_or_detached,
|
||||
},
|
||||
util::get_private_value,
|
||||
web_api_interfaces, webidl,
|
||||
};
|
||||
use moli_webapi_declare::WebApiFunctionTemplate;
|
||||
|
||||
pub(super) const DOCUMENT_SLOT: &str = "__moliNavigatorDocument";
|
||||
pub(super) const ORIGIN_SLOT: &str = "__moliNavigatorOrigin";
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::Navigator, receiver)]
|
||||
struct NavigatorProtocolHandlersDeclaration {
|
||||
#[webapi(method, enumerable, length = 2, callback = protocol_handler_callback)]
|
||||
register_protocol_handler: (),
|
||||
#[webapi(method, enumerable, length = 2, callback = protocol_handler_callback)]
|
||||
unregister_protocol_handler: (),
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "Navigator protocol handler")]
|
||||
struct ProtocolHandlerArgs {
|
||||
#[webidl(required)]
|
||||
scheme: String,
|
||||
#[webidl(required, converter = "usv_string")]
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub(super) fn install<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_, ()>,
|
||||
prototype: v8::Local<'s, v8::ObjectTemplate>,
|
||||
) {
|
||||
NavigatorProtocolHandlersDeclaration::initialize_prototype_template(scope, prototype);
|
||||
}
|
||||
|
||||
fn allowed_scheme(scheme: &str) -> bool {
|
||||
matches!(
|
||||
scheme,
|
||||
"bitcoin"
|
||||
| "ftp"
|
||||
| "ftps"
|
||||
| "geo"
|
||||
| "im"
|
||||
| "irc"
|
||||
| "ircs"
|
||||
| "magnet"
|
||||
| "mailto"
|
||||
| "matrix"
|
||||
| "mms"
|
||||
| "news"
|
||||
| "nntp"
|
||||
| "openpgp4fpr"
|
||||
| "sftp"
|
||||
| "sip"
|
||||
| "sms"
|
||||
| "smsto"
|
||||
| "ssh"
|
||||
| "tel"
|
||||
| "urn"
|
||||
| "webcal"
|
||||
| "wtai"
|
||||
| "xmpp"
|
||||
) || scheme.strip_prefix("web+").is_some_and(|suffix| {
|
||||
!suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_lowercase())
|
||||
})
|
||||
}
|
||||
|
||||
fn protocol_handler_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
// The generated binding checks the native Navigator brand before conversion.
|
||||
let Some(parsed) = webidl::parse_args::<ProtocolHandlerArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if !allowed_scheme(&parsed.scheme.to_ascii_lowercase()) {
|
||||
webidl::throw_dom_exception(scope, "SecurityError", "The protocol cannot be registered.");
|
||||
return;
|
||||
}
|
||||
if !parsed.url.contains("%s") {
|
||||
webidl::throw_dom_exception(scope, "SyntaxError", "The handler URL must contain %s.");
|
||||
return;
|
||||
}
|
||||
let Some(document) = get_private_value(scope, args.this(), DOCUMENT_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
else {
|
||||
webidl::throw_dom_exception(
|
||||
scope,
|
||||
"SecurityError",
|
||||
"The Navigator has no associated Document.",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Ok((host_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, document)
|
||||
else {
|
||||
webidl::throw_dom_exception(
|
||||
scope,
|
||||
"SecurityError",
|
||||
"The Navigator Document is unavailable.",
|
||||
);
|
||||
return;
|
||||
};
|
||||
// Retain the receiver's Document and origin, including when its browsing
|
||||
// context navigates. Public baseURI/origin getters and the callee realm do
|
||||
// not supply the relevant environment settings.
|
||||
let host = unsafe { &*host_ptr };
|
||||
let base = host.document_base_url_for_handle(handle);
|
||||
let Ok(url) = parse_url_with_document_query_encoding(host, handle, &base, &parsed.url) else {
|
||||
webidl::throw_dom_exception(scope, "SyntaxError", "The handler URL cannot be parsed.");
|
||||
return;
|
||||
};
|
||||
let origin = get_private_value(scope, args.this(), ORIGIN_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::String>::try_from(value).ok())
|
||||
.map(|value| value.to_rust_string_lossy(scope));
|
||||
if !matches!(url.scheme(), "http" | "https")
|
||||
|| origin.as_deref() != Some(url.origin().ascii_serialization().as_str())
|
||||
{
|
||||
webidl::throw_dom_exception(
|
||||
scope,
|
||||
"SecurityError",
|
||||
"The handler URL must have the Navigator's origin.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// HTML permits the user agent to cancel a registration request. With no
|
||||
// registration UI, there are no accepted handlers for unregister to remove.
|
||||
rv.set_undefined();
|
||||
}
|
||||
@@ -686,15 +686,14 @@ pub(super) use tree_mutation::{
|
||||
node_insert_adjacent_element_callback, node_insert_adjacent_html_callback,
|
||||
node_insert_adjacent_node_callback, node_insert_adjacent_text_callback,
|
||||
};
|
||||
pub(in crate::native_bridge) use url_attributes::iframe_uses_detached_content_cache;
|
||||
pub(crate) use url_attributes::parse_url_with_document_query_encoding;
|
||||
use url_attributes::{
|
||||
disconnected_iframe_can_materialize_detached_content, iframe_has_inactive_child_context,
|
||||
iframe_is_in_own_child_document, iframe_is_inside_its_own_child_context_document,
|
||||
parsed_url_like_attribute, resolve_url_like_attribute, set_resolved_url_attribute,
|
||||
should_block_dangling_markup_subresource,
|
||||
};
|
||||
pub(in crate::native_bridge) use url_attributes::{
|
||||
iframe_uses_detached_content_cache, parse_url_with_document_query_encoding,
|
||||
};
|
||||
pub(super) use url_attributes::{
|
||||
live_frame_owner_content_window_for_handle, update_iframe_snapshot_navigation,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod helpers;
|
||||
mod iframe;
|
||||
|
||||
pub(in crate::native_bridge) use self::helpers::parse_url_with_document_query_encoding;
|
||||
pub(crate) use self::helpers::parse_url_with_document_query_encoding;
|
||||
pub(super) use self::helpers::{
|
||||
parsed_url_like_attribute, resolve_url_like_attribute, set_resolved_url_attribute,
|
||||
should_block_dangling_markup_subresource,
|
||||
|
||||
@@ -61,7 +61,7 @@ pub(in crate::native_bridge::element) fn should_block_dangling_markup_subresourc
|
||||
.any(|byte| matches!(byte, b'\r' | b'\n' | b'\t'))
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge) fn parse_url_with_document_query_encoding(
|
||||
pub(crate) fn parse_url_with_document_query_encoding(
|
||||
runtime: &JsContextHost,
|
||||
handle: DomHandle,
|
||||
base: &Url,
|
||||
|
||||
@@ -67,6 +67,7 @@ mod popup_document_stream;
|
||||
mod popup_hyperlinks;
|
||||
mod popup_live_document_stream;
|
||||
mod promise_rejection;
|
||||
mod protocol_handlers;
|
||||
mod rtc_error;
|
||||
mod security_policy;
|
||||
mod security_policy_event_init;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
(() => {
|
||||
const rows = [], errors = [];
|
||||
const record = (label, checks, observed = null) => rows.push({label, checks, observed});
|
||||
const names = ['registerProtocolHandler', 'unregisterProtocolHandler'];
|
||||
try {
|
||||
const child = document.getElementById('child').contentWindow;
|
||||
const realms = [window, child];
|
||||
const cases = [
|
||||
['relative', ['web+sample', '%s'], null],
|
||||
['case-fold', ['WeB+SaMpLe', './path/%s'], null],
|
||||
['safe-case-fold', ['MAILTO', '?url=%s'], null],
|
||||
['surrogate-url', ['web+sample', 'path\uD800?url=%s'], null],
|
||||
['two-placeholders', ['tel', '%s/foo/%s'], null],
|
||||
['credentials', ['tel', location.origin.replace('://', '://user:pass@') + '/%s'], null],
|
||||
['unknown-scheme', ['http', '%s'], 'SecurityError'],
|
||||
['scheme-colon', ['mailto:', '%s'], 'SecurityError'],
|
||||
['empty-custom', ['web+', '%s'], 'SecurityError'],
|
||||
['custom-digit', ['web+abc1', '%s'], 'SecurityError'],
|
||||
['unicode-case', ['web+\u212Aelvin', '%s'], 'SecurityError'],
|
||||
['custom-surrogate', ['web+\uD800', '%s'], 'SecurityError'],
|
||||
['null-scheme', [null, '%s'], 'SecurityError'],
|
||||
['missing-token', ['tel', 'path'], 'SyntaxError'],
|
||||
['upper-token', ['tel', '%S'], 'SyntaxError'],
|
||||
['null-url', ['tel', null], 'SyntaxError'],
|
||||
['undefined-url', ['tel', undefined], 'SyntaxError'],
|
||||
['host-token', ['tel', 'https://%s.invalid/'], 'SyntaxError'],
|
||||
['port-token', ['tel', 'https://example.test:%s/'], 'SyntaxError'],
|
||||
['other-origin', ['tel', 'https://elsewhere.test/%s'], 'SecurityError'],
|
||||
['non-http-url', ['tel', 'mailto:%s'], 'SecurityError'],
|
||||
['scheme-before-url-syntax', ['http', 'https://test:test/'], 'SecurityError'],
|
||||
['scheme-before-url-origin', ['http', 'https://elsewhere.test/%s'], 'SecurityError'],
|
||||
['scheme-symbol', [Symbol('scheme'), '%s'], 'TypeError'],
|
||||
['url-symbol-before-scheme-validation', ['http', Symbol('url')], 'TypeError'],
|
||||
['missing-arguments', [], 'TypeError'],
|
||||
['missing-url', ['tel'], 'TypeError'],
|
||||
];
|
||||
function invoke(fn, receiver, args, realm, expected) {
|
||||
if (typeof fn !== 'function') return {checks: {present: false, result: false, realm: false}, observed: 'missing'};
|
||||
try {
|
||||
const result = fn.call(receiver, ...args);
|
||||
return {checks: {present: true, result: expected === null && result === undefined, realm: expected === null}, observed: 'returned'};
|
||||
} catch (error) {
|
||||
return {checks: {present: true, result: error.name === expected,
|
||||
realm: expected === 'TypeError' ? error instanceof realm.TypeError : error instanceof realm.DOMException}, observed: error.name};
|
||||
}
|
||||
}
|
||||
for (const [r, realm] of realms.entries()) {
|
||||
for (const name of names) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(realm.Navigator.prototype, name);
|
||||
record(`${r}/${name}/exposure`, realm.isSecureContext ? {
|
||||
present: !!descriptor, ownPrototype: typeof descriptor?.value === 'function',
|
||||
metadata: descriptor?.value.name === name && descriptor?.value.length === 2,
|
||||
flags: descriptor?.writable === true && descriptor?.enumerable === true && descriptor?.configurable === true,
|
||||
inherited: !Object.hasOwn(realm.navigator, name),
|
||||
} : {absentPrototype: !descriptor, absentInstance: !(name in realm.navigator)});
|
||||
}
|
||||
}
|
||||
if (isSecureContext) {
|
||||
for (const [f, realm] of realms.entries()) for (const [r, receiverRealm] of realms.entries()) {
|
||||
for (const name of names) for (const [label, args, expected] of cases) {
|
||||
const result = invoke(realm.navigator[name], receiverRealm.navigator, args, realm, expected);
|
||||
record(`${f}/${r}/${name}/${label}`, result.checks, result.observed);
|
||||
}
|
||||
}
|
||||
for (const [f, realm] of realms.entries()) for (const name of names) {
|
||||
const fn = realm.navigator[name];
|
||||
for (const [r, real] of realms.entries()) {
|
||||
const revoked = Proxy.revocable(real.navigator, {}); revoked.revoke();
|
||||
const receivers = [null, undefined, {}, real.Navigator.prototype, Object.create(real.navigator), new Proxy(real.navigator, {}), revoked.proxy];
|
||||
for (const [i, receiver] of receivers.entries()) {
|
||||
let conversions = 0, traps = 0;
|
||||
const tracked = i === 5 ? new Proxy(real.navigator, {get() {traps++; throw new Error('author trap');}}) : receiver;
|
||||
const value = {toString() {conversions++; return '%s';}};
|
||||
const result = invoke(fn, tracked, [value, value], realm, 'TypeError');
|
||||
record(`${f}/${r}/${name}/receiver-${i}`, {...result.checks, noConversions: conversions === 0, noTraps: traps === 0}, result.observed);
|
||||
}
|
||||
}
|
||||
for (const behavior of ['scheme-throws', 'url-throws', 'invalid-scheme-converts-url', 'missing-url-no-conversion', 'extra-argument-ignored']) {
|
||||
const order = [], sentinel = {};
|
||||
const scheme = {toString() {order.push('scheme'); if (behavior === 'scheme-throws') throw sentinel; return behavior === 'invalid-scheme-converts-url' ? 'http' : 'tel';}};
|
||||
const url = {toString() {order.push('url'); if (behavior === 'url-throws') throw sentinel; return '%s';}};
|
||||
const ignored = {toString() {order.push('extra'); throw sentinel;}};
|
||||
const args = behavior === 'missing-url-no-conversion' ? [scheme] : [scheme, url, ignored];
|
||||
let caught, returned = false;
|
||||
if (typeof fn === 'function') {try {returned = fn.call(realm.navigator, ...args) === undefined;} catch (error) {caught = error;}}
|
||||
const expectedOrder = behavior === 'scheme-throws' ? ['scheme'] : behavior === 'missing-url-no-conversion' ? [] : ['scheme', 'url'];
|
||||
const result = behavior.endsWith('-throws') ? caught === sentinel : behavior === 'missing-url-no-conversion' ? caught instanceof realm.TypeError : behavior === 'invalid-scheme-converts-url' ? caught?.name === 'SecurityError' && caught instanceof realm.DOMException : returned;
|
||||
record(`${f}/${name}/${behavior}`, {present: typeof fn === 'function', result, order: JSON.stringify(order) === JSON.stringify(expectedOrder)}, order);
|
||||
}
|
||||
}
|
||||
// A foreign base belongs to the receiver Document, independently of the callee realm.
|
||||
const base = child.document.createElement('base'); base.href = 'https://elsewhere.test/'; child.document.head.appendChild(base);
|
||||
const descriptor = Object.getOwnPropertyDescriptor(child.document, 'baseURI');
|
||||
Object.defineProperty(child.document, 'baseURI', {configurable: true, get() {throw new Error('public baseURI');}});
|
||||
try {
|
||||
for (const [f, realm] of realms.entries()) for (const [r, receiverRealm] of realms.entries()) for (const name of names) {
|
||||
const result = invoke(realm.navigator[name], receiverRealm.navigator, ['web+sample', '%s'], realm, r ? 'SecurityError' : null);
|
||||
record(`${f}/${r}/${name}/receiver-base`, result.checks, result.observed);
|
||||
}
|
||||
} finally {
|
||||
base.remove(); if (descriptor) Object.defineProperty(child.document, 'baseURI', descriptor); else delete child.document.baseURI;
|
||||
}
|
||||
}
|
||||
} catch (error) {errors.push(String(error.stack || error));}
|
||||
globalThis.__uiEventResults = {rows, errors};
|
||||
return errors.length === 0 && rows.every(row => Object.values(row.checks).every(value => value === true));
|
||||
})()
|
||||
@@ -0,0 +1,74 @@
|
||||
use super::service_worker_drain::drain_service_worker_test_turn;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn protocol_handlers_validate_parameters_and_the_receivers_document() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://protocol-handlers.test/",
|
||||
&loader,
|
||||
);
|
||||
vm.eval(
|
||||
r#"
|
||||
if (!document.documentElement) document.appendChild(document.createElement('html'));
|
||||
if (!document.body) document.documentElement.appendChild(document.createElement('body'));
|
||||
const frame = document.body.appendChild(document.createElement('iframe'));
|
||||
frame.id = 'child'; frame.srcdoc = '<head></head><body></body>'; void frame.contentWindow;
|
||||
'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
vm.run_one_child_frame_task_executor_turn(
|
||||
ChildFrameSemanticTurnKind::RealmMaterialization,
|
||||
&loader
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(include_str!("protocol_handlers.js")).unwrap(), "true", "{}",
|
||||
vm.eval("JSON.stringify({errors:__uiEventResults.errors,failures:__uiEventResults.rows.filter(row=>Object.values(row.checks).some(value=>value!==true))})").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_handlers_are_absent_in_insecure_contexts() {
|
||||
let mut vm = new_storage_test_vm("http://protocol-handlers.test/");
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
!isSecureContext &&
|
||||
['registerProtocolHandler', 'unregisterProtocolHandler'].every(name =>
|
||||
!(name in navigator) && !Object.hasOwn(Navigator.prototype, name))
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn protocol_handlers_are_absent_from_worker_navigators() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let (mut vm, browser_context_runtime) =
|
||||
new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
|
||||
"https://protocol-worker.test/",
|
||||
&loader,
|
||||
);
|
||||
let source = include_str!("protocol_worker.js");
|
||||
vm.eval(&format!("({source}).then(value => globalThis.__protocolWorker = value, error => globalThis.__protocolWorker = String(error));")).unwrap();
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
while vm
|
||||
.eval("globalThis.__protocolWorker !== undefined")
|
||||
.unwrap()
|
||||
!= "true"
|
||||
{
|
||||
drain_service_worker_test_turn(&mut vm, &browser_context_runtime, &loader).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("protocol worker probe should settle");
|
||||
assert_eq!(vm.eval("globalThis.__protocolWorker").unwrap(), "true");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
(async () => {
|
||||
const rows = [], errors = [];
|
||||
const source = `postMessage({secure: isSecureContext,
|
||||
register: 'registerProtocolHandler' in navigator,
|
||||
unregister: 'unregisterProtocolHandler' in navigator,
|
||||
registerPrototype: Object.hasOwn(WorkerNavigator.prototype, 'registerProtocolHandler'),
|
||||
unregisterPrototype: Object.hasOwn(WorkerNavigator.prototype, 'unregisterProtocolHandler')});`;
|
||||
const url = URL.createObjectURL(new Blob([source], {type: 'text/javascript'}));
|
||||
const worker = new Worker(url);
|
||||
try {
|
||||
const facts = await new Promise((resolve, reject) => {
|
||||
worker.onmessage = event => resolve(event.data);
|
||||
worker.onerror = event => reject(new Error(event.message));
|
||||
});
|
||||
rows.push({label: 'worker-navigator', checks: {
|
||||
absent: !facts.register && !facts.unregister,
|
||||
absentPrototype: !facts.registerPrototype && !facts.unregisterPrototype,
|
||||
}, observed: facts});
|
||||
} catch (error) {errors.push(String(error.stack || error));}
|
||||
finally {worker.terminate(); URL.revokeObjectURL(url);}
|
||||
globalThis.__uiEventResults = {rows, errors};
|
||||
return errors.length === 0 && rows.every(row => Object.values(row.checks).every(value => value === true));
|
||||
})()
|
||||
Reference in New Issue
Block a user