From bb923983fc5435cca14bf82e847ce774daac9fb6 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 23 May 2024 13:57:08 +0100 Subject: [PATCH] LibWeb: Implement `WorkerGlobalScope.importScripts()` This method allows workers to synchronously import one or more scripts. --- .../expected/Worker/Worker-importScripts.txt | 1 + .../input/Worker/Worker-importScripts.html | 10 +++ .../worker-importScripts-scriptToImport.js | 3 + .../Text/input/Worker/worker-importScripts.js | 3 + .../LibWeb/HTML/Scripting/Fetching.cpp | 79 +++++++++++++++++++ .../LibWeb/HTML/Scripting/Fetching.h | 1 + .../LibWeb/HTML/WorkerGlobalScope.cpp | 46 ++++++++--- .../Libraries/LibWeb/HTML/WorkerGlobalScope.h | 3 +- 8 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/Worker/Worker-importScripts.txt create mode 100644 Tests/LibWeb/Text/input/Worker/Worker-importScripts.html create mode 100644 Tests/LibWeb/Text/input/Worker/worker-importScripts-scriptToImport.js create mode 100644 Tests/LibWeb/Text/input/Worker/worker-importScripts.js diff --git a/Tests/LibWeb/Text/expected/Worker/Worker-importScripts.txt b/Tests/LibWeb/Text/expected/Worker/Worker-importScripts.txt new file mode 100644 index 00000000000..cf32c9d3624 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Worker/Worker-importScripts.txt @@ -0,0 +1 @@ +importScripts() works as expected: YES diff --git a/Tests/LibWeb/Text/input/Worker/Worker-importScripts.html b/Tests/LibWeb/Text/input/Worker/Worker-importScripts.html new file mode 100644 index 00000000000..80403b87b06 --- /dev/null +++ b/Tests/LibWeb/Text/input/Worker/Worker-importScripts.html @@ -0,0 +1,10 @@ + + diff --git a/Tests/LibWeb/Text/input/Worker/worker-importScripts-scriptToImport.js b/Tests/LibWeb/Text/input/Worker/worker-importScripts-scriptToImport.js new file mode 100644 index 00000000000..8d6e77a857b --- /dev/null +++ b/Tests/LibWeb/Text/input/Worker/worker-importScripts-scriptToImport.js @@ -0,0 +1,3 @@ +function importedFunction() { + return "YES"; +} diff --git a/Tests/LibWeb/Text/input/Worker/worker-importScripts.js b/Tests/LibWeb/Text/input/Worker/worker-importScripts.js new file mode 100644 index 00000000000..677b777c5e9 --- /dev/null +++ b/Tests/LibWeb/Text/input/Worker/worker-importScripts.js @@ -0,0 +1,3 @@ +importScripts("worker-importScripts-scriptToImport.js"); +const fromImportedScript = importedFunction(); +self.postMessage(fromImportedScript); diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.cpp b/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.cpp index 1bfe7abb10d..c7222789f88 100644 --- a/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.cpp +++ b/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.cpp @@ -1,9 +1,11 @@ /* * Copyright (c) 2022-2023, networkException + * Copyright (c) 2024, Tim Ledbetter * * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -423,6 +425,83 @@ WebIDL::ExceptionOr fetch_classic_worker_script(URL::URL const& url, Envir return {}; } +// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-worker-imported-script +WebIDL::ExceptionOr> fetch_a_classic_worker_imported_script(URL::URL const& url, HTML::EnvironmentSettingsObject& settings_object, PerformTheFetchHook perform_fetch) +{ + auto& realm = settings_object.realm(); + auto& vm = realm.vm(); + + // 1. Let response be null. + JS::GCPtr response = nullptr; + + // 2. Let bodyBytes be null. + Fetch::Infrastructure::FetchAlgorithms::BodyBytes body_bytes; + + // 3. Let request be a new request whose URL is url, client is settingsObject, destination is "script", initiator type is "other", + // parser metadata is "not parser-inserted", and whose use-URL-credentials flag is set. + auto request = Fetch::Infrastructure::Request::create(vm); + request->set_url(url); + request->set_client(&settings_object); + request->set_destination(Fetch::Infrastructure::Request::Destination::Script); + request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Other); + request->set_parser_metadata(Fetch::Infrastructure::Request::ParserMetadata::NotParserInserted); + request->set_use_url_credentials(true); + + auto process_response_consume_body = [&response, &body_bytes](JS::NonnullGCPtr res, Fetch::Infrastructure::FetchAlgorithms::BodyBytes bb) { + // 1. Set bodyBytes to bb. + body_bytes = move(bb); + + // 2. Set response to res. + response = res; + }; + + // 4. If performFetch was given, run performFetch with request, isTopLevel, and with processResponseConsumeBody as defined below. + if (perform_fetch) { + TRY(perform_fetch->function()(request, TopLevelModule::Yes, move(process_response_consume_body))); + } + // Otherwise, fetch request with processResponseConsumeBody set to processResponseConsumeBody as defined below. + else { + Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {}; + fetch_algorithms_input.process_response_consume_body = move(process_response_consume_body); + TRY(Fetch::Fetching::fetch(realm, request, Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input)))); + } + + // 5. Pause until response is not null. + auto& event_loop = settings_object.responsible_event_loop(); + event_loop.spin_until([&]() { + return response; + }); + + // 6. Set response to response's unsafe response. + response = response->unsafe_response(); + + // 7. If any of the following are true: + // - bodyBytes is null or failure; + // - response's status is not an ok status; or + // - the result of extracting a MIME type from response's header list is not a JavaScript MIME type, + // then throw a "NetworkError" DOMException. + if (body_bytes.template has() || body_bytes.template has() + || !Fetch::Infrastructure::is_ok_status(response->status()) + || !response->header_list()->extract_mime_type().has_value() || !response->header_list()->extract_mime_type()->is_javascript()) { + return WebIDL::NetworkError::create(realm, "Network error"_fly_string); + } + + // 8. Let sourceText be the result of UTF-8 decoding bodyBytes. + auto decoder = TextCodec::decoder_for("UTF-8"sv); + VERIFY(decoder.has_value()); + auto source_text = TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, body_bytes.get()).release_value_but_fixme_should_propagate_errors(); + + // 9. Let mutedErrors be true if response was CORS-cross-origin, and false otherwise. + auto muted_errors = response->is_cors_cross_origin() ? ClassicScript::MutedErrors::Yes : ClassicScript::MutedErrors::No; + + // 10. Let script be the result of creating a classic script given sourceText, settingsObject, response's URL, the default classic script fetch options, and mutedErrors. + auto response_url = response->url().value_or({}); + auto script = ClassicScript::create(response_url.to_byte_string(), source_text, settings_object, response_url, 1, muted_errors); + + // 11. Return script. + return script; +} + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-module-worker-script-tree WebIDL::ExceptionOr fetch_module_worker_script_graph(URL::URL const& url, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination destination, EnvironmentSettingsObject& settings_object, PerformTheFetchHook perform_fetch, OnFetchScriptComplete on_complete) { diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.h b/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.h index 8df74d475d5..a43cba57cfb 100644 --- a/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.h +++ b/Userland/Libraries/LibWeb/HTML/Scripting/Fetching.h @@ -89,6 +89,7 @@ Optional resolve_url_like_module_specifier(ByteString const& specifier WebIDL::ExceptionOr fetch_classic_script(JS::NonnullGCPtr, URL::URL const&, EnvironmentSettingsObject& settings_object, ScriptFetchOptions options, CORSSettingAttribute cors_setting, String character_encoding, OnFetchScriptComplete on_complete); WebIDL::ExceptionOr fetch_classic_worker_script(URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, EnvironmentSettingsObject& settings_object, PerformTheFetchHook, OnFetchScriptComplete); +WebIDL::ExceptionOr> fetch_a_classic_worker_imported_script(URL::URL const&, HTML::EnvironmentSettingsObject&, PerformTheFetchHook = nullptr); WebIDL::ExceptionOr fetch_module_worker_script_graph(URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, EnvironmentSettingsObject& settings_object, PerformTheFetchHook, OnFetchScriptComplete); WebIDL::ExceptionOr fetch_worklet_module_worker_script_graph(URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, EnvironmentSettingsObject& settings_object, PerformTheFetchHook, OnFetchScriptComplete); void fetch_internal_module_script_graph(JS::Realm&, JS::ModuleRequest const& module_request, EnvironmentSettingsObject& fetch_client_settings_object, Fetch::Infrastructure::Request::Destination, ScriptFetchOptions const&, Script& referring_script, HashTable const& visited_set, PerformTheFetchHook, OnFetchScriptComplete on_complete); diff --git a/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.cpp b/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.cpp index 157424f4314..97b0333d873 100644 --- a/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.cpp +++ b/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -70,28 +71,51 @@ void WorkerGlobalScope::set_internal_port(JS::NonnullGCPtr port) } // https://html.spec.whatwg.org/multipage/workers.html#importing-scripts-and-libraries -WebIDL::ExceptionOr WorkerGlobalScope::import_scripts(Vector urls) +WebIDL::ExceptionOr WorkerGlobalScope::import_scripts(Vector const& urls, PerformTheFetchHook perform_fetch) { // The algorithm may optionally be customized by supplying custom perform the fetch hooks, // which if provided will be used when invoking fetch a classic worker-imported script. // NOTE: Service Workers is an example of a specification that runs this algorithm with its own options for the perform the fetch hook. // FIXME: 1. If worker global scope's type is "module", throw a TypeError exception. - // FIXME: 2. Let settings object be the current settings object. + + // 2. Let settings object be the current settings object. + auto& settings_object = HTML::current_settings_object(); // 3. If urls is empty, return. if (urls.is_empty()) return {}; - // FIXME: 4. Parse each value in urls relative to settings object. If any fail, throw a "SyntaxError" DOMException. - // FIXME: 5. For each url in the resulting URL records, run these substeps: - // 1. Fetch a classic worker-imported script given url and settings object, passing along any custom perform the fetch steps provided. - // If this succeeds, let script be the result. Otherwise, rethrow the exception. - // 2. Run the classic script script, with the rethrow errors argument set to true. - // NOTE: script will run until it either returns, fails to parse, fails to catch an exception, - // or gets prematurely aborted by the terminate a worker algorithm defined above. - // If an exception was thrown or if the script was prematurely aborted, then abort all these steps, - // letting the exception or aborting continue to be processed by the calling script. + // 4. Let urlRecords be « ». + Vector url_records; + url_records.ensure_capacity(urls.size()); + + // 5. For each url of urls: + for (auto const& url : urls) { + // 1. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings object. + auto url_record = settings_object.parse_url(url); + + // 2. If urlRecord is failure, then throw a "SyntaxError" DOMException. + if (!url_record.is_valid()) + return WebIDL::SyntaxError::create(realm(), "Invalid URL"_fly_string); + + // 3. Append urlRecord to urlRecords. + url_records.unchecked_append(url_record); + } + + // 6. For each urlRecord of urlRecords: + for (auto const& url_record : url_records) { + // 1. Fetch a classic worker-imported script given urlRecord and settings object, passing along performFetch if provided. + // If this succeeds, let script be the result. Otherwise, rethrow the exception. + auto classic_script = TRY(HTML::fetch_a_classic_worker_imported_script(url_record, settings_object, perform_fetch)); + + // 2. Run the classic script script, with the rethrow errors argument set to true. + // NOTE: script will run until it either returns, fails to parse, fails to catch an exception, + // or gets prematurely aborted by the terminate a worker algorithm defined above. + // If an exception was thrown or if the script was prematurely aborted, then abort all these steps, + // letting the exception or aborting continue to be processed by the calling script. + TRY(classic_script->run(ClassicScript::RethrowErrors::Yes)); + } return {}; } diff --git a/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.h b/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.h index 4f92f18cca2..65307d268fa 100644 --- a/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.h +++ b/Userland/Libraries/LibWeb/HTML/WorkerGlobalScope.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -66,7 +67,7 @@ public: JS::NonnullGCPtr location() const; JS::NonnullGCPtr navigator() const; - WebIDL::ExceptionOr import_scripts(Vector urls); + WebIDL::ExceptionOr import_scripts(Vector const& urls, PerformTheFetchHook = nullptr); #undef __ENUMERATE #define __ENUMERATE(attribute_name, event_name) \