Browse Source

LibWeb: Start implementing serviceWorker.register

This is mostly the fun boilerplate. Actually creating the Job queue
to do the heavy lifting is next.
Andrew Kaster 9 months ago
parent
commit
c77d9a2732

+ 1 - 0
Tests/LibWeb/Text/expected/ServiceWorker/service-worker-register.txt

@@ -0,0 +1 @@
+ServiceWorker registration failed: InternalError: TODO(ServiceWorkerContainer::start_register is not implemented in LibJS)

+ 20 - 0
Tests/LibWeb/Text/input/ServiceWorker/service-worker-register.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script src="../include.js"></script>
+<script>
+    asyncTest(done => {
+
+        spoofCurrentURL("https://example.com/service-worker-register.html");
+
+        let swPromise = navigator.serviceWorker.register("service-worker.js");
+
+        swPromise
+            .then(registration => {
+                println(`ServiceWorker registration successful with scope: ${registration.scope}`);
+                done();
+            })
+            .catch(err => {
+                println(`ServiceWorker registration failed: ${err}`);
+                done();
+            });
+    });
+</script>

+ 2 - 0
Tests/LibWeb/Text/input/ServiceWorker/service-worker.js

@@ -0,0 +1,2 @@
+// FIXME: Add service worker code here
+console.log("hi from service worker");

+ 123 - 0
Userland/Libraries/LibWeb/HTML/ServiceWorkerContainer.cpp

@@ -8,8 +8,10 @@
 #include <LibJS/Runtime/Realm.h>
 #include <LibWeb/Bindings/Intrinsics.h>
 #include <LibWeb/Bindings/ServiceWorkerContainerPrototype.h>
+#include <LibWeb/DOMURL/DOMURL.h>
 #include <LibWeb/HTML/EventNames.h>
 #include <LibWeb/HTML/ServiceWorkerContainer.h>
+#include <LibWeb/StorageAPI/StorageKey.h>
 
 namespace Web::HTML {
 
@@ -40,6 +42,127 @@ JS::NonnullGCPtr<ServiceWorkerContainer> ServiceWorkerContainer::create(JS::Real
     return realm.heap().allocate<ServiceWorkerContainer>(realm, realm);
 }
 
+// https://w3c.github.io/ServiceWorker/#navigator-service-worker-register
+JS::NonnullGCPtr<JS::Promise> ServiceWorkerContainer::register_(String 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);
+
+    // FIXME: 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".
+
+    // 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 = relevant_settings_object(*this).api_base_url();
+    auto parsed_script_url = DOMURL::parse(script_url, 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 verify_cast<JS::Promise>(*p->promise());
+}
+
+// https://w3c.github.io/ServiceWorker/#start-register-algorithm
+void ServiceWorkerContainer::start_register(Optional<URL::URL> scope_url, URL::URL script_url, JS::NonnullGCPtr<WebIDL::Promise> promise, EnvironmentSettingsObject& client, URL::URL, Bindings::WorkerType, Bindings::ServiceWorkerUpdateViaCache)
+{
+    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.is_valid()) {
+        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->is_valid()) {
+        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;
+    }
+
+    // FIXME: Schedule the job
+    // 11. Let job be the result of running Create Job with register, storage key, scopeURL, scriptURL, promise, and client.
+    // 12. Set job’s worker type to workerType.
+    // 13. Set job’s update via cache to updateViaCache.
+    // 14. Set job’s referrer to referrer.
+    // 15. Invoke Schedule Job with job.
+
+    WebIDL::reject_promise(realm, promise, *vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "ServiceWorkerContainer::start_register"sv).value());
+}
+
 #undef __ENUMERATE
 #define __ENUMERATE(attribute_name, event_name)                                    \
     void ServiceWorkerContainer::set_##attribute_name(WebIDL::CallbackType* value) \

+ 14 - 0
Userland/Libraries/LibWeb/HTML/ServiceWorkerContainer.h

@@ -7,7 +7,11 @@
 
 #pragma once
 
+#include <LibWeb/Bindings/ServiceWorkerRegistrationPrototype.h>
+#include <LibWeb/Bindings/WorkerPrototype.h>
 #include <LibWeb/DOM/EventTarget.h>
+#include <LibWeb/WebIDL/ExceptionOr.h>
+#include <LibWeb/WebIDL/Promise.h>
 
 #define ENUMERATE_SERVICE_WORKER_CONTAINER_EVENT_HANDLERS(E)  \
     E(oncontrollerchange, HTML::EventNames::controllerchange) \
@@ -16,6 +20,12 @@
 
 namespace Web::HTML {
 
+struct RegistrationOptions {
+    Optional<String> scope;
+    Bindings::WorkerType type = Bindings::WorkerType::Classic;
+    Bindings::ServiceWorkerUpdateViaCache update_via_cache = Bindings::ServiceWorkerUpdateViaCache::Imports;
+};
+
 class ServiceWorkerContainer : public DOM::EventTarget {
     WEB_PLATFORM_OBJECT(ServiceWorkerContainer, DOM::EventTarget);
     JS_DECLARE_ALLOCATOR(ServiceWorkerContainer);
@@ -24,6 +34,8 @@ public:
     [[nodiscard]] static JS::NonnullGCPtr<ServiceWorkerContainer> create(JS::Realm& realm);
     virtual ~ServiceWorkerContainer() override;
 
+    JS::NonnullGCPtr<JS::Promise> register_(String script_url, RegistrationOptions const& options);
+
 #undef __ENUMERATE
 #define __ENUMERATE(attribute_name, event_name)       \
     void set_##attribute_name(WebIDL::CallbackType*); \
@@ -37,6 +49,8 @@ private:
     virtual void initialize(JS::Realm&) override;
     virtual void visit_edges(Cell::Visitor&) override;
 
+    void start_register(Optional<URL::URL> scope_url, URL::URL script_url, JS::NonnullGCPtr<WebIDL::Promise>, EnvironmentSettingsObject&, URL::URL referrer, Bindings::WorkerType, Bindings::ServiceWorkerUpdateViaCache);
+
     JS::NonnullGCPtr<EnvironmentSettingsObject> m_service_worker_client;
 };
 

+ 2 - 1
Userland/Libraries/LibWeb/HTML/ServiceWorkerContainer.idl

@@ -9,7 +9,8 @@ interface ServiceWorkerContainer : EventTarget {
     [FIXME] readonly attribute ServiceWorker? controller;
     [FIXME] readonly attribute Promise<ServiceWorkerRegistration> ready;
 
-    [FIXME, NewObject] Promise<ServiceWorkerRegistration> register((TrustedScriptURL or USVString) scriptURL, optional RegistrationOptions options = {});
+    // 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 = "");
     [FIXME, NewObject] Promise<FrozenArray<ServiceWorkerRegistration>> getRegistrations();

+ 1 - 0
Userland/Libraries/LibWeb/HTML/ServiceWorkerRegistration.idl

@@ -1,5 +1,6 @@
 #import <DOM/EventTarget.idl>
 #import <DOM/EventHandler.idl>
+#import <HTML/ServiceWorker.idl>
 
 // https://w3c.github.io/ServiceWorker/#serviceworkerregistration-interface
 [SecureContext, Exposed=(Window,Worker)]