mirror of
https://github.com/fergalmoran/ladybird.git
synced 2025-12-22 09:19:03 +00:00
LibWeb+WebContent+WebDriver: Bring session start and close up to spec
Lots of editorial spec bugs here, but these changes largely affect how the unhandledPromptBehavior capability is handled. We also now set an additional capability for the default User Agent string.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
|
||||
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
|
||||
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
* Copyright (c) 2022-2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
@@ -13,8 +13,8 @@
|
||||
#include <AK/JsonValue.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/Timer.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWeb/WebDriver/Capabilities.h>
|
||||
#include <LibWeb/WebDriver/UserPrompt.h>
|
||||
#include <WebDriver/Client.h>
|
||||
|
||||
namespace WebDriver {
|
||||
@@ -50,12 +50,40 @@ ErrorOr<NonnullRefPtr<Session>, Web::WebDriver::Error> Client::find_session_with
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidSessionId, "Invalid session id");
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-close-the-session
|
||||
void Client::close_session(String const& session_id)
|
||||
{
|
||||
// FIXME: 1. If session's HTTP flag is set, remove session from active HTTP sessions.
|
||||
|
||||
// 2. Remove session from active sessions.
|
||||
if (s_sessions.remove(session_id))
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Shut down session {}", session_id);
|
||||
else
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Unable to shut down session {}: Not found", session_id);
|
||||
|
||||
// 3. Perform the following substeps based on the remote end's type:
|
||||
// -> Remote end is an endpoint node
|
||||
// 1. If the list of active sessions is empty:
|
||||
if (s_sessions.is_empty()) {
|
||||
// 1. Set the webdriver-active flag to false
|
||||
// NOTE: This is handled by the WebContent process.
|
||||
|
||||
// 2. Set the user prompt handler to null.
|
||||
Web::WebDriver::set_user_prompt_handler({});
|
||||
|
||||
// FIXME: 3. Unset the accept insecure TLS flag.
|
||||
// FIXME: 4. Reset the has proxy configuration flag to its default value.
|
||||
|
||||
// 5. Optionally, close all top-level browsing contexts, without prompting to unload.
|
||||
// NOTE: This is handled by the WebContent process.
|
||||
}
|
||||
// -> Remote end is an intermediary node
|
||||
// 1. Close the associated session. If this causes an error to occur, complete the remainder of this algorithm
|
||||
// before returning the error.
|
||||
|
||||
// 4. Perform any implementation-specific cleanup steps.
|
||||
|
||||
// 5. If an error has occurred in any of the steps above, return the error, otherwise return success with data null.
|
||||
}
|
||||
|
||||
// 8.1 New Session, https://w3c.github.io/webdriver/#dfn-new-sessions
|
||||
@@ -64,69 +92,50 @@ Web::WebDriver::Response Client::new_session(Web::WebDriver::Parameters, JsonVal
|
||||
{
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session");
|
||||
|
||||
// FIXME: 1. If the maximum active sessions is equal to the length of the list of active sessions,
|
||||
// return error with error code session not created.
|
||||
// FIXME: 1. If the implementation is an endpoint node, and the list of active HTTP sessions is not empty, or otherwise if
|
||||
// the implementation is unable to start an additional session, return error with error code session not created.
|
||||
|
||||
// FIXME: 2. If the remote end is an intermediary node, take implementation-defined steps that either
|
||||
// result in returning an error with error code session not created, or in returning a
|
||||
// success with data that is isomorphic to that returned by remote ends according to the
|
||||
// rest of this algorithm. If an error is not returned, the intermediary node must retain a
|
||||
// reference to the session created on the upstream node as the associated session such
|
||||
// that commands may be forwarded to this associated session on subsequent commands.
|
||||
// FIXME: 2. If the remote end is an intermediary node, take implementation-defined steps that either result in returning
|
||||
// an error with error code session not created, or in returning a success with data that is isomorphic to that
|
||||
// returned by remote ends according to the rest of this algorithm. If an error is not returned, the intermediary
|
||||
// node must retain a reference to the session created on the upstream node as the associated session such that
|
||||
// commands may be forwarded to this associated session on subsequent commands.
|
||||
|
||||
// FIXME: 3. If the maximum active sessions is equal to the length of the list of active sessions,
|
||||
// return error with error code session not created.
|
||||
// 3. Let flags be a set containing "http".
|
||||
static constexpr Array flags { "http"sv };
|
||||
|
||||
// 4. Let capabilities be the result of trying to process capabilities with parameters as an argument.
|
||||
auto capabilities = TRY(Web::WebDriver::process_capabilities(payload));
|
||||
// 4. Let capabilities be the result of trying to process capabilities with parameters and flags.
|
||||
auto capabilities = TRY(Web::WebDriver::process_capabilities(payload, flags));
|
||||
|
||||
// 5. If capabilities’s is null, return error with error code session not created.
|
||||
// 5. If capabilities's is null, return error with error code session not created.
|
||||
if (capabilities.is_null())
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::SessionNotCreated, "Could not match capabilities"sv);
|
||||
|
||||
// 6. Let session id be the result of generating a UUID.
|
||||
auto session_id = MUST(Web::Crypto::generate_random_uuid());
|
||||
// 6. Let session be the result of create a session, with capabilities, and flags.
|
||||
auto maybe_session = Session::create(*this, capabilities.as_object(), flags);
|
||||
if (maybe_session.is_error())
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::SessionNotCreated, ByteString::formatted("Failed to start session: {}", maybe_session.error()));
|
||||
|
||||
// 7. Let session be a new session with the session ID of session id.
|
||||
Web::WebDriver::LadybirdOptions options { capabilities.as_object() };
|
||||
auto session = make_ref_counted<Session>(session_id, *this, move(options));
|
||||
auto session = maybe_session.release_value();
|
||||
s_sessions.set(session->session_id(), session);
|
||||
|
||||
if (auto start_result = session->start(m_callbacks); start_result.is_error())
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::SessionNotCreated, ByteString::formatted("Failed to start session: {}", start_result.error()));
|
||||
|
||||
auto& web_content_connection = session->web_content_connection();
|
||||
|
||||
// FIXME: 8. Set the current session to session.
|
||||
|
||||
// FIXME: 9. Run any WebDriver new session algorithm defined in external specifications,
|
||||
// with arguments session and capabilities.
|
||||
|
||||
// 10. Append session to active sessions.
|
||||
s_sessions.set(session_id, session);
|
||||
|
||||
// NOTE: We do step 12 before 11 because step 12 mutates the capabilities we set in step 11.
|
||||
|
||||
// 12. Initialize the following from capabilities:
|
||||
session->initialize_from_capabilities(capabilities.as_object());
|
||||
|
||||
// 11. Let body be a JSON Object initialized with:
|
||||
// 7. Let body be a JSON Object initialized with:
|
||||
JsonObject body;
|
||||
// "sessionId"
|
||||
// session id
|
||||
body.set("sessionId", JsonValue { session_id });
|
||||
// session's session ID.
|
||||
body.set("sessionId", JsonValue { session->session_id() });
|
||||
// "capabilities"
|
||||
// capabilities
|
||||
body.set("capabilities", move(capabilities));
|
||||
|
||||
// 13. Set the webdriver-active flag to true.
|
||||
web_content_connection.async_set_is_webdriver_active(true);
|
||||
// 8. Set session' current top-level browsing context to one of the endpoint node's top-level browsing contexts,
|
||||
// preferring the top-level browsing context that has system focus, or otherwise preferring any top-level
|
||||
// browsing context whose visibility state is visible.
|
||||
// NOTE: This happens in the WebContent process.
|
||||
|
||||
// FIXME: 14. Set the current top-level browsing context for session with the top-level browsing context
|
||||
// of the UA’s current browsing context.
|
||||
// FIXME: 9. Set the request queue to a new queue.
|
||||
|
||||
// FIXME: 15. Set the request queue to a new queue.
|
||||
|
||||
// 16. Return success with data body.
|
||||
// 10. Return success with data body.
|
||||
return JsonValue { move(body) };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
|
||||
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
|
||||
* Copyright (c) 2022-2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
static ErrorOr<NonnullRefPtr<Client>> try_create(NonnullOwnPtr<Core::BufferedTCPSocket>, LaunchBrowserCallbacks, Core::EventReceiver* parent);
|
||||
virtual ~Client() override;
|
||||
|
||||
LaunchBrowserCallbacks const& launch_browser_callbacks() const { return m_callbacks; }
|
||||
void close_session(String const& session_id);
|
||||
|
||||
private:
|
||||
|
||||
@@ -14,16 +14,99 @@
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibCore/StandardPaths.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWeb/WebDriver/TimeoutsConfiguration.h>
|
||||
#include <LibWeb/WebDriver/UserPrompt.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace WebDriver {
|
||||
|
||||
Session::Session(String session_id, NonnullRefPtr<Client> client, Web::WebDriver::LadybirdOptions options)
|
||||
// https://w3c.github.io/webdriver/#dfn-create-a-session
|
||||
ErrorOr<NonnullRefPtr<Session>> Session::create(NonnullRefPtr<Client> client, JsonObject& capabilities, ReadonlySpan<StringView> flags)
|
||||
{
|
||||
// 1. Let session id be the result of generating a UUID.
|
||||
auto session_id = MUST(Web::Crypto::generate_random_uuid());
|
||||
|
||||
// 2. Let session be a new session with session ID session id, and HTTP flag flags contains "http".
|
||||
auto session = adopt_ref(*new Session(client, capabilities, move(session_id), flags.contains_slow("http"sv)));
|
||||
TRY(session->start(client->launch_browser_callbacks()));
|
||||
|
||||
// FIXME: 3. Let proxy be the result of getting property "proxy" from capabilities and run the substeps of the first matching statement:
|
||||
// -> proxy is a proxy configuration object
|
||||
// Take implementation-defined steps to set the user agent proxy using the extracted proxy configuration. If the defined proxy cannot be configured return error with error code session not created. Otherwise set the has proxy configuration flag to true.
|
||||
// -> Otherwise
|
||||
// Set a property of capabilities with name "proxy" and a value that is a new JSON Object.
|
||||
|
||||
// FIXME: 4. If capabilites has a property named "acceptInsecureCerts", set the endpoint node's accept insecure TLS flag
|
||||
// to the result of getting a property named "acceptInsecureCerts" from capabilities.
|
||||
|
||||
// 5. Let user prompt handler capability be the result of getting property "unhandledPromptBehavior" from capabilities.
|
||||
auto user_prompt_handler_capability = capabilities.get_object("unhandledPromptBehavior"sv);
|
||||
|
||||
// 6. If user prompt handler capability is not undefined, update the user prompt handler with user prompt handler capability.
|
||||
if (user_prompt_handler_capability.has_value())
|
||||
Web::WebDriver::update_the_user_prompt_handler(*user_prompt_handler_capability);
|
||||
|
||||
session->web_content_connection().async_set_user_prompt_handler(Web::WebDriver::user_prompt_handler());
|
||||
|
||||
// 7. Let serialized user prompt handler be serialize the user prompt handler.
|
||||
auto serialized_user_prompt_handler = Web::WebDriver::serialize_the_user_prompt_handler();
|
||||
|
||||
// 8. Set a property on capabilities with the name "unhandledPromptBehavior", and the value serialized user prompt handler.
|
||||
capabilities.set("unhandledPromptBehavior"sv, move(serialized_user_prompt_handler));
|
||||
|
||||
// 9. If flags contains "http":
|
||||
if (flags.contains_slow("http"sv)) {
|
||||
// 1. Let strategy be the result of getting property "pageLoadStrategy" from capabilities. If strategy is a
|
||||
// string, set the session's page loading strategy to strategy. Otherwise, set the page loading strategy to
|
||||
// normal and set a property of capabilities with name "pageLoadStrategy" and value "normal".
|
||||
if (auto strategy = capabilities.get_byte_string("pageLoadStrategy"sv); strategy.has_value()) {
|
||||
session->m_page_load_strategy = Web::WebDriver::page_load_strategy_from_string(*strategy);
|
||||
session->web_content_connection().async_set_page_load_strategy(session->m_page_load_strategy);
|
||||
} else {
|
||||
capabilities.set("pageLoadStrategy"sv, "normal"sv);
|
||||
}
|
||||
|
||||
// 3. Let strictFileInteractability be the result of getting property "strictFileInteractability" from .
|
||||
// capabilities. If strictFileInteractability is a boolean, set session's strict file interactability to
|
||||
// strictFileInteractability.
|
||||
if (auto strict_file_interactiblity = capabilities.get_bool("strictFileInteractability"sv); strict_file_interactiblity.has_value()) {
|
||||
session->m_strict_file_interactiblity = *strict_file_interactiblity;
|
||||
session->web_content_connection().async_set_strict_file_interactability(session->m_strict_file_interactiblity);
|
||||
}
|
||||
|
||||
// 4. Let timeouts be the result of getting a property "timeouts" from capabilities. If timeouts is not
|
||||
// undefined, set session's session timeouts to timeouts.
|
||||
if (auto timeouts = capabilities.get_object("timeouts"sv); timeouts.has_value()) {
|
||||
MUST(session->set_timeouts(*timeouts));
|
||||
}
|
||||
|
||||
// 5. Set a property on capabilities with name "timeouts" and value serialize the timeouts configuration with
|
||||
// session's session timeouts.
|
||||
capabilities.set("timeouts"sv, session->m_timeouts_configuration.value_or_lazy_evaluated([]() {
|
||||
return Web::WebDriver::timeouts_object({});
|
||||
}));
|
||||
}
|
||||
|
||||
// FIXME: 10. Process any extension capabilities in capabilities in an implementation-defined manner.
|
||||
|
||||
// FIXME: 11. Run any WebDriver new session algorithm defined in external specifications, with arguments session, capabilities, and flags.
|
||||
|
||||
// 12. Append session to active sessions.
|
||||
// 13. If flags contains "http", append session to active HTTP sessions.
|
||||
// NOTE: These steps are handled by WebDriver::Client.
|
||||
|
||||
// 14. Set the webdriver-active flag to true.
|
||||
session->web_content_connection().async_set_is_webdriver_active(true);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
Session::Session(NonnullRefPtr<Client> client, JsonObject const& capabilities, String session_id, bool http)
|
||||
: m_client(move(client))
|
||||
, m_options(move(options))
|
||||
, m_options(capabilities)
|
||||
, m_id(move(session_id))
|
||||
, m_http(http)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -53,61 +136,6 @@ Session::~Session()
|
||||
}
|
||||
}
|
||||
|
||||
// Step 12 of https://w3c.github.io/webdriver/#dfn-new-sessions
|
||||
void Session::initialize_from_capabilities(JsonObject& capabilities)
|
||||
{
|
||||
auto& connection = web_content_connection();
|
||||
|
||||
// 1. Let strategy be the result of getting property "pageLoadStrategy" from capabilities.
|
||||
auto strategy = capabilities.get_byte_string("pageLoadStrategy"sv);
|
||||
|
||||
// 2. If strategy is a string, set the current session’s page loading strategy to strategy. Otherwise, set the page loading strategy to normal and set a property of capabilities with name "pageLoadStrategy" and value "normal".
|
||||
if (strategy.has_value()) {
|
||||
m_page_load_strategy = Web::WebDriver::page_load_strategy_from_string(*strategy);
|
||||
connection.async_set_page_load_strategy(m_page_load_strategy);
|
||||
} else {
|
||||
capabilities.set("pageLoadStrategy"sv, "normal"sv);
|
||||
}
|
||||
|
||||
// 3. Let strictFileInteractability be the result of getting property "strictFileInteractability" from capabilities.
|
||||
auto strict_file_interactiblity = capabilities.get_bool("strictFileInteractability"sv);
|
||||
|
||||
// 4. If strictFileInteractability is a boolean, set the current session’s strict file interactability to strictFileInteractability. Otherwise set the current session’s strict file interactability to false.
|
||||
if (strict_file_interactiblity.has_value()) {
|
||||
m_strict_file_interactiblity = *strict_file_interactiblity;
|
||||
connection.async_set_strict_file_interactability(m_strict_file_interactiblity);
|
||||
} else {
|
||||
capabilities.set("strictFileInteractability"sv, false);
|
||||
}
|
||||
|
||||
// FIXME: 5. Let proxy be the result of getting property "proxy" from capabilities and run the substeps of the first matching statement:
|
||||
// FIXME: proxy is a proxy configuration object
|
||||
// FIXME: Take implementation-defined steps to set the user agent proxy using the extracted proxy configuration. If the defined proxy cannot be configured return error with error code session not created.
|
||||
// FIXME: Otherwise
|
||||
// FIXME: Set a property of capabilities with name "proxy" and a value that is a new JSON Object.
|
||||
|
||||
// 6. If capabilities has a property with the key "timeouts":
|
||||
if (auto timeouts = capabilities.get_object("timeouts"sv); timeouts.has_value()) {
|
||||
// a. Let timeouts be the result of trying to JSON deserialize as a timeouts configuration the value of the "timeouts" property.
|
||||
// NOTE: This happens on the remote end.
|
||||
|
||||
// b. Make the session timeouts the new timeouts.
|
||||
MUST(set_timeouts(*timeouts));
|
||||
} else {
|
||||
// 7. Set a property on capabilities with name "timeouts" and value that of the JSON deserialization of the session timeouts.
|
||||
capabilities.set("timeouts"sv, Web::WebDriver::timeouts_object({}));
|
||||
}
|
||||
|
||||
// 8. Apply changes to the user agent for any implementation-defined capabilities selected during the capabilities processing step.
|
||||
if (auto behavior = capabilities.get_object("unhandledPromptBehavior"sv); behavior.has_value()) {
|
||||
Web::WebDriver::update_the_user_prompt_handler(*behavior);
|
||||
} else {
|
||||
capabilities.set("unhandledPromptBehavior"sv, "dismiss and notify"sv);
|
||||
}
|
||||
|
||||
connection.async_set_user_prompt_handler(Web::WebDriver::user_prompt_handler());
|
||||
}
|
||||
|
||||
ErrorOr<NonnullRefPtr<Core::LocalServer>> Session::create_server(NonnullRefPtr<ServerPromise> promise)
|
||||
{
|
||||
static_assert(IsSame<IPC::Transport, IPC::TransportSocket>, "Need to handle other IPC transports here");
|
||||
|
||||
@@ -29,11 +29,9 @@ struct LaunchBrowserCallbacks;
|
||||
|
||||
class Session : public RefCounted<Session> {
|
||||
public:
|
||||
Session(String session_id, NonnullRefPtr<Client> client, Web::WebDriver::LadybirdOptions options);
|
||||
static ErrorOr<NonnullRefPtr<Session>> create(NonnullRefPtr<Client> client, JsonObject& capabilities, ReadonlySpan<StringView> flags);
|
||||
~Session();
|
||||
|
||||
void initialize_from_capabilities(JsonObject&);
|
||||
|
||||
String session_id() const { return m_id; }
|
||||
|
||||
struct Window {
|
||||
@@ -56,8 +54,6 @@ public:
|
||||
|
||||
bool has_window_handle(StringView handle) const { return m_windows.contains(handle); }
|
||||
|
||||
ErrorOr<void> start(LaunchBrowserCallbacks const&);
|
||||
|
||||
Web::WebDriver::Response set_timeouts(JsonValue);
|
||||
Web::WebDriver::Response close_window();
|
||||
Web::WebDriver::Response switch_to_window(StringView);
|
||||
@@ -83,6 +79,10 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
Session(NonnullRefPtr<Client> client, JsonObject const& capabilities, String session_id, bool http);
|
||||
|
||||
ErrorOr<void> start(LaunchBrowserCallbacks const&);
|
||||
|
||||
using ServerPromise = Core::Promise<ErrorOr<void>>;
|
||||
ErrorOr<NonnullRefPtr<Core::LocalServer>> create_server(NonnullRefPtr<ServerPromise> promise);
|
||||
|
||||
@@ -90,7 +90,9 @@ private:
|
||||
Web::WebDriver::LadybirdOptions m_options;
|
||||
|
||||
bool m_started { false };
|
||||
|
||||
String m_id;
|
||||
bool m_http { false };
|
||||
|
||||
HashMap<String, Window> m_windows;
|
||||
String m_current_window_handle;
|
||||
|
||||
Reference in New Issue
Block a user