diff options
author | Luke Wilde <lukew@serenityos.org> | 2021-10-14 16:12:53 +0100 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-02-08 17:47:44 +0000 |
commit | f71f404e0c56497bdf1b65a4a69b45de51e8e42c (patch) | |
tree | b02b0d04c3b34c3a167f1ed95ab6d22431d35519 /Userland/Libraries/LibWeb/HTML | |
parent | 4db5406d6234bad17d34fe8a0b461dea766271e6 (diff) | |
download | serenity-f71f404e0c56497bdf1b65a4a69b45de51e8e42c.zip |
LibWeb: Introduce the Environment Settings Object
The environment settings object is effectively the context a piece of
script is running under, for example, it contains the origin,
responsible document, realm, global object and event loop for the
current context. This effectively replaces ScriptExecutionContext, but
it cannot be removed in this commit as EventTarget still depends on it.
https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
Diffstat (limited to 'Userland/Libraries/LibWeb/HTML')
10 files changed, 680 insertions, 41 deletions
diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index 4bdd50c33b..b4d8e57c78 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -12,6 +12,7 @@ #include <LibWeb/DOM/Window.h> #include <LibWeb/HTML/BrowsingContext.h> #include <LibWeb/HTML/EventLoop/EventLoop.h> +#include <LibWeb/HTML/Scripting/Environments.h> #include <LibWeb/HighResolutionTime/Performance.h> namespace Web::HTML { @@ -207,15 +208,33 @@ void EventLoop::process() schedule(); } -// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task -void queue_global_task(HTML::Task::Source source, DOM::Document& document, Function<void()> steps) +// FIXME: This is here to paper over an issue in the HTML parser where it'll create new interpreters (and thus ESOs) on temporary documents created for innerHTML if it uses Document::realm() to get the global object. +// Use queue_global_task instead. +void old_queue_global_task_with_document(HTML::Task::Source source, DOM::Document& document, Function<void()> steps) { - // FIXME: This should take a global object as input and find the relevant document for it. main_thread_event_loop().task_queue().add(HTML::Task::create(source, &document, move(steps))); } +// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task +void queue_global_task(HTML::Task::Source source, JS::GlobalObject& global_object, Function<void()> steps) +{ + // 1. Let event loop be global's relevant agent's event loop. + auto& global_custom_data = verify_cast<Bindings::WebEngineCustomData>(*global_object.vm().custom_data()); + auto& event_loop = global_custom_data.event_loop; + + // 2. Let document be global's associated Document, if global is a Window object; otherwise null. + DOM::Document* document { nullptr }; + if (is<Bindings::WindowObject>(global_object)) { + auto& window_object = verify_cast<Bindings::WindowObject>(global_object); + document = &window_object.impl().associated_document(); + } + + // 3. Queue a task given source, event loop, document, and steps. + event_loop.task_queue().add(HTML::Task::create(source, document, move(steps))); +} + // https://html.spec.whatwg.org/#queue-a-microtask -void queue_a_microtask(DOM::Document& document, Function<void()> steps) +void queue_a_microtask(DOM::Document* document, Function<void()> steps) { // 1. If event loop was not given, set event loop to the implied event loop. auto& event_loop = HTML::main_thread_event_loop(); @@ -226,7 +245,7 @@ void queue_a_microtask(DOM::Document& document, Function<void()> steps) // 4. Set microtask's steps to steps. // 5. Set microtask's source to the microtask task source. // 6. Set microtask's document to document. - auto microtask = HTML::Task::create(HTML::Task::Source::Microtask, &document, move(steps)); + auto microtask = HTML::Task::create(HTML::Task::Source::Microtask, document, move(steps)); // FIXME: 7. Set microtask's script evaluation environment settings object set to an empty set. @@ -259,7 +278,9 @@ void EventLoop::perform_a_microtask_checkpoint() m_currently_running_task = nullptr; } - // FIXME: 4. For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object. + // 4. For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object. + for (auto& environment_settings_object : m_related_environment_settings_objects) + environment_settings_object.notify_about_rejected_promises({}); // FIXME: 5. Cleanup Indexed Database transactions. @@ -290,4 +311,30 @@ void EventLoop::unregister_document(Badge<DOM::Document>, DOM::Document& documen VERIFY(did_remove); } +void EventLoop::push_onto_backup_incumbent_settings_object_stack(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object) +{ + m_backup_incumbent_settings_object_stack.append(environment_settings_object); +} + +void EventLoop::pop_backup_incumbent_settings_object_stack(Badge<EnvironmentSettingsObject>) +{ + m_backup_incumbent_settings_object_stack.take_last(); +} + +EnvironmentSettingsObject& EventLoop::top_of_backup_incumbent_settings_object_stack() +{ + return m_backup_incumbent_settings_object_stack.last(); +} + +void EventLoop::register_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object) +{ + m_related_environment_settings_objects.append(environment_settings_object); +} + +void EventLoop::unregister_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object) +{ + bool did_remove = m_related_environment_settings_objects.remove_first_matching([&](auto& entry) { return &entry == &environment_settings_object; }); + VERIFY(did_remove); +} + } diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.h b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.h index b3ebdd5e07..658ff67dae 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.h +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.h @@ -55,6 +55,14 @@ public: NonnullRefPtrVector<DOM::Document> documents_in_this_event_loop() const; + void push_onto_backup_incumbent_settings_object_stack(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object); + void pop_backup_incumbent_settings_object_stack(Badge<EnvironmentSettingsObject>); + EnvironmentSettingsObject& top_of_backup_incumbent_settings_object_stack(); + bool is_backup_incumbent_settings_object_stack_empty() const { return m_backup_incumbent_settings_object_stack.is_empty(); } + + void register_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject&); + void unregister_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject&); + private: Type m_type { Type::Window }; @@ -72,11 +80,18 @@ private: bool m_performing_a_microtask_checkpoint { false }; Vector<WeakPtr<DOM::Document>> m_documents; + + // Used to implement step 4 of "perform a microtask checkpoint". + Vector<EnvironmentSettingsObject&> m_related_environment_settings_objects; + + // https://html.spec.whatwg.org/multipage/webappapis.html#backup-incumbent-settings-object-stack + Vector<EnvironmentSettingsObject&> m_backup_incumbent_settings_object_stack; }; EventLoop& main_thread_event_loop(); -void queue_global_task(HTML::Task::Source, DOM::Document&, Function<void()> steps); -void queue_a_microtask(DOM::Document&, Function<void()> steps); +void old_queue_global_task_with_document(HTML::Task::Source, DOM::Document&, Function<void()> steps); +void queue_global_task(HTML::Task::Source, JS::GlobalObject&, Function<void()> steps); +void queue_a_microtask(DOM::Document*, Function<void()> steps); void perform_a_microtask_checkpoint(); } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLScriptElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLScriptElement.cpp index 65caedc22c..6ecae73b9f 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLScriptElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLScriptElement.cpp @@ -85,7 +85,7 @@ void HTMLScriptElement::execute_script() dbgln_if(HTML_SCRIPT_DEBUG, "HTMLScriptElement: Running inline script"); // 3. Run the classic script given by the script's script for scriptElement. - verify_cast<ClassicScript>(*m_script).run(); + (void)verify_cast<ClassicScript>(*m_script).run(); // 4. Set document's currentScript attribute to oldCurrentScript. node_document->set_current_script({}, old_current_script); @@ -259,7 +259,8 @@ void HTMLScriptElement::prepare_script() // FIXME: 24. Let options be a script fetch options whose cryptographic nonce is cryptographic nonce, integrity metadata is integrity metadata, parser metadata is parser metadata, // credentials mode is module script credentials mode, and referrer policy is referrer policy. - // FIXME: 25. Let settings object be the element's node document's relevant settings object. + // 25. Let settings object be the element's node document's relevant settings object. + // NOTE: This will be done manually when this is required, as two of the use cases are inside lambdas that get executed later and thus a reference cannot be taken. // 26. If the element has a src content attribute, then: if (has_attribute(HTML::AttributeNames::src)) { @@ -302,7 +303,7 @@ void HTMLScriptElement::prepare_script() } // FIXME: This is all ad-hoc and needs work. - auto script = ClassicScript::create(url.to_string(), data, document().realm(), AK::URL()); + auto script = ClassicScript::create(url.to_string(), data, document().relevant_settings_object(), AK::URL()); // When the chosen algorithm asynchronously completes, set the script's script to the result. At that time, the script is ready. m_script = script; @@ -328,7 +329,7 @@ void HTMLScriptElement::prepare_script() // 1. Let script be the result of creating a classic script using source text, settings object, base URL, and options. // FIXME: Pass settings, base URL and options. - auto script = ClassicScript::create(m_document->url().to_string(), source_text, document().realm(), AK::URL()); + auto script = ClassicScript::create(m_document->url().to_string(), source_text, document().relevant_settings_object(), AK::URL()); // 2. Set the script's script to script. m_script = script; diff --git a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp index 8797cb1588..e6f95179d9 100644 --- a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp +++ b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp @@ -222,7 +222,7 @@ void HTMLParser::the_end() } // 6. Queue a global task on the DOM manipulation task source given the Document's relevant global object to run the following substeps: - queue_global_task(HTML::Task::Source::DOMManipulation, *m_document, [document = m_document]() mutable { + old_queue_global_task_with_document(HTML::Task::Source::DOMManipulation, m_document, [document = m_document]() mutable { // FIXME: 1. Set the Document's load timing info's DOM content loaded event start time to the current high resolution time given the Document's relevant global object. // 2. Fire an event named DOMContentLoaded at the Document object, with its bubbles attribute initialized to true. @@ -249,7 +249,7 @@ void HTMLParser::the_end() }); // 9. Queue a global task on the DOM manipulation task source given the Document's relevant global object to run the following steps: - queue_global_task(HTML::Task::Source::DOMManipulation, *m_document, [document = m_document]() mutable { + old_queue_global_task_with_document(HTML::Task::Source::DOMManipulation, m_document, [document = m_document]() mutable { // 1. Update the current document readiness to "complete". document->update_readiness(HTML::DocumentReadyState::Complete); diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp index 4d43864e83..8749048c74 100644 --- a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp +++ b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp @@ -7,13 +7,16 @@ #include <AK/Debug.h> #include <LibCore/ElapsedTimer.h> #include <LibJS/Interpreter.h> +#include <LibWeb/Bindings/ExceptionOrUtils.h> +#include <LibWeb/DOM/DOMException.h> #include <LibWeb/HTML/Scripting/ClassicScript.h> +#include <LibWeb/HTML/Scripting/Environments.h> #include <LibWeb/HTML/Scripting/ExceptionReporter.h> namespace Web::HTML { // https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-classic-script -NonnullRefPtr<ClassicScript> ClassicScript::create(String filename, StringView source, JS::Realm& realm, AK::URL base_url, MutedErrors muted_errors) +NonnullRefPtr<ClassicScript> ClassicScript::create(String filename, StringView source, EnvironmentSettingsObject& environment_settings_object, AK::URL base_url, MutedErrors muted_errors) { // 1. If muted errors was not provided, let it be false. (NOTE: This is taken care of by the default argument.) @@ -24,9 +27,9 @@ NonnullRefPtr<ClassicScript> ClassicScript::create(String filename, StringView s // FIXME: 3. If scripting is disabled for settings, then set source to the empty string. // 4. Let script be a new classic script that this algorithm will subsequently initialize. - auto script = adopt_ref(*new ClassicScript(move(base_url), move(filename))); + auto script = adopt_ref(*new ClassicScript(move(base_url), move(filename), environment_settings_object)); - // FIXME: 5. Set script's settings object to settings. + // 5. Set script's settings object to settings. (NOTE: This was already done when constructing.) // 6. Set script's base URL to baseURL. (NOTE: This was already done when constructing.) @@ -36,16 +39,21 @@ NonnullRefPtr<ClassicScript> ClassicScript::create(String filename, StringView s script->m_muted_errors = muted_errors; // FIXME: 9. Set script's parse error and error to rethrow to null. + // NOTE: Error to rethrow was set to null in the construction of ClassicScript. We do not have parse error as it would currently go unused. // 10. Let result be ParseScript(source, settings's Realm, script). auto parse_timer = Core::ElapsedTimer::start_new(); - auto result = JS::Script::parse(source, realm, script->filename()); + auto result = JS::Script::parse(source, environment_settings_object.realm(), script->filename()); dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Parsed {} in {}ms", script->filename(), parse_timer.elapsed()); // 11. If result is a list of errors, then: - if (result.is_error()) { - // FIXME: 1. Set script's parse error and its error to rethrow to result[0]. + auto& parse_error = result.error().first(); + dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Failed to parse: {}", parse_error.to_string()); + + // FIXME: 1. Set script's parse error and its error to rethrow to result[0]. + // We do not have parse error as it would currently go unused. + script->m_error_to_rethrow = parse_error; // 2. Return script. return script; @@ -59,37 +67,88 @@ NonnullRefPtr<ClassicScript> ClassicScript::create(String filename, StringView s } // https://html.spec.whatwg.org/multipage/webappapis.html#run-a-classic-script -JS::Value ClassicScript::run(RethrowErrors rethrow_errors) +JS::Completion ClassicScript::run(RethrowErrors rethrow_errors) { - if (!m_script_record) { - // FIXME: Throw a SyntaxError per the spec. - dbgln("ClassicScript: Unable to run script {}", filename()); - return {}; + auto& global_object = m_settings_object.global_object(); + auto& vm = global_object.vm(); + + // 1. Let settings be the settings object of script. (NOTE: Not necessary) + + // 2. Check if we can run script with settings. If this returns "do not run" then return NormalCompletion(empty). + if (m_settings_object.can_run_script() == RunScriptDecision::DoNotRun) + return JS::normal_completion({}); + + // 3. Prepare to run script given settings. + m_settings_object.prepare_to_run_script(); + + // 4. Let evaluationStatus be null. + JS::Completion evaluation_status; + + // 5. If script's error to rethrow is not null, then set evaluationStatus to Completion { [[Type]]: throw, [[Value]]: script's error to rethrow, [[Target]]: empty }. + if (m_error_to_rethrow.has_value()) { + evaluation_status = vm.throw_completion<JS::SyntaxError>(global_object, m_error_to_rethrow.value().to_string()); + } else { + auto timer = Core::ElapsedTimer::start_new(); + + // 6. Otherwise, set evaluationStatus to ScriptEvaluation(script's record). + auto interpreter = JS::Interpreter::create_with_existing_realm(m_script_record->realm()); + + evaluation_status = interpreter->run(*m_script_record); + + // FIXME: If ScriptEvaluation does not complete because the user agent has aborted the running script, leave evaluationStatus as null. + + dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Finished running script {}, Duration: {}ms", filename(), timer.elapsed()); } - dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Running script {}", filename()); - (void)rethrow_errors; + // 7. If evaluationStatus is an abrupt completion, then: + if (evaluation_status.is_abrupt()) { + // 1. If rethrow errors is true and script's muted errors is false, then: + if (rethrow_errors == RethrowErrors::Yes && m_muted_errors == MutedErrors::No) { + // 1. Clean up after running script with settings. + m_settings_object.clean_up_after_running_script(); - auto timer = Core::ElapsedTimer::start_new(); + // 2. Rethrow evaluationStatus.[[Value]]. + return JS::throw_completion(*evaluation_status.value()); + } - // 6. Otherwise, set evaluationStatus to ScriptEvaluation(script's record). - auto interpreter = JS::Interpreter::create_with_existing_realm(m_script_record->realm()); + // 2. If rethrow errors is true and script's muted errors is true, then: + if (rethrow_errors == RethrowErrors::Yes && m_muted_errors == MutedErrors::Yes) { + // 1. Clean up after running script with settings. + m_settings_object.clean_up_after_running_script(); - auto evaluation_status = interpreter->run(*m_script_record); + // 2. Throw a "NetworkError" DOMException. + return Bindings::throw_dom_exception_if_needed(global_object, [] { + return DOM::NetworkError::create("Script error."); + }).release_error(); + } - // FIXME: If ScriptEvaluation does not complete because the user agent has aborted the running script, leave evaluationStatus as null. + // 3. Otherwise, rethrow errors is false. Perform the following steps: + VERIFY(rethrow_errors == RethrowErrors::No); - dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Finished running script {}, Duration: {}ms", filename(), timer.elapsed()); - if (evaluation_status.is_error()) { - // FIXME: Propagate error according to the spec. + // 1. Report the exception given by evaluationStatus.[[Value]] for script. report_exception(evaluation_status); - return {}; + + // 2. Clean up after running script with settings. + m_settings_object.clean_up_after_running_script(); + + // 3. Return evaluationStatus. + return evaluation_status; } - return evaluation_status.value(); + + // 8. Clean up after running script with settings. + m_settings_object.clean_up_after_running_script(); + + // 9. If evaluationStatus is a normal completion, then return evaluationStatus. + VERIFY(!evaluation_status.is_abrupt()); + return evaluation_status; + + // FIXME: 10. If we've reached this point, evaluationStatus was left as null because the script was aborted prematurely during evaluation. + // Return Completion { [[Type]]: throw, [[Value]]: a new "QuotaExceededError" DOMException, [[Target]]: empty }. } -ClassicScript::ClassicScript(AK::URL base_url, String filename) +ClassicScript::ClassicScript(AK::URL base_url, String filename, EnvironmentSettingsObject& environment_settings_object) : Script(move(base_url), move(filename)) + , m_settings_object(environment_settings_object) { } diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h index 34af98d50c..80affbec8f 100644 --- a/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h +++ b/Userland/Libraries/LibWeb/HTML/Scripting/ClassicScript.h @@ -7,6 +7,7 @@ #pragma once #include <LibJS/Script.h> +#include <LibWeb/Forward.h> #include <LibWeb/HTML/Scripting/Script.h> namespace Web::HTML { @@ -20,22 +21,26 @@ public: No, Yes, }; - static NonnullRefPtr<ClassicScript> create(String filename, StringView source, JS::Realm&, AK::URL base_url, MutedErrors = MutedErrors::No); + static NonnullRefPtr<ClassicScript> create(String filename, StringView source, EnvironmentSettingsObject&, AK::URL base_url, MutedErrors = MutedErrors::No); JS::Script* script_record() { return m_script_record; } JS::Script const* script_record() const { return m_script_record; } + EnvironmentSettingsObject& settings_object() { return m_settings_object; } + enum class RethrowErrors { No, Yes, }; - JS::Value run(RethrowErrors = RethrowErrors::No); + JS::Completion run(RethrowErrors = RethrowErrors::No); private: - ClassicScript(AK::URL base_url, String filename); + ClassicScript(AK::URL base_url, String filename, EnvironmentSettingsObject& environment_settings_object); + EnvironmentSettingsObject& m_settings_object; RefPtr<JS::Script> m_script_record; MutedErrors m_muted_errors { MutedErrors::No }; + Optional<JS::Parser::Error> m_error_to_rethrow; }; } diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp new file mode 100644 index 0000000000..7f46a11ff6 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibWeb/Bindings/MainThreadVM.h> +#include <LibWeb/Bindings/WindowObject.h> +#include <LibWeb/DOM/Document.h> +#include <LibWeb/DOM/Window.h> +#include <LibWeb/HTML/PromiseRejectionEvent.h> +#include <LibWeb/HTML/Scripting/Environments.h> + +namespace Web::HTML { + +EnvironmentSettingsObject::EnvironmentSettingsObject(JS::ExecutionContext& realm_execution_context) + : m_realm_execution_context(realm_execution_context) +{ + // Register with the responsible event loop so we can perform step 4 of "perform a microtask checkpoint". + responsible_event_loop().register_environment_settings_object({}, *this); +} + +EnvironmentSettingsObject::~EnvironmentSettingsObject() +{ + responsible_event_loop().unregister_environment_settings_object({}, *this); +} + +JS::ExecutionContext& EnvironmentSettingsObject::realm_execution_context() +{ + // NOTE: All environment settings objects are created with a realm execution context, so it's stored and returned here in the base class. + return m_realm_execution_context; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object%27s-realm +JS::Realm& EnvironmentSettingsObject::realm() +{ + // An environment settings object's realm execution context's Realm component is the environment settings object's Realm. + return *realm_execution_context().realm; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-global +JS::GlobalObject& EnvironmentSettingsObject::global_object() +{ + // An environment settings object's Realm then has a [[GlobalObject]] field, which contains the environment settings object's global object. + return realm().global_object(); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#responsible-event-loop +EventLoop& EnvironmentSettingsObject::responsible_event_loop() +{ + // An environment settings object's responsible event loop is its global object's relevant agent's event loop. + // This is here in case the realm that is holding onto this ESO is destroyed before the ESO is. The responsible event loop pointer is needed in the ESO destructor to deregister from the event loop. + // FIXME: Figure out why the realm can be destroyed before the ESO, as the realm is holding onto this with an OwnPtr, but the heap block deallocator calls the ESO destructor directly instead of through the realm destructor. + if (m_responsible_event_loop) + return *m_responsible_event_loop; + + auto& vm = global_object().vm(); + auto& event_loop = verify_cast<Bindings::WebEngineCustomData>(vm.custom_data())->event_loop; + m_responsible_event_loop = &event_loop; + return event_loop; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#check-if-we-can-run-script +RunScriptDecision EnvironmentSettingsObject::can_run_script() +{ + // 1. If the global object specified by settings is a Window object whose Document object is not fully active, then return "do not run". + if (is<Bindings::WindowObject>(global_object()) && !verify_cast<Bindings::WindowObject>(global_object()).impl().associated_document().is_fully_active()) + return RunScriptDecision::DoNotRun; + + // FIXME: 2. If scripting is disabled for settings, then return "do not run". + + // 3. Return "run". + return RunScriptDecision::Run; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#prepare-to-run-script +void EnvironmentSettingsObject::prepare_to_run_script() +{ + // 1. Push settings's realm execution context onto the JavaScript execution context stack; it is now the running JavaScript execution context. + global_object().vm().push_execution_context(realm_execution_context(), global_object()); + + // FIXME: 2. Add settings to the currently running task's script evaluation environment settings object set. +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script +void EnvironmentSettingsObject::clean_up_after_running_script() +{ + auto& vm = global_object().vm(); + + // 1. Assert: settings's realm execution context is the running JavaScript execution context. + VERIFY(&realm_execution_context() == &vm.running_execution_context()); + + // 2. Remove settings's realm execution context from the JavaScript execution context stack. + vm.pop_execution_context(); + + // 3. If the JavaScript execution context stack is now empty, perform a microtask checkpoint. (If this runs scripts, these algorithms will be invoked reentrantly.) + if (vm.execution_context_stack().is_empty()) + responsible_event_loop().perform_a_microtask_checkpoint(); +} + +static JS::ExecutionContext* top_most_script_having_execution_context(JS::VM& vm) +{ + // Here, the topmost script-having execution context is the topmost entry of the JavaScript execution context stack that has a non-null ScriptOrModule component, + // or null if there is no such entry in the JavaScript execution context stack. + auto execution_context = vm.execution_context_stack().last_matching([&](JS::ExecutionContext* context) { + return !context->script_or_module.has<Empty>(); + }); + + if (!execution_context.has_value()) + return nullptr; + + return execution_context.value(); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#prepare-to-run-a-callback +void EnvironmentSettingsObject::prepare_to_run_callback() +{ + auto& vm = global_object().vm(); + + // 1. Push settings onto the backup incumbent settings object stack. + // NOTE: The spec doesn't say which event loop's stack to put this on. However, all the examples of the incumbent settings object use iframes and cross browsing context communication to demonstrate the concept. + // This means that it must rely on some global state that can be accessed by all browsing contexts, which is the main thread event loop. + HTML::main_thread_event_loop().push_onto_backup_incumbent_settings_object_stack({}, *this); + + // 2. Let context be the topmost script-having execution context. + auto* context = top_most_script_having_execution_context(vm); + + // 3. If context is not null, increment context's skip-when-determining-incumbent counter. + if (context) + context->skip_when_determining_incumbent_counter++; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-a-callback +void EnvironmentSettingsObject::clean_up_after_running_callback() +{ + auto& vm = global_object().vm(); + + // 1. Let context be the topmost script-having execution context. + auto* context = top_most_script_having_execution_context(vm); + + // 2. If context is not null, decrement context's skip-when-determining-incumbent counter. + if (context) + context->skip_when_determining_incumbent_counter--; + + // 3. Assert: the topmost entry of the backup incumbent settings object stack is settings. + auto& event_loop = HTML::main_thread_event_loop(); + VERIFY(&event_loop.top_of_backup_incumbent_settings_object_stack() == this); + + // 4. Remove settings from the backup incumbent settings object stack. + event_loop.pop_backup_incumbent_settings_object_stack({}); +} + +void EnvironmentSettingsObject::push_onto_outstanding_rejected_promises_weak_set(JS::Promise* promise) +{ + m_outstanding_rejected_promises_weak_set.append(promise); +} + +bool EnvironmentSettingsObject::remove_from_outstanding_rejected_promises_weak_set(JS::Promise* promise) +{ + return m_outstanding_rejected_promises_weak_set.remove_first_matching([&](JS::Promise* promise_in_set) { + return promise == promise_in_set; + }); +} + +void EnvironmentSettingsObject::push_onto_about_to_be_notified_rejected_promises_list(JS::Handle<JS::Promise> promise) +{ + m_about_to_be_notified_rejected_promises_list.append(move(promise)); +} + +bool EnvironmentSettingsObject::remove_from_about_to_be_notified_rejected_promises_list(JS::Promise* promise) +{ + return m_about_to_be_notified_rejected_promises_list.remove_first_matching([&](JS::Handle<JS::Promise> promise_in_list) { + return promise == promise_in_list.cell(); + }); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#notify-about-rejected-promises +void EnvironmentSettingsObject::notify_about_rejected_promises(Badge<EventLoop>) +{ + // 1. Let list be a copy of settings object's about-to-be-notified rejected promises list. + auto list = m_about_to_be_notified_rejected_promises_list; + + // 2. If list is empty, return. + if (list.is_empty()) + return; + + // 3. Clear settings object's about-to-be-notified rejected promises list. + m_about_to_be_notified_rejected_promises_list.clear(); + + // 4. Let global be settings object's global object. + auto& global = global_object(); + + // 5. Queue a global task on the DOM manipulation task source given global to run the following substep: + queue_global_task(Task::Source::DOMManipulation, global, [this, global = JS::make_handle(&global), list = move(list)]() mutable { + // 1. For each promise p in list: + for (auto promise_handle : list) { + auto& promise = *promise_handle.cell(); + + // 1. If p's [[PromiseIsHandled]] internal slot is true, continue to the next iteration of the loop. + if (promise.is_handled()) + continue; + + // 2. Let notHandled be the result of firing an event named unhandledrejection at global, using PromiseRejectionEvent, with the cancelable attribute initialized to true, + // the promise attribute initialized to p, and the reason attribute initialized to the value of p's [[PromiseResult]] internal slot. + PromiseRejectionEventInit event_init { + { + .bubbles = false, + .cancelable = true, + .composed = false, + }, + // Sadly we can't use .promise and .reason here, as we can't use the designator on the initialization of DOM::EventInit above. + /* .promise = */ promise_handle, + /* .reason = */ promise.result(), + }; + auto promise_rejection_event = PromiseRejectionEvent::create(HTML::EventNames::unhandledrejection, event_init); + + // FIXME: This currently assumes that global is a WindowObject. + auto& window = verify_cast<Bindings::WindowObject>(*global.cell()); + + bool not_handled = window.impl().dispatch_event(move(promise_rejection_event)); + + // 3. If notHandled is false, then the promise rejection is handled. Otherwise, the promise rejection is not handled. + + // 4. If p's [[PromiseIsHandled]] internal slot is false, add p to settings object's outstanding rejected promises weak set. + if (!promise.is_handled()) + m_outstanding_rejected_promises_weak_set.append(&promise); + + // This algorithm results in promise rejections being marked as handled or not handled. These concepts parallel handled and not handled script errors. + // If a rejection is still not handled after this, then the rejection may be reported to a developer console. + if (not_handled) + dbgln("WARNING: A promise was rejected without any handlers. promise={:p}, result={}", &promise, promise.result().to_string_without_side_effects()); + } + }); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#incumbent-settings-object +EnvironmentSettingsObject& incumbent_settings_object() +{ + auto& event_loop = HTML::main_thread_event_loop(); + auto& vm = event_loop.vm(); + + // 1. Let context be the topmost script-having execution context. + auto* context = top_most_script_having_execution_context(vm); + + // 2. If context is null, or if context's skip-when-determining-incumbent counter is greater than zero, then: + if (!context || context->skip_when_determining_incumbent_counter > 0) { + // 1. Assert: the backup incumbent settings object stack is not empty. + // NOTE: If this assertion fails, it's because the incumbent settings object was used with no involvement of JavaScript. + VERIFY(!event_loop.is_backup_incumbent_settings_object_stack_empty()); + + // 2. Return the topmost entry of the backup incumbent settings object stack. + return event_loop.top_of_backup_incumbent_settings_object_stack(); + } + + // 3. Return context's Realm component's settings object. + return verify_cast<EnvironmentSettingsObject>(*context->realm->host_defined()); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#concept-incumbent-realm +JS::Realm& incumbent_realm() +{ + // Then, the incumbent Realm is the Realm of the incumbent settings object. + return incumbent_settings_object().realm(); +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#concept-incumbent-global +JS::GlobalObject& incumbent_global_object() +{ + // Similarly, the incumbent global object is the global object of the incumbent settings object. + return incumbent_settings_object().global_object(); +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/Environments.h b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.h new file mode 100644 index 0000000000..a006b80257 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.h @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/URL.h> +#include <LibJS/Runtime/ExecutionContext.h> +#include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/Realm.h> +#include <LibWeb/HTML/BrowsingContext.h> +#include <LibWeb/HTML/EventLoop/EventLoop.h> +#include <LibWeb/Origin.h> + +namespace Web::HTML { + +// https://html.spec.whatwg.org/multipage/webappapis.html#environment +struct Environment { + // FIXME: An id https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-id + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-creation-url + AK::URL creation_url; + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-creation-url + AK::URL top_level_creation_url; + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-top-level-origin + Origin top_level_origin; + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-target-browsing-context + RefPtr<BrowsingContext> target_browsing_context; + + // FIXME: An active service worker https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-active-service-worker + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-execution-ready-flag + bool execution_ready { false }; +}; + +enum class CanUseCrossOriginIsolatedAPIs { + No, + Yes, +}; + +enum class RunScriptDecision { + Run, + DoNotRun, +}; + +// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object +struct EnvironmentSettingsObject + : public Environment + , public JS::Realm::HostDefined { + virtual ~EnvironmentSettingsObject() override; + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-target-browsing-context + JS::ExecutionContext& realm_execution_context(); + + // FIXME: A module map https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-module-map + + // https://html.spec.whatwg.org/multipage/webappapis.html#responsible-document + virtual RefPtr<DOM::Document> responsible_document() = 0; + + // https://html.spec.whatwg.org/multipage/webappapis.html#api-url-character-encoding + virtual String api_url_character_encoding() = 0; + + // https://html.spec.whatwg.org/multipage/webappapis.html#api-base-url + virtual AK::URL api_base_url() = 0; + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-origin + virtual Origin origin() = 0; + + // FIXME: A policy container https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-policy-container + + // https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-cross-origin-isolated-capability + virtual CanUseCrossOriginIsolatedAPIs cross_origin_isolated_capability() = 0; + + JS::Realm& realm(); + JS::GlobalObject& global_object(); + EventLoop& responsible_event_loop(); + + RunScriptDecision can_run_script(); + void prepare_to_run_script(); + void clean_up_after_running_script(); + + void prepare_to_run_callback(); + void clean_up_after_running_callback(); + + void push_onto_outstanding_rejected_promises_weak_set(JS::Promise*); + + // Returns true if removed, false otherwise. + bool remove_from_outstanding_rejected_promises_weak_set(JS::Promise*); + + void push_onto_about_to_be_notified_rejected_promises_list(JS::Handle<JS::Promise>); + + // Returns true if removed, false otherwise. + bool remove_from_about_to_be_notified_rejected_promises_list(JS::Promise*); + + void notify_about_rejected_promises(Badge<EventLoop>); + +protected: + explicit EnvironmentSettingsObject(JS::ExecutionContext& realm_execution_context); + +private: + JS::ExecutionContext& m_realm_execution_context; + EventLoop* m_responsible_event_loop { nullptr }; + + // https://html.spec.whatwg.org/multipage/webappapis.html#outstanding-rejected-promises-weak-set + // The outstanding rejected promises weak set must not create strong references to any of its members, and implementations are free to limit its size, e.g. by removing old entries from it when new ones are added. + Vector<JS::Promise*> m_outstanding_rejected_promises_weak_set; + + // https://html.spec.whatwg.org/multipage/webappapis.html#about-to-be-notified-rejected-promises-list + Vector<JS::Handle<JS::Promise>> m_about_to_be_notified_rejected_promises_list; +}; + +EnvironmentSettingsObject& incumbent_settings_object(); +JS::Realm& incumbent_realm(); +JS::GlobalObject& incumbent_global_object(); + +} diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.cpp b/Userland/Libraries/LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.cpp new file mode 100644 index 0000000000..089aaed189 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibWeb/Bindings/WindowObject.h> +#include <LibWeb/DOM/Document.h> +#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h> + +namespace Web::HTML { + +WindowEnvironmentSettingsObject::WindowEnvironmentSettingsObject(DOM::Window& window, JS::ExecutionContext& execution_context) + : EnvironmentSettingsObject(execution_context) + , m_window(window) +{ +} + +// https://html.spec.whatwg.org/multipage/window-object.html#set-up-a-window-environment-settings-object +void WindowEnvironmentSettingsObject::setup(AK::URL& creation_url, JS::ExecutionContext& execution_context) +{ + // 1. Let realm be the value of execution context's Realm component. + auto* realm = execution_context.realm; + VERIFY(realm); + + // 2. Let window be realm's global object. + // NOTE: We want to store the Window impl rather than the WindowObject. + auto& window = verify_cast<Bindings::WindowObject>(realm->global_object()).impl(); + + // 3. Let settings object be a new environment settings object whose algorithms are defined as follows: + // NOTE: See the functions defined for this class. + auto settings_object = adopt_own(*new WindowEnvironmentSettingsObject(window, execution_context)); + + // FIXME: 4. If reservedEnvironment is non-null, then: + // FIXME: 1. Set settings object's id to reservedEnvironment's id, target browsing context to reservedEnvironment's target browsing context, and active service worker to reservedEnvironment's active service worker. + // FIXME: 2. Set reservedEnvironment's id to the empty string. + + // FIXME: 5. Otherwise, set settings object's id to a new unique opaque string, settings object's target browsing context to null, and settings object's active service worker to null. + settings_object->target_browsing_context = nullptr; + + // FIXME: 6. Set settings object's creation URL to creationURL, settings object's top-level creation URL to topLevelCreationURL, and settings object's top-level origin to topLevelOrigin. + settings_object->creation_url = creation_url; + + // 7. Set realm's [[HostDefined]] field to settings object. + realm->set_host_defined(move(settings_object)); +} + +// https://html.spec.whatwg.org/multipage/window-object.html#script-settings-for-window-objects:responsible-document +RefPtr<DOM::Document> WindowEnvironmentSettingsObject::responsible_document() +{ + // Return window's associated Document. + return m_window->associated_document(); +} + +// https://html.spec.whatwg.org/multipage/window-object.html#script-settings-for-window-objects:api-url-character-encoding +String WindowEnvironmentSettingsObject::api_url_character_encoding() +{ + // Return the current character encoding of window's associated Document. + return m_window->associated_document().encoding_or_default(); +} + +// https://html.spec.whatwg.org/multipage/window-object.html#script-settings-for-window-objects:api-base-url +AK::URL WindowEnvironmentSettingsObject::api_base_url() +{ + // FIXME: Return the current base URL of window's associated Document. + // (This currently just returns the current document URL, not accounting for <base> elements and such) + return m_window->associated_document().url(); +} + +// https://html.spec.whatwg.org/multipage/window-object.html#script-settings-for-window-objects:concept-settings-object-origin +Origin WindowEnvironmentSettingsObject::origin() +{ + // Return the origin of window's associated Document. + return m_window->associated_document().origin(); +} + +// https://html.spec.whatwg.org/multipage/window-object.html#script-settings-for-window-objects:concept-settings-object-cross-origin-isolated-capability +CanUseCrossOriginIsolatedAPIs WindowEnvironmentSettingsObject::cross_origin_isolated_capability() +{ + // FIXME: Return true if both of the following hold, and false otherwise: + // 1. realm's agent cluster's cross-origin-isolation mode is "concrete", and + // 2. window's associated Document is allowed to use the "cross-origin-isolated" feature. + TODO(); +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h b/Userland/Libraries/LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h new file mode 100644 index 0000000000..8577f82e04 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibWeb/DOM/Window.h> +#include <LibWeb/HTML/Scripting/Environments.h> + +namespace Web::HTML { + +class WindowEnvironmentSettingsObject final : public EnvironmentSettingsObject { +public: + static void setup(AK::URL& creation_url, JS::ExecutionContext& execution_context /* FIXME: null or an environment reservedEnvironment, a URL topLevelCreationURL, and an origin topLevelOrigin */); + + virtual ~WindowEnvironmentSettingsObject() override = default; + + virtual RefPtr<DOM::Document> responsible_document() override; + virtual String api_url_character_encoding() override; + virtual AK::URL api_base_url() override; + virtual Origin origin() override; + virtual CanUseCrossOriginIsolatedAPIs cross_origin_isolated_capability() override; + +private: + WindowEnvironmentSettingsObject(DOM::Window&, JS::ExecutionContext& execution_context); + + NonnullRefPtr<DOM::Window> m_window; +}; + +} |