Serenity Operating System
1/*
2 * Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/JsonArray.h>
8#include <AK/JsonObject.h>
9#include <AK/JsonValue.h>
10#include <AK/NumericLimits.h>
11#include <AK/ScopeGuard.h>
12#include <AK/Time.h>
13#include <AK/Variant.h>
14#include <LibJS/Parser.h>
15#include <LibJS/Runtime/Array.h>
16#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
17#include <LibJS/Runtime/GlobalEnvironment.h>
18#include <LibJS/Runtime/JSONObject.h>
19#include <LibJS/Runtime/Promise.h>
20#include <LibJS/Runtime/PromiseConstructor.h>
21#include <LibWeb/DOM/Document.h>
22#include <LibWeb/DOM/HTMLCollection.h>
23#include <LibWeb/DOM/NodeList.h>
24#include <LibWeb/FileAPI/FileList.h>
25#include <LibWeb/HTML/BrowsingContext.h>
26#include <LibWeb/HTML/HTMLOptionsCollection.h>
27#include <LibWeb/HTML/Scripting/Environments.h>
28#include <LibWeb/HTML/Window.h>
29#include <LibWeb/Page/Page.h>
30#include <LibWeb/WebDriver/ExecuteScript.h>
31
32namespace Web::WebDriver {
33
34#define TRY_OR_JS_ERROR(expression) \
35 ({ \
36 auto&& _temporary_result = (expression); \
37 if (_temporary_result.is_error()) [[unlikely]] \
38 return ExecuteScriptResultType::JavaScriptError; \
39 static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
40 "Do not return a reference from a fallible expression"); \
41 _temporary_result.release_value(); \
42 })
43
44static ErrorOr<JsonValue, ExecuteScriptResultType> internal_json_clone_algorithm(JS::Realm&, JS::Value, HashTable<JS::Object*>& seen);
45static ErrorOr<JsonValue, ExecuteScriptResultType> clone_an_object(JS::Realm&, JS::Object&, HashTable<JS::Object*>& seen, auto const& clone_algorithm);
46
47// https://w3c.github.io/webdriver/#dfn-collection
48static bool is_collection(JS::Object const& value)
49{
50 // A collection is an Object that implements the Iterable interface, and whose:
51 return (
52 // - initial value of the toString own property is "Arguments"
53 value.has_parameter_map()
54 // - instance of Array
55 || is<JS::Array>(value)
56 // - instance of FileList
57 || is<FileAPI::FileList>(value)
58 // - instance of HTMLAllCollection
59 || false // FIXME
60 // - instance of HTMLCollection
61 || is<DOM::HTMLCollection>(value)
62 // - instance of HTMLFormControlsCollection
63 || false // FIXME
64 // - instance of HTMLOptionsCollection
65 || is<HTML::HTMLOptionsCollection>(value)
66 // - instance of NodeList
67 || is<DOM::NodeList>(value));
68}
69
70// https://w3c.github.io/webdriver/#dfn-json-clone
71static ErrorOr<JsonValue, ExecuteScriptResultType> json_clone(JS::Realm& realm, JS::Value value)
72{
73 // To perform a JSON clone return the result of calling the internal JSON clone algorithm with arguments value and an empty List.
74 auto seen = HashTable<JS::Object*> {};
75 return internal_json_clone_algorithm(realm, value, seen);
76}
77
78// https://w3c.github.io/webdriver/#dfn-internal-json-clone-algorithm
79static ErrorOr<JsonValue, ExecuteScriptResultType> internal_json_clone_algorithm(JS::Realm& realm, JS::Value value, HashTable<JS::Object*>& seen)
80{
81 auto& vm = realm.vm();
82
83 // When required to run the internal JSON clone algorithm with arguments value and seen, a remote end must return the value of the first matching statement, matching on value:
84 // -> undefined
85 // -> null
86 if (value.is_nullish()) {
87 // Success with data null.
88 return JsonValue {};
89 }
90
91 // -> type Boolean
92 // -> type Number
93 // -> type String
94 // Success with data value.
95 if (value.is_boolean())
96 return JsonValue { value.as_bool() };
97 if (value.is_number())
98 return JsonValue { value.as_double() };
99 if (value.is_string())
100 return JsonValue { TRY_OR_JS_ERROR(value.as_string().deprecated_string()) };
101
102 // NOTE: BigInt and Symbol not mentioned anywhere in the WebDriver spec, as it references ES5.
103 // It assumes that all primitives are handled above, and the value is an object for the remaining steps.
104 if (value.is_bigint() || value.is_symbol())
105 return ExecuteScriptResultType::JavaScriptError;
106
107 // FIXME: - a collection
108 // FIXME: - instance of element
109 // FIXME: - instance of shadow root
110 // FIXME: - a WindowProxy object
111
112 // -> has an own property named "toJSON" that is a Function
113 auto to_json = value.as_object().get_without_side_effects(vm.names.toJSON);
114 if (to_json.is_function()) {
115 // Return success with the value returned by Function.[[Call]](toJSON) with value as the this value.
116 auto to_json_result = TRY_OR_JS_ERROR(to_json.as_function().internal_call(value, JS::MarkedVector<JS::Value> { vm.heap() }));
117 if (!to_json_result.is_string())
118 return ExecuteScriptResultType::JavaScriptError;
119 return TRY_OR_JS_ERROR(to_json_result.as_string().deprecated_string());
120 }
121
122 // -> Otherwise
123 // 1. If value is in seen, return error with error code javascript error.
124 if (seen.contains(&value.as_object()))
125 return ExecuteScriptResultType::JavaScriptError;
126
127 // 2. Append value to seen.
128 seen.set(&value.as_object());
129
130 ScopeGuard remove_seen { [&] {
131 // 4. Remove the last element of seen.
132 seen.remove(&value.as_object());
133 } };
134
135 // 3. Let result be the value of running the clone an object algorithm with arguments value and seen, and the internal JSON clone algorithm as the clone algorithm.
136 auto result = TRY(clone_an_object(realm, value.as_object(), seen, internal_json_clone_algorithm));
137
138 // 5. Return result.
139 return result;
140}
141
142// https://w3c.github.io/webdriver/#dfn-clone-an-object
143static ErrorOr<JsonValue, ExecuteScriptResultType> clone_an_object(JS::Realm& realm, JS::Object& value, HashTable<JS::Object*>& seen, auto const& clone_algorithm)
144{
145 auto& vm = realm.vm();
146
147 // 1. Let result be the value of the first matching statement, matching on value:
148 auto get_result = [&]() -> ErrorOr<Variant<JsonArray, JsonObject>, ExecuteScriptResultType> {
149 // -> a collection
150 if (is_collection(value)) {
151 // A new Array which length property is equal to the result of getting the property length of value.
152 auto length_property = TRY_OR_JS_ERROR(value.internal_get_own_property(vm.names.length));
153 if (!length_property->value.has_value())
154 return ExecuteScriptResultType::JavaScriptError;
155 auto length = TRY_OR_JS_ERROR(length_property->value->to_length(vm));
156 if (length > NumericLimits<u32>::max())
157 return ExecuteScriptResultType::JavaScriptError;
158 auto array = JsonArray {};
159 for (size_t i = 0; i < length; ++i)
160 array.append(JsonValue {});
161 return array;
162 }
163 // -> Otherwise
164 else {
165 // A new Object.
166 return JsonObject {};
167 }
168 };
169 auto result = TRY(get_result());
170
171 // 2. For each enumerable own property in value, run the following substeps:
172 for (auto& key : MUST(value.Object::internal_own_property_keys())) {
173 // 1. Let name be the name of the property.
174 auto name = MUST(JS::PropertyKey::from_value(vm, key));
175
176 if (!value.storage_get(name)->attributes.is_enumerable())
177 continue;
178
179 // 2. Let source property value be the result of getting a property named name from value. If doing so causes script to be run and that script throws an error, return error with error code javascript error.
180 auto source_property_value = TRY_OR_JS_ERROR(value.internal_get_own_property(name));
181 if (!source_property_value.has_value() || !source_property_value->value.has_value())
182 continue;
183
184 // 3. Let cloned property result be the result of calling the clone algorithm with arguments source property value and seen.
185 auto cloned_property_result = clone_algorithm(realm, *source_property_value->value, seen);
186
187 // 4. If cloned property result is a success, set a property of result with name name and value equal to cloned property result’s data.
188 if (!cloned_property_result.is_error()) {
189 result.visit(
190 [&](JsonArray& array) {
191 // NOTE: If this was a JS array, only indexed properties would be serialized anyway.
192 if (name.is_number())
193 array.set(name.as_number(), cloned_property_result.value());
194 },
195 [&](JsonObject& object) {
196 object.set(name.to_string(), cloned_property_result.value());
197 });
198 }
199 // 5. Otherwise, return cloned property result.
200 else {
201 return cloned_property_result;
202 }
203 }
204
205 return result.visit([&](auto const& value) -> JsonValue { return value; });
206}
207
208// https://w3c.github.io/webdriver/#dfn-execute-a-function-body
209static JS::ThrowCompletionOr<JS::Value> execute_a_function_body(Web::Page& page, DeprecatedString const& body, JS::MarkedVector<JS::Value> parameters)
210{
211 // FIXME: If at any point during the algorithm a user prompt appears, immediately return Completion { [[Type]]: normal, [[Value]]: null, [[Target]]: empty }, but continue to run the other steps of this algorithm in parallel.
212
213 // 1. Let window be the associated window of the current browsing context’s active document.
214 // FIXME: This will need adjusting when WebDriver supports frames.
215 auto& window = page.top_level_browsing_context().active_document()->window();
216
217 // 2. Let environment settings be the environment settings object for window.
218 auto& environment_settings = Web::HTML::relevant_settings_object(window);
219
220 // 3. Let global scope be environment settings realm’s global environment.
221 auto& global_scope = environment_settings.realm().global_environment();
222
223 auto& realm = window.realm();
224
225 bool contains_direct_call_to_eval = false;
226 auto source_text = DeprecatedString::formatted("function() {{ {} }}", body);
227 auto parser = JS::Parser { JS::Lexer { source_text } };
228 auto function_expression = parser.parse_function_node<JS::FunctionExpression>();
229
230 // 4. If body is not parsable as a FunctionBody or if parsing detects an early error, return Completion { [[Type]]: normal, [[Value]]: null, [[Target]]: empty }.
231 if (parser.has_errors())
232 return JS::js_null();
233
234 // 5. If body begins with a directive prologue that contains a use strict directive then let strict be true, otherwise let strict be false.
235 // NOTE: Handled in step 8 below.
236
237 // 6. Prepare to run a script with environment settings.
238 environment_settings.prepare_to_run_script();
239
240 // 7. Prepare to run a callback with environment settings.
241 environment_settings.prepare_to_run_callback();
242
243 // 8. Let function be the result of calling FunctionCreate, with arguments:
244 // kind
245 // Normal.
246 // list
247 // An empty List.
248 // body
249 // The result of parsing body above.
250 // global scope
251 // The result of parsing global scope above.
252 // strict
253 // The result of parsing strict above.
254 auto function = JS::ECMAScriptFunctionObject::create(realm, "", move(source_text), function_expression->body(), function_expression->parameters(), function_expression->function_length(), &global_scope, nullptr, function_expression->kind(), function_expression->is_strict_mode(), function_expression->might_need_arguments_object(), contains_direct_call_to_eval);
255
256 // 9. Let completion be Function.[[Call]](window, parameters) with function as the this value.
257 // NOTE: This is not entirely clear, but I don't think they mean actually passing `function` as
258 // the this value argument, but using it as the object [[Call]] is executed on.
259 auto completion = function->internal_call(&window, move(parameters));
260
261 // 10. Clean up after running a callback with environment settings.
262 environment_settings.clean_up_after_running_callback();
263
264 // 11. Clean up after running a script with environment settings.
265 environment_settings.clean_up_after_running_script();
266
267 // 12. Return completion.
268 return completion;
269}
270
271ExecuteScriptResultSerialized execute_script(Web::Page& page, DeprecatedString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout)
272{
273 // FIXME: Use timeout.
274 (void)timeout;
275
276 auto* window = page.top_level_browsing_context().active_window();
277 auto& realm = window->realm();
278
279 // 4. Let promise be a new Promise.
280 // NOTE: For now we skip this and handle a throw completion manually instead of using 'promise-calling'.
281
282 // FIXME: 5. Run the following substeps in parallel:
283 auto result = [&] {
284 // 1. Let scriptPromise be the result of promise-calling execute a function body, with arguments body and arguments.
285 auto completion = execute_a_function_body(page, body, move(arguments));
286
287 // 2. Upon fulfillment of scriptPromise with value v, resolve promise with value v.
288 // 3. Upon rejection of scriptPromise with value r, reject promise with value r.
289 auto result_type = completion.is_error()
290 ? ExecuteScriptResultType::PromiseRejected
291 : ExecuteScriptResultType::PromiseResolved;
292 auto result_value = completion.is_error()
293 ? *completion.throw_completion().value()
294 : completion.value();
295
296 return ExecuteScriptResult { result_type, result_value };
297 }();
298
299 // FIXME: 6. If promise is still pending and the session script timeout is reached, return error with error code script timeout.
300 // 7. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
301 // 8. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result.
302 auto json_value_or_error = json_clone(realm, result.value);
303 if (json_value_or_error.is_error()) {
304 auto error_object = JsonObject {};
305 error_object.set("name", "Error");
306 error_object.set("message", "Could not clone result value");
307 return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
308 }
309 return { result.type, json_value_or_error.release_value() };
310}
311
312ExecuteScriptResultSerialized execute_async_script(Web::Page& page, DeprecatedString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout)
313{
314 auto* document = page.top_level_browsing_context().active_document();
315 auto& settings_object = document->relevant_settings_object();
316 auto* window = page.top_level_browsing_context().active_window();
317 auto& realm = window->realm();
318 auto& vm = window->vm();
319 auto start = Time::now_monotonic();
320
321 // 4. Let promise be a new Promise.
322 auto promise = JS::Promise::create(realm);
323
324 // FIXME: 5 Run the following substeps in parallel:
325 auto result = [&] {
326 // NOTE: We need to push an execution context in order to make create_resolving_functions() succeed.
327 vm.push_execution_context(settings_object.realm_execution_context());
328
329 // 1. Let resolvingFunctions be CreateResolvingFunctions(promise).
330 auto resolving_functions = promise->create_resolving_functions();
331
332 VERIFY(&settings_object.realm_execution_context() == &vm.running_execution_context());
333 vm.pop_execution_context();
334
335 // 2. Append resolvingFunctions.[[Resolve]] to arguments.
336 arguments.append(&resolving_functions.resolve);
337
338 // 3. Let result be the result of calling execute a function body, with arguments body and arguments.
339 // FIXME: 'result' -> 'scriptResult' (spec issue)
340 auto script_result = execute_a_function_body(page, body, move(arguments));
341
342 // 4.If scriptResult.[[Type]] is not normal, then reject promise with value scriptResult.[[Value]], and abort these steps.
343 // NOTE: Prior revisions of this specification did not recognize the return value of the provided script.
344 // In order to preserve legacy behavior, the return value only influences the command if it is a
345 // "thenable" object or if determining this produces an exception.
346 if (script_result.is_throw_completion())
347 return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, *script_result.throw_completion().value() };
348
349 // 5. If Type(scriptResult.[[Value]]) is not Object, then abort these steps.
350 if (!script_result.value().is_object())
351 return ExecuteScriptResult { ExecuteScriptResultType::PromiseResolved, JS::js_null() };
352
353 // 6. Let then be Get(scriptResult.[[Value]], "then").
354 auto then = script_result.value().as_object().get(vm.names.then);
355
356 // 7. If then.[[Type]] is not normal, then reject promise with value then.[[Value]], and abort these steps.
357 if (then.is_throw_completion())
358 return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, *then.throw_completion().value() };
359
360 // 8. If IsCallable(then.[[Type]]) is false, then abort these steps.
361 if (!then.value().is_function())
362 return ExecuteScriptResult { ExecuteScriptResultType::PromiseResolved, JS::js_null() };
363
364 // 9. Let scriptPromise be PromiseResolve(Promise, scriptResult.[[Value]]).
365 auto script_promise_or_error = JS::promise_resolve(vm, *realm.intrinsics().promise_constructor(), script_result.value());
366 if (script_promise_or_error.is_throw_completion())
367 return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, *script_promise_or_error.throw_completion().value() };
368 auto& script_promise = static_cast<JS::Promise&>(*script_promise_or_error.value());
369
370 vm.custom_data()->spin_event_loop_until([&] {
371 if (script_promise.state() != JS::Promise::State::Pending)
372 return true;
373 if (timeout.has_value() && (Time::now_monotonic() - start) > Time::from_seconds(static_cast<i64>(*timeout)))
374 return true;
375 return false;
376 });
377
378 // 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v.
379 if (script_promise.state() == JS::Promise::State::Fulfilled)
380 return ExecuteScriptResult { ExecuteScriptResultType::PromiseResolved, script_promise.result() };
381
382 // 11. Upon rejection of scriptPromise with value r, reject promise with value r.
383 if (script_promise.state() == JS::Promise::State::Rejected)
384 return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, script_promise.result() };
385
386 return ExecuteScriptResult { ExecuteScriptResultType::Timeout, script_promise.result() };
387 }();
388
389 // 6. If promise is still pending and session script timeout milliseconds is reached, return error with error code script timeout.
390 // 7. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
391 // 8. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result.
392 auto json_value_or_error = json_clone(realm, result.value);
393 if (json_value_or_error.is_error()) {
394 auto error_object = JsonObject {};
395 error_object.set("name", "Error");
396 error_object.set("message", "Could not clone result value");
397 return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
398 }
399 return { result.type, json_value_or_error.release_value() };
400}
401
402}