Serenity Operating System
1/*
2 * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
3 * Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
4 * Copyright (c) 2022, networkException <networkexception@serenityos.org>
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <LibWeb/Bindings/MainThreadVM.h>
10#include <LibWeb/DOM/Document.h>
11#include <LibWeb/HTML/PromiseRejectionEvent.h>
12#include <LibWeb/HTML/Scripting/Environments.h>
13#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
14#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
15#include <LibWeb/HTML/Window.h>
16#include <LibWeb/HTML/WorkerGlobalScope.h>
17#include <LibWeb/Page/Page.h>
18#include <LibWeb/SecureContexts/AbstractOperations.h>
19
20namespace Web::HTML {
21
22EnvironmentSettingsObject::EnvironmentSettingsObject(NonnullOwnPtr<JS::ExecutionContext> realm_execution_context)
23 : m_realm_execution_context(move(realm_execution_context))
24{
25 m_realm_execution_context->context_owner = this;
26
27 // Register with the responsible event loop so we can perform step 4 of "perform a microtask checkpoint".
28 responsible_event_loop().register_environment_settings_object({}, *this);
29}
30
31EnvironmentSettingsObject::~EnvironmentSettingsObject()
32{
33 responsible_event_loop().unregister_environment_settings_object({}, *this);
34}
35
36void EnvironmentSettingsObject::visit_edges(Cell::Visitor& visitor)
37{
38 Base::visit_edges(visitor);
39 visitor.visit(target_browsing_context);
40}
41
42JS::ExecutionContext& EnvironmentSettingsObject::realm_execution_context()
43{
44 // NOTE: All environment settings objects are created with a realm execution context, so it's stored and returned here in the base class.
45 return *m_realm_execution_context;
46}
47
48ModuleMap& EnvironmentSettingsObject::module_map()
49{
50 return m_module_map;
51}
52
53// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object%27s-realm
54JS::Realm& EnvironmentSettingsObject::realm()
55{
56 // An environment settings object's realm execution context's Realm component is the environment settings object's Realm.
57 return *realm_execution_context().realm;
58}
59
60// https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-global
61JS::Object& EnvironmentSettingsObject::global_object()
62{
63 // An environment settings object's Realm then has a [[GlobalObject]] field, which contains the environment settings object's global object.
64 return realm().global_object();
65}
66
67// https://html.spec.whatwg.org/multipage/webappapis.html#responsible-event-loop
68EventLoop& EnvironmentSettingsObject::responsible_event_loop()
69{
70 // An environment settings object's responsible event loop is its global object's relevant agent's event loop.
71 // 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.
72 // 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.
73 if (m_responsible_event_loop)
74 return *m_responsible_event_loop;
75
76 auto& vm = global_object().vm();
77 auto& event_loop = verify_cast<Bindings::WebEngineCustomData>(vm.custom_data())->event_loop;
78 m_responsible_event_loop = &event_loop;
79 return event_loop;
80}
81
82// https://html.spec.whatwg.org/multipage/webappapis.html#check-if-we-can-run-script
83RunScriptDecision EnvironmentSettingsObject::can_run_script()
84{
85 // 1. If the global object specified by settings is a Window object whose Document object is not fully active, then return "do not run".
86 if (is<HTML::Window>(global_object()) && !verify_cast<HTML::Window>(global_object()).associated_document().is_fully_active())
87 return RunScriptDecision::DoNotRun;
88
89 // 2. If scripting is disabled for settings, then return "do not run".
90 if (is_scripting_disabled())
91 return RunScriptDecision::DoNotRun;
92
93 // 3. Return "run".
94 return RunScriptDecision::Run;
95}
96
97// https://html.spec.whatwg.org/multipage/webappapis.html#prepare-to-run-script
98void EnvironmentSettingsObject::prepare_to_run_script()
99{
100 // 1. Push settings's realm execution context onto the JavaScript execution context stack; it is now the running JavaScript execution context.
101 global_object().vm().push_execution_context(realm_execution_context());
102
103 // FIXME: 2. Add settings to the currently running task's script evaluation environment settings object set.
104}
105
106// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
107void EnvironmentSettingsObject::clean_up_after_running_script()
108{
109 auto& vm = global_object().vm();
110
111 // 1. Assert: settings's realm execution context is the running JavaScript execution context.
112 VERIFY(&realm_execution_context() == &vm.running_execution_context());
113
114 // 2. Remove settings's realm execution context from the JavaScript execution context stack.
115 vm.pop_execution_context();
116
117 // 3. If the JavaScript execution context stack is now empty, perform a microtask checkpoint. (If this runs scripts, these algorithms will be invoked reentrantly.)
118 if (vm.execution_context_stack().is_empty())
119 responsible_event_loop().perform_a_microtask_checkpoint();
120}
121
122static JS::ExecutionContext* top_most_script_having_execution_context(JS::VM& vm)
123{
124 // Here, the topmost script-having execution context is the topmost entry of the JavaScript execution context stack that has a non-null ScriptOrModule component,
125 // or null if there is no such entry in the JavaScript execution context stack.
126 auto execution_context = vm.execution_context_stack().last_matching([&](JS::ExecutionContext* context) {
127 return !context->script_or_module.has<Empty>();
128 });
129
130 if (!execution_context.has_value())
131 return nullptr;
132
133 return execution_context.value();
134}
135
136// https://html.spec.whatwg.org/multipage/webappapis.html#prepare-to-run-a-callback
137void EnvironmentSettingsObject::prepare_to_run_callback()
138{
139 auto& vm = global_object().vm();
140
141 // 1. Push settings onto the backup incumbent settings object stack.
142 // 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.
143 // 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.
144 HTML::main_thread_event_loop().push_onto_backup_incumbent_settings_object_stack({}, *this);
145
146 // 2. Let context be the topmost script-having execution context.
147 auto* context = top_most_script_having_execution_context(vm);
148
149 // 3. If context is not null, increment context's skip-when-determining-incumbent counter.
150 if (context)
151 context->skip_when_determining_incumbent_counter++;
152}
153
154// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#parse-a-url
155AK::URL EnvironmentSettingsObject::parse_url(StringView url)
156{
157 // 1. Let encoding be document's character encoding, if document was given, and environment settings object's API URL character encoding otherwise.
158 // FIXME: Pass in environment settings object's API URL character encoding.
159
160 // 2. Let baseURL be document's base URL, if document was given, and environment settings object's API base URL otherwise.
161 auto base_url = api_base_url();
162
163 // 3. Let urlRecord be the result of applying the URL parser to url, with baseURL and encoding.
164 // 4. If urlRecord is failure, then return failure.
165 // 5. Let urlString be the result of applying the URL serializer to urlRecord.
166 // 6. Return urlString as the resulting URL string and urlRecord as the resulting URL record.
167 return base_url.complete_url(url);
168}
169
170// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-a-callback
171void EnvironmentSettingsObject::clean_up_after_running_callback()
172{
173 auto& vm = global_object().vm();
174
175 // 1. Let context be the topmost script-having execution context.
176 auto* context = top_most_script_having_execution_context(vm);
177
178 // 2. If context is not null, decrement context's skip-when-determining-incumbent counter.
179 if (context)
180 context->skip_when_determining_incumbent_counter--;
181
182 // 3. Assert: the topmost entry of the backup incumbent settings object stack is settings.
183 auto& event_loop = HTML::main_thread_event_loop();
184 VERIFY(&event_loop.top_of_backup_incumbent_settings_object_stack() == this);
185
186 // 4. Remove settings from the backup incumbent settings object stack.
187 event_loop.pop_backup_incumbent_settings_object_stack({});
188}
189
190void EnvironmentSettingsObject::push_onto_outstanding_rejected_promises_weak_set(JS::Promise* promise)
191{
192 m_outstanding_rejected_promises_weak_set.append(promise);
193}
194
195bool EnvironmentSettingsObject::remove_from_outstanding_rejected_promises_weak_set(JS::Promise* promise)
196{
197 return m_outstanding_rejected_promises_weak_set.remove_first_matching([&](JS::Promise* promise_in_set) {
198 return promise == promise_in_set;
199 });
200}
201
202void EnvironmentSettingsObject::push_onto_about_to_be_notified_rejected_promises_list(JS::NonnullGCPtr<JS::Promise> promise)
203{
204 m_about_to_be_notified_rejected_promises_list.append(JS::make_handle(promise));
205}
206
207bool EnvironmentSettingsObject::remove_from_about_to_be_notified_rejected_promises_list(JS::NonnullGCPtr<JS::Promise> promise)
208{
209 return m_about_to_be_notified_rejected_promises_list.remove_first_matching([&](auto& promise_in_list) {
210 return promise == promise_in_list;
211 });
212}
213
214// https://html.spec.whatwg.org/multipage/webappapis.html#notify-about-rejected-promises
215void EnvironmentSettingsObject::notify_about_rejected_promises(Badge<EventLoop>)
216{
217 // 1. Let list be a copy of settings object's about-to-be-notified rejected promises list.
218 auto list = m_about_to_be_notified_rejected_promises_list;
219
220 // 2. If list is empty, return.
221 if (list.is_empty())
222 return;
223
224 // 3. Clear settings object's about-to-be-notified rejected promises list.
225 m_about_to_be_notified_rejected_promises_list.clear();
226
227 // 4. Let global be settings object's global object.
228 auto& global = global_object();
229
230 // 5. Queue a global task on the DOM manipulation task source given global to run the following substep:
231 queue_global_task(Task::Source::DOMManipulation, global, [this, &global, list = move(list)] {
232 // 1. For each promise p in list:
233 for (auto promise : list) {
234
235 // 1. If p's [[PromiseIsHandled]] internal slot is true, continue to the next iteration of the loop.
236 if (promise->is_handled())
237 continue;
238
239 // 2. Let notHandled be the result of firing an event named unhandledrejection at global, using PromiseRejectionEvent, with the cancelable attribute initialized to true,
240 // the promise attribute initialized to p, and the reason attribute initialized to the value of p's [[PromiseResult]] internal slot.
241 PromiseRejectionEventInit event_init {
242 {
243 .bubbles = false,
244 .cancelable = true,
245 .composed = false,
246 },
247 // Sadly we can't use .promise and .reason here, as we can't use the designator on the initialization of DOM::EventInit above.
248 /* .promise = */ JS::make_handle(*promise),
249 /* .reason = */ promise->result(),
250 };
251 // FIXME: This currently assumes that global is a WindowObject.
252 auto& window = verify_cast<HTML::Window>(global);
253
254 auto promise_rejection_event = PromiseRejectionEvent::create(window.realm(), String::from_deprecated_string(HTML::EventNames::unhandledrejection).release_value_but_fixme_should_propagate_errors(), event_init).release_value_but_fixme_should_propagate_errors();
255
256 bool not_handled = window.dispatch_event(*promise_rejection_event);
257
258 // 3. If notHandled is false, then the promise rejection is handled. Otherwise, the promise rejection is not handled.
259
260 // 4. If p's [[PromiseIsHandled]] internal slot is false, add p to settings object's outstanding rejected promises weak set.
261 if (!promise->is_handled())
262 m_outstanding_rejected_promises_weak_set.append(promise);
263
264 // This algorithm results in promise rejections being marked as handled or not handled. These concepts parallel handled and not handled script errors.
265 // If a rejection is still not handled after this, then the rejection may be reported to a developer console.
266 if (not_handled)
267 HTML::report_exception_to_console(promise->result(), realm(), ErrorInPromise::Yes);
268 }
269 });
270}
271
272// https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-script
273bool EnvironmentSettingsObject::is_scripting_enabled() const
274{
275 // Scripting is enabled for an environment settings object settings when all of the following conditions are true:
276 // The user agent supports scripting.
277 // NOTE: This is always true in LibWeb :^)
278
279 // FIXME: Do the right thing for workers.
280 if (!is<HTML::Window>(m_realm_execution_context->realm->global_object()))
281 return true;
282
283 // The user has not disabled scripting for settings 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 environment settings objects.)
284 auto document = const_cast<EnvironmentSettingsObject&>(*this).responsible_document();
285 VERIFY(document);
286 if (!document->page() || !document->page()->is_scripting_enabled())
287 return false;
288
289 // FIXME: 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.
290
291 return true;
292}
293
294// https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-noscript
295bool EnvironmentSettingsObject::is_scripting_disabled() const
296{
297 // 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.
298 return !is_scripting_enabled();
299}
300
301// https://html.spec.whatwg.org/multipage/webappapis.html#module-type-allowed
302bool EnvironmentSettingsObject::module_type_allowed(AK::DeprecatedString const& module_type) const
303{
304 // 1. If moduleType is not "javascript", "css", or "json", then return false.
305 if (module_type != "javascript"sv && module_type != "css"sv && module_type != "json"sv)
306 return false;
307
308 // FIXME: 2. If moduleType is "css" and the CSSStyleSheet interface is not exposed in settings's Realm, then return false.
309
310 // 3. Return true.
311 return true;
312}
313
314// https://html.spec.whatwg.org/multipage/webappapis.html#disallow-further-import-maps
315void EnvironmentSettingsObject::disallow_further_import_maps()
316{
317 // 1. Let global be settingsObject's global object.
318 auto& global = global_object();
319
320 // 2. If global does not implement Window, then return.
321 if (!is<Window>(global))
322 return;
323
324 // 3. Set global's import maps allowed to false.
325 verify_cast<Window>(global).set_import_maps_allowed(false);
326}
327
328// https://html.spec.whatwg.org/multipage/webappapis.html#incumbent-settings-object
329EnvironmentSettingsObject& incumbent_settings_object()
330{
331 auto& event_loop = HTML::main_thread_event_loop();
332 auto& vm = event_loop.vm();
333
334 // 1. Let context be the topmost script-having execution context.
335 auto* context = top_most_script_having_execution_context(vm);
336
337 // 2. If context is null, or if context's skip-when-determining-incumbent counter is greater than zero, then:
338 if (!context || context->skip_when_determining_incumbent_counter > 0) {
339 // 1. Assert: the backup incumbent settings object stack is not empty.
340 // NOTE: If this assertion fails, it's because the incumbent settings object was used with no involvement of JavaScript.
341 VERIFY(!event_loop.is_backup_incumbent_settings_object_stack_empty());
342
343 // 2. Return the topmost entry of the backup incumbent settings object stack.
344 return event_loop.top_of_backup_incumbent_settings_object_stack();
345 }
346
347 // 3. Return context's Realm component's settings object.
348 return Bindings::host_defined_environment_settings_object(*context->realm);
349}
350
351// https://html.spec.whatwg.org/multipage/webappapis.html#concept-incumbent-realm
352JS::Realm& incumbent_realm()
353{
354 // Then, the incumbent Realm is the Realm of the incumbent settings object.
355 return incumbent_settings_object().realm();
356}
357
358// https://html.spec.whatwg.org/multipage/webappapis.html#concept-incumbent-global
359JS::Object& incumbent_global_object()
360{
361 // Similarly, the incumbent global object is the global object of the incumbent settings object.
362 return incumbent_settings_object().global_object();
363}
364
365// https://html.spec.whatwg.org/multipage/webappapis.html#current-settings-object
366EnvironmentSettingsObject& current_settings_object()
367{
368 auto& event_loop = HTML::main_thread_event_loop();
369 auto& vm = event_loop.vm();
370
371 // Then, the current settings object is the environment settings object of the current Realm Record.
372 return Bindings::host_defined_environment_settings_object(*vm.current_realm());
373}
374
375// https://html.spec.whatwg.org/multipage/webappapis.html#current-global-object
376JS::Object& current_global_object()
377{
378 auto& event_loop = HTML::main_thread_event_loop();
379 auto& vm = event_loop.vm();
380
381 // Similarly, the current global object is the global object of the current Realm Record.
382 return vm.current_realm()->global_object();
383}
384
385// https://html.spec.whatwg.org/multipage/webappapis.html#concept-relevant-realm
386JS::Realm& relevant_realm(JS::Object const& object)
387{
388 // The relevant Realm for a platform object is the value of its [[Realm]] field.
389 return object.shape().realm();
390}
391
392// https://html.spec.whatwg.org/multipage/webappapis.html#relevant-settings-object
393EnvironmentSettingsObject& relevant_settings_object(JS::Object const& object)
394{
395 // Then, the relevant settings object for a platform object o is the environment settings object of the relevant Realm for o.
396 return Bindings::host_defined_environment_settings_object(relevant_realm(object));
397}
398
399EnvironmentSettingsObject& relevant_settings_object(DOM::Node const& node)
400{
401 // Then, the relevant settings object for a platform object o is the environment settings object of the relevant Realm for o.
402 return const_cast<DOM::Document&>(node.document()).relevant_settings_object();
403}
404
405// https://html.spec.whatwg.org/multipage/webappapis.html#concept-relevant-global
406JS::Object& relevant_global_object(JS::Object const& object)
407{
408 // Similarly, the relevant global object for a platform object o is the global object of the relevant Realm for o.
409 return relevant_realm(object).global_object();
410}
411
412// https://html.spec.whatwg.org/multipage/webappapis.html#concept-entry-realm
413JS::Realm& entry_realm()
414{
415 auto& event_loop = HTML::main_thread_event_loop();
416 auto& vm = event_loop.vm();
417
418 // 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.
419 // The entry realm is the entry execution context's Realm component.
420 // NOTE: Currently all execution contexts in LibJS are realm execution contexts
421 return *vm.running_execution_context().realm;
422}
423
424// https://html.spec.whatwg.org/multipage/webappapis.html#entry-settings-object
425EnvironmentSettingsObject& entry_settings_object()
426{
427 // Then, the entry settings object is the environment settings object of the entry realm.
428 return Bindings::host_defined_environment_settings_object(entry_realm());
429}
430
431// https://html.spec.whatwg.org/multipage/webappapis.html#entry-global-object
432JS::Object& entry_global_object()
433{
434 // Similarly, the entry global object is the global object of the entry realm.
435 return entry_realm().global_object();
436}
437
438// https://html.spec.whatwg.org/multipage/webappapis.html#secure-context
439bool is_secure_context(Environment const& environment)
440{
441 // 1. If environment is an environment settings object, then:
442 if (is<EnvironmentSettingsObject>(environment)) {
443 // 1. Let global be environment's global object.
444 // FIXME: Add a const global_object() getter to ESO
445 auto& global = static_cast<EnvironmentSettingsObject&>(const_cast<Environment&>(environment)).global_object();
446
447 // 2. If global is a WorkerGlobalScope, then:
448 if (is<WorkerGlobalScope>(global)) {
449 // FIXME: 1. If global's owner set[0]'s relevant settings object is a secure context, then return true.
450 // NOTE: We only need to check the 0th item since they will necessarily all be consistent.
451
452 // 2. Return false.
453 return false;
454 }
455
456 // FIXME: 3. If global is a WorkletGlobalScope, then return true.
457 // NOTE: Worklets can only be created in secure contexts.
458 }
459
460 // 2. If the result of Is url potentially trustworthy? given environment's top-level creation URL is "Potentially Trustworthy", then return true.
461 if (SecureContexts::is_url_potentially_trustworthy(environment.top_level_creation_url) == SecureContexts::Trustworthiness::PotentiallyTrustworthy)
462 return true;
463
464 // 3. Return false.
465 return false;
466}
467
468// https://html.spec.whatwg.org/multipage/webappapis.html#non-secure-context
469bool is_non_secure_context(Environment const& environment)
470{
471 // An environment is a non-secure context if it is not a secure context.
472 return !is_secure_context(environment);
473}
474
475}