Serenity Operating System
at master 402 lines 20 kB view raw
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}