LibWeb: Implement nagivator.serviceWorker.getRegistration()

This commit is contained in:
Feng Yu 2025-01-02 17:18:49 -08:00 committed by Andrew Kaster
parent 8e410f959c
commit 37e1d6ece1
Notes: github-actions[bot] 2025-01-30 22:19:44 +00:00
12 changed files with 280 additions and 36 deletions

View file

@ -21,6 +21,8 @@
#include <LibWeb/HTML/WorkerGlobalScope.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/SecureContexts/AbstractOperations.h>
#include <LibWeb/ServiceWorker/ServiceWorker.h>
#include <LibWeb/ServiceWorker/ServiceWorkerRegistration.h>
#include <LibWeb/StorageAPI/StorageManager.h>
namespace Web::HTML {
@ -61,6 +63,8 @@ void EnvironmentSettingsObject::visit_edges(Cell::Visitor& visitor)
m_realm_execution_context->visit_edges(visitor);
visitor.visit(m_fetch_group);
visitor.visit(m_storage_manager);
visitor.visit(m_service_worker_registration_object_map);
visitor.visit(m_service_worker_object_map);
}
JS::ExecutionContext& EnvironmentSettingsObject::realm_execution_context()
@ -574,4 +578,63 @@ GC::Ref<StorageAPI::StorageManager> EnvironmentSettingsObject::storage_manager()
return *m_storage_manager;
}
// https://w3c.github.io/ServiceWorker/#get-the-service-worker-registration-object
GC::Ref<ServiceWorker::ServiceWorkerRegistration> EnvironmentSettingsObject::get_service_worker_registration_object(ServiceWorker::Registration const& registration)
{
// 1. Let objectMap be environments service worker registration object map.
auto& object_map = this->m_service_worker_registration_object_map;
// FIXME: File spec issue asking if this should be keyed on the registration's scope url only or on the url and the storage key
auto const key = ServiceWorker::RegistrationKey { registration.storage_key(), registration.scope_url().serialize(URL::ExcludeFragment::Yes).to_byte_string() };
// 2. If objectMap[registration] does not exist, then:
if (!object_map.contains(key)) {
// 1. Let registrationObject be a new ServiceWorkerRegistration in environments Realm.
// 2. Set registrationObjects service worker registration to registration.
// 3. Set registrationObjects installing attribute to null.
// 4. Set registrationObjects waiting attribute to null.
// 5. Set registrationObjects active attribute to null.
auto registration_object = ServiceWorker::ServiceWorkerRegistration::create(realm(), registration);
// 6. If registrations installing worker is not null, then set registrationObjects installing attribute to the result of getting the service worker object that represents registrations installing worker in environment.
if (registration.installing_worker())
registration_object->set_installing(get_service_worker_object(registration.installing_worker()));
// 7. If registrations waiting worker is not null, then set registrationObjects waiting attribute to the result of getting the service worker object that represents registrations waiting worker in environment.
if (registration.waiting_worker())
registration_object->set_waiting(get_service_worker_object(registration.waiting_worker()));
// 8. If registrations active worker is not null, then set registrationObjects active attribute to the result of getting the service worker object that represents registrations active worker in environment.
if (registration.active_worker())
registration_object->set_active(get_service_worker_object(registration.active_worker()));
// 9. Set objectMap[registration] to registrationObject.
object_map.set(key, registration_object);
}
// 3. Return objectMap[registration].
return *object_map.get(key);
}
GC::Ref<ServiceWorker::ServiceWorker> EnvironmentSettingsObject::get_service_worker_object(ServiceWorker::ServiceWorkerRecord* service_worker)
{
// 1. Let objectMap be environments service worker object map.
auto& object_map = this->m_service_worker_object_map;
// 2. If objectMap[serviceWorker] does not exist, then:
if (!object_map.contains(service_worker)) {
// 1. Let serviceWorkerObj be a new ServiceWorker in environments Realm, and associate it with serviceWorker.
auto service_worker_obj = ServiceWorker::ServiceWorker::create(realm(), service_worker);
// 2. Set serviceWorkerObjs state to serviceWorkers state.
service_worker_obj->set_service_worker_state(service_worker->state);
// 3. Set objectMap[serviceWorker] to serviceWorkerObj.
object_map.set(service_worker, service_worker_obj);
}
// 3. Return objectMap[serviceWorker].
return *object_map.get(service_worker);
}
}

View file

@ -15,6 +15,7 @@
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/Scripting/ModuleMap.h>
#include <LibWeb/HTML/Scripting/SerializedEnvironmentSettingsObject.h>
#include <LibWeb/ServiceWorker/Registration.h>
namespace Web::HTML {
@ -105,6 +106,12 @@ public:
GC::Ref<StorageAPI::StorageManager> storage_manager();
// https://w3c.github.io/ServiceWorker/#get-the-service-worker-registration-object
GC::Ref<ServiceWorker::ServiceWorkerRegistration> get_service_worker_registration_object(ServiceWorker::Registration const&);
// https://w3c.github.io/ServiceWorker/#get-the-service-worker-object
GC::Ref<ServiceWorker::ServiceWorker> get_service_worker_object(ServiceWorker::ServiceWorkerRecord*);
[[nodiscard]] bool discarded() const { return m_discarded; }
void set_discarded(bool b) { m_discarded = b; }
@ -127,6 +134,16 @@ private:
// Each environment settings object has an associated StorageManager object.
GC::Ptr<StorageAPI::StorageManager> m_storage_manager;
// https://w3c.github.io/ServiceWorker/#environment-settings-object-service-worker-registration-object-map
// An environment settings object has a service worker registration object map,
// a map where the keys are service worker registrations and the values are ServiceWorkerRegistration objects.
HashMap<ServiceWorker::RegistrationKey, GC::Ref<ServiceWorker::ServiceWorkerRegistration>> m_service_worker_registration_object_map;
// https://w3c.github.io/ServiceWorker/#environment-settings-object-service-worker-object-map
// An environment settings object has a service worker object map,
// a map where the keys are service workers and the values are ServiceWorker objects.
HashMap<ServiceWorker::ServiceWorkerRecord*, GC::Ref<ServiceWorker::ServiceWorker>> m_service_worker_object_map;
// https://w3c.github.io/ServiceWorker/#service-worker-client-discarded-flag
// A service worker client has an associated discarded flag. It is initially unset.
bool m_discarded { false };

View file

@ -5,17 +5,12 @@
*/
#include <AK/HashMap.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/ServiceWorker/Registration.h>
namespace Web::ServiceWorker {
struct RegistrationKey {
StorageAPI::StorageKey key;
ByteString serialized_scope_url;
bool operator==(RegistrationKey const&) const = default;
};
// FIXME: Surely this needs hooks to be cleared and manipulated at the UA level
// Does this need to be serialized to disk as well?
static HashMap<RegistrationKey, Registration> s_registrations;
@ -84,6 +79,53 @@ Registration& Registration::set(StorageAPI::StorageKey const& storage_key, URL::
return s_registrations.get(key).value();
}
// https://w3c.github.io/ServiceWorker/#scope-match-algorithm
Optional<Registration&> Registration::match(StorageAPI::StorageKey const& storage_key, URL::URL const& client_url)
{
// FIXME: 1. Run the following steps atomically.
// 2. Let clientURLString be serialized clientURL.
auto client_url_string = client_url.serialize();
// 3. Let matchingScopeString be the empty string.
ByteString matching_scope_string;
// 4. Let scopeStringSet be an empty list.
Vector<ByteString> scope_string_set;
// 5. For each (entry storage key, entry scope) of registration map's keys:
for (auto& [entry_storage_key, entry_scope] : s_registrations.keys()) {
// 1. If storage key equals entry storage key, then append entry scope to the end of scopeStringSet.
if (entry_storage_key == storage_key)
scope_string_set.append(entry_scope);
}
// 6. Set matchingScopeString to the longest value in scopeStringSet which the value of clientURLString starts with, if it exists.
// NOTE: The URL string matching in this step is prefix-based rather than path-structural. E.g. a client
// URL string with "https://example.com/prefix-of/resource.html" will match a registration for a
// scope with "https://example.com/prefix". The URL string comparison is safe for the same-origin
// security as HTTP(S) URLs are always serialized with a trailing slash at the end of the origin
// part of the URLs.
for (auto& scope_string : scope_string_set) {
if (client_url_string.starts_with_bytes(scope_string) && scope_string.length() > matching_scope_string.length())
matching_scope_string = scope_string;
}
// 7. Let matchingScope be null.
Optional<URL::URL> matching_scope;
// 8. If matchingScopeString is not the empty string, then:
if (!matching_scope_string.is_empty()) {
// 1. Let matchingScope be the result of parsing matchingScopeString.
matching_scope = DOMURL::parse(matching_scope_string);
// 2. Assert: matchingScopes origin and clientURLs origin are same origin.
VERIFY(matching_scope.value().origin().is_same_origin(client_url.origin()));
}
// 9. Return the result of running Get Registration given storage key and matchingScope.
return get(storage_key, matching_scope);
}
void Registration::remove(StorageAPI::StorageKey const& key, URL::URL const& scope)
{
(void)s_registrations.remove({ key, scope.serialize(URL::ExcludeFragment::Yes).to_byte_string() });
@ -104,13 +146,3 @@ ServiceWorkerRecord* Registration::newest_worker() const
}
}
namespace AK {
template<>
struct Traits<Web::ServiceWorker::RegistrationKey> : public DefaultTraits<Web::ServiceWorker::RegistrationKey> {
static unsigned hash(Web::ServiceWorker::RegistrationKey const& key)
{
return pair_int_hash(Traits<Web::StorageAPI::StorageKey>::hash(key.key), Traits<ByteString>::hash(key.serialized_scope_url));
}
};
}

View file

@ -31,6 +31,9 @@ public:
// https://w3c.github.io/ServiceWorker/#set-registration-algorithm
static Registration& set(StorageAPI::StorageKey const&, URL::URL const&, Bindings::ServiceWorkerUpdateViaCache);
// https://w3c.github.io/ServiceWorker/#scope-match-algorithm
static Optional<Registration&> match(StorageAPI::StorageKey const&, URL::URL const&);
static void remove(StorageAPI::StorageKey const&, URL::URL const&);
bool is_unregistered();
@ -42,6 +45,10 @@ public:
void set_last_update_check_time(MonotonicTime time) { m_last_update_check_time = time; }
ServiceWorkerRecord* newest_worker() const;
ServiceWorkerRecord* installing_worker() const { return m_installing_worker; }
ServiceWorkerRecord* waiting_worker() const { return m_waiting_worker; }
ServiceWorkerRecord* active_worker() const { return m_active_worker; }
bool is_stale() const;
private:
@ -65,4 +72,22 @@ private:
ByteString m_navigation_preload_header_value; // https://w3c.github.io/ServiceWorker/#service-worker-registration-navigation-preload-header-value
};
struct RegistrationKey {
StorageAPI::StorageKey key;
ByteString serialized_scope_url;
bool operator==(RegistrationKey const&) const = default;
};
}
namespace AK {
template<>
struct Traits<Web::ServiceWorker::RegistrationKey> : public DefaultTraits<Web::ServiceWorker::RegistrationKey> {
static unsigned hash(Web::ServiceWorker::RegistrationKey const& key)
{
return pair_int_hash(Traits<Web::StorageAPI::StorageKey>::hash(key.key), Traits<ByteString>::hash(key.serialized_scope_url));
}
};
}

View file

@ -12,17 +12,17 @@
namespace Web::ServiceWorker {
ServiceWorker::ServiceWorker(JS::Realm& realm, String script_url)
ServiceWorker::ServiceWorker(JS::Realm& realm, ServiceWorkerRecord* service_worker_record)
: DOM::EventTarget(realm)
, m_script_url(move(script_url))
, m_service_worker_record(service_worker_record)
{
}
ServiceWorker::~ServiceWorker() = default;
GC::Ref<ServiceWorker> ServiceWorker::create(JS::Realm& realm)
GC::Ref<ServiceWorker> ServiceWorker::create(JS::Realm& realm, ServiceWorkerRecord* service_worker_record)
{
return realm.create<ServiceWorker>(realm, ""_string);
return realm.create<ServiceWorker>(realm, service_worker_record);
}
void ServiceWorker::initialize(JS::Realm& realm)
@ -31,6 +31,15 @@ void ServiceWorker::initialize(JS::Realm& realm)
WEB_SET_PROTOTYPE_FOR_INTERFACE(ServiceWorker);
}
// https://w3c.github.io/ServiceWorker/#dom-serviceworker-scripturl
String ServiceWorker::script_url() const
{
if (!m_service_worker_record)
return {};
return m_service_worker_record->script_url.serialize();
}
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name) \
void ServiceWorker::set_##attribute_name(WebIDL::CallbackType* value) \

View file

@ -15,16 +15,18 @@
namespace Web::ServiceWorker {
// https://w3c.github.io/ServiceWorker/#serviceworker-interface
class ServiceWorker : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(ServiceWorker, DOM::EventTarget);
public:
[[nodiscard]] static GC::Ref<ServiceWorker> create(JS::Realm& realm);
[[nodiscard]] static GC::Ref<ServiceWorker> create(JS::Realm& realm, ServiceWorkerRecord*);
virtual ~ServiceWorker() override;
String script_url() const { return m_script_url; }
String script_url() const;
Bindings::ServiceWorkerState service_worker_state() const { return m_state; }
void set_service_worker_state(Bindings::ServiceWorkerState state) { m_state = state; }
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name) \
@ -34,12 +36,12 @@ public:
#undef __ENUMERATE
private:
ServiceWorker(JS::Realm&, String script_url);
ServiceWorker(JS::Realm&, ServiceWorkerRecord*);
virtual void initialize(JS::Realm&) override;
String m_script_url;
Bindings::ServiceWorkerState m_state { Bindings::ServiceWorkerState::Parsed };
ServiceWorkerRecord* m_service_worker_record;
};
}

View file

@ -10,9 +10,13 @@
#include <LibWeb/Bindings/ServiceWorkerContainerPrototype.h>
#include <LibWeb/DOMURL/DOMURL.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>
namespace Web::ServiceWorker {
@ -80,6 +84,61 @@ GC::Ref<WebIDL::Promise> ServiceWorkerContainer::register_(String script_url, Re
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 objects 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 clientURLs fragment to null.
parsed_client_url->set_fragment({});
// 6. If the origin of clientURL is not clients 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"_string));
// 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 promises 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/#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)
{

View file

@ -36,6 +36,8 @@ public:
GC::Ref<WebIDL::Promise> register_(String script_url, RegistrationOptions const& options);
GC::Ref<WebIDL::Promise> get_registration(String const& client_url);
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name) \
void set_##attribute_name(WebIDL::CallbackType*); \

View file

@ -12,7 +12,7 @@ interface ServiceWorkerContainer : EventTarget {
// FIXME: [NewObject] Promise<ServiceWorkerRegistration> register((TrustedScriptURL or USVString) scriptURL, optional RegistrationOptions options = {});
[NewObject, ImplementedAs=register_] Promise<ServiceWorkerRegistration> register(USVString scriptURL, optional RegistrationOptions options = {});
[FIXME, NewObject] Promise<(ServiceWorkerRegistration or undefined)> getRegistration(optional USVString clientURL = "");
[NewObject] Promise<(ServiceWorkerRegistration or undefined)> getRegistration(optional USVString clientURL = "");
[FIXME, NewObject] Promise<FrozenArray<ServiceWorkerRegistration>> getRegistrations();
[FIXME] undefined startMessages();

View file

@ -7,14 +7,16 @@
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/ServiceWorkerRegistrationPrototype.h>
#include <LibWeb/ServiceWorker/ServiceWorker.h>
#include <LibWeb/ServiceWorker/ServiceWorkerRegistration.h>
namespace Web::ServiceWorker {
GC_DEFINE_ALLOCATOR(ServiceWorkerRegistration);
ServiceWorkerRegistration::ServiceWorkerRegistration(JS::Realm& realm)
ServiceWorkerRegistration::ServiceWorkerRegistration(JS::Realm& realm, Registration const& registration)
: DOM::EventTarget(realm)
, m_registration(registration)
{
}
@ -24,8 +26,16 @@ void ServiceWorkerRegistration::initialize(JS::Realm& realm)
WEB_SET_PROTOTYPE_FOR_INTERFACE(ServiceWorkerRegistration);
}
GC::Ref<ServiceWorkerRegistration> ServiceWorkerRegistration::create(JS::Realm& realm)
void ServiceWorkerRegistration::visit_edges(Cell::Visitor& visitor)
{
return realm.create<ServiceWorkerRegistration>(realm);
Base::visit_edges(visitor);
visitor.visit(m_installing);
visitor.visit(m_waiting);
visitor.visit(m_active);
}
GC::Ref<ServiceWorkerRegistration> ServiceWorkerRegistration::create(JS::Realm& realm, Registration const& registration)
{
return realm.create<ServiceWorkerRegistration>(realm, registration);
}
}

View file

@ -10,17 +10,42 @@
namespace Web::ServiceWorker {
// https://w3c.github.io/ServiceWorker/#serviceworkerregistration-interface
class ServiceWorkerRegistration : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(ServiceWorkerRegistration, DOM::EventTarget);
GC_DECLARE_ALLOCATOR(ServiceWorkerRegistration);
public:
[[nodiscard]] static GC::Ref<ServiceWorkerRegistration> create(JS::Realm& realm);
[[nodiscard]] static GC::Ref<ServiceWorkerRegistration> create(JS::Realm& realm, Registration const& registration);
explicit ServiceWorkerRegistration(JS::Realm&);
Registration const& registration() { return m_registration; }
GC::Ptr<ServiceWorker> installing() const { return m_installing; }
void set_installing(GC::Ptr<ServiceWorker> installing) { m_installing = installing; }
GC::Ptr<ServiceWorker> waiting() const { return m_waiting; }
void set_waiting(GC::Ptr<ServiceWorker> waiting) { m_waiting = waiting; }
GC::Ptr<ServiceWorker> active() const { return m_active; }
void set_active(GC::Ptr<ServiceWorker> active) { m_active = active; }
// https://w3c.github.io/ServiceWorker/#dom-serviceworkerregistration-scope
String scope() const { return m_registration.scope_url().serialize(); }
// https://w3c.github.io/ServiceWorker/#dom-serviceworkerregistration-updateviacache
Bindings::ServiceWorkerUpdateViaCache update_via_cache() const { return m_registration.update_via_cache(); }
explicit ServiceWorkerRegistration(JS::Realm&, Registration const&);
virtual ~ServiceWorkerRegistration() override = default;
private:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(JS::Cell::Visitor&) override;
Registration const& m_registration;
GC::Ptr<ServiceWorker> m_installing;
GC::Ptr<ServiceWorker> m_waiting;
GC::Ptr<ServiceWorker> m_active;
};
}

View file

@ -5,13 +5,13 @@
// https://w3c.github.io/ServiceWorker/#serviceworkerregistration-interface
[SecureContext, Exposed=(Window,Worker)]
interface ServiceWorkerRegistration : EventTarget {
[FIXME] readonly attribute ServiceWorker? installing;
[FIXME] readonly attribute ServiceWorker? waiting;
[FIXME] readonly attribute ServiceWorker? active;
readonly attribute ServiceWorker? installing;
readonly attribute ServiceWorker? waiting;
readonly attribute ServiceWorker? active;
[FIXME, SameObject] readonly attribute NavigationPreloadManager navigationPreload;
[FIXME] readonly attribute USVString scope;
[FIXME] readonly attribute ServiceWorkerUpdateViaCache updateViaCache;
readonly attribute USVString scope;
readonly attribute ServiceWorkerUpdateViaCache updateViaCache;
[FIXME, NewObject] Promise<undefined> update();
[FIXME, NewObject] Promise<boolean> unregister();