/src/serenity/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2021-2023, Luke Wilde <lukew@serenityos.org> |
4 | | * Copyright (c) 2022-2023, networkException <networkexception@serenityos.org> |
5 | | * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org> |
6 | | * |
7 | | * SPDX-License-Identifier: BSD-2-Clause |
8 | | */ |
9 | | |
10 | | #include <LibJS/AST.h> |
11 | | #include <LibJS/Heap/DeferGC.h> |
12 | | #include <LibJS/Module.h> |
13 | | #include <LibJS/Runtime/Array.h> |
14 | | #include <LibJS/Runtime/Environment.h> |
15 | | #include <LibJS/Runtime/FinalizationRegistry.h> |
16 | | #include <LibJS/Runtime/ModuleRequest.h> |
17 | | #include <LibJS/Runtime/NativeFunction.h> |
18 | | #include <LibJS/Runtime/VM.h> |
19 | | #include <LibJS/SourceTextModule.h> |
20 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
21 | | #include <LibWeb/Bindings/Intrinsics.h> |
22 | | #include <LibWeb/Bindings/MainThreadVM.h> |
23 | | #include <LibWeb/Bindings/WindowExposedInterfaces.h> |
24 | | #include <LibWeb/DOM/Document.h> |
25 | | #include <LibWeb/DOM/MutationType.h> |
26 | | #include <LibWeb/HTML/AttributeNames.h> |
27 | | #include <LibWeb/HTML/CustomElements/CustomElementDefinition.h> |
28 | | #include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h> |
29 | | #include <LibWeb/HTML/EventNames.h> |
30 | | #include <LibWeb/HTML/Location.h> |
31 | | #include <LibWeb/HTML/PromiseRejectionEvent.h> |
32 | | #include <LibWeb/HTML/Scripting/ClassicScript.h> |
33 | | #include <LibWeb/HTML/Scripting/Environments.h> |
34 | | #include <LibWeb/HTML/Scripting/ExceptionReporter.h> |
35 | | #include <LibWeb/HTML/Scripting/Fetching.h> |
36 | | #include <LibWeb/HTML/Scripting/ModuleScript.h> |
37 | | #include <LibWeb/HTML/Scripting/Script.h> |
38 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
39 | | #include <LibWeb/HTML/TagNames.h> |
40 | | #include <LibWeb/HTML/Window.h> |
41 | | #include <LibWeb/HTML/WindowProxy.h> |
42 | | #include <LibWeb/MathML/TagNames.h> |
43 | | #include <LibWeb/Namespace.h> |
44 | | #include <LibWeb/NavigationTiming/EntryNames.h> |
45 | | #include <LibWeb/PerformanceTimeline/EntryTypes.h> |
46 | | #include <LibWeb/Platform/EventLoopPlugin.h> |
47 | | #include <LibWeb/SVG/AttributeNames.h> |
48 | | #include <LibWeb/SVG/TagNames.h> |
49 | | #include <LibWeb/UIEvents/EventNames.h> |
50 | | #include <LibWeb/UIEvents/InputTypes.h> |
51 | | #include <LibWeb/WebGL/EventNames.h> |
52 | | #include <LibWeb/WebIDL/AbstractOperations.h> |
53 | | #include <LibWeb/XHR/EventNames.h> |
54 | | #include <LibWeb/XLink/AttributeNames.h> |
55 | | |
56 | | namespace Web::Bindings { |
57 | | |
58 | | static RefPtr<JS::VM> s_main_thread_vm; |
59 | | |
60 | | // https://html.spec.whatwg.org/multipage/webappapis.html#active-script |
61 | | HTML::Script* active_script() |
62 | 0 | { |
63 | | // 1. Let record be GetActiveScriptOrModule(). |
64 | 0 | auto record = main_thread_vm().get_active_script_or_module(); |
65 | | |
66 | | // 2. If record is null, return null. |
67 | | // 3. Return record.[[HostDefined]]. |
68 | 0 | return record.visit( |
69 | 0 | [](JS::NonnullGCPtr<JS::Script>& js_script) -> HTML::Script* { |
70 | 0 | return verify_cast<HTML::ClassicScript>(js_script->host_defined()); |
71 | 0 | }, |
72 | 0 | [](JS::NonnullGCPtr<JS::Module>& js_module) -> HTML::Script* { |
73 | 0 | return verify_cast<HTML::ModuleScript>(js_module->host_defined()); |
74 | 0 | }, |
75 | 0 | [](Empty) -> HTML::Script* { |
76 | 0 | return nullptr; |
77 | 0 | }); |
78 | 0 | } |
79 | | |
80 | | ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type) |
81 | 0 | { |
82 | 0 | VERIFY(!s_main_thread_vm); |
83 | | |
84 | 0 | s_main_thread_vm = TRY(JS::VM::create(make<WebEngineCustomData>())); |
85 | 0 | s_main_thread_vm->on_unimplemented_property_access = [](auto const& object, auto const& property_key) { |
86 | 0 | dbgln("FIXME: Unimplemented IDL interface: '{}.{}'", object.class_name(), property_key.to_string()); |
87 | 0 | }; |
88 | | |
89 | | // NOTE: We intentionally leak the main thread JavaScript VM. |
90 | | // This avoids doing an exhaustive garbage collection on process exit. |
91 | 0 | s_main_thread_vm->ref(); |
92 | |
|
93 | 0 | auto& custom_data = verify_cast<WebEngineCustomData>(*s_main_thread_vm->custom_data()); |
94 | 0 | custom_data.event_loop = s_main_thread_vm->heap().allocate_without_realm<HTML::EventLoop>(type); |
95 | | |
96 | | // These strings could potentially live on the VM similar to CommonPropertyNames. |
97 | 0 | DOM::MutationType::initialize_strings(); |
98 | 0 | HTML::AttributeNames::initialize_strings(); |
99 | 0 | HTML::CustomElementReactionNames::initialize_strings(); |
100 | 0 | HTML::EventNames::initialize_strings(); |
101 | 0 | HTML::TagNames::initialize_strings(); |
102 | 0 | MathML::TagNames::initialize_strings(); |
103 | 0 | Namespace::initialize_strings(); |
104 | 0 | NavigationTiming::EntryNames::initialize_strings(); |
105 | 0 | PerformanceTimeline::EntryTypes::initialize_strings(); |
106 | 0 | SVG::AttributeNames::initialize_strings(); |
107 | 0 | SVG::TagNames::initialize_strings(); |
108 | 0 | UIEvents::EventNames::initialize_strings(); |
109 | 0 | UIEvents::InputTypes::initialize_strings(); |
110 | 0 | WebGL::EventNames::initialize_strings(); |
111 | 0 | XHR::EventNames::initialize_strings(); |
112 | 0 | XLink::AttributeNames::initialize_strings(); |
113 | | |
114 | | // 8.1.5.1 HostEnsureCanAddPrivateElement(O), https://html.spec.whatwg.org/multipage/webappapis.html#the-hostensurecanaddprivateelement-implementation |
115 | 0 | s_main_thread_vm->host_ensure_can_add_private_element = [](JS::Object const& object) -> JS::ThrowCompletionOr<void> { |
116 | | // 1. If O is a WindowProxy object, or implements Location, then return Completion { [[Type]]: throw, [[Value]]: a new TypeError }. |
117 | 0 | if (is<HTML::WindowProxy>(object) || is<HTML::Location>(object)) |
118 | 0 | return s_main_thread_vm->throw_completion<JS::TypeError>("Cannot add private elements to window or location object"sv); |
119 | | |
120 | | // 2. Return NormalCompletion(unused). |
121 | 0 | return {}; |
122 | 0 | }; |
123 | | |
124 | | // FIXME: Implement 8.1.5.2 HostEnsureCanCompileStrings(callerRealm, calleeRealm), https://html.spec.whatwg.org/multipage/webappapis.html#hostensurecancompilestrings(callerrealm,-calleerealm) |
125 | | |
126 | | // 8.1.5.3 HostPromiseRejectionTracker(promise, operation), https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation |
127 | 0 | s_main_thread_vm->host_promise_rejection_tracker = [](JS::Promise& promise, JS::Promise::RejectionOperation operation) { |
128 | | // 1. Let script be the running script. |
129 | | // The running script is the script in the [[HostDefined]] field in the ScriptOrModule component of the running JavaScript execution context. |
130 | 0 | HTML::Script* script { nullptr }; |
131 | 0 | s_main_thread_vm->running_execution_context().script_or_module.visit( |
132 | 0 | [&script](JS::NonnullGCPtr<JS::Script>& js_script) { |
133 | 0 | script = verify_cast<HTML::ClassicScript>(js_script->host_defined()); |
134 | 0 | }, |
135 | 0 | [&script](JS::NonnullGCPtr<JS::Module>& js_module) { |
136 | 0 | script = verify_cast<HTML::ModuleScript>(js_module->host_defined()); |
137 | 0 | }, |
138 | 0 | [](Empty) { |
139 | 0 | }); |
140 | | |
141 | | // 2. If script is a classic script and script's muted errors is true, then return. |
142 | | // NOTE: is<T>() returns false if nullptr is passed. |
143 | 0 | if (is<HTML::ClassicScript>(script)) { |
144 | 0 | auto const& classic_script = static_cast<HTML::ClassicScript const&>(*script); |
145 | 0 | if (classic_script.muted_errors() == HTML::ClassicScript::MutedErrors::Yes) |
146 | 0 | return; |
147 | 0 | } |
148 | | |
149 | | // 3. Let settings object be the current settings object. |
150 | | // 4. If script is not null, then set settings object to script's settings object. |
151 | 0 | auto& settings_object = script ? script->settings_object() : HTML::current_settings_object(); |
152 | | |
153 | | // 5. Let global be settingsObject's global object. |
154 | 0 | auto* global_mixin = dynamic_cast<HTML::WindowOrWorkerGlobalScopeMixin*>(&settings_object.global_object()); |
155 | 0 | VERIFY(global_mixin); |
156 | 0 | auto& global = global_mixin->this_impl(); |
157 | |
|
158 | 0 | switch (operation) { |
159 | | // 6. If operation is "reject", |
160 | 0 | case JS::Promise::RejectionOperation::Reject: |
161 | | // 1. Append promise to global's about-to-be-notified rejected promises list. |
162 | 0 | global_mixin->push_onto_about_to_be_notified_rejected_promises_list(promise); |
163 | 0 | break; |
164 | | // 7. If operation is "handle", |
165 | 0 | case JS::Promise::RejectionOperation::Handle: { |
166 | | // 1. If global's about-to-be-notified rejected promises list contains promise, then remove promise from that list and return. |
167 | 0 | bool removed_about_to_be_notified_rejected_promise = global_mixin->remove_from_about_to_be_notified_rejected_promises_list(promise); |
168 | 0 | if (removed_about_to_be_notified_rejected_promise) |
169 | 0 | return; |
170 | | |
171 | | // 3. Remove promise from global's outstanding rejected promises weak set. |
172 | 0 | bool removed_outstanding_rejected_promise = global_mixin->remove_from_outstanding_rejected_promises_weak_set(&promise); |
173 | | |
174 | | // 2. If global's outstanding rejected promises weak set does not contain promise, then return. |
175 | | // NOTE: This is done out of order because removed_outstanding_rejected_promise will be false if the promise wasn't in the set or true if it was and got removed. |
176 | 0 | if (!removed_outstanding_rejected_promise) |
177 | 0 | return; |
178 | | |
179 | | // 4. Queue a global task on the DOM manipulation task source given global to fire an event named rejectionhandled at global, using PromiseRejectionEvent, |
180 | | // with the promise attribute initialized to promise, and the reason attribute initialized to the value of promise's [[PromiseResult]] internal slot. |
181 | 0 | HTML::queue_global_task(HTML::Task::Source::DOMManipulation, global, JS::create_heap_function(s_main_thread_vm->heap(), [&global, &promise] { |
182 | | // FIXME: This currently assumes that global is a WindowObject. |
183 | 0 | auto& window = verify_cast<HTML::Window>(global); |
184 | |
|
185 | 0 | HTML::PromiseRejectionEventInit event_init { |
186 | 0 | {}, // Initialize the inherited DOM::EventInit |
187 | 0 | /* .promise = */ promise, |
188 | 0 | /* .reason = */ promise.result(), |
189 | 0 | }; |
190 | 0 | auto promise_rejection_event = HTML::PromiseRejectionEvent::create(HTML::relevant_realm(global), HTML::EventNames::rejectionhandled, event_init); |
191 | 0 | window.dispatch_event(promise_rejection_event); |
192 | 0 | })); |
193 | 0 | break; |
194 | 0 | } |
195 | 0 | default: |
196 | 0 | VERIFY_NOT_REACHED(); |
197 | 0 | } |
198 | 0 | }; |
199 | | |
200 | | // 8.1.5.4.1 HostCallJobCallback(callback, V, argumentsList), https://html.spec.whatwg.org/multipage/webappapis.html#hostcalljobcallback |
201 | 0 | s_main_thread_vm->host_call_job_callback = [](JS::JobCallback& callback, JS::Value this_value, ReadonlySpan<JS::Value> arguments_list) { |
202 | 0 | auto& callback_host_defined = verify_cast<WebEngineCustomJobCallbackData>(*callback.custom_data()); |
203 | | |
204 | | // 1. Let incumbent settings be callback.[[HostDefined]].[[IncumbentSettings]]. (NOTE: Not necessary) |
205 | | // 2. Let script execution context be callback.[[HostDefined]].[[ActiveScriptContext]]. (NOTE: Not necessary) |
206 | | |
207 | | // 3. Prepare to run a callback with incumbent settings. |
208 | 0 | callback_host_defined.incumbent_settings->prepare_to_run_callback(); |
209 | | |
210 | | // 4. If script execution context is not null, then push script execution context onto the JavaScript execution context stack. |
211 | 0 | if (callback_host_defined.active_script_context) |
212 | 0 | s_main_thread_vm->push_execution_context(*callback_host_defined.active_script_context); |
213 | | |
214 | | // 5. Let result be Call(callback.[[Callback]], V, argumentsList). |
215 | 0 | auto result = JS::call(*s_main_thread_vm, callback.callback(), this_value, arguments_list); |
216 | | |
217 | | // 6. If script execution context is not null, then pop script execution context from the JavaScript execution context stack. |
218 | 0 | if (callback_host_defined.active_script_context) { |
219 | 0 | VERIFY(&s_main_thread_vm->running_execution_context() == callback_host_defined.active_script_context.ptr()); |
220 | 0 | s_main_thread_vm->pop_execution_context(); |
221 | 0 | } |
222 | | |
223 | | // 7. Clean up after running a callback with incumbent settings. |
224 | 0 | callback_host_defined.incumbent_settings->clean_up_after_running_callback(); |
225 | | |
226 | | // 8. Return result. |
227 | 0 | return result; |
228 | 0 | }; |
229 | | |
230 | | // 8.1.5.4.2 HostEnqueueFinalizationRegistryCleanupJob(finalizationRegistry), https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuefinalizationregistrycleanupjob |
231 | 0 | s_main_thread_vm->host_enqueue_finalization_registry_cleanup_job = [](JS::FinalizationRegistry& finalization_registry) { |
232 | | // 1. Let global be finalizationRegistry.[[Realm]]'s global object. |
233 | 0 | auto& global = finalization_registry.realm().global_object(); |
234 | | |
235 | | // 2. Queue a global task on the JavaScript engine task source given global to perform the following steps: |
236 | 0 | HTML::queue_global_task(HTML::Task::Source::JavaScriptEngine, global, JS::create_heap_function(s_main_thread_vm->heap(), [&finalization_registry] { |
237 | | // 1. Let entry be finalizationRegistry.[[CleanupCallback]].[[Callback]].[[Realm]]'s environment settings object. |
238 | 0 | auto& entry = host_defined_environment_settings_object(*finalization_registry.cleanup_callback().callback().realm()); |
239 | | |
240 | | // 2. Check if we can run script with entry. If this returns "do not run", then return. |
241 | 0 | if (entry.can_run_script() == HTML::RunScriptDecision::DoNotRun) |
242 | 0 | return; |
243 | | |
244 | | // 3. Prepare to run script with entry. |
245 | 0 | entry.prepare_to_run_script(); |
246 | | |
247 | | // 4. Let result be the result of performing CleanupFinalizationRegistry(finalizationRegistry). |
248 | 0 | auto result = finalization_registry.cleanup(); |
249 | | |
250 | | // 5. Clean up after running script with entry. |
251 | 0 | entry.clean_up_after_running_script(); |
252 | | |
253 | | // 6. If result is an abrupt completion, then report the exception given by result.[[Value]]. |
254 | 0 | if (result.is_error()) |
255 | 0 | HTML::report_exception(result, finalization_registry.realm()); |
256 | 0 | })); |
257 | 0 | }; |
258 | | |
259 | | // 8.1.5.4.3 HostEnqueuePromiseJob(job, realm), https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuepromisejob |
260 | 0 | s_main_thread_vm->host_enqueue_promise_job = [](JS::NonnullGCPtr<JS::HeapFunction<JS::ThrowCompletionOr<JS::Value>()>> job, JS::Realm* realm) { |
261 | | // 1. If realm is not null, then let job settings be the settings object for realm. Otherwise, let job settings be null. |
262 | 0 | HTML::EnvironmentSettingsObject* job_settings { nullptr }; |
263 | 0 | if (realm) |
264 | 0 | job_settings = &host_defined_environment_settings_object(*realm); |
265 | | |
266 | | // IMPLEMENTATION DEFINED: The JS spec says we must take implementation defined steps to make the currently active script or module at the time of HostEnqueuePromiseJob being invoked |
267 | | // also be the active script or module of the job at the time of its invocation. |
268 | | // This means taking it here now and passing it through to the lambda. |
269 | 0 | auto script_or_module = s_main_thread_vm->get_active_script_or_module(); |
270 | | |
271 | | // 2. Queue a microtask on the surrounding agent's event loop to perform the following steps: |
272 | | // This instance of "queue a microtask" uses the "implied document". The best fit for "implied document" here is "If the task is being queued by or for a script, then return the script's settings object's responsible document." |
273 | | // Do note that "implied document" from the spec is handwavy and the spec authors are trying to get rid of it: https://github.com/whatwg/html/issues/4980 |
274 | 0 | auto* script = active_script(); |
275 | |
|
276 | 0 | auto& heap = realm ? realm->heap() : s_main_thread_vm->heap(); |
277 | | // NOTE: This keeps job_settings alive by keeping realm alive, which is holding onto job_settings. |
278 | 0 | HTML::queue_a_microtask(script ? script->settings_object().responsible_document().ptr() : nullptr, JS::create_heap_function(heap, [job_settings, job = move(job), script_or_module = move(script_or_module)] { |
279 | | // The dummy execution context has to be kept up here to keep it alive for the duration of the function. |
280 | 0 | OwnPtr<JS::ExecutionContext> dummy_execution_context; |
281 | |
|
282 | 0 | if (job_settings) { |
283 | | // 1. If job settings is not null, then check if we can run script with job settings. If this returns "do not run" then return. |
284 | 0 | if (job_settings->can_run_script() == HTML::RunScriptDecision::DoNotRun) |
285 | 0 | return; |
286 | | |
287 | | // 2. If job settings is not null, then prepare to run script with job settings. |
288 | 0 | job_settings->prepare_to_run_script(); |
289 | | |
290 | | // IMPLEMENTATION DEFINED: Additionally to preparing to run a script, we also prepare to run a callback here. This matches WebIDL's |
291 | | // invoke_callback() / call_user_object_operation() functions, and prevents a crash in host_make_job_callback() |
292 | | // when getting the incumbent settings object. |
293 | 0 | job_settings->prepare_to_run_callback(); |
294 | | |
295 | | // IMPLEMENTATION DEFINED: Per the previous "implementation defined" comment, we must now make the script or module the active script or module. |
296 | | // Since the only active execution context currently is the realm execution context of job settings, lets attach it here. |
297 | 0 | job_settings->realm_execution_context().script_or_module = script_or_module; |
298 | 0 | } else { |
299 | | // FIXME: We need to setup a dummy execution context in case a JS::NativeFunction is called when processing the job. |
300 | | // This is because JS::NativeFunction::call excepts something to be on the execution context stack to be able to get the caller context to initialize the environment. |
301 | | // Do note that the JS spec gives _no_ guarantee that the execution context stack has something on it if HostEnqueuePromiseJob was called with a null realm: https://tc39.es/ecma262/#job-preparedtoevaluatecode |
302 | 0 | dummy_execution_context = JS::ExecutionContext::create(); |
303 | 0 | dummy_execution_context->script_or_module = script_or_module; |
304 | 0 | s_main_thread_vm->push_execution_context(*dummy_execution_context); |
305 | 0 | } |
306 | | |
307 | | // 3. Let result be job(). |
308 | 0 | auto result = job->function()(); |
309 | | |
310 | | // 4. If job settings is not null, then clean up after running script with job settings. |
311 | 0 | if (job_settings) { |
312 | | // IMPLEMENTATION DEFINED: Disassociate the realm execution context from the script or module. |
313 | 0 | job_settings->realm_execution_context().script_or_module = Empty {}; |
314 | | |
315 | | // IMPLEMENTATION DEFINED: See comment above, we need to clean up the non-standard prepare_to_run_callback() call. |
316 | 0 | job_settings->clean_up_after_running_callback(); |
317 | |
|
318 | 0 | job_settings->clean_up_after_running_script(); |
319 | 0 | } else { |
320 | | // Pop off the dummy execution context. See the above FIXME block about why this is done. |
321 | 0 | s_main_thread_vm->pop_execution_context(); |
322 | 0 | } |
323 | | |
324 | | // 5. If result is an abrupt completion, then report the exception given by result.[[Value]]. |
325 | 0 | if (result.is_error()) |
326 | 0 | HTML::report_exception(result, job_settings->realm()); |
327 | 0 | })); |
328 | 0 | }; |
329 | | |
330 | | // 8.1.5.4.4 HostMakeJobCallback(callable), https://html.spec.whatwg.org/multipage/webappapis.html#hostmakejobcallback |
331 | 0 | s_main_thread_vm->host_make_job_callback = [](JS::FunctionObject& callable) -> JS::NonnullGCPtr<JS::JobCallback> { |
332 | | // 1. Let incumbent settings be the incumbent settings object. |
333 | 0 | auto& incumbent_settings = HTML::incumbent_settings_object(); |
334 | | |
335 | | // 2. Let active script be the active script. |
336 | 0 | auto* script = active_script(); |
337 | | |
338 | | // 3. Let script execution context be null. |
339 | 0 | OwnPtr<JS::ExecutionContext> script_execution_context; |
340 | | |
341 | | // 4. If active script is not null, set script execution context to a new JavaScript execution context, with its Function field set to null, |
342 | | // its Realm field set to active script's settings object's Realm, and its ScriptOrModule set to active script's record. |
343 | 0 | if (script) { |
344 | 0 | script_execution_context = JS::ExecutionContext::create(); |
345 | 0 | script_execution_context->function = nullptr; |
346 | 0 | script_execution_context->realm = &script->settings_object().realm(); |
347 | 0 | if (is<HTML::ClassicScript>(script)) { |
348 | 0 | script_execution_context->script_or_module = JS::NonnullGCPtr<JS::Script>(*verify_cast<HTML::ClassicScript>(script)->script_record()); |
349 | 0 | } else if (is<HTML::ModuleScript>(script)) { |
350 | 0 | if (is<HTML::JavaScriptModuleScript>(script)) { |
351 | 0 | script_execution_context->script_or_module = JS::NonnullGCPtr<JS::Module>(*verify_cast<HTML::JavaScriptModuleScript>(script)->record()); |
352 | 0 | } else { |
353 | | // NOTE: Handle CSS and JSON module scripts once we have those. |
354 | 0 | VERIFY_NOT_REACHED(); |
355 | 0 | } |
356 | 0 | } else { |
357 | 0 | VERIFY_NOT_REACHED(); |
358 | 0 | } |
359 | 0 | } |
360 | | |
361 | | // 5. Return the JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context } }. |
362 | 0 | auto host_defined = adopt_own(*new WebEngineCustomJobCallbackData(incumbent_settings, move(script_execution_context))); |
363 | 0 | return JS::JobCallback::create(*s_main_thread_vm, callable, move(host_defined)); |
364 | 0 | }; |
365 | | |
366 | | // 8.1.5.5.1 HostGetImportMetaProperties(moduleRecord), https://html.spec.whatwg.org/multipage/webappapis.html#hostgetimportmetaproperties |
367 | 0 | s_main_thread_vm->host_get_import_meta_properties = [](JS::SourceTextModule& module_record) { |
368 | 0 | auto& realm = module_record.realm(); |
369 | 0 | auto& vm = realm.vm(); |
370 | | |
371 | | // 1. Let moduleScript be moduleRecord.[[HostDefined]]. |
372 | 0 | auto& module_script = *verify_cast<HTML::Script>(module_record.host_defined()); |
373 | | |
374 | | // 2. Assert: moduleScript's base URL is not null, as moduleScript is a JavaScript module script. |
375 | 0 | VERIFY(module_script.base_url().is_valid()); |
376 | | |
377 | | // 3. Let urlString be moduleScript's base URL, serialized. |
378 | 0 | auto url_string = module_script.base_url().serialize(); |
379 | | |
380 | | // 4. Let steps be the following steps, given the argument specifier: |
381 | 0 | auto steps = [module_script = JS::NonnullGCPtr { module_script }](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> { |
382 | 0 | auto specifier = vm.argument(0); |
383 | | |
384 | | // 1. Set specifier to ? ToString(specifier). |
385 | 0 | auto specifier_string = TRY(specifier.to_string(vm)); |
386 | | |
387 | | // 2. Let url be the result of resolving a module specifier given moduleScript and specifier. |
388 | 0 | auto url = TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { |
389 | 0 | return HTML::resolve_module_specifier(*module_script, specifier_string.to_byte_string()); |
390 | 0 | })); |
391 | | |
392 | | // 3. Return the serialization of url. |
393 | 0 | return JS::PrimitiveString::create(vm, url.serialize()); |
394 | 0 | }; |
395 | | |
396 | | // 4. Let resolveFunction be ! CreateBuiltinFunction(steps, 1, "resolve", « »). |
397 | 0 | auto resolve_function = JS::NativeFunction::create(realm, move(steps), 1, vm.names.resolve); |
398 | | |
399 | | // 5. Return « Record { [[Key]]: "url", [[Value]]: urlString }, Record { [[Key]]: "resolve", [[Value]]: resolveFunction } ». |
400 | 0 | HashMap<JS::PropertyKey, JS::Value> meta; |
401 | 0 | meta.set("url", JS::PrimitiveString::create(vm, move(url_string))); |
402 | 0 | meta.set("resolve", resolve_function); |
403 | |
|
404 | 0 | return meta; |
405 | 0 | }; |
406 | | |
407 | | // FIXME: Implement 8.1.5.5.2 HostImportModuleDynamically(referencingScriptOrModule, moduleRequest, promiseCapability), https://html.spec.whatwg.org/multipage/webappapis.html#hostimportmoduledynamically(referencingscriptormodule,-modulerequest,-promisecapability) |
408 | | // FIXME: Implement 8.1.5.5.3 HostResolveImportedModule(referencingScriptOrModule, moduleRequest), https://html.spec.whatwg.org/multipage/webappapis.html#hostresolveimportedmodule(referencingscriptormodule,-modulerequest) |
409 | | |
410 | | // 8.1.6.5.2 HostGetSupportedImportAttributes(), https://html.spec.whatwg.org/multipage/webappapis.html#hostgetsupportedimportassertions |
411 | 0 | s_main_thread_vm->host_get_supported_import_attributes = []() -> Vector<ByteString> { |
412 | | // 1. Return « "type" ». |
413 | 0 | return { "type"sv }; |
414 | 0 | }; |
415 | | |
416 | | // 8.1.6.5.3 HostLoadImportedModule(referrer, moduleRequest, loadState, payload), https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule |
417 | 0 | s_main_thread_vm->host_load_imported_module = [](JS::ImportedModuleReferrer referrer, JS::ModuleRequest const& module_request, JS::GCPtr<JS::GraphLoadingState::HostDefined> load_state, JS::ImportedModulePayload payload) -> void { |
418 | 0 | auto& vm = *s_main_thread_vm; |
419 | 0 | auto& realm = *vm.current_realm(); |
420 | | |
421 | | // 1. Let settingsObject be the current settings object. |
422 | 0 | Optional<HTML::EnvironmentSettingsObject&> settings_object = HTML::current_settings_object(); |
423 | | |
424 | | // FIXME: 2. If settingsObject's global object implements WorkletGlobalScope or ServiceWorkerGlobalScope and loadState is undefined, then: |
425 | | |
426 | | // 3. Let referencingScript be null. |
427 | 0 | Optional<HTML::Script&> referencing_script; |
428 | | |
429 | | // 4. Let originalFetchOptions be the default classic script fetch options. |
430 | 0 | auto original_fetch_options = HTML::default_classic_script_fetch_options(); |
431 | | |
432 | | // 5. Let fetchReferrer be "client". |
433 | 0 | Fetch::Infrastructure::Request::ReferrerType fetch_referrer = Fetch::Infrastructure::Request::Referrer::Client; |
434 | | |
435 | | // 6. If referrer is a Script Record or a Module Record, then: |
436 | 0 | if (referrer.has<JS::NonnullGCPtr<JS::Script>>() || referrer.has<JS::NonnullGCPtr<JS::CyclicModule>>()) { |
437 | | // 1. Set referencingScript to referrer.[[HostDefined]]. |
438 | 0 | referencing_script = verify_cast<HTML::Script>(referrer.has<JS::NonnullGCPtr<JS::Script>>() ? *referrer.get<JS::NonnullGCPtr<JS::Script>>()->host_defined() : *referrer.get<JS::NonnullGCPtr<JS::CyclicModule>>()->host_defined()); |
439 | | |
440 | | // 2. Set settingsObject to referencingScript's settings object. |
441 | 0 | settings_object = referencing_script->settings_object(); |
442 | | |
443 | | // 3. Set fetchReferrer to referencingScript's base URL. |
444 | 0 | fetch_referrer = referencing_script->base_url(); |
445 | | |
446 | | // FIXME: 4. Set originalFetchOptions to referencingScript's fetch options. |
447 | 0 | } |
448 | | |
449 | | // 7. Disallow further import maps given settingsObject. |
450 | 0 | settings_object->disallow_further_import_maps(); |
451 | | |
452 | | // 8. Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]], |
453 | | // catching any exceptions. If they throw an exception, let resolutionError be the thrown exception. |
454 | 0 | auto url = HTML::resolve_module_specifier(referencing_script, module_request.module_specifier); |
455 | | |
456 | | // 9. If the previous step threw an exception, then: |
457 | 0 | if (url.is_exception()) { |
458 | | // 1. Let completion be Completion Record { [[Type]]: throw, [[Value]]: resolutionError, [[Target]]: empty }. |
459 | 0 | auto completion = dom_exception_to_throw_completion(main_thread_vm(), url.exception()); |
460 | | |
461 | | // 2. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion). |
462 | 0 | HTML::TemporaryExecutionContext context { host_defined_environment_settings_object(realm) }; |
463 | 0 | JS::finish_loading_imported_module(referrer, module_request, payload, completion); |
464 | | |
465 | | // 3. Return. |
466 | 0 | return; |
467 | 0 | } |
468 | | |
469 | | // 10. Let fetchOptions be the result of getting the descendant script fetch options given originalFetchOptions, url, and settingsObject. |
470 | 0 | auto fetch_options = MUST(HTML::get_descendant_script_fetch_options(original_fetch_options, url.value(), *settings_object)); |
471 | | |
472 | | // 11. Let destination be "script". |
473 | 0 | auto destination = Fetch::Infrastructure::Request::Destination::Script; |
474 | | |
475 | | // 12. Let fetchClient be settingsObject. |
476 | 0 | JS::NonnullGCPtr fetch_client { *settings_object }; |
477 | | |
478 | | // 13. If loadState is not undefined, then: |
479 | 0 | HTML::PerformTheFetchHook perform_fetch; |
480 | 0 | if (load_state) { |
481 | 0 | auto& fetch_context = static_cast<HTML::FetchContext&>(*load_state); |
482 | | |
483 | | // 1. Set destination to loadState.[[Destination]]. |
484 | 0 | destination = fetch_context.destination; |
485 | | |
486 | | // 2. Set fetchClient loadState.[[FetchClient]]. |
487 | 0 | fetch_client = fetch_context.fetch_client; |
488 | | |
489 | | // For step 13 |
490 | 0 | perform_fetch = fetch_context.perform_fetch; |
491 | 0 | } |
492 | |
|
493 | 0 | auto on_single_fetch_complete = HTML::create_on_fetch_script_complete(realm.heap(), [referrer, &realm, load_state, module_request, payload](JS::GCPtr<HTML::Script> const& module_script) -> void { |
494 | | // onSingleFetchComplete given moduleScript is the following algorithm: |
495 | | // 1. Let completion be null. |
496 | | // NOTE: Our JS::Completion does not support non JS::Value types for its [[Value]], a such we |
497 | | // use JS::ThrowCompletionOr here. |
498 | |
|
499 | 0 | auto& vm = realm.vm(); |
500 | 0 | JS::GCPtr<JS::Module> module = nullptr; |
501 | |
|
502 | 0 | auto completion = [&]() -> JS::ThrowCompletionOr<JS::NonnullGCPtr<JS::Module>> { |
503 | | // 2. If moduleScript is null, then set completion to Completion Record { [[Type]]: throw, [[Value]]: a new TypeError, [[Target]]: empty }. |
504 | 0 | if (!module_script) { |
505 | 0 | return JS::throw_completion(JS::TypeError::create(realm, ByteString::formatted("Loading imported module '{}' failed.", module_request.module_specifier))); |
506 | 0 | } |
507 | | // 3. Otherwise, if moduleScript's parse error is not null, then: |
508 | 0 | else if (!module_script->parse_error().is_null()) { |
509 | | // 1. Let parseError be moduleScript's parse error. |
510 | 0 | auto parse_error = module_script->parse_error(); |
511 | | |
512 | | // 2. Set completion to Completion Record { [[Type]]: throw, [[Value]]: parseError, [[Target]]: empty }. |
513 | 0 | auto completion = JS::throw_completion(parse_error); |
514 | | |
515 | | // 3. If loadState is not undefined and loadState.[[ParseError]] is null, set loadState.[[ParseError]] to parseError. |
516 | 0 | if (load_state) { |
517 | 0 | auto& load_state_as_fetch_context = static_cast<HTML::FetchContext&>(*load_state); |
518 | 0 | if (load_state_as_fetch_context.parse_error.is_null()) { |
519 | 0 | load_state_as_fetch_context.parse_error = parse_error; |
520 | 0 | } |
521 | 0 | } |
522 | |
|
523 | 0 | return completion; |
524 | 0 | } |
525 | | // 4. Otherwise, set completion to Completion Record { [[Type]]: normal, [[Value]]: result's record, [[Target]]: empty }. |
526 | 0 | else { |
527 | 0 | module = static_cast<HTML::JavaScriptModuleScript&>(*module_script).record(); |
528 | 0 | return JS::ThrowCompletionOr<JS::NonnullGCPtr<JS::Module>>(*module); |
529 | 0 | } |
530 | 0 | }(); |
531 | | |
532 | | // 5. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion). |
533 | | // NON-STANDARD: To ensure that LibJS can find the module on the stack, we push a new execution context. |
534 | |
|
535 | 0 | auto module_execution_context = JS::ExecutionContext::create(); |
536 | 0 | module_execution_context->realm = realm; |
537 | 0 | if (module) |
538 | 0 | module_execution_context->script_or_module = JS::NonnullGCPtr { *module }; |
539 | 0 | vm.push_execution_context(*module_execution_context); |
540 | |
|
541 | 0 | JS::finish_loading_imported_module(referrer, module_request, payload, completion); |
542 | |
|
543 | 0 | vm.pop_execution_context(); |
544 | 0 | }); |
545 | | |
546 | | // 14. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject, fetchReferrer, |
547 | | // moduleRequest, and onSingleFetchComplete as defined below. |
548 | | // If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well. |
549 | 0 | HTML::fetch_single_imported_module_script(realm, url.release_value(), *fetch_client, destination, fetch_options, *settings_object, fetch_referrer, module_request, perform_fetch, on_single_fetch_complete); |
550 | 0 | }; |
551 | |
|
552 | 0 | s_main_thread_vm->host_unrecognized_date_string = [](StringView date) { |
553 | 0 | dbgln("Unable to parse date string: \"{}\"", date); |
554 | 0 | }; |
555 | |
|
556 | 0 | return {}; |
557 | 0 | } |
558 | | |
559 | | JS::VM& main_thread_vm() |
560 | 0 | { |
561 | 0 | VERIFY(s_main_thread_vm); |
562 | 0 | return *s_main_thread_vm; |
563 | 0 | } |
564 | | |
565 | | // https://dom.spec.whatwg.org/#queue-a-mutation-observer-compound-microtask |
566 | | void queue_mutation_observer_microtask(DOM::Document const& document) |
567 | 0 | { |
568 | 0 | auto& vm = main_thread_vm(); |
569 | 0 | auto& custom_data = verify_cast<WebEngineCustomData>(*vm.custom_data()); |
570 | | |
571 | | // 1. If the surrounding agent’s mutation observer microtask queued is true, then return. |
572 | 0 | if (custom_data.mutation_observer_microtask_queued) |
573 | 0 | return; |
574 | | |
575 | | // 2. Set the surrounding agent’s mutation observer microtask queued to true. |
576 | 0 | custom_data.mutation_observer_microtask_queued = true; |
577 | | |
578 | | // 3. Queue a microtask to notify mutation observers. |
579 | | // NOTE: This uses the implied document concept. In the case of mutation observers, it is always done in a node context, so document should be that node's document. |
580 | | // FIXME: Is it safe to pass custom_data through? |
581 | 0 | HTML::queue_a_microtask(&document, JS::create_heap_function(vm.heap(), [&custom_data, &heap = document.heap()]() { |
582 | | // 1. Set the surrounding agent’s mutation observer microtask queued to false. |
583 | 0 | custom_data.mutation_observer_microtask_queued = false; |
584 | | |
585 | | // 2. Let notifySet be a clone of the surrounding agent’s mutation observers. |
586 | 0 | JS::MarkedVector<DOM::MutationObserver*> notify_set(heap); |
587 | 0 | for (auto& observer : custom_data.mutation_observers) |
588 | 0 | notify_set.append(observer); |
589 | | |
590 | | // FIXME: 3. Let signalSet be a clone of the surrounding agent’s signal slots. |
591 | | |
592 | | // FIXME: 4. Empty the surrounding agent’s signal slots. |
593 | | |
594 | | // 5. For each mo of notifySet: |
595 | 0 | for (auto& mutation_observer : notify_set) { |
596 | | // 1. Let records be a clone of mo’s record queue. |
597 | | // 2. Empty mo’s record queue. |
598 | 0 | auto records = mutation_observer->take_records(); |
599 | | |
600 | | // 3. For each node of mo’s node list, remove all transient registered observers whose observer is mo from node’s registered observer list. |
601 | 0 | for (auto& node : mutation_observer->node_list()) { |
602 | | // FIXME: Is this correct? |
603 | 0 | if (node.is_null()) |
604 | 0 | continue; |
605 | | |
606 | 0 | if (node->registered_observer_list()) { |
607 | 0 | node->registered_observer_list()->remove_all_matching([&mutation_observer](DOM::RegisteredObserver& registered_observer) { |
608 | 0 | return is<DOM::TransientRegisteredObserver>(registered_observer) && static_cast<DOM::TransientRegisteredObserver&>(registered_observer).observer().ptr() == mutation_observer; |
609 | 0 | }); |
610 | 0 | } |
611 | 0 | } |
612 | | |
613 | | // 4. If records is not empty, then invoke mo’s callback with « records, mo », and mo. If this throws an exception, catch it, and report the exception. |
614 | 0 | if (!records.is_empty()) { |
615 | 0 | auto& callback = mutation_observer->callback(); |
616 | 0 | auto& realm = callback.callback_context->realm(); |
617 | |
|
618 | 0 | auto wrapped_records = MUST(JS::Array::create(realm, 0)); |
619 | 0 | for (size_t i = 0; i < records.size(); ++i) { |
620 | 0 | auto& record = records.at(i); |
621 | 0 | auto property_index = JS::PropertyKey { i }; |
622 | 0 | MUST(wrapped_records->create_data_property(property_index, record.ptr())); |
623 | 0 | } |
624 | |
|
625 | 0 | auto result = WebIDL::invoke_callback(callback, mutation_observer, wrapped_records, mutation_observer); |
626 | 0 | if (result.is_abrupt()) |
627 | 0 | HTML::report_exception(result, realm); |
628 | 0 | } |
629 | 0 | } |
630 | | |
631 | | // FIXME: 6. For each slot of signalSet, fire an event named slotchange, with its bubbles attribute set to true, at slot. |
632 | 0 | })); |
633 | 0 | } |
634 | | |
635 | | // https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-new-javascript-realm |
636 | | NonnullOwnPtr<JS::ExecutionContext> create_a_new_javascript_realm(JS::VM& vm, Function<JS::Object*(JS::Realm&)> create_global_object, Function<JS::Object*(JS::Realm&)> create_global_this_value) |
637 | 0 | { |
638 | | // 1. Perform InitializeHostDefinedRealm() with the provided customizations for creating the global object and the global this binding. |
639 | | // 2. Let realm execution context be the running JavaScript execution context. |
640 | 0 | auto realm_execution_context = MUST(JS::Realm::initialize_host_defined_realm(vm, move(create_global_object), move(create_global_this_value))); |
641 | | |
642 | | // 3. Remove realm execution context from the JavaScript execution context stack. |
643 | 0 | vm.execution_context_stack().remove_first_matching([&realm_execution_context](auto execution_context) { |
644 | 0 | return execution_context == realm_execution_context.ptr(); |
645 | 0 | }); |
646 | | |
647 | | // NO-OP: 4. Let realm be realm execution context's Realm component. |
648 | | // NO-OP: 5. Set realm's agent to agent. |
649 | | |
650 | | // FIXME: 6. If agent's agent cluster's cross-origin isolation mode is "none", then: |
651 | | // 1. Let global be realm's global object. |
652 | | // 2. Let status be ! global.[[Delete]]("SharedArrayBuffer"). |
653 | | // 3. Assert: status is true. |
654 | | |
655 | | // 7. Return realm execution context. |
656 | 0 | return realm_execution_context; |
657 | 0 | } |
658 | | |
659 | | void WebEngineCustomData::spin_event_loop_until(JS::SafeFunction<bool()> goal_condition) |
660 | 0 | { |
661 | 0 | Platform::EventLoopPlugin::the().spin_until(move(goal_condition)); |
662 | 0 | } |
663 | | |
664 | | // https://html.spec.whatwg.org/multipage/custom-elements.html#invoke-custom-element-reactions |
665 | | void invoke_custom_element_reactions(Vector<JS::Handle<DOM::Element>>& element_queue) |
666 | 0 | { |
667 | | // 1. While queue is not empty: |
668 | 0 | while (!element_queue.is_empty()) { |
669 | | // 1. Let element be the result of dequeuing from queue. |
670 | 0 | auto element = element_queue.take_first(); |
671 | | |
672 | | // 2. Let reactions be element's custom element reaction queue. |
673 | 0 | auto* reactions = element->custom_element_reaction_queue(); |
674 | | |
675 | | // 3. Repeat until reactions is empty: |
676 | 0 | if (!reactions) |
677 | 0 | continue; |
678 | 0 | while (!reactions->is_empty()) { |
679 | | // 1. Remove the first element of reactions, and let reaction be that element. Switch on reaction's type: |
680 | 0 | auto reaction = reactions->take_first(); |
681 | |
|
682 | 0 | auto maybe_exception = reaction.visit( |
683 | 0 | [&](DOM::CustomElementUpgradeReaction const& custom_element_upgrade_reaction) -> JS::ThrowCompletionOr<void> { |
684 | | // -> upgrade reaction |
685 | | // Upgrade element using reaction's custom element definition. |
686 | 0 | return element->upgrade_element(*custom_element_upgrade_reaction.custom_element_definition); |
687 | 0 | }, |
688 | 0 | [&](DOM::CustomElementCallbackReaction& custom_element_callback_reaction) -> JS::ThrowCompletionOr<void> { |
689 | | // -> callback reaction |
690 | | // Invoke reaction's callback function with reaction's arguments, and with element as the callback this value. |
691 | 0 | auto result = WebIDL::invoke_callback(*custom_element_callback_reaction.callback, element.ptr(), custom_element_callback_reaction.arguments); |
692 | 0 | if (result.is_abrupt()) |
693 | 0 | return result.release_error(); |
694 | 0 | return {}; |
695 | 0 | }); |
696 | | |
697 | | // If this throws an exception, catch it, and report the exception. |
698 | 0 | if (maybe_exception.is_throw_completion()) |
699 | 0 | HTML::report_exception(maybe_exception, element->realm()); |
700 | 0 | } |
701 | 0 | } |
702 | 0 | } |
703 | | |
704 | | } |