mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 08:00:56 +00:00
fix(service-worker): use shared WebIDL promise bindings
This commit is contained in:
@@ -416,13 +416,34 @@ struct ServiceWorkerContainerDeclaration {
|
||||
#[webapi(accessor_property = "controller", enumerable, getter = navigator_service_worker_controller_getter_callback)]
|
||||
controller: (),
|
||||
|
||||
#[webapi(method, enumerable, callback = navigator_service_worker_register_callback, length = 1)]
|
||||
#[webapi(
|
||||
method,
|
||||
enumerable,
|
||||
callback = navigator_service_worker_register_callback,
|
||||
receiver = web_api_interfaces::ServiceWorkerContainer::is_instance,
|
||||
returns_promise,
|
||||
length = 1
|
||||
)]
|
||||
register: (),
|
||||
|
||||
#[webapi(method, enumerable, callback = navigator_service_worker_get_registration_callback, length = 1)]
|
||||
#[webapi(
|
||||
method,
|
||||
enumerable,
|
||||
callback = navigator_service_worker_get_registration_callback,
|
||||
receiver = web_api_interfaces::ServiceWorkerContainer::is_instance,
|
||||
returns_promise,
|
||||
length = 0
|
||||
)]
|
||||
get_registration: (),
|
||||
|
||||
#[webapi(method, enumerable, callback = navigator_service_worker_get_registrations_callback, length = 0)]
|
||||
#[webapi(
|
||||
method,
|
||||
enumerable,
|
||||
callback = navigator_service_worker_get_registrations_callback,
|
||||
receiver = web_api_interfaces::ServiceWorkerContainer::is_instance,
|
||||
returns_promise,
|
||||
length = 0
|
||||
)]
|
||||
get_registrations: (),
|
||||
|
||||
#[webapi(method, enumerable, callback = simple_event_target_add_event_listener_callback)]
|
||||
|
||||
@@ -331,6 +331,44 @@ struct BackgroundSyncOptions {
|
||||
min_interval: u64,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "RegistrationOptions")]
|
||||
struct ServiceWorkerRegistrationOptions {
|
||||
#[webidl(name = "scope", converter = "usv_string")]
|
||||
requested_scope: Option<String>,
|
||||
|
||||
#[webidl(
|
||||
name = "type",
|
||||
converter = "enum",
|
||||
default = WorkerScriptKind::Classic
|
||||
)]
|
||||
script_kind: WorkerScriptKind,
|
||||
|
||||
#[webidl(
|
||||
name = "updateViaCache",
|
||||
converter = "enum",
|
||||
default = ServiceWorkerUpdateViaCache::Imports
|
||||
)]
|
||||
update_via_cache: ServiceWorkerUpdateViaCache,
|
||||
}
|
||||
|
||||
impl Default for ServiceWorkerRegistrationOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
requested_scope: None,
|
||||
script_kind: WorkerScriptKind::Classic,
|
||||
update_via_cache: ServiceWorkerUpdateViaCache::Imports,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "ServiceWorkerContainer.getRegistration")]
|
||||
struct ServiceWorkerGetRegistrationArgs {
|
||||
#[webidl(name = "clientURL", converter = "usv_string", default = "")]
|
||||
client_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ServiceWorkerRegistrationPhase<'a> {
|
||||
Snapshot(&'a crate::service_worker_runtime::ServiceWorkerRegistrationSnapshot),
|
||||
@@ -341,6 +379,30 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
if args.length() == 0 {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"Failed to execute 'register' on 'ServiceWorkerContainer': 1 argument required, but only 0 present.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Convert the union argument before the options dictionary. The Trusted
|
||||
// Types sink algorithm runs only after all WebIDL argument conversion.
|
||||
let script_argument = args.get(0);
|
||||
let script_argument = match v8::Local::<v8::Object>::try_from(script_argument) {
|
||||
Ok(object) if web_api_interfaces::TrustedScriptURL::is_instance(scope, object) => {
|
||||
script_argument
|
||||
}
|
||||
_ => {
|
||||
let Some(script) = script_argument.to_string(scope) else {
|
||||
return;
|
||||
};
|
||||
script.into()
|
||||
}
|
||||
};
|
||||
let Some(options) = service_worker_registration_options(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
return;
|
||||
@@ -350,13 +412,6 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
rv.set(promise.into());
|
||||
return;
|
||||
};
|
||||
if args.length() == 0 {
|
||||
throw_type_error(
|
||||
scope,
|
||||
"Failed to execute 'register' on 'ServiceWorkerContainer': 1 argument required, but only 0 present.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let owner = service_worker_container_owner_scope(scope, args.this());
|
||||
let Some(request_context) =
|
||||
(unsafe { &mut *host_ptr }).service_worker_window_request_context(owner)
|
||||
@@ -390,7 +445,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
};
|
||||
let Some(script) = crate::context_bootstrap::trusted_script_url_string_or_throw(
|
||||
scope,
|
||||
args.get(0),
|
||||
script_argument,
|
||||
requirements,
|
||||
"ServiceWorkerContainer register",
|
||||
"register",
|
||||
@@ -404,7 +459,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
let Some(script_url) =
|
||||
resolve_service_worker_script_url(request_context.document_url(), &script)
|
||||
else {
|
||||
reject_service_worker_promise(
|
||||
reject_service_worker_promise_with_type_error(
|
||||
scope,
|
||||
resolver,
|
||||
"failed to resolve service worker script URL",
|
||||
@@ -412,24 +467,22 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
rv.set(promise.into());
|
||||
return;
|
||||
};
|
||||
let script_kind = match service_worker_script_kind(scope, &args) {
|
||||
Ok(script_kind) => script_kind,
|
||||
Err(message) => {
|
||||
reject_service_worker_promise(scope, resolver, message);
|
||||
let scope_url = match service_worker_scope_url(
|
||||
request_context.document_url(),
|
||||
&script_url,
|
||||
options.requested_scope.as_deref(),
|
||||
) {
|
||||
Some(scope_url) => scope_url,
|
||||
None => {
|
||||
reject_service_worker_promise_with_type_error(
|
||||
scope,
|
||||
resolver,
|
||||
"failed to resolve service worker scope URL",
|
||||
);
|
||||
rv.set(promise.into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let update_via_cache = match service_worker_update_via_cache(scope, &args) {
|
||||
Ok(update_via_cache) => update_via_cache,
|
||||
Err(message) => {
|
||||
reject_service_worker_promise(scope, resolver, message);
|
||||
rv.set(promise.into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let scope_url =
|
||||
service_worker_scope_url(scope, request_context.document_url(), &script_url, &args);
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
let Some(request_client) = host
|
||||
.document_resource_loader_for_window_owner(request_context.owner().window_document_owner())
|
||||
@@ -448,8 +501,8 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
host.start_service_worker_runtime(
|
||||
script_url,
|
||||
scope_url,
|
||||
script_kind,
|
||||
update_via_cache,
|
||||
options.script_kind,
|
||||
options.update_via_cache,
|
||||
&request_context,
|
||||
request_client,
|
||||
request_id,
|
||||
@@ -1068,6 +1121,9 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_get_registration_ca
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<ServiceWorkerGetRegistrationArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
return;
|
||||
};
|
||||
@@ -1079,11 +1135,10 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_get_registration_ca
|
||||
let state = host
|
||||
.service_worker_window_request_context(owner)
|
||||
.and_then(|request_context| {
|
||||
service_worker_client_url(scope, request_context.document_url(), &args).and_then(
|
||||
|client_url| {
|
||||
service_worker_client_url(request_context.document_url(), &parsed.client_url)
|
||||
.and_then(|client_url| {
|
||||
host.service_worker_registration_for_client(&request_context, &client_url)
|
||||
},
|
||||
)
|
||||
})
|
||||
});
|
||||
if let Some(state) = state {
|
||||
build_service_worker_registration_object_for_container(
|
||||
@@ -2085,98 +2140,46 @@ fn resolve_service_worker_script_url(document_url: &url::Url, script: &str) -> O
|
||||
Some(script_url)
|
||||
}
|
||||
|
||||
fn service_worker_registration_options<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
) -> Option<ServiceWorkerRegistrationOptions> {
|
||||
let context = webidl::Context::argument("ServiceWorkerContainer.register", 2);
|
||||
match webidl::dictionary_arg(args, 1, context) {
|
||||
Ok(Some(options)) => {
|
||||
match webidl::parse_dictionary_object::<ServiceWorkerRegistrationOptions>(
|
||||
scope, options,
|
||||
) {
|
||||
Ok(options) => Some(options),
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => Some(ServiceWorkerRegistrationOptions::default()),
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn service_worker_scope_url(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
document_url: &url::Url,
|
||||
script_url: &url::Url,
|
||||
args: &v8::FunctionCallbackArguments<'_>,
|
||||
) -> url::Url {
|
||||
if args.length() > 1
|
||||
&& let Ok(options) = v8::Local::<v8::Object>::try_from(args.get(1))
|
||||
&& let Some(scope_value) = options.get(scope, v8str(scope, "scope").into())
|
||||
&& !scope_value.is_null_or_undefined()
|
||||
&& let Some(scope_string) = scope_value.to_string(scope)
|
||||
&& let Ok(mut scope_url) = document_url.join(&scope_string.to_rust_string_lossy(scope))
|
||||
{
|
||||
scope_url.set_fragment(None);
|
||||
return scope_url;
|
||||
}
|
||||
default_service_worker_scope_url(script_url)
|
||||
}
|
||||
|
||||
fn service_worker_script_kind(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'_>,
|
||||
) -> Result<WorkerScriptKind, &'static str> {
|
||||
if args.length() <= 1 {
|
||||
return Ok(WorkerScriptKind::Classic);
|
||||
}
|
||||
let value = args.get(1);
|
||||
if value.is_null_or_undefined() {
|
||||
return Ok(WorkerScriptKind::Classic);
|
||||
}
|
||||
let Ok(options) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
return Ok(WorkerScriptKind::Classic);
|
||||
};
|
||||
let Some(type_value) = options.get(scope, v8str(scope, "type").into()) else {
|
||||
return Ok(WorkerScriptKind::Classic);
|
||||
};
|
||||
if type_value.is_null_or_undefined() {
|
||||
return Ok(WorkerScriptKind::Classic);
|
||||
}
|
||||
let Some(type_string) = type_value.to_string(scope) else {
|
||||
return Err("failed to parse service worker type");
|
||||
};
|
||||
match type_string.to_rust_string_lossy(scope).as_str() {
|
||||
"classic" => Ok(WorkerScriptKind::Classic),
|
||||
"module" => Ok(WorkerScriptKind::Module),
|
||||
_ => Err("invalid service worker type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn service_worker_update_via_cache(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'_>,
|
||||
) -> Result<ServiceWorkerUpdateViaCache, &'static str> {
|
||||
if args.length() <= 1 {
|
||||
return Ok(ServiceWorkerUpdateViaCache::default());
|
||||
}
|
||||
let value = args.get(1);
|
||||
if value.is_null_or_undefined() {
|
||||
return Ok(ServiceWorkerUpdateViaCache::default());
|
||||
}
|
||||
let Ok(options) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
return Ok(ServiceWorkerUpdateViaCache::default());
|
||||
};
|
||||
let Some(update_via_cache_value) = options.get(scope, v8str(scope, "updateViaCache").into())
|
||||
else {
|
||||
return Ok(ServiceWorkerUpdateViaCache::default());
|
||||
};
|
||||
if update_via_cache_value.is_null_or_undefined() {
|
||||
return Ok(ServiceWorkerUpdateViaCache::default());
|
||||
}
|
||||
let Some(update_via_cache_string) = update_via_cache_value.to_string(scope) else {
|
||||
return Err("failed to parse service worker updateViaCache");
|
||||
};
|
||||
ServiceWorkerUpdateViaCache::parse_webidl_token(
|
||||
&update_via_cache_string.to_rust_string_lossy(scope),
|
||||
)
|
||||
.ok_or("invalid service worker updateViaCache")
|
||||
}
|
||||
|
||||
fn service_worker_client_url(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
document_url: &url::Url,
|
||||
args: &v8::FunctionCallbackArguments<'_>,
|
||||
requested_scope: Option<&str>,
|
||||
) -> Option<url::Url> {
|
||||
let client = if args.length() > 0 && !args.get(0).is_null_or_undefined() {
|
||||
args.get(0)
|
||||
.to_string(scope)
|
||||
.map(|value| value.to_rust_string_lossy(scope))?
|
||||
} else {
|
||||
String::new()
|
||||
let Some(requested_scope) = requested_scope else {
|
||||
return Some(default_service_worker_scope_url(script_url));
|
||||
};
|
||||
let mut client_url = document_url.join(&client).ok()?;
|
||||
let mut scope_url = document_url.join(requested_scope).ok()?;
|
||||
scope_url.set_fragment(None);
|
||||
Some(scope_url)
|
||||
}
|
||||
|
||||
fn service_worker_client_url(document_url: &url::Url, client: &str) -> Option<url::Url> {
|
||||
let mut client_url = document_url.join(client).ok()?;
|
||||
client_url.set_fragment(None);
|
||||
Some(client_url)
|
||||
}
|
||||
|
||||
@@ -12032,7 +12032,7 @@ async fn navigator_service_worker_shim_controls_window_fetch() {
|
||||
let expected_fetch_url = format!("{base_url}/app/api/data.txt");
|
||||
let expected_out_of_scope_url = format!("{base_url}/outside/data.txt");
|
||||
let expected_result = format!(
|
||||
r#"{{"containerType":"object","controllerIsNull":true,"readyType":"object","serviceWorkerConstructorType":"function","registrationConstructorType":"function","registerType":"function","registerName":"register","registerLength":1,"getRegistrationType":"function","getRegistrationName":"getRegistration","getRegistrationLength":1,"getRegistrationsType":"function","getRegistrationsName":"getRegistrations","getRegistrationsLength":0,"removeEventListenerType":"function","controllerChangeHandlerType":"object","controllerChangeLog":["listener:controllerchange:true","handler:controllerchange:true"],"registration":true,"registrations":"true|0","afterRegisterController":true,"controllerStable":true,"controllerMatchesActive":true,"controllerBrand":true,"registrationBrand":true,"registrationTag":"[object ServiceWorkerRegistration]","installingBrand":false,"installingScriptURL":null,"installingState":null,"waitingIsNull":true,"activeIsNull":false,"activeBrand":true,"activeScriptURL":"{expected_worker_url}","activeState":"activated","registrationScope":"{expected_scope}","readyMatchesRegister":true,"responseStatusText":"Handled by worker","responseText":"sw:/app/api/data.txt","dataResponseStatusText":"OK","dataResponseContentType":"text/plain","dataResponseText":"data-url","outOfScopeResult":"resolved:Handled by worker:sw:/outside/data.txt","firstUnregister":true,"secondUnregister":false,"controllerRetained":true,"controllerStateAfterUnregister":"activated","registrationRemoved":true,"afterUnregisterText":"sw:/app/after-unregister.txt"}}"#
|
||||
r#"{{"containerType":"object","controllerIsNull":true,"readyType":"object","serviceWorkerConstructorType":"function","registrationConstructorType":"function","registerType":"function","registerName":"register","registerLength":1,"getRegistrationType":"function","getRegistrationName":"getRegistration","getRegistrationLength":0,"getRegistrationsType":"function","getRegistrationsName":"getRegistrations","getRegistrationsLength":0,"removeEventListenerType":"function","controllerChangeHandlerType":"object","controllerChangeLog":["listener:controllerchange:true","handler:controllerchange:true"],"registration":true,"registrations":"true|0","afterRegisterController":true,"controllerStable":true,"controllerMatchesActive":true,"controllerBrand":true,"registrationBrand":true,"registrationTag":"[object ServiceWorkerRegistration]","installingBrand":false,"installingScriptURL":null,"installingState":null,"waitingIsNull":true,"activeIsNull":false,"activeBrand":true,"activeScriptURL":"{expected_worker_url}","activeState":"activated","registrationScope":"{expected_scope}","readyMatchesRegister":true,"responseStatusText":"Handled by worker","responseText":"sw:/app/api/data.txt","dataResponseStatusText":"OK","dataResponseContentType":"text/plain","dataResponseText":"data-url","outOfScopeResult":"resolved:Handled by worker:sw:/outside/data.txt","firstUnregister":true,"secondUnregister":false,"controllerRetained":true,"controllerStateAfterUnregister":"activated","registrationRemoved":true,"afterUnregisterText":"sw:/app/after-unregister.txt"}}"#
|
||||
);
|
||||
assert_eq!(result, expected_result);
|
||||
|
||||
@@ -17381,7 +17381,7 @@ async fn navigator_service_worker_update_via_cache_option_reflects_registration(
|
||||
&browser_context_runtime,
|
||||
&loader,
|
||||
"String(globalThis.__serviceWorkerUpdateViaCacheProbe)",
|
||||
"all|all|Error",
|
||||
"all|all|TypeError",
|
||||
)
|
||||
.await;
|
||||
server
|
||||
@@ -22466,6 +22466,60 @@ async fn navigator_service_worker_registry_queries_are_scope_based() {
|
||||
.expect("service worker registry script server should finish");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigator_service_worker_argument_errors_reject_with_the_original_exception() {
|
||||
let mut vm = new_storage_test_vm("https://service-worker-webidl.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const sw = navigator.serviceWorker;
|
||||
const marker = {sentinel: true};
|
||||
globalThis.serviceWorkerArgumentFailures = [];
|
||||
globalThis.serviceWorkerArgumentRejections = 0;
|
||||
function check(callback, expected) {
|
||||
const promise = callback();
|
||||
if (!(promise instanceof Promise)) serviceWorkerArgumentFailures.push('not a Promise');
|
||||
promise.then(
|
||||
() => serviceWorkerArgumentFailures.push('resolved'),
|
||||
error => {
|
||||
serviceWorkerArgumentRejections++;
|
||||
if (expected === marker ? error !== marker : !(error instanceof TypeError)) {
|
||||
serviceWorkerArgumentFailures.push('wrong rejection reason');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
check(() => sw.register(), TypeError);
|
||||
check(() => sw.register(Symbol('script')), TypeError);
|
||||
check(() => sw.register('https://[', {scope: Symbol('scope')}), TypeError);
|
||||
check(() => sw.getRegistration({toString() { throw marker; }}), marker);
|
||||
const steps = ['script', 'scope-get', 'scope-string', 'type-get', 'type-string', 'cache-get', 'cache-string'];
|
||||
for (let index = 0; index < steps.length; index++) {
|
||||
const log = [];
|
||||
const step = name => { log.push(name); if (name === steps[index]) throw marker; };
|
||||
check(() => sw.register(
|
||||
{toString() { step('script'); return 'https://['; }},
|
||||
{
|
||||
get scope() { step('scope-get'); return {toString() { step('scope-string'); return './'; }}; },
|
||||
get type() { step('type-get'); return {toString() { step('type-string'); return 'classic'; }}; },
|
||||
get updateViaCache() { step('cache-get'); return {toString() { step('cache-string'); return 'imports'; }}; }
|
||||
}
|
||||
), marker);
|
||||
if (JSON.stringify(log) !== JSON.stringify(steps.slice(0, index + 1))) {
|
||||
serviceWorkerArgumentFailures.push('conversion order: ' + log);
|
||||
}
|
||||
}
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("ServiceWorkerContainer conversion errors should return Promises");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify([serviceWorkerArgumentRejections, serviceWorkerArgumentFailures])")
|
||||
.unwrap(),
|
||||
"[11,[]]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_event_source_constructor_exposes_connecting_instance() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/");
|
||||
|
||||
@@ -1134,27 +1134,19 @@ fn service_worker_register_gates_script_url_before_url_resolution() {
|
||||
let mut vm = new_storage_test_vm("https://service-worker-register-trusted-types.test/");
|
||||
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const errorName = callback => {
|
||||
try {
|
||||
callback();
|
||||
return "none";
|
||||
} catch (error) {
|
||||
return error && error.name;
|
||||
}
|
||||
};
|
||||
const errorName = promise => promise.then(() => 'resolved', error => error && error.name);
|
||||
const policy = trustedTypes.createPolicy("service-worker-register", {
|
||||
createHTML: value => value,
|
||||
createScriptURL: value => value
|
||||
});
|
||||
const blockedString = errorName(() => navigator.serviceWorker.register("worker.js"));
|
||||
const blockedWrongType = errorName(() =>
|
||||
const blockedString = errorName(navigator.serviceWorker.register("worker.js"));
|
||||
const blockedWrongType = errorName(
|
||||
navigator.serviceWorker.register(policy.createHTML("worker.js"))
|
||||
);
|
||||
const missing = errorName(() => navigator.serviceWorker.register());
|
||||
const missing = errorName(navigator.serviceWorker.register());
|
||||
|
||||
const trustedPromise = navigator.serviceWorker.register(
|
||||
policy.createScriptURL("http://[")
|
||||
@@ -1171,19 +1163,24 @@ fn service_worker_register_gates_script_url_before_url_resolution() {
|
||||
const defaultPromise = navigator.serviceWorker.register("worker.potato");
|
||||
defaultPromise.catch(() => {});
|
||||
|
||||
return JSON.stringify({
|
||||
blockedString,
|
||||
blockedWrongType,
|
||||
missing,
|
||||
trustedPromise: trustedPromise instanceof Promise,
|
||||
defaultPromise: defaultPromise instanceof Promise,
|
||||
defaultCalls
|
||||
Promise.all([blockedString, blockedWrongType, missing]).then(([blockedString, blockedWrongType, missing]) => {
|
||||
globalThis.serviceWorkerTrustedUrlResult = {
|
||||
blockedString,
|
||||
blockedWrongType,
|
||||
missing,
|
||||
trustedPromise: trustedPromise instanceof Promise,
|
||||
defaultPromise: defaultPromise instanceof Promise,
|
||||
defaultCalls
|
||||
};
|
||||
});
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("ServiceWorkerContainer.register TrustedScriptURL probe should evaluate");
|
||||
|
||||
let result = vm
|
||||
.eval("JSON.stringify(globalThis.serviceWorkerTrustedUrlResult)")
|
||||
.expect("ServiceWorkerContainer.register errors should reject their Promises");
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"blockedString":"TypeError","blockedWrongType":"TypeError","missing":"TypeError","trustedPromise":true,"defaultPromise":true,"defaultCalls":[["worker.potato","TrustedScriptURL","ServiceWorkerContainer register"]]}"#
|
||||
@@ -1270,3 +1267,139 @@ fn dom_parser_gates_converted_union_source_after_webidl_argument_conversion() {
|
||||
r#"{"blocked":["TypeError","TypeError","TypeError"],"accepted":["trusted","root"],"defaultValues":["default","null","root"],"sourceConversions":2,"invalidType":"TypeError","invalidTypeSkippedPolicy":true,"symbolSource":"TypeError","defaultCalls":[["source","TrustedHTML","DOMParser parseFromString"],["null","TrustedHTML","DOMParser parseFromString"],["<root/>","TrustedHTML","DOMParser parseFromString"]]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_worker_register_converts_options_before_the_default_policy() {
|
||||
let mut vm = new_storage_test_vm("https://service-worker-register-options.test/");
|
||||
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
|
||||
vm.eval(
|
||||
r#"
|
||||
(async () => {
|
||||
const sw = navigator.serviceWorker;
|
||||
const marker = {sentinel: true};
|
||||
const rows = [];
|
||||
async function probe(label, callback, log) {
|
||||
const promise = callback();
|
||||
log.push('returned');
|
||||
try {
|
||||
await promise;
|
||||
rows.push([label, 'fulfilled']);
|
||||
} catch (error) {
|
||||
rows.push([label, promise instanceof Promise, error === marker ? 'sentinel' : error.name, [...log]]);
|
||||
}
|
||||
}
|
||||
const options = log => ({
|
||||
get scope() { log.push('scope'); return './'; },
|
||||
get type() { log.push('type'); return 'classic'; },
|
||||
get updateViaCache() { log.push('cache'); return 'imports'; }
|
||||
});
|
||||
let log = [];
|
||||
await probe('blocked-after-options', () => sw.register(
|
||||
{toString() { log.push('script'); return 'worker.js'; }}, options(log)
|
||||
), log);
|
||||
log = [];
|
||||
await probe('options-exception-before-policy', () => sw.register('worker.js', {
|
||||
get scope() { log.push('scope'); throw marker; }
|
||||
}), log);
|
||||
log = [];
|
||||
await probe('script-exception', () => sw.register(
|
||||
{toString() { log.push('script'); throw marker; }}, options(log)
|
||||
), log);
|
||||
const policy = trustedTypes.createPolicy('registration-tests', {createScriptURL: v => v});
|
||||
const trusted = policy.createScriptURL('https://[');
|
||||
trusted.toString = () => { throw marker; };
|
||||
log = [];
|
||||
await probe('trusted-input', () => sw.register(trusted, options(log)), log);
|
||||
let defaultThrows = false;
|
||||
let policyLog = [];
|
||||
trustedTypes.createPolicy('default', {
|
||||
createScriptURL(value, type, sink) {
|
||||
policyLog.push(['policy', value, type, sink]);
|
||||
if (defaultThrows) throw marker;
|
||||
return 'https://[';
|
||||
}
|
||||
});
|
||||
log = []; policyLog = log;
|
||||
await probe('default-after-options', () => sw.register(
|
||||
{toString() { log.push('script'); return 'worker\ud800.js'; }}, options(log)
|
||||
), log);
|
||||
log = []; policyLog = log; defaultThrows = true;
|
||||
await probe('default-exception', () => sw.register('worker.js', options(log)), log);
|
||||
log = []; policyLog = log;
|
||||
await probe('invalid-options-before-default', () => sw.register('worker.js', {
|
||||
get type() { log.push('type'); return null; }
|
||||
}), log);
|
||||
return rows;
|
||||
})().then(rows => { globalThis.serviceWorkerOptionsResult = rows; });
|
||||
"#,
|
||||
)
|
||||
.expect("ServiceWorkerContainer.register argument and policy probes should evaluate");
|
||||
let result = vm
|
||||
.eval("JSON.stringify(globalThis.serviceWorkerOptionsResult)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!([
|
||||
[
|
||||
"blocked-after-options",
|
||||
true,
|
||||
"TypeError",
|
||||
["script", "scope", "type", "cache", "returned"]
|
||||
],
|
||||
[
|
||||
"options-exception-before-policy",
|
||||
true,
|
||||
"sentinel",
|
||||
["scope", "returned"]
|
||||
],
|
||||
["script-exception", true, "sentinel", ["script", "returned"]],
|
||||
[
|
||||
"trusted-input",
|
||||
true,
|
||||
"TypeError",
|
||||
["scope", "type", "cache", "returned"]
|
||||
],
|
||||
[
|
||||
"default-after-options",
|
||||
true,
|
||||
"TypeError",
|
||||
[
|
||||
"script",
|
||||
"scope",
|
||||
"type",
|
||||
"cache",
|
||||
[
|
||||
"policy",
|
||||
"worker�.js",
|
||||
"TrustedScriptURL",
|
||||
"ServiceWorkerContainer register"
|
||||
],
|
||||
"returned"
|
||||
]
|
||||
],
|
||||
[
|
||||
"default-exception",
|
||||
true,
|
||||
"sentinel",
|
||||
[
|
||||
"scope",
|
||||
"type",
|
||||
"cache",
|
||||
[
|
||||
"policy",
|
||||
"worker.js",
|
||||
"TrustedScriptURL",
|
||||
"ServiceWorkerContainer register"
|
||||
],
|
||||
"returned"
|
||||
]
|
||||
],
|
||||
[
|
||||
"invalid-options-before-default",
|
||||
true,
|
||||
"TypeError",
|
||||
["type", "returned"]
|
||||
]
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -190,3 +190,101 @@ if (parentGetLoaded.call(face) !== face.loaded) realmFailures.push('cross-realm
|
||||
"[2,[]]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webidl_receiver_service_worker_operations_reject_before_argument_conversion() {
|
||||
let mut vm = new_storage_test_vm("https://service-worker-receiver.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const sw = navigator.serviceWorker;
|
||||
const register = sw.register;
|
||||
const getRegistration = sw.getRegistration;
|
||||
const getRegistrations = sw.getRegistrations;
|
||||
globalThis.serviceWorkerReceiverFailures = [];
|
||||
globalThis.serviceWorkerReceiverRejections = 0;
|
||||
let conversions = 0;
|
||||
const script = {toString() { conversions++; return 'https://['; }};
|
||||
const options = {get scope() { conversions++; return './'; }};
|
||||
function check(promise) {
|
||||
if (!(promise instanceof Promise)) serviceWorkerReceiverFailures.push('not a Promise');
|
||||
promise.then(
|
||||
() => serviceWorkerReceiverFailures.push('resolved'),
|
||||
error => {
|
||||
serviceWorkerReceiverRejections++;
|
||||
if (!(error instanceof TypeError)) serviceWorkerReceiverFailures.push('wrong error');
|
||||
}
|
||||
);
|
||||
}
|
||||
for (const receiver of [{}, Object.create(sw), new Proxy(sw, {}), null]) {
|
||||
check(register.call(receiver, script, options));
|
||||
check(getRegistration.call(receiver, script));
|
||||
check(getRegistrations.call(receiver));
|
||||
}
|
||||
if (conversions !== 0) serviceWorkerReceiverFailures.push('converted invalid receiver arguments');
|
||||
const prototype = Object.getPrototypeOf(sw);
|
||||
Object.setPrototypeOf(sw, null);
|
||||
check(register.call(sw, script));
|
||||
Object.setPrototypeOf(sw, prototype);
|
||||
if (conversions !== 1) serviceWorkerReceiverFailures.push('lost native receiver identity');
|
||||
if (register.length !== 1 || getRegistration.length !== 0 || getRegistrations.length !== 0) {
|
||||
serviceWorkerReceiverFailures.push('operation length');
|
||||
}
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("ServiceWorkerContainer receiver checks should return rejected Promises");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify([serviceWorkerReceiverRejections, serviceWorkerReceiverFailures])")
|
||||
.unwrap(),
|
||||
"[13,[]]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webidl_receiver_service_worker_promises_use_the_callee_realm() {
|
||||
let mut vm = new_storage_test_vm("https://service-worker-receiver.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const html = document.appendChild(document.createElement('html'));
|
||||
html.appendChild(document.createElement('body'));
|
||||
const child = document.body.appendChild(document.createElement('iframe')).contentWindow;
|
||||
const parentSw = navigator.serviceWorker;
|
||||
const childSw = child.navigator.serviceWorker;
|
||||
const marker = {sentinel: true};
|
||||
const throwingScript = {toString() { throw marker; }};
|
||||
globalThis.serviceWorkerRealmFailures = [];
|
||||
globalThis.serviceWorkerRealmRejections = 0;
|
||||
for (const [callback, P, E, expected] of [
|
||||
[() => childSw.register.call(parentSw, Symbol('script')), child.Promise, child.TypeError, null],
|
||||
[() => parentSw.register.call(childSw, Symbol('script')), Promise, TypeError, null],
|
||||
[() => childSw.getRegistration.call(parentSw, Symbol('client')), child.Promise, child.TypeError, null],
|
||||
[() => childSw.register.call({}), child.Promise, child.TypeError, null],
|
||||
[() => childSw.register.call(parentSw, throwingScript), child.Promise, child.TypeError, marker],
|
||||
[() => parentSw.register.call(childSw, throwingScript), Promise, TypeError, marker]
|
||||
]) {
|
||||
const promise = callback();
|
||||
const otherP = P === Promise ? child.Promise : Promise;
|
||||
const otherE = E === TypeError ? child.TypeError : TypeError;
|
||||
if (!(promise instanceof P) || promise instanceof otherP) serviceWorkerRealmFailures.push('Promise realm');
|
||||
promise.then(
|
||||
() => serviceWorkerRealmFailures.push('resolved'),
|
||||
error => {
|
||||
serviceWorkerRealmRejections++;
|
||||
if (expected === marker ? error !== marker : (!(error instanceof E) || error instanceof otherE)) {
|
||||
serviceWorkerRealmFailures.push('rejection identity or realm');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("ServiceWorkerContainer cross-realm operations should return Promises");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify([serviceWorkerRealmRejections, serviceWorkerRealmFailures])")
|
||||
.unwrap(),
|
||||
"[6,[]]"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ use super::{
|
||||
|
||||
pub(crate) const DEFAULT_NAVIGATION_PRELOAD_HEADER_VALUE: &str = "true";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, crate::webidl::WebIdlEnum)]
|
||||
#[webidl(
|
||||
name = "ServiceWorkerUpdateViaCache",
|
||||
parse_with = Self::parse_webidl_token
|
||||
)]
|
||||
pub(crate) enum ServiceWorkerUpdateViaCache {
|
||||
#[default]
|
||||
Imports,
|
||||
|
||||
Reference in New Issue
Block a user