summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/WebDriver/ExecuteScript.cpp
blob: ec1bb716c8141d4692872d828b0d81a27a4e3232 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
/*
 * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/NumericLimits.h>
#include <AK/ScopeGuard.h>
#include <AK/Time.h>
#include <AK/Variant.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
#include <LibJS/Runtime/GlobalEnvironment.h>
#include <LibJS/Runtime/JSONObject.h>
#include <LibJS/Runtime/Promise.h>
#include <LibJS/Runtime/PromiseConstructor.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/NodeList.h>
#include <LibWeb/FileAPI/FileList.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/HTMLOptionsCollection.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/WebDriver/ExecuteScript.h>

namespace Web::WebDriver {

#define TRY_OR_JS_ERROR(expression)                                                                  \
    ({                                                                                               \
        auto&& _temporary_result = (expression);                                                     \
        if (_temporary_result.is_error()) [[unlikely]]                                               \
            return ExecuteScriptResultType::JavaScriptError;                                         \
        static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
            "Do not return a reference from a fallible expression");                                 \
        _temporary_result.release_value();                                                           \
    })

static ErrorOr<JsonValue, ExecuteScriptResultType> internal_json_clone_algorithm(JS::Realm&, JS::Value, HashTable<JS::Object*>& seen);
static ErrorOr<JsonValue, ExecuteScriptResultType> clone_an_object(JS::Realm&, JS::Object&, HashTable<JS::Object*>& seen, auto const& clone_algorithm);

// https://w3c.github.io/webdriver/#dfn-collection
static bool is_collection(JS::Object const& value)
{
    // A collection is an Object that implements the Iterable interface, and whose:
    return (
        // - initial value of the toString own property is "Arguments"
        value.has_parameter_map()
        // - instance of Array
        || is<JS::Array>(value)
        // - instance of FileList
        || is<FileAPI::FileList>(value)
        // - instance of HTMLAllCollection
        || false // FIXME
        // - instance of HTMLCollection
        || is<DOM::HTMLCollection>(value)
        // - instance of HTMLFormControlsCollection
        || false // FIXME
        // - instance of HTMLOptionsCollection
        || is<HTML::HTMLOptionsCollection>(value)
        // - instance of NodeList
        || is<DOM::NodeList>(value));
}

// https://w3c.github.io/webdriver/#dfn-json-clone
static ErrorOr<JsonValue, ExecuteScriptResultType> json_clone(JS::Realm& realm, JS::Value value)
{
    // To perform a JSON clone return the result of calling the internal JSON clone algorithm with arguments value and an empty List.
    auto seen = HashTable<JS::Object*> {};
    return internal_json_clone_algorithm(realm, value, seen);
}

// https://w3c.github.io/webdriver/#dfn-internal-json-clone-algorithm
static ErrorOr<JsonValue, ExecuteScriptResultType> internal_json_clone_algorithm(JS::Realm& realm, JS::Value value, HashTable<JS::Object*>& seen)
{
    auto& vm = realm.vm();

    // 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:
    // -> undefined
    // -> null
    if (value.is_nullish()) {
        // Success with data null.
        return JsonValue {};
    }

    // -> type Boolean
    // -> type Number
    // -> type String
    //     Success with data value.
    if (value.is_boolean())
        return JsonValue { value.as_bool() };
    if (value.is_number())
        return JsonValue { value.as_double() };
    if (value.is_string())
        return JsonValue { TRY_OR_JS_ERROR(value.as_string().deprecated_string()) };

    // NOTE: BigInt and Symbol not mentioned anywhere in the WebDriver spec, as it references ES5.
    //       It assumes that all primitives are handled above, and the value is an object for the remaining steps.
    if (value.is_bigint() || value.is_symbol())
        return ExecuteScriptResultType::JavaScriptError;

    // FIXME: - a collection
    // FIXME: - instance of element
    // FIXME: - instance of shadow root
    // FIXME: - a WindowProxy object

    // -> has an own property named "toJSON" that is a Function
    auto to_json = value.as_object().get_without_side_effects(vm.names.toJSON);
    if (to_json.is_function()) {
        // Return success with the value returned by Function.[[Call]](toJSON) with value as the this value.
        auto to_json_result = TRY_OR_JS_ERROR(to_json.as_function().internal_call(value, JS::MarkedVector<JS::Value> { vm.heap() }));
        if (!to_json_result.is_string())
            return ExecuteScriptResultType::JavaScriptError;
        return TRY_OR_JS_ERROR(to_json_result.as_string().deprecated_string());
    }

    // -> Otherwise
    // 1. If value is in seen, return error with error code javascript error.
    if (seen.contains(&value.as_object()))
        return ExecuteScriptResultType::JavaScriptError;

    // 2. Append value to seen.
    seen.set(&value.as_object());

    ScopeGuard remove_seen { [&] {
        // 4. Remove the last element of seen.
        seen.remove(&value.as_object());
    } };

    // 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.
    auto result = TRY(clone_an_object(realm, value.as_object(), seen, internal_json_clone_algorithm));

    // 5. Return result.
    return result;
}

// https://w3c.github.io/webdriver/#dfn-clone-an-object
static ErrorOr<JsonValue, ExecuteScriptResultType> clone_an_object(JS::Realm& realm, JS::Object& value, HashTable<JS::Object*>& seen, auto const& clone_algorithm)
{
    auto& vm = realm.vm();

    // 1. Let result be the value of the first matching statement, matching on value:
    auto get_result = [&]() -> ErrorOr<Variant<JsonArray, JsonObject>, ExecuteScriptResultType> {
        // -> a collection
        if (is_collection(value)) {
            // A new Array which length property is equal to the result of getting the property length of value.
            auto length_property = TRY_OR_JS_ERROR(value.internal_get_own_property(vm.names.length));
            if (!length_property->value.has_value())
                return ExecuteScriptResultType::JavaScriptError;
            auto length = TRY_OR_JS_ERROR(length_property->value->to_length(vm));
            if (length > NumericLimits<u32>::max())
                return ExecuteScriptResultType::JavaScriptError;
            auto array = JsonArray {};
            for (size_t i = 0; i < length; ++i)
                array.append(JsonValue {});
            return array;
        }
        // -> Otherwise
        else {
            // A new Object.
            return JsonObject {};
        }
    };
    auto result = TRY(get_result());

    // 2. For each enumerable own property in value, run the following substeps:
    for (auto& key : MUST(value.Object::internal_own_property_keys())) {
        // 1. Let name be the name of the property.
        auto name = MUST(JS::PropertyKey::from_value(vm, key));

        if (!value.storage_get(name)->attributes.is_enumerable())
            continue;

        // 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.
        auto source_property_value = TRY_OR_JS_ERROR(value.internal_get_own_property(name));
        if (!source_property_value.has_value() || !source_property_value->value.has_value())
            continue;

        // 3. Let cloned property result be the result of calling the clone algorithm with arguments source property value and seen.
        auto cloned_property_result = clone_algorithm(realm, *source_property_value->value, seen);

        // 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.
        if (!cloned_property_result.is_error()) {
            result.visit(
                [&](JsonArray& array) {
                    // NOTE: If this was a JS array, only indexed properties would be serialized anyway.
                    if (name.is_number())
                        array.set(name.as_number(), cloned_property_result.value());
                },
                [&](JsonObject& object) {
                    object.set(name.to_string(), cloned_property_result.value());
                });
        }
        // 5. Otherwise, return cloned property result.
        else {
            return cloned_property_result;
        }
    }

    return result.visit([&](auto const& value) -> JsonValue { return value; });
}

// https://w3c.github.io/webdriver/#dfn-execute-a-function-body
static JS::ThrowCompletionOr<JS::Value> execute_a_function_body(Web::Page& page, DeprecatedString const& body, JS::MarkedVector<JS::Value> parameters)
{
    // 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.

    // 1. Let window be the associated window of the current browsing context’s active document.
    // FIXME: This will need adjusting when WebDriver supports frames.
    auto& window = page.top_level_browsing_context().active_document()->window();

    // 2. Let environment settings be the environment settings object for window.
    auto& environment_settings = Web::HTML::relevant_settings_object(window);

    // 3. Let global scope be environment settings realm’s global environment.
    auto& global_scope = environment_settings.realm().global_environment();

    auto& realm = window.realm();

    bool contains_direct_call_to_eval = false;
    auto source_text = DeprecatedString::formatted("function() {{ {} }}", body);
    auto parser = JS::Parser { JS::Lexer { source_text } };
    auto function_expression = parser.parse_function_node<JS::FunctionExpression>();

    // 4. If body is not parsable as a FunctionBody or if parsing detects an early error, return Completion { [[Type]]: normal, [[Value]]: null, [[Target]]: empty }.
    if (parser.has_errors())
        return JS::js_null();

    // 5. If body begins with a directive prologue that contains a use strict directive then let strict be true, otherwise let strict be false.
    // NOTE: Handled in step 8 below.

    // 6. Prepare to run a script with environment settings.
    environment_settings.prepare_to_run_script();

    // 7. Prepare to run a callback with environment settings.
    environment_settings.prepare_to_run_callback();

    // 8. Let function be the result of calling FunctionCreate, with arguments:
    // kind
    //    Normal.
    // list
    //    An empty List.
    // body
    //    The result of parsing body above.
    // global scope
    //    The result of parsing global scope above.
    // strict
    //    The result of parsing strict above.
    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);

    // 9. Let completion be Function.[[Call]](window, parameters) with function as the this value.
    // NOTE: This is not entirely clear, but I don't think they mean actually passing `function` as
    // the this value argument, but using it as the object [[Call]] is executed on.
    auto completion = function->internal_call(&window, move(parameters));

    // 10. Clean up after running a callback with environment settings.
    environment_settings.clean_up_after_running_callback();

    // 11. Clean up after running a script with environment settings.
    environment_settings.clean_up_after_running_script();

    // 12. Return completion.
    return completion;
}

ExecuteScriptResultSerialized execute_script(Web::Page& page, DeprecatedString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout)
{
    // FIXME: Use timeout.
    (void)timeout;

    auto* window = page.top_level_browsing_context().active_window();
    auto& realm = window->realm();

    // 4. Let promise be a new Promise.
    // NOTE: For now we skip this and handle a throw completion manually instead of using 'promise-calling'.

    // FIXME: 5. Run the following substeps in parallel:
    auto result = [&] {
        // 1. Let scriptPromise be the result of promise-calling execute a function body, with arguments body and arguments.
        auto completion = execute_a_function_body(page, body, move(arguments));

        // 2. Upon fulfillment of scriptPromise with value v, resolve promise with value v.
        // 3. Upon rejection of scriptPromise with value r, reject promise with value r.
        auto result_type = completion.is_error()
            ? ExecuteScriptResultType::PromiseRejected
            : ExecuteScriptResultType::PromiseResolved;
        auto result_value = completion.is_error()
            ? *completion.throw_completion().value()
            : completion.value();

        return ExecuteScriptResult { result_type, result_value };
    }();

    // FIXME: 6. If promise is still pending and the session script timeout is reached, return error with error code script timeout.
    // 7. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
    // 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.
    auto json_value_or_error = json_clone(realm, result.value);
    if (json_value_or_error.is_error()) {
        auto error_object = JsonObject {};
        error_object.set("name", "Error");
        error_object.set("message", "Could not clone result value");
        return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
    }
    return { result.type, json_value_or_error.release_value() };
}

ExecuteScriptResultSerialized execute_async_script(Web::Page& page, DeprecatedString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout)
{
    auto* document = page.top_level_browsing_context().active_document();
    auto& settings_object = document->relevant_settings_object();
    auto* window = page.top_level_browsing_context().active_window();
    auto& realm = window->realm();
    auto& vm = window->vm();
    auto start = Time::now_monotonic();

    // 4. Let promise be a new Promise.
    auto promise = JS::Promise::create(realm);

    // FIXME: 5 Run the following substeps in parallel:
    auto result = [&] {
        // NOTE: We need to push an execution context in order to make create_resolving_functions() succeed.
        vm.push_execution_context(settings_object.realm_execution_context());

        // 1. Let resolvingFunctions be CreateResolvingFunctions(promise).
        auto resolving_functions = promise->create_resolving_functions();

        VERIFY(&settings_object.realm_execution_context() == &vm.running_execution_context());
        vm.pop_execution_context();

        // 2. Append resolvingFunctions.[[Resolve]] to arguments.
        arguments.append(resolving_functions.resolve);

        // 3. Let result be the result of calling execute a function body, with arguments body and arguments.
        // FIXME: 'result' -> 'scriptResult' (spec issue)
        auto script_result = execute_a_function_body(page, body, move(arguments));

        // 4.If scriptResult.[[Type]] is not normal, then reject promise with value scriptResult.[[Value]], and abort these steps.
        // NOTE: Prior revisions of this specification did not recognize the return value of the provided script.
        //       In order to preserve legacy behavior, the return value only influences the command if it is a
        //       "thenable"  object or if determining this produces an exception.
        if (script_result.is_throw_completion())
            return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, *script_result.throw_completion().value() };

        // 5. If Type(scriptResult.[[Value]]) is not Object, then abort these steps.
        if (!script_result.value().is_object())
            return ExecuteScriptResult { ExecuteScriptResultType::PromiseResolved, JS::js_null() };

        // 6. Let then be Get(scriptResult.[[Value]], "then").
        auto then = script_result.value().as_object().get(vm.names.then);

        // 7. If then.[[Type]] is not normal, then reject promise with value then.[[Value]], and abort these steps.
        if (then.is_throw_completion())
            return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, *then.throw_completion().value() };

        // 8. If IsCallable(then.[[Type]]) is false, then abort these steps.
        if (!then.value().is_function())
            return ExecuteScriptResult { ExecuteScriptResultType::PromiseResolved, JS::js_null() };

        // 9. Let scriptPromise be PromiseResolve(Promise, scriptResult.[[Value]]).
        auto script_promise_or_error = JS::promise_resolve(vm, realm.intrinsics().promise_constructor(), script_result.value());
        if (script_promise_or_error.is_throw_completion())
            return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, *script_promise_or_error.throw_completion().value() };
        auto& script_promise = static_cast<JS::Promise&>(*script_promise_or_error.value());

        vm.custom_data()->spin_event_loop_until([&] {
            if (script_promise.state() != JS::Promise::State::Pending)
                return true;
            if (timeout.has_value() && (Time::now_monotonic() - start) > Time::from_seconds(static_cast<i64>(*timeout)))
                return true;
            return false;
        });

        // 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v.
        if (script_promise.state() == JS::Promise::State::Fulfilled)
            return ExecuteScriptResult { ExecuteScriptResultType::PromiseResolved, script_promise.result() };

        // 11. Upon rejection of scriptPromise with value r, reject promise with value r.
        if (script_promise.state() == JS::Promise::State::Rejected)
            return ExecuteScriptResult { ExecuteScriptResultType::PromiseRejected, script_promise.result() };

        return ExecuteScriptResult { ExecuteScriptResultType::Timeout, script_promise.result() };
    }();

    // 6. If promise is still pending and session script timeout milliseconds is reached, return error with error code script timeout.
    // 7. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
    // 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.
    auto json_value_or_error = json_clone(realm, result.value);
    if (json_value_or_error.is_error()) {
        auto error_object = JsonObject {};
        error_object.set("name", "Error");
        error_object.set("message", "Could not clone result value");
        return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
    }
    return { result.type, json_value_or_error.release_value() };
}

}