/* * Copyright (c) 2022-2023, Linus Groh * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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, \ "Do not return a reference from a fallible expression"); \ _temporary_result.release_value(); \ }) static ErrorOr internal_json_clone_algorithm(JS::Realm&, JS::Value, HashTable& seen); static ErrorOr clone_an_object(JS::Realm&, JS::Object&, HashTable& 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(value) // - instance of FileList || is(value) // - instance of HTMLAllCollection || false // FIXME // - instance of HTMLCollection || is(value) // - instance of HTMLFormControlsCollection || false // FIXME // - instance of HTMLOptionsCollection || is(value) // - instance of NodeList || is(value)); } // https://w3c.github.io/webdriver/#dfn-json-clone static ErrorOr 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 {}; return internal_json_clone_algorithm(realm, value, seen); } // https://w3c.github.io/webdriver/#dfn-internal-json-clone-algorithm static ErrorOr internal_json_clone_algorithm(JS::Realm& realm, JS::Value value, HashTable& 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 // -> a WindowProxy object if (is(value.as_object())) { auto const& window_proxy = static_cast(value.as_object()); // If the associated browsing context of the WindowProxy object in value has been discarded, return error with // error code stale element reference. if (window_proxy.associated_browsing_context()->has_been_discarded()) return ExecuteScriptResultType::BrowsingContextDiscarded; // Otherwise return success with data set to WindowProxy reference object for value. return window_proxy_reference_object(window_proxy); } // -> 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 { 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 clone_an_object(JS::Realm& realm, JS::Object& value, HashTable& 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, 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::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 execute_a_function_body(Web::Page& page, DeprecatedString const& body, JS::MarkedVector 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(); // 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 arguments, Optional 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 arguments, Optional 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(*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(*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() }; } }