mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-26 01:35:08 +02:00
Previously, the LibWeb bindings generator would output multiple per interface files like Prototype/Constructor/Namespace/GlobalMixin depending on the contents of that IDL file. This complicates the build system as it means that it does not know what files will be generated without knowledge of the contents of that IDL file. Instead, for each IDL file only generate a single Bindings/<IDLFile>.h and Bindings/<IDLFile>.cpp.
307 lines
15 KiB
C++
307 lines
15 KiB
C++
/*
|
||
* Copyright (c) 2024, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
|
||
*
|
||
* SPDX-License-Identifier: BSD-2-Clause
|
||
*/
|
||
|
||
#include <LibJS/Runtime/Realm.h>
|
||
#include <LibWeb/Bindings/Intrinsics.h>
|
||
#include <LibWeb/Bindings/ServiceWorkerContainer.h>
|
||
#include <LibWeb/DOMURL/DOMURL.h>
|
||
#include <LibWeb/HTML/EventLoop/EventLoop.h>
|
||
#include <LibWeb/HTML/EventNames.h>
|
||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||
#include <LibWeb/ServiceWorker/Job.h>
|
||
#include <LibWeb/ServiceWorker/Registration.h>
|
||
#include <LibWeb/ServiceWorker/ServiceWorker.h>
|
||
#include <LibWeb/ServiceWorker/ServiceWorkerContainer.h>
|
||
#include <LibWeb/ServiceWorker/ServiceWorkerRegistration.h>
|
||
#include <LibWeb/StorageAPI/StorageKey.h>
|
||
#include <LibWeb/TrustedTypes/RequireTrustedTypesForDirective.h>
|
||
#include <LibWeb/TrustedTypes/TrustedScriptURL.h>
|
||
#include <LibWeb/TrustedTypes/TrustedTypePolicy.h>
|
||
|
||
namespace Web::ServiceWorker {
|
||
|
||
GC_DEFINE_ALLOCATOR(ServiceWorkerContainer);
|
||
|
||
ServiceWorkerContainer::ServiceWorkerContainer(JS::Realm& realm)
|
||
: DOM::EventTarget(realm)
|
||
, m_service_worker_client(HTML::relevant_settings_object(*this))
|
||
{
|
||
}
|
||
|
||
ServiceWorkerContainer::~ServiceWorkerContainer() = default;
|
||
|
||
void ServiceWorkerContainer::initialize(JS::Realm& realm)
|
||
{
|
||
WEB_SET_PROTOTYPE_FOR_INTERFACE(ServiceWorkerContainer);
|
||
Base::initialize(realm);
|
||
}
|
||
|
||
void ServiceWorkerContainer::visit_edges(Cell::Visitor& visitor)
|
||
{
|
||
Base::visit_edges(visitor);
|
||
visitor.visit(m_service_worker_client);
|
||
visitor.visit(m_ready_promise);
|
||
}
|
||
|
||
GC::Ref<ServiceWorkerContainer> ServiceWorkerContainer::create(JS::Realm& realm)
|
||
{
|
||
return realm.create<ServiceWorkerContainer>(realm);
|
||
}
|
||
|
||
// https://w3c.github.io/ServiceWorker/#navigator-service-worker-register
|
||
GC::Ref<WebIDL::Promise> ServiceWorkerContainer::register_(TrustedTypes::TrustedScriptURLOrString script_url, RegistrationOptions const& options)
|
||
{
|
||
auto& realm = this->realm();
|
||
// Note: The register(scriptURL, options) method creates or updates a service worker registration for the given scope url.
|
||
// If successful, a service worker registration ties the provided scriptURL to a scope url,
|
||
// which is subsequently used for navigation matching.
|
||
|
||
// 1. Let p be a promise.
|
||
auto p = WebIDL::create_promise(realm);
|
||
|
||
// 2. Set scriptURL to the result of invoking Get Trusted Type compliant string with TrustedScriptURL,
|
||
// this's relevant global object, scriptURL, "ServiceWorkerContainer register", and "script".
|
||
auto const compliant_script_url = MUST(TrustedTypes::get_trusted_type_compliant_string(
|
||
TrustedTypes::TrustedTypeName::TrustedScriptURL,
|
||
HTML::relevant_global_object(*this),
|
||
script_url,
|
||
TrustedTypes::InjectionSink::ServiceWorkerContainer_register,
|
||
TrustedTypes::Script.to_string()));
|
||
|
||
// 3 Let client be this's service worker client.
|
||
auto client = m_service_worker_client;
|
||
|
||
// 4. Let scriptURL be the result of parsing scriptURL with this's relevant settings object’s API base URL.
|
||
auto base_url = HTML::relevant_settings_object(*this).api_base_url();
|
||
auto parsed_script_url = DOMURL::parse(compliant_script_url.to_utf8_but_should_be_ported_to_utf16(), base_url);
|
||
|
||
// 5. Let scopeURL be null.
|
||
Optional<URL::URL> scope_url;
|
||
|
||
// 6. If options["scope"] exists, set scopeURL to the result of parsing options["scope"] with this's relevant settings object’s API base URL.
|
||
if (options.scope.has_value()) {
|
||
scope_url = DOMURL::parse(options.scope.value(), base_url);
|
||
}
|
||
|
||
// 7. Invoke Start Register with scopeURL, scriptURL, p, client, client’s creation URL, options["type"], and options["updateViaCache"].
|
||
start_register(scope_url, parsed_script_url, p, client, client->creation_url, options.type, options.update_via_cache);
|
||
|
||
// 8. Return p.
|
||
return p;
|
||
}
|
||
|
||
// https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration
|
||
GC::Ref<WebIDL::Promise> ServiceWorkerContainer::get_registration(String const& client_url)
|
||
{
|
||
auto& realm = this->realm();
|
||
|
||
// 1. Let client be this's service worker client.
|
||
auto client = m_service_worker_client;
|
||
|
||
// 2. Let storage key be the result of running obtain a storage key given client.
|
||
auto storage_key = StorageAPI::obtain_a_storage_key(client);
|
||
|
||
// FIXME: Ad-Hoc. Spec should handle this failure.
|
||
if (!storage_key.has_value())
|
||
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Failed to obtain a storage key"sv));
|
||
|
||
// 3. Let clientURL be the result of parsing clientURL with this's relevant settings object’s API base URL.
|
||
auto base_url = HTML::relevant_settings_object(*this).api_base_url();
|
||
auto parsed_client_url = DOMURL::parse(client_url, base_url);
|
||
|
||
// 4. If clientURL is failure, return a promise rejected with a TypeError.
|
||
if (!parsed_client_url.has_value())
|
||
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "clientURL is not a valid URL"sv));
|
||
|
||
// 5. Set clientURL’s fragment to null.
|
||
parsed_client_url->set_fragment({});
|
||
|
||
// 6. If the origin of clientURL is not client’s origin, return a promise rejected with a "SecurityError" DOMException.
|
||
if (!parsed_client_url->origin().is_same_origin(client->origin()))
|
||
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "clientURL is not the same origin as the client's origin"_utf16));
|
||
|
||
// 7. Let promise be a new promise.
|
||
auto promise = WebIDL::create_promise(realm);
|
||
|
||
// 8. Run the following substeps in parallel:
|
||
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [promise, storage_key, parsed_client_url = *parsed_client_url]() {
|
||
auto& realm = HTML::relevant_realm(promise->promise());
|
||
HTML::TemporaryExecutionContext const execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||
|
||
// 1. Let registration be the result of running Match Service Worker Registration given storage key and clientURL.
|
||
auto maybe_registration = Registration::match(storage_key.value(), parsed_client_url);
|
||
|
||
// 2. If registration is null, resolve promise with undefined and abort these steps.
|
||
if (!maybe_registration.has_value()) {
|
||
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
|
||
return;
|
||
}
|
||
|
||
// 3. Resolve promise with the result of getting the service worker registration object that represents registration in promise’s relevant settings object.
|
||
auto registration_object = HTML::relevant_settings_object(promise->promise()).get_service_worker_registration_object(maybe_registration.value());
|
||
WebIDL::resolve_promise(realm, promise, registration_object);
|
||
}));
|
||
|
||
return promise;
|
||
}
|
||
|
||
// https://w3c.github.io/ServiceWorker/#navigator-service-worker-ready
|
||
GC::Ref<WebIDL::Promise> ServiceWorkerContainer::ready()
|
||
{
|
||
auto& realm = this->realm();
|
||
|
||
// 1. If this’s ready promise is null, then set this’s ready promise to a new promise.
|
||
if (!m_ready_promise) {
|
||
m_ready_promise = WebIDL::create_promise(realm);
|
||
|
||
// 2. Let readyPromise be this’s ready promise.
|
||
auto ready_promise = GC::Ref { *m_ready_promise };
|
||
|
||
// 3. If readyPromise is pending, run the following substeps in parallel:
|
||
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, ready_promise]() {
|
||
// 1. Let client be this’s service worker client.
|
||
auto client = m_service_worker_client;
|
||
|
||
// 2. Let storage key be the result of running obtain a storage key given client.
|
||
auto storage_key = StorageAPI::obtain_a_storage_key(client);
|
||
if (!storage_key.has_value()) {
|
||
// AD-HOC: Spec doesn't say what to do here, but we can't continue without a storage key.
|
||
return;
|
||
}
|
||
|
||
// 3. Let registration be the result of running Match Service Worker Registration given storage key and client’s creation URL.
|
||
auto maybe_registration = Registration::match(storage_key.value(), client->creation_url);
|
||
|
||
// 4. If registration is not null, and registration’s active worker is not null, queue a task on
|
||
// readyPromise’s relevant settings object’s responsible event loop, using the DOM manipulation task source,
|
||
// to resolve readyPromise with the result of getting the service worker registration object that represents
|
||
// registration in readyPromise’s relevant settings object.
|
||
if (maybe_registration.has_value() && maybe_registration->active_worker() != nullptr) {
|
||
auto registration_object = HTML::relevant_settings_object(ready_promise->promise()).get_service_worker_registration_object(maybe_registration.value());
|
||
queue_a_task(HTML::Task::Source::DOMManipulation, nullptr, nullptr, GC::create_function(heap(), [ready_promise, registration_object]() {
|
||
auto& realm = HTML::relevant_realm(ready_promise->promise());
|
||
HTML::TemporaryExecutionContext const execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||
WebIDL::resolve_promise(realm, ready_promise, registration_object);
|
||
}));
|
||
}
|
||
// NOTE: The returned ready promise will never reject.
|
||
// If it does not resolve in this algorithm, it will eventually resolve when a matching service worker
|
||
// registration is registered and its active worker is set. (See the relevant Activate algorithm step.)
|
||
}));
|
||
}
|
||
|
||
// 4. Return readyPromise.
|
||
return *m_ready_promise;
|
||
}
|
||
|
||
// https://w3c.github.io/ServiceWorker/#start-register-algorithm
|
||
void ServiceWorkerContainer::start_register(Optional<URL::URL> scope_url, Optional<URL::URL> script_url, GC::Ref<WebIDL::Promise> promise, HTML::EnvironmentSettingsObject& client, URL::URL referrer, Bindings::WorkerType worker_type, Bindings::ServiceWorkerUpdateViaCache update_via_cache)
|
||
{
|
||
auto& realm = this->realm();
|
||
auto& vm = realm.vm();
|
||
|
||
// 1. If scriptURL is failure, reject promise with a TypeError and abort these steps.
|
||
if (!script_url.has_value()) {
|
||
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "scriptURL is not a valid URL"sv));
|
||
return;
|
||
}
|
||
|
||
// 2. Set scriptURL’s fragment to null.
|
||
// Note: The user agent does not store the fragment of the script’s url.
|
||
// This means that the fragment does not have an effect on identifying service workers.
|
||
script_url->set_fragment({});
|
||
|
||
// 3. If scriptURL’s scheme is not one of "http" and "https", reject promise with a TypeError and abort these steps.
|
||
if (!script_url->scheme().is_one_of("http"sv, "https"sv)) {
|
||
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "scriptURL must have a scheme of 'http' or 'https'"sv));
|
||
return;
|
||
}
|
||
|
||
// 4. If any of the strings in scriptURL’s path contains either ASCII case-insensitive "%2f" or ASCII case-insensitive "%5c",
|
||
// reject promise with a TypeError and abort these steps.
|
||
auto invalid_path = script_url->paths().first_matching([&](auto& path) {
|
||
return path.contains("%2f"sv, CaseSensitivity::CaseInsensitive) || path.contains("%5c"sv, CaseSensitivity::CaseInsensitive);
|
||
});
|
||
if (invalid_path.has_value()) {
|
||
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "scriptURL path must not contain '%2f' or '%5c'"sv));
|
||
return;
|
||
}
|
||
|
||
// 5. If scopeURL is null, set scopeURL to the result of parsing the string "./" with scriptURL.
|
||
// Note: The scope url for the registration is set to the location of the service worker script by default.
|
||
if (!scope_url.has_value()) {
|
||
scope_url = DOMURL::parse("./"sv, script_url);
|
||
}
|
||
|
||
// 6. If scopeURL is failure, reject promise with a TypeError and abort these steps.
|
||
if (!scope_url.has_value()) {
|
||
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "scopeURL is not a valid URL"sv));
|
||
return;
|
||
}
|
||
|
||
// 7. Set scopeURL’s fragment to null.
|
||
// Note: The user agent does not store the fragment of the scope url.
|
||
// This means that the fragment does not have an effect on identifying service worker registrations.
|
||
scope_url->set_fragment({});
|
||
|
||
// 8. If scopeURL’s scheme is not one of "http" and "https", reject promise with a TypeError and abort these steps.
|
||
if (!scope_url->scheme().is_one_of("http"sv, "https"sv)) {
|
||
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "scopeURL must have a scheme of 'http' or 'https'"sv));
|
||
return;
|
||
}
|
||
|
||
// 9. If any of the strings in scopeURL’s path contains either ASCII case-insensitive "%2f" or ASCII case-insensitive "%5c",
|
||
// reject promise with a TypeError and abort these steps.
|
||
invalid_path = scope_url->paths().first_matching([&](auto& path) {
|
||
return path.contains("%2f"sv, CaseSensitivity::CaseInsensitive) || path.contains("%5c"sv, CaseSensitivity::CaseInsensitive);
|
||
});
|
||
if (invalid_path.has_value()) {
|
||
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "scopeURL path must not contain '%2f' or '%5c'"sv));
|
||
return;
|
||
}
|
||
|
||
// 10. Let storage key be the result of running obtain a storage key given client.
|
||
auto storage_key = StorageAPI::obtain_a_storage_key(client);
|
||
|
||
// FIXME: Ad-Hoc. Spec should handle this failure here, or earlier.
|
||
if (!storage_key.has_value()) {
|
||
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Failed to obtain a storage key"sv));
|
||
return;
|
||
}
|
||
|
||
// 11. Let job be the result of running Create Job with register, storage key, scopeURL, scriptURL, promise, and client.
|
||
auto job = Job::create(vm, Job::Type::Register, storage_key.value(), scope_url.value(), script_url.release_value(), promise, client);
|
||
|
||
// 12. Set job’s worker type to workerType.
|
||
job->worker_type = worker_type;
|
||
|
||
// 13. Set job’s update via cache to updateViaCache.
|
||
job->update_via_cache = update_via_cache;
|
||
|
||
// 14. Set job’s referrer to referrer.
|
||
job->referrer = move(referrer);
|
||
|
||
// 15. Invoke Schedule Job with job.
|
||
schedule_job(vm, job);
|
||
}
|
||
|
||
#undef __ENUMERATE
|
||
#define __ENUMERATE(attribute_name, event_name) \
|
||
void ServiceWorkerContainer::set_##attribute_name(WebIDL::CallbackType* value) \
|
||
{ \
|
||
set_event_handler_attribute(event_name, move(value)); \
|
||
} \
|
||
WebIDL::CallbackType* ServiceWorkerContainer::attribute_name() \
|
||
{ \
|
||
return event_handler_attribute(event_name); \
|
||
}
|
||
ENUMERATE_SERVICE_WORKER_CONTAINER_EVENT_HANDLERS(__ENUMERATE)
|
||
#undef __ENUMERATE
|
||
|
||
}
|