2022-11-02 18:02:19 +00:00
/*
2023-04-13 00:47:15 +02:00
* Copyright ( c ) 2022 - 2023 , Linus Groh < linusg @ serenityos . org >
2022-11-02 18:02:19 +00:00
*
* 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>
2022-11-23 13:28:01 +01:00
# include <LibJS/Parser.h>
2022-11-02 18:02:19 +00:00
# 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>
2023-04-20 17:41:32 +01:00
# include <LibWeb/WebDriver/Contexts.h>
2022-11-02 18:02:19 +00:00
# include <LibWeb/WebDriver/ExecuteScript.h>
namespace Web : : WebDriver {
2023-01-14 16:34:44 -07:00
# define TRY_OR_JS_ERROR(expression) \
( { \
2023-02-09 13:27:43 -05:00
auto & & _temporary_result = ( expression ) ; \
2023-01-14 16:34:44 -07:00
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 ( ) ; \
2022-11-02 18:02:19 +00:00
} )
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 ( ) )
2023-01-07 12:24:05 -05:00
return JsonValue { TRY_OR_JS_ERROR ( value . as_string ( ) . deprecated_string ( ) ) } ;
2022-11-02 18:02:19 +00:00
// 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 ;
2023-04-20 17:41:32 +01:00
// FIXME: -> a collection
// FIXME: -> instance of element
// FIXME: -> instance of shadow root
// -> a WindowProxy object
if ( is < HTML : : WindowProxy > ( value . as_object ( ) ) ) {
auto const & window_proxy = static_cast < HTML : : WindowProxy & > ( 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 ) ;
}
2022-11-02 18:02:19 +00:00
// -> 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 ;
2023-01-07 12:24:05 -05:00
return TRY_OR_JS_ERROR ( to_json_result . as_string ( ) . deprecated_string ( ) ) ;
2022-11-02 18:02:19 +00:00
}
// -> 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 )
2023-04-17 15:38:27 +10:00
array . must_append ( JsonValue { } ) ;
2022-11-02 18:02:19 +00:00
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 ( ) )
2023-04-17 15:38:27 +10:00
array . must_set ( name . as_number ( ) , cloned_property_result . value ( ) ) ;
2022-11-02 18:02:19 +00:00
} ,
[ & ] ( 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
2022-12-04 18:02:33 +00:00
static JS : : ThrowCompletionOr < JS : : Value > execute_a_function_body ( Web : : Page & page , DeprecatedString const & body , JS : : MarkedVector < JS : : Value > parameters )
2022-11-02 18:02:19 +00:00
{
// 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 ;
2022-12-04 18:02:33 +00:00
auto source_text = DeprecatedString : : formatted ( " function() {{ {} }} " , body ) ;
2022-11-02 18:02:19 +00:00
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.
2022-12-13 20:49:50 +00:00
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 ) ;
2022-11-02 18:02:19 +00:00
// 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 ;
}
2022-12-04 18:02:33 +00:00
ExecuteScriptResultSerialized execute_script ( Web : : Page & page , DeprecatedString const & body , JS : : MarkedVector < JS : : Value > arguments , Optional < u64 > const & timeout )
2022-11-02 18:02:19 +00:00
{
// 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 ( ) } ;
}
2022-12-04 18:02:33 +00:00
ExecuteScriptResultSerialized execute_async_script ( Web : : Page & page , DeprecatedString const & body , JS : : MarkedVector < JS : : Value > arguments , Optional < u64 > const & timeout )
2022-11-02 18:02:19 +00:00
{
2023-03-11 05:47:16 +03:00
auto * document = page . top_level_browsing_context ( ) . active_document ( ) ;
auto & settings_object = document - > relevant_settings_object ( ) ;
2022-11-02 18:02:19 +00:00
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.
2022-12-13 20:49:50 +00:00
auto promise = JS : : Promise : : create ( realm ) ;
2022-11-02 18:02:19 +00:00
// FIXME: 5 Run the following substeps in parallel:
auto result = [ & ] {
2023-03-11 05:47:16 +03:00
// 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 ( ) ) ;
2022-11-02 18:02:19 +00:00
// 1. Let resolvingFunctions be CreateResolvingFunctions(promise).
auto resolving_functions = promise - > create_resolving_functions ( ) ;
2023-03-11 05:47:16 +03:00
VERIFY ( & settings_object . realm_execution_context ( ) = = & vm . running_execution_context ( ) ) ;
vm . pop_execution_context ( ) ;
2022-11-02 18:02:19 +00:00
// 2. Append resolvingFunctions.[[Resolve]] to arguments.
2023-02-26 16:09:02 -07:00
arguments . append ( resolving_functions . resolve ) ;
2022-11-02 18:02:19 +00:00
// 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]]).
2023-04-13 00:47:15 +02:00
auto script_promise_or_error = JS : : promise_resolve ( vm , realm . intrinsics ( ) . promise_constructor ( ) , script_result . value ( ) ) ;
2022-11-02 18:02:19 +00:00
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 ( ) } ;
}
}