/* * Copyright (c) 2021-2025, Luke Wilde * Copyright (c) 2022, Linus Groh * Copyright (c) 2022, networkException * Copyright (c) 2024-2026, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::HTML { GC_DEFINE_ALLOCATOR(Environment); Environment::~Environment() = default; void Environment::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(target_browsing_context); } EnvironmentSettingsObject::EnvironmentSettingsObject(NonnullOwnPtr realm_execution_context) : m_realm_execution_context(move(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); } void EnvironmentSettingsObject::finalize() { responsible_event_loop().unregister_environment_settings_object({}, *this); Base::finalize(); } void EnvironmentSettingsObject::initialize(JS::Realm& realm) { Base::initialize(realm); m_module_map = realm.heap().allocate(); m_universal_global_scope = &as(global_object()); } void EnvironmentSettingsObject::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_responsible_event_loop); visitor.visit(m_module_map); m_realm_execution_context->visit_edges(visitor); visitor.visit(m_storage_manager); visitor.visit(m_service_worker_registration_object_map); visitor.visit(m_service_worker_object_map); } void EnvironmentSettingsObject::discard_environment() { // https://w3c.github.io/ServiceWorker/#ref-for-environment-discarding-steps // Each service worker client has the following environment discarding steps: // 1. Set client’s discarded flag. set_discarded(true); } 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; } JS::ExecutionContext const& EnvironmentSettingsObject::realm_execution_context() const { // 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; } ModuleMap& EnvironmentSettingsObject::module_map() { return *m_module_map; } // 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::Object& 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(); } UniversalGlobalScopeMixin& EnvironmentSettingsObject::universal_global_scope() { return *m_universal_global_scope; } // 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; m_responsible_event_loop = relevant_agent(global_object()).event_loop; return *m_responsible_event_loop; } // https://html.spec.whatwg.org/multipage/webappapis.html#check-if-we-can-run-script RunScriptDecision can_run_script(EnvironmentSettingsObject const& settings) { // 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 (auto const* window = as_if(settings.global_object())) { if (!window->associated_document().is_fully_active()) return RunScriptDecision::DoNotRun; } // 2. If scripting is disabled for settings, then return "do not run". if (is_scripting_disabled(settings)) return RunScriptDecision::DoNotRun; // 3. Return "run". return RunScriptDecision::Run; } // https://html.spec.whatwg.org/multipage/webappapis.html#prepare-to-run-script void prepare_to_run_script(EnvironmentSettingsObject& settings) { // 1. Push settings's realm execution context onto the JavaScript execution context stack; it is now the running JavaScript execution context. settings.realm().vm().push_execution_context(settings.realm_execution_context()); // FIXME: 2. Add settings to the surrounding agent's event loop's currently running task's script evaluation environment settings object set. } // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script void clean_up_after_running_script(EnvironmentSettingsObject const& settings) { auto& vm = settings.global_object().vm(); // 1. Assert: settings's realm execution context is the running JavaScript execution context. VERIFY(&settings.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()) main_thread_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(); }); 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 prepare_to_run_callback(EnvironmentSettingsObject& settings) { auto& vm = settings.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_realm_stack(settings); // 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/urls-and-fetching.html#parse-a-url Optional EnvironmentSettingsObject::parse_url(StringView url) { // 1. Let baseURL be environment's base URL, if environment is a Document object; otherwise environment's API base URL. auto base_url = api_base_url(); // 2. Return the result of applying the URL parser to url, with baseURL. return DOMURL::parse(url, base_url); } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#encoding-parsing-a-url Optional EnvironmentSettingsObject::encoding_parse_url(StringView url) { // 1. Let encoding be UTF-8. auto encoding = "UTF-8"_string; // 2. If environment is a Document object, then set encoding to environment's character encoding. // 3. Otherwise, if environment's relevant global object is a Window object, set encoding to environment's relevant // global object's associated Document's character encoding. if (is(global_object())) encoding = static_cast(global_object()).associated_document().encoding_or_default(); // 4. Let baseURL be environment's base URL, if environment is a Document object; otherwise environment's API base URL. auto base_url = api_base_url(); // 5. Return the result of applying the URL parser to url, with baseURL and encoding. return DOMURL::parse(url, base_url, encoding); } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#encoding-parsing-and-serializing-a-url Optional EnvironmentSettingsObject::encoding_parse_and_serialize_url(StringView url) { // 1. Let url be the result of encoding-parsing a URL given url, relative to environment. auto parsed_url = encoding_parse_url(url); // 2. If url is failure, then return failure. if (!parsed_url.has_value()) return {}; // 3. Return the result of applying the URL serializer to url. return parsed_url->serialize(); } // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-a-callback void clean_up_after_running_callback(EnvironmentSettingsObject const& settings) { auto& vm = settings.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 realm stack is settings. auto& event_loop = HTML::main_thread_event_loop(); VERIFY(&event_loop.top_of_backup_incumbent_realm_stack() == &settings); // 4. Remove settings from the backup incumbent realm stack. event_loop.pop_backup_incumbent_realm_stack(); } // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-script bool is_scripting_enabled(EnvironmentSettingsObject const& settings) { // Scripting is enabled for an environment settings object settings when all of the following conditions are true: // The user agent supports scripting. // NOTE: This is always true in LibWeb :^) // FIXME: Do the right thing for workers. if (!is(settings.global_object())) return true; // The user has not disabled scripting for realm at this time. (User agents may provide users with the option to disable scripting globally, or in a finer-grained manner, e.g., on a per-origin basis, down to the level of individual realms.) auto const& document = as(settings.global_object()).associated_document(); // NB: about:settings and about:processes are internal pages using javascript, so we do not consider user configuration for these pages. if (document.url() != URL::about_settings() && document.url() != URL::about_processes() && !document.page().is_scripting_enabled()) return false; // Either settings's global object is not a Window object, or settings's global object's associated Document's active sandboxing flag set does not have its sandboxed scripts browsing context flag set. if (has_flag(document.active_sandboxing_flag_set(), SandboxingFlagSet::SandboxedScripts)) return false; return true; } // https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-noscript bool is_scripting_disabled(EnvironmentSettingsObject const& settings) { // Scripting is disabled for an environment settings object when scripting is not enabled for it, i.e., when any of the above conditions are false. return !is_scripting_enabled(settings); } // https://html.spec.whatwg.org/multipage/webappapis.html#module-type-allowed bool module_type_allowed(EnvironmentSettingsObject const&, StringView module_type) { // 1. If moduleType is not "javascript-or-wasm", "css", or "json", then return false. if (module_type != "javascript-or-wasm"sv && module_type != "css"sv && module_type != "json"sv) return false; // FIXME: 2. If moduleType is "css" and the CSSStyleSheet interface is not exposed in settings's realm, then return false. // 3. Return true. return true; } // https://html.spec.whatwg.org/multipage/webappapis.html#add-module-to-resolved-module-set void add_module_to_resolved_module_set(EnvironmentSettingsObject& settings_object, String const& serialized_base_url, String const& normalized_specifier, Optional const& as_url) { // 1. Let global be settingsObject's global object. auto& global = settings_object.global_object(); // 2. If global does not implement Window, then return. if (!is(global)) return; // 3. Let record be a new specifier resolution record, with serialized base URL set to serializedBaseURL, // specifier set to normalizedSpecifier, and specifier as a URL set to asURL. // // NOTE: We set 'specifier as a URL set to asURL' as a bool to simplify logic when merging import maps. SpecifierResolution resolution { .serialized_base_url = serialized_base_url, .specifier = normalized_specifier, .specifier_is_null_or_url_like_that_is_special = !as_url.has_value() || as_url->is_special(), }; // 4. Append record to global's resolved module set. as(global).append_resolved_module(move(resolution)); } // 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#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. // 1. Assert: the backup incumbent realm stack is not empty. // NOTE: If this assertion fails, it's because the incumbent realm was used with no involvement of JavaScript. VERIFY(!event_loop.is_backup_incumbent_realm_stack_empty()); // 2. Return the topmost entry of the backup incumbent realm stack. return event_loop.top_of_backup_incumbent_realm_stack(); } // 3. Return context's Realm component's settings object. return principal_realm_settings_object(*context->realm); } // https://html.spec.whatwg.org/multipage/webappapis.html#concept-incumbent-global JS::Object& incumbent_global_object() { // Similarly, the incumbent global object is the global object of the incumbent settings object. return incumbent_settings_object().global_object(); } // https://whatpr.org/html/9893/webappapis.html#concept-realm-settings-object EnvironmentSettingsObject& principal_realm_settings_object(JS::Realm& realm) { // A principal realm has a [[HostDefined]] field, which contains the principal realm's settings object. return Bindings::principal_host_defined_environment_settings_object(realm); } // https://html.spec.whatwg.org/multipage/webappapis.html#current-settings-object EnvironmentSettingsObject& current_settings_object() { // Then, the current settings object is the environment settings object of the current realm. return principal_realm_settings_object(*Bindings::main_thread_vm().current_realm()); } // https://html.spec.whatwg.org/multipage/webappapis.html#current-global-object JS::Object& current_global_object() { // Similarly, the current global object is the global object of the current realm. return Bindings::main_thread_vm().current_realm()->global_object(); } // https://html.spec.whatwg.org/multipage/webappapis.html#concept-relevant-realm JS::Realm& relevant_realm(JS::Object const& object) { // The relevant Realm for a platform object is the value of its [[Realm]] field. return object.shape().realm(); } // https://html.spec.whatwg.org/multipage/webappapis.html#relevant-settings-object EnvironmentSettingsObject& relevant_settings_object(JS::Object const& object) { // Then, the relevant settings object for a platform object o is the environment settings object of the relevant realm for o. return Bindings::principal_host_defined_environment_settings_object(relevant_realm(object)); } EnvironmentSettingsObject& relevant_settings_object(DOM::Node const& node) { // Then, the relevant settings object for a platform object o is the environment settings object of the relevant Realm for o. return const_cast(node.document()).relevant_settings_object(); } // https://html.spec.whatwg.org/multipage/webappapis.html#concept-relevant-global JS::Object& relevant_global_object(JS::Object const& object) { // Similarly, the relevant global object for a platform object o is the global object of the relevant Realm for o. return relevant_realm(object).global_object(); } // https://html.spec.whatwg.org/multipage/webappapis.html#concept-entry-realm JS::Realm& entry_realm() { auto& event_loop = HTML::main_thread_event_loop(); auto& vm = event_loop.vm(); // With this in hand, we define the entry execution context to be the most recently pushed item in the JavaScript execution context stack that is a realm execution context. // The entry realm is the entry execution context's Realm component. auto entry_execution_context = vm.execution_context_stack().last_matching([](JS::ExecutionContext* context) { if (!context->realm) return false; return &principal_realm_settings_object(*context->realm).realm_execution_context() == context; }); VERIFY(entry_execution_context.has_value()); return *entry_execution_context.value()->realm; } // https://html.spec.whatwg.org/multipage/webappapis.html#entry-settings-object EnvironmentSettingsObject& entry_settings_object() { // Then, the entry settings object is the environment settings object of the entry realm. return Bindings::principal_host_defined_environment_settings_object(entry_realm()); } // https://html.spec.whatwg.org/multipage/webappapis.html#entry-global-object JS::Object& entry_global_object() { // Similarly, the entry global object is the global object of the entry realm. return entry_realm().global_object(); } // https://html.spec.whatwg.org/multipage/webappapis.html#secure-context bool is_secure_context(Environment const& environment) { // 1. If environment is an environment settings object, then: if (auto const* environment_settings_object = as_if(environment)) { // 1. Let global be environment's global object. auto const& global = environment_settings_object->global_object(); // 2. If global is a WorkerGlobalScope, then: if (auto const* worker = as_if(global)) { // 1. If global's owner set[0]'s relevant settings object is a secure context, then return true. // NOTE: We only need to check the 0th item since they will necessarily all be consistent. if (worker->owner_set().at(0).visit([](auto const& owner) { return owner.relevant_settings_object_is_secure_context; })) return true; // 2. Return false. return false; } // FIXME: 3. If global is a WorkletGlobalScope, then return true. // NOTE: Worklets can only be created in secure contexts. } // 2. If the result of Is url potentially trustworthy? given environment's top-level creation URL is "Potentially Trustworthy", then return true. if (SecureContexts::is_url_potentially_trustworthy(environment.top_level_creation_url.value()) == SecureContexts::Trustworthiness::PotentiallyTrustworthy) return true; // 3. Return false. return false; } // https://html.spec.whatwg.org/multipage/webappapis.html#non-secure-context bool is_non_secure_context(Environment const& environment) { // An environment is a non-secure context if it is not a secure context. return !is_secure_context(environment); } SerializedEnvironmentSettingsObject EnvironmentSettingsObject::serialize() { auto serialized_global = [this]() -> SerializedGlobal { bool relevant_settings_object_is_secure_context = is_secure_context(*this); if (auto const* window = as_if(global_object())) { return SerializedWindow { .associated_document { .url = window->associated_document().url(), .relevant_settings_object_is_secure_context = relevant_settings_object_is_secure_context, } }; } VERIFY(is(global_object())); return SerializedWorkerGlobalScope { .relevant_settings_object_is_secure_context = relevant_settings_object_is_secure_context, }; }(); return SerializedEnvironmentSettingsObject { .id = this->id, .creation_url = this->creation_url, .top_level_creation_url = this->top_level_creation_url, .top_level_origin = this->top_level_origin, .api_base_url = api_base_url(), .origin = origin(), .has_cross_site_ancestor = has_cross_site_ancestor(), .policy_container = policy_container()->serialize(), .cross_origin_isolated_capability = cross_origin_isolated_capability(), .time_origin = this->time_origin(), .global = move(serialized_global), }; } GC::Ref EnvironmentSettingsObject::storage_manager() { if (!m_storage_manager) m_storage_manager = realm().create(realm()); return *m_storage_manager; } // https://w3c.github.io/ServiceWorker/#get-the-service-worker-registration-object GC::Ref EnvironmentSettingsObject::get_service_worker_registration_object(ServiceWorker::Registration const& registration) { // 1. Let objectMap be environment’s 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 environment’s Realm. // 2. Set registrationObject’s service worker registration to registration. // 3. Set registrationObject’s installing attribute to null. // 4. Set registrationObject’s waiting attribute to null. // 5. Set registrationObject’s active attribute to null. auto registration_object = ServiceWorker::ServiceWorkerRegistration::create(realm(), registration); // 6. If registration’s installing worker is not null, then set registrationObject’s installing attribute to the result of getting the service worker object that represents registration’s installing worker in environment. if (registration.installing_worker()) registration_object->set_installing(get_service_worker_object(registration.installing_worker())); // 7. If registration’s waiting worker is not null, then set registrationObject’s waiting attribute to the result of getting the service worker object that represents registration’s waiting worker in environment. if (registration.waiting_worker()) registration_object->set_waiting(get_service_worker_object(registration.waiting_worker())); // 8. If registration’s active worker is not null, then set registrationObject’s active attribute to the result of getting the service worker object that represents registration’s 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 EnvironmentSettingsObject::get_service_worker_object(ServiceWorker::ServiceWorkerRecord* service_worker) { // 1. Let objectMap be environment’s 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 environment’s Realm, and associate it with serviceWorker. auto service_worker_obj = ServiceWorker::ServiceWorker::create(realm(), service_worker); // 2. Set serviceWorkerObj’s state to serviceWorker’s 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); } }