| /* |
| * Copyright (C) 2017 Igalia S.L. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "WebDriverService.h" |
| |
| #include "Capabilities.h" |
| #include "CommandResult.h" |
| #include "Logging.h" |
| #include "SessionHost.h" |
| #include <wtf/Compiler.h> |
| #include <wtf/LoggerHelper.h> |
| #include <wtf/RunLoop.h> |
| #include <wtf/SortedArrayMap.h> |
| #include <wtf/text/MakeString.h> |
| #include <wtf/text/StringToIntegerConversion.h> |
| #include <wtf/text/WTFString.h> |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| #include "HTTPServer.h" |
| #include "WebSocketServer.h" |
| #include <algorithm> |
| #include <array> |
| #include <cstdio> |
| #include <limits> |
| #include <optional> |
| #include <wtf/JSONValues.h> |
| #include <wtf/StdLibExtras.h> |
| #include <wtf/glib/GTypedefs.h> |
| #endif |
| |
| namespace WebDriver { |
| |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-maximum-safe-integer |
| static const double maxSafeInteger = 9007199254740991.0; // 2 ^ 53 - 1 |
| |
| WebDriverService::WebDriverService() |
| : m_server(*this) |
| #if ENABLE(WEBDRIVER_BIDI) |
| , m_bidiServer(*this, *this) |
| , m_browserTerminatedObserver([this](const String& sessionID) { onBrowserTerminated(sessionID); }) |
| #endif |
| { |
| #if ENABLE(WEBDRIVER_BIDI) |
| SessionHost::addBrowserTerminatedObserver(m_browserTerminatedObserver); |
| #endif |
| } |
| |
| WebDriverService::~WebDriverService() |
| { |
| #if ENABLE(WEBDRIVER_BIDI) |
| SessionHost::removeBrowserTerminatedObserver(m_browserTerminatedObserver); |
| #endif |
| } |
| |
| static void printUsageStatement(const char* programName) |
| { |
| SAFE_PRINTF("Usage: %s options\n", String::fromLatin1(programName).utf8()); |
| SAFE_PRINTF(" -h, --help Prints this help message\n"); |
| SAFE_PRINTF(" -p <port>, --port=<port> Port number the driver will use\n"); |
| SAFE_PRINTF(" --host=<host> Host IP the driver will use, or either 'local' or 'all' (default: 'local')\n"); |
| SAFE_PRINTF(" -t <ip:port> --target=<ip:port> Target IP and port\n"); |
| #if ENABLE(WEBDRIVER_BIDI) |
| SAFE_PRINTF(" --bidi-port=<port> Port number to use for BiDi's WebSocket connections\n"); |
| #endif |
| SAFE_PRINTF(" --replace-on-new-session Replace the existing session on new session request\n"); |
| } |
| |
| int WebDriverService::run(int argc, char** argv) |
| { |
| String portString; |
| std::optional<String> host; |
| #if ENABLE(WEBDRIVER_BIDI) |
| String bidiPortString; |
| #endif |
| String targetString; |
| if (const char* targetEnvVar = getenv("WEBDRIVER_TARGET_ADDR")) |
| targetString = String::fromLatin1(targetEnvVar); |
| for (int i = 1 ; i < argc; ++i) { |
| auto arg = unsafeSpan(argv[i]); |
| if (equalSpans(arg, "-h"_span) || equalSpans(arg, "--help"_span)) { |
| printUsageStatement(argv[0]); |
| return EXIT_SUCCESS; |
| } |
| |
| if (equalSpans(arg, "-p"_span) && portString.isNull()) { |
| if (++i == argc) { |
| printUsageStatement(argv[0]); |
| return EXIT_FAILURE; |
| } |
| portString = String::fromLatin1(argv[i]); |
| continue; |
| } |
| |
| static constexpr auto portArgument = "--port="_span; |
| if (spanHasPrefix(arg, portArgument) && portString.isNull()) { |
| portString = arg.subspan(portArgument.size()); |
| continue; |
| } |
| |
| static constexpr auto hostArgument = "--host="_span; |
| if (spanHasPrefix(arg, hostArgument) && !host) { |
| host = arg.subspan(hostArgument.size()); |
| continue; |
| } |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| static constexpr auto bidiPortArgument = "--bidi-port="_span; |
| if (spanHasPrefix(arg, bidiPortArgument) && bidiPortString.isNull()) { |
| bidiPortString = arg.subspan(bidiPortArgument.size()); |
| continue; |
| } |
| #endif |
| |
| if (equalSpans(arg, "-t"_span) && targetString.isNull()) { |
| if (++i == argc) { |
| printUsageStatement(argv[0]); |
| return EXIT_FAILURE; |
| } |
| targetString = String::fromLatin1(argv[i]); |
| continue; |
| } |
| |
| static constexpr auto targetArgument = "--target="_span; |
| if (spanHasPrefix(arg, targetArgument) && targetString.isNull()) { |
| targetString = arg.subspan(targetArgument.size()); |
| continue; |
| } |
| |
| if (equalSpans(arg, "--replace-on-new-session"_span)) { |
| m_replaceOnNewSession = true; |
| continue; |
| } |
| } |
| |
| if (portString.isNull()) { |
| printUsageStatement(argv[0]); |
| return EXIT_FAILURE; |
| } |
| |
| if (!targetString.isEmpty()) { |
| auto position = targetString.reverseFind(':'); |
| if (position != notFound) { |
| m_targetAddress = targetString.left(position); |
| m_targetPort = parseIntegerAllowingTrailingJunk<uint16_t>(StringView { targetString }.substring(position + 1)).value_or(0); |
| } |
| } |
| |
| auto port = parseInteger<uint16_t>(portString); |
| if (!port) { |
| fprintf(stderr, "Invalid port %s provided\n", portString.utf8().data()); |
| return EXIT_FAILURE; |
| } |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| auto bidiPort = parseInteger<uint16_t>(bidiPortString); |
| if (!bidiPort) { |
| const int16_t bidiPortIncrement = *port == std::numeric_limits<uint16_t>::max() ? -1 : 1; |
| bidiPort = { *port + bidiPortIncrement }; |
| fprintf(stderr, "Invalid WebSocket BiDi port %s provided. Defaulting to %d.\n", bidiPortString.utf8().data(), *bidiPort); |
| } |
| #endif |
| |
| WTF::initializeMainThread(); |
| |
| const char* hostStr = host && host->utf8().data() ? host->utf8().data() : "local"; |
| #if ENABLE(WEBDRIVER_BIDI) |
| if (!m_bidiServer.listen(host ? *host : nullString(), *bidiPort)) { |
| fprintf(stderr, "FATAL: Unable to listen for WebSocket BiDi server at host %s and port %d.\n", hostStr, *bidiPort); |
| return EXIT_FAILURE; |
| } |
| RELEASE_LOG(WebDriverBiDi, "Started WebSocket BiDi server with host %s and port %d", hostStr, *bidiPort); |
| #endif // ENABLE(WEBDRIVER_BIDI) |
| if (!m_server.listen(host, *port)) { |
| fprintf(stderr, "FATAL: Unable to listen for HTTP server at host %s and port %d.\n", hostStr, *port); |
| return EXIT_FAILURE; |
| } |
| RELEASE_LOG(WebDriverClassic, "Started HTTP server with host %s and port %d", hostStr, *port); |
| |
| RunLoop::run(); |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| m_bidiServer.disconnect(); |
| #endif |
| m_server.disconnect(); |
| |
| return EXIT_SUCCESS; |
| } |
| |
| const WebDriverService::Command WebDriverService::s_commands[] = { |
| { HTTPMethod::Post, "/session", &WebDriverService::newSession }, |
| { HTTPMethod::Delete, "/session/$sessionId", &WebDriverService::deleteSession }, |
| { HTTPMethod::Get, "/status", &WebDriverService::status }, |
| { HTTPMethod::Get, "/session/$sessionId/timeouts", &WebDriverService::getTimeouts }, |
| { HTTPMethod::Post, "/session/$sessionId/timeouts", &WebDriverService::setTimeouts }, |
| |
| { HTTPMethod::Post, "/session/$sessionId/url", &WebDriverService::go }, |
| { HTTPMethod::Get, "/session/$sessionId/url", &WebDriverService::getCurrentURL }, |
| { HTTPMethod::Post, "/session/$sessionId/back", &WebDriverService::back }, |
| { HTTPMethod::Post, "/session/$sessionId/forward", &WebDriverService::forward }, |
| { HTTPMethod::Post, "/session/$sessionId/refresh", &WebDriverService::refresh }, |
| { HTTPMethod::Get, "/session/$sessionId/title", &WebDriverService::getTitle }, |
| |
| { HTTPMethod::Get, "/session/$sessionId/window", &WebDriverService::getWindowHandle }, |
| { HTTPMethod::Delete, "/session/$sessionId/window", &WebDriverService::closeWindow }, |
| { HTTPMethod::Post, "/session/$sessionId/window", &WebDriverService::switchToWindow }, |
| { HTTPMethod::Get, "/session/$sessionId/window/handles", &WebDriverService::getWindowHandles }, |
| { HTTPMethod::Post, "/session/$sessionId/window/new", &WebDriverService::newWindow }, |
| { HTTPMethod::Post, "/session/$sessionId/frame", &WebDriverService::switchToFrame }, |
| { HTTPMethod::Post, "/session/$sessionId/frame/parent", &WebDriverService::switchToParentFrame }, |
| { HTTPMethod::Get, "/session/$sessionId/window/rect", &WebDriverService::getWindowRect }, |
| { HTTPMethod::Post, "/session/$sessionId/window/rect", &WebDriverService::setWindowRect }, |
| { HTTPMethod::Post, "/session/$sessionId/window/maximize", &WebDriverService::maximizeWindow }, |
| { HTTPMethod::Post, "/session/$sessionId/window/minimize", &WebDriverService::minimizeWindow }, |
| { HTTPMethod::Post, "/session/$sessionId/window/fullscreen", &WebDriverService::fullscreenWindow }, |
| |
| { HTTPMethod::Post, "/session/$sessionId/element", &WebDriverService::findElement }, |
| { HTTPMethod::Post, "/session/$sessionId/elements", &WebDriverService::findElements }, |
| { HTTPMethod::Post, "/session/$sessionId/element/$elementId/element", &WebDriverService::findElementFromElement }, |
| { HTTPMethod::Post, "/session/$sessionId/element/$elementId/elements", &WebDriverService::findElementsFromElement }, |
| { HTTPMethod::Post, "/session/$sessionId/shadow/$shadowId/element", &WebDriverService::findElementFromShadowRoot }, |
| { HTTPMethod::Post, "/session/$sessionId/shadow/$shadowId/elements", &WebDriverService::findElementsFromShadowRoot }, |
| { HTTPMethod::Get, "/session/$sessionId/element/active", &WebDriverService::getActiveElement }, |
| |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/shadow", &WebDriverService::getElementShadowRoot }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/selected", &WebDriverService::isElementSelected }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/attribute/$name", &WebDriverService::getElementAttribute }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/property/$name", &WebDriverService::getElementProperty }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/css/$name", &WebDriverService::getElementCSSValue }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/text", &WebDriverService::getElementText }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/name", &WebDriverService::getElementTagName }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/rect", &WebDriverService::getElementRect }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/enabled", &WebDriverService::isElementEnabled }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/computedrole", &WebDriverService::getComputedRole }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/computedlabel", &WebDriverService::getComputedLabel }, |
| |
| { HTTPMethod::Post, "/session/$sessionId/element/$elementId/click", &WebDriverService::elementClick }, |
| { HTTPMethod::Post, "/session/$sessionId/element/$elementId/clear", &WebDriverService::elementClear }, |
| { HTTPMethod::Post, "/session/$sessionId/element/$elementId/value", &WebDriverService::elementSendKeys }, |
| |
| { HTTPMethod::Get, "/session/$sessionId/source", &WebDriverService::getPageSource }, |
| { HTTPMethod::Post, "/session/$sessionId/execute/sync", &WebDriverService::executeScript }, |
| { HTTPMethod::Post, "/session/$sessionId/execute/async", &WebDriverService::executeAsyncScript }, |
| |
| { HTTPMethod::Get, "/session/$sessionId/cookie", &WebDriverService::getAllCookies }, |
| { HTTPMethod::Get, "/session/$sessionId/cookie/$name", &WebDriverService::getNamedCookie }, |
| { HTTPMethod::Post, "/session/$sessionId/cookie", &WebDriverService::addCookie }, |
| { HTTPMethod::Delete, "/session/$sessionId/cookie/$name", &WebDriverService::deleteCookie }, |
| { HTTPMethod::Delete, "/session/$sessionId/cookie", &WebDriverService::deleteAllCookies }, |
| |
| { HTTPMethod::Post, "/session/$sessionId/actions", &WebDriverService::performActions }, |
| { HTTPMethod::Delete, "/session/$sessionId/actions", &WebDriverService::releaseActions }, |
| |
| { HTTPMethod::Post, "/session/$sessionId/alert/dismiss", &WebDriverService::dismissAlert }, |
| { HTTPMethod::Post, "/session/$sessionId/alert/accept", &WebDriverService::acceptAlert }, |
| { HTTPMethod::Get, "/session/$sessionId/alert/text", &WebDriverService::getAlertText }, |
| { HTTPMethod::Post, "/session/$sessionId/alert/text", &WebDriverService::sendAlertText }, |
| |
| { HTTPMethod::Get, "/session/$sessionId/screenshot", &WebDriverService::takeScreenshot }, |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/screenshot", &WebDriverService::takeElementScreenshot }, |
| |
| |
| { HTTPMethod::Get, "/session/$sessionId/element/$elementId/displayed", &WebDriverService::isElementDisplayed }, |
| }; |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| const WebDriverService::BidiCommand WebDriverService::s_bidiCommands[] = { |
| { "session.status"_s, &WebDriverService::bidiSessionStatus }, |
| { "session.subscribe"_s, &WebDriverService::bidiSessionSubscribe }, |
| { "session.unsubscribe"_s, &WebDriverService::bidiSessionUnsubscribe }, |
| }; |
| #endif |
| |
| std::optional<WebDriverService::HTTPMethod> WebDriverService::toCommandHTTPMethod(const String& method) |
| { |
| static constexpr std::pair<ComparableLettersLiteral, WebDriverService::HTTPMethod> httpMethodMappings[] = { |
| { "delete"_s, WebDriverService::HTTPMethod::Delete }, |
| { "get"_s, WebDriverService::HTTPMethod::Get }, |
| { "post"_s, WebDriverService::HTTPMethod::Post }, |
| { "put"_s, WebDriverService::HTTPMethod::Post }, |
| }; |
| static constexpr SortedArrayMap httpMethods { httpMethodMappings }; |
| |
| if (auto* methodValue = httpMethods.tryGet(method)) |
| return *methodValue; |
| return std::nullopt; |
| } |
| |
| bool WebDriverService::findCommand(HTTPMethod method, const String& path, CommandHandler* handler, HashMap<String, String>& parameters) |
| { |
| size_t length = std::size(s_commands); |
| for (size_t i = 0; i < length; ++i) { |
| if (s_commands[i].method != method) |
| continue; |
| |
| Vector<String> pathTokens = path.split('/'); |
| Vector<String> commandTokens = String::fromUTF8(s_commands[i].uriTemplate).split('/'); |
| if (pathTokens.size() != commandTokens.size()) |
| continue; |
| |
| bool allMatched = true; |
| for (size_t j = 0; j < pathTokens.size() && allMatched; ++j) { |
| if (commandTokens[j][0] == '$') |
| parameters.set(commandTokens[j].substring(1), pathTokens[j]); |
| else if (commandTokens[j] != pathTokens[j]) |
| allMatched = false; |
| } |
| |
| if (allMatched) { |
| *handler = s_commands[i].handler; |
| return true; |
| } |
| |
| parameters.clear(); |
| } |
| |
| return false; |
| } |
| |
| void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Function<void (HTTPRequestHandler::Response&&)>&& replyHandler) |
| { |
| auto method = toCommandHTTPMethod(request.method); |
| if (!method) { |
| sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::UnknownCommand, makeString("Unknown method: "_s, request.method))); |
| return; |
| } |
| CommandHandler handler; |
| HashMap<String, String> parameters; |
| if (!findCommand(method.value(), request.path, &handler, parameters)) { |
| sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::UnknownCommand, makeString("Unknown command: "_s, request.path))); |
| return; |
| } |
| |
| RefPtr<JSON::Object> parametersObject; |
| if (method.value() == HTTPMethod::Post) { |
| auto messageValue = JSON::Value::parseJSON(String::fromUTF8({ request.data, request.dataLength })); |
| if (!messageValue) { |
| sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| parametersObject = messageValue->asObject(); |
| if (!parametersObject) { |
| sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| } else |
| parametersObject = JSON::Object::create(); |
| for (const auto& parameter : parameters) |
| parametersObject->setString(parameter.key, parameter.value); |
| |
| ((*this).*handler)(WTFMove(parametersObject), [this, replyHandler = WTFMove(replyHandler)](CommandResult&& result) mutable { |
| sendResponse(WTFMove(replyHandler), WTFMove(result)); |
| }); |
| } |
| |
| void WebDriverService::sendResponse(Function<void (HTTPRequestHandler::Response&&)>&& replyHandler, CommandResult&& result) const |
| { |
| // §6.3 Processing Model. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#processing-model |
| RefPtr<JSON::Value> resultValue; |
| if (result.isError()) { |
| // When required to send an error. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-send-an-error |
| // Let body be a new JSON Object initialised with the following properties: "error", "message", "stacktrace". |
| auto errorObject = JSON::Object::create(); |
| errorObject->setString("error"_s, result.errorString()); |
| errorObject->setString("message"_s, result.errorMessage().value_or(emptyString())); |
| errorObject->setString("stacktrace"_s, emptyString()); |
| // If the error data dictionary contains any entries, set the "data" field on body to a new JSON Object populated with the dictionary. |
| if (auto& additionalData = result.additionalErrorData()) |
| errorObject->setObject("data"_s, *additionalData); |
| // Send a response with status and body as arguments. |
| resultValue = WTFMove(errorObject); |
| } else if (auto value = result.result()) |
| resultValue = WTFMove(value); |
| else |
| resultValue = JSON::Value::null(); |
| |
| // When required to send a response. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-send-a-response |
| auto responseObject = JSON::Object::create(); |
| responseObject->setValue("value"_s, resultValue.releaseNonNull()); |
| replyHandler({ result.httpStatusCode(), responseObject->toJSONString().utf8(), "application/json; charset=utf-8"_s }); |
| } |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| |
| bool WebDriverService::acceptHandshake(HTTPRequestHandler::Request&& request) |
| { |
| // https://w3c.github.io/webdriver-bidi/#transport |
| auto& resourceName = request.path; |
| |
| auto& resources = m_bidiServer.listener()->resources; |
| auto foundResource = std::find(resources.begin(), resources.end(), resourceName); |
| if (foundResource == resources.end()) { |
| RELEASE_LOG(WebDriverBiDi, "Resource name %s not found in listener's list of WebSocket resources. Rejecting handshake.", resourceName.utf8().data()); |
| return false; |
| } |
| |
| if (*foundResource == "/session"_s) { |
| // FIXME Add support for bidi-only sessions |
| RELEASE_LOG(WebDriverBiDi, "BiDi-only sessions are not supported yet. Rejecting handshake."); |
| return false; |
| } |
| |
| auto sessionID = m_bidiServer.getSessionID(resourceName); |
| if (sessionID.isNull()) { |
| RELEASE_LOG(WebDriverBiDi, "No session ID found for resource name %s. Rejecting handshake.", resourceName.utf8().data()); |
| return false; |
| } |
| |
| // FIXME Properly support multiple sessions in the future |
| if (sessionID != m_session->id()) { |
| RELEASE_LOG(WebDriverBiDi, "No active session found for session ID %s. Rejecting handshake.", sessionID.utf8().data()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void WebDriverService::handleMessage(WebSocketMessageHandler::Message&& message, Function<void(WebSocketMessageHandler::Message&&)>&& completionHandler) |
| { |
| // https://w3c.github.io/webdriver-bidi/#handle-an-incoming-message |
| |
| if (!message.connection) { |
| RELEASE_LOG(WebDriverBiDi, "Incoming message without attached connection. Ignoring message."); |
| completionHandler(WebSocketMessageHandler::Message::fail(CommandResult::ErrorCode::UnknownError, std::nullopt)); |
| return; |
| } |
| |
| auto connection = message.connection; |
| auto session = m_bidiServer.session(connection); |
| if (!session) { |
| if (!m_bidiServer.isStaticConnection(connection)) { |
| RELEASE_LOG(WebDriverBiDi, "Unknown connection. Ignoring message."); |
| completionHandler(WebSocketMessageHandler::Message::fail(CommandResult::ErrorCode::InvalidSessionID, connection)); |
| return; |
| } |
| } |
| // 6.6 If session is null and command is not a static command, then send an error response given connection, command id, and invalid session id, and return. |
| // FIXME support checking static vs non-static methods https://bugs.webkit.org/show_bug.cgi?id=281721 |
| |
| auto parsedMessageValue = JSON::Value::parseJSON(String::fromUTF8(message.payload.data())); |
| if (!parsedMessageValue) { |
| RELEASE_LOG(WebDriverBiDi, "WebDriver handle Message: Failed to parse incoming message"); |
| completionHandler(WebSocketMessageHandler::Message::fail(CommandResult::ErrorCode::InvalidArgument, message.connection)); |
| return; |
| } |
| |
| if (session && m_session && session->id() != m_session->id()) { |
| RELEASE_LOG(WebDriverBiDi, "Not an active session. Ignoring message."); |
| return; |
| } |
| |
| BidiCommandHandler handler; |
| unsigned id = 0; |
| RefPtr<JSON::Object> parameters; |
| if (!findBidiCommand(parsedMessageValue, &handler, id, parameters)) { |
| RELEASE_LOG(WebDriverBiDi, "Failed to find appropriate BiDi command"); |
| std::optional<int> commandId; |
| if (auto parsedMessageObject = parsedMessageValue->asObject()) { |
| auto parsedCommandId = parsedMessageObject->getInteger("id"_s); |
| if (parsedCommandId && *parsedCommandId >= 0) |
| commandId = parsedCommandId; |
| } |
| |
| auto errorCode = CommandResult::ErrorCode::UnknownCommand; |
| auto errorReply = WebSocketMessageHandler::Message::fail(errorCode, connection, { "Command not supported"_s }, commandId); |
| completionHandler(WTFMove(errorReply)); |
| return; |
| } |
| |
| ((*this).*handler)(id, WTFMove(parameters), [completionHandler = WTFMove(completionHandler), message](WebSocketMessageHandler::Message&& resultMessage) { |
| // 6.7.5 If method is "session.new", let session be the entry in the list of active sessions whose session ID is equal to the "sessionId" property of value, append connection to session’s session WebSocket connections, and remove connection from the WebSocket connections not associated with a session. |
| // FIXME https://bugs.webkit.org/show_bug.cgi?id=281722 |
| resultMessage.connection = message.connection; |
| completionHandler(WTFMove(resultMessage)); |
| }); |
| } |
| |
| bool WebDriverService::findBidiCommand(RefPtr<JSON::Value>& parameters, BidiCommandHandler* handler, unsigned& id, RefPtr<JSON::Object>& parsedParams) |
| { |
| if (!parameters) |
| return false; |
| |
| const auto& asObject = parameters->asObject(); |
| if (!asObject) |
| return false; |
| |
| std::optional<int> idOpt = asObject->getInteger("id"_s); |
| if (!idOpt) |
| return false; |
| |
| const String& method = asObject->getString("method"_s); |
| if (!method) |
| return false; |
| |
| auto candidate = std::find_if(std::begin(s_bidiCommands), std::end(s_bidiCommands), |
| [method](const BidiCommand& command) { |
| return method == command.method; |
| }); |
| |
| if (candidate == std::end(s_bidiCommands)) |
| return false; |
| |
| id = *idOpt; |
| parsedParams = asObject->getObject("params"_s); |
| *handler = candidate->handler; |
| return true; |
| } |
| |
| #endif // ENABLE(WEBDRIVER_BIDI) |
| |
| static std::optional<double> valueAsNumberInRange(const JSON::Value& value, double minAllowed = 0, double maxAllowed = std::numeric_limits<int>::max()) |
| { |
| auto number = value.asDouble(); |
| if (!number) |
| return std::nullopt; |
| |
| if (std::isnan(*number) || std::isinf(*number)) |
| return std::nullopt; |
| |
| if (*number < minAllowed || *number > maxAllowed) |
| return std::nullopt; |
| |
| return *number; |
| } |
| |
| static std::optional<uint64_t> unsignedValue(JSON::Value& value) |
| { |
| auto number = valueAsNumberInRange(value, 0, maxSafeInteger); |
| if (!number) |
| return std::nullopt; |
| |
| auto intValue = static_cast<uint64_t>(number.value()); |
| // If the contained value is a double, bail in case it doesn't match the integer |
| // value, i.e. if the double value was not originally in integer form. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-integer |
| if (number.value() != intValue) |
| return std::nullopt; |
| |
| return intValue; |
| } |
| |
| enum class IgnoreUnknownTimeout : bool { No, Yes }; |
| |
| static std::optional<Timeouts> deserializeTimeouts(JSON::Object& timeoutsObject, IgnoreUnknownTimeout ignoreUnknownTimeout) |
| { |
| // §8.5 Set Timeouts. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-deserialize-as-a-timeout |
| Timeouts timeouts; |
| auto end = timeoutsObject.end(); |
| for (auto it = timeoutsObject.begin(); it != end; ++it) { |
| if (it->key == "sessionId"_s) |
| continue; |
| |
| if (it->key == "script"_s && it->value->isNull()) { |
| timeouts.script = std::numeric_limits<double>::infinity(); |
| continue; |
| } |
| |
| // If value is not an integer, or it is less than 0 or greater than the maximum safe integer, return error with error code invalid argument. |
| auto timeoutMS = unsignedValue(it->value); |
| if (!timeoutMS) |
| return std::nullopt; |
| |
| if (it->key == "script"_s) |
| timeouts.script = timeoutMS.value(); |
| else if (it->key == "pageLoad"_s) |
| timeouts.pageLoad = timeoutMS.value(); |
| else if (it->key == "implicit"_s) |
| timeouts.implicit = timeoutMS.value(); |
| else if (ignoreUnknownTimeout == IgnoreUnknownTimeout::No) |
| return std::nullopt; |
| } |
| return timeouts; |
| } |
| |
| static std::optional<Proxy> deserializeProxy(JSON::Object& proxyObject) |
| { |
| // §7.1 Proxy. |
| // https://w3c.github.io/webdriver/#proxy |
| Proxy proxy; |
| |
| proxy.type = proxyObject.getString("proxyType"_s); |
| if (!proxy.type) |
| return std::nullopt; |
| |
| if (proxy.type == "direct"_s || proxy.type == "autodetect"_s || proxy.type == "system"_s) |
| return proxy; |
| |
| if (proxy.type == "pac"_s) { |
| auto autoconfigURL = proxyObject.getString("proxyAutoconfigUrl"_s); |
| if (!autoconfigURL) |
| return std::nullopt; |
| |
| proxy.autoconfigURL = URL({ }, autoconfigURL); |
| if (!proxy.autoconfigURL->isValid()) |
| return std::nullopt; |
| |
| return proxy; |
| } |
| |
| if (proxy.type == "manual"_s) { |
| if (auto value = proxyObject.getValue("ftpProxy"_s)) { |
| auto ftpProxy = value->asString(); |
| if (!ftpProxy) |
| return std::nullopt; |
| |
| proxy.ftpURL = URL({ }, makeString("ftp://"_s, ftpProxy)); |
| if (!proxy.ftpURL->isValid()) |
| return std::nullopt; |
| } |
| if (auto value = proxyObject.getValue("httpProxy"_s)) { |
| auto httpProxy = value->asString(); |
| if (!httpProxy) |
| return std::nullopt; |
| |
| proxy.httpURL = URL({ }, makeString("http://"_s, httpProxy)); |
| if (!proxy.httpURL->isValid()) |
| return std::nullopt; |
| } |
| if (auto value = proxyObject.getValue("sslProxy"_s)) { |
| auto sslProxy = value->asString(); |
| if (!sslProxy) |
| return std::nullopt; |
| |
| proxy.httpsURL = URL({ }, makeString("https://"_s, sslProxy)); |
| if (!proxy.httpsURL->isValid()) |
| return std::nullopt; |
| } |
| if (auto value = proxyObject.getValue("socksProxy"_s)) { |
| auto socksProxy = value->asString(); |
| if (!socksProxy) |
| return std::nullopt; |
| |
| proxy.socksURL = URL({ }, makeString("socks://"_s, socksProxy)); |
| if (!proxy.socksURL->isValid()) |
| return std::nullopt; |
| |
| auto socksVersionValue = proxyObject.getValue("socksVersion"_s); |
| if (!socksVersionValue) |
| return std::nullopt; |
| |
| auto socksVersion = unsignedValue(*socksVersionValue); |
| if (!socksVersion || socksVersion.value() > 255) |
| return std::nullopt; |
| proxy.socksVersion = socksVersion.value(); |
| } |
| if (auto value = proxyObject.getValue("noProxy"_s)) { |
| auto noProxy = value->asArray(); |
| if (!noProxy) |
| return std::nullopt; |
| |
| auto noProxyLength = noProxy->length(); |
| for (unsigned i = 0; i < noProxyLength; ++i) { |
| auto address = noProxy->get(i)->asString(); |
| if (!address) |
| return std::nullopt; |
| proxy.ignoreAddressList.append(address); |
| } |
| } |
| |
| return proxy; |
| } |
| |
| return std::nullopt; |
| } |
| |
| static std::optional<PageLoadStrategy> deserializePageLoadStrategy(const String& pageLoadStrategy) |
| { |
| if (pageLoadStrategy == "none"_s) |
| return PageLoadStrategy::None; |
| if (pageLoadStrategy == "normal"_s) |
| return PageLoadStrategy::Normal; |
| if (pageLoadStrategy == "eager"_s) |
| return PageLoadStrategy::Eager; |
| return std::nullopt; |
| } |
| |
| static std::optional<UnhandledPromptBehavior> deserializeUnhandledPromptBehavior(const String& unhandledPromptBehavior) |
| { |
| if (unhandledPromptBehavior == "dismiss"_s) |
| return UnhandledPromptBehavior::Dismiss; |
| if (unhandledPromptBehavior == "accept"_s) |
| return UnhandledPromptBehavior::Accept; |
| if (unhandledPromptBehavior == "dismiss and notify"_s) |
| return UnhandledPromptBehavior::DismissAndNotify; |
| if (unhandledPromptBehavior == "accept and notify"_s) |
| return UnhandledPromptBehavior::AcceptAndNotify; |
| if (unhandledPromptBehavior == "ignore"_s) |
| return UnhandledPromptBehavior::Ignore; |
| return std::nullopt; |
| } |
| |
| void WebDriverService::parseCapabilities(const JSON::Object& matchedCapabilities, Capabilities& capabilities) const |
| { |
| // Matched capabilities have already been validated. |
| auto acceptInsecureCerts = matchedCapabilities.getBoolean("acceptInsecureCerts"_s); |
| if (acceptInsecureCerts) |
| capabilities.acceptInsecureCerts = *acceptInsecureCerts; |
| |
| auto setWindowRect = matchedCapabilities.getBoolean("setWindowRect"_s); |
| if (setWindowRect) |
| capabilities.setWindowRect = *setWindowRect; |
| |
| auto browserName = matchedCapabilities.getString("browserName"_s); |
| if (!!browserName) |
| capabilities.browserName = browserName; |
| |
| auto browserVersion = matchedCapabilities.getString("browserVersion"_s); |
| if (!!browserVersion) |
| capabilities.browserVersion = browserVersion; |
| |
| auto platformName = matchedCapabilities.getString("platformName"_s); |
| if (!!platformName) |
| capabilities.platformName = platformName; |
| |
| auto proxy = matchedCapabilities.getObject("proxy"_s); |
| if (proxy) |
| capabilities.proxy = deserializeProxy(*proxy); |
| |
| auto strictFileInteractability = matchedCapabilities.getBoolean("strictFileInteractability"_s); |
| if (strictFileInteractability) |
| capabilities.strictFileInteractability = *strictFileInteractability; |
| |
| auto timeouts = matchedCapabilities.getObject("timeouts"_s); |
| if (timeouts) |
| capabilities.timeouts = deserializeTimeouts(*timeouts, IgnoreUnknownTimeout::No); |
| |
| auto pageLoadStrategy = matchedCapabilities.getString("pageLoadStrategy"_s); |
| if (!!pageLoadStrategy) |
| capabilities.pageLoadStrategy = deserializePageLoadStrategy(pageLoadStrategy); |
| |
| auto unhandledPromptBehavior = matchedCapabilities.getString("unhandledPromptBehavior"_s); |
| if (!!unhandledPromptBehavior) |
| capabilities.unhandledPromptBehavior = deserializeUnhandledPromptBehavior(unhandledPromptBehavior); |
| |
| if (auto webSocketURL = matchedCapabilities.getBoolean("webSocketUrl"_s)) |
| capabilities.webSocketURL = *webSocketURL; |
| |
| platformParseCapabilities(matchedCapabilities, capabilities); |
| } |
| |
| bool WebDriverService::findSessionOrCompleteWithError(JSON::Object& parameters, Function<void (CommandResult&&)>& completionHandler) |
| { |
| auto sessionID = parameters.getString("sessionId"_s); |
| if (!sessionID) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return false; |
| } |
| |
| if (!m_session || m_session->id() != sessionID) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidSessionID)); |
| return false; |
| } |
| |
| if (!m_session->isConnected()) { |
| m_session = nullptr; |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidSessionID, String("session deleted because of page crash or hang."_s))); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| RefPtr<JSON::Object> WebDriverService::validatedCapabilities(const JSON::Object& capabilities) const |
| { |
| // §7.2 Processing Capabilities. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-validate-capabilities |
| auto result = JSON::Object::create(); |
| auto end = capabilities.end(); |
| for (auto it = capabilities.begin(); it != end; ++it) { |
| if (it->value->isNull()) |
| continue; |
| if (it->key == "acceptInsecureCerts"_s) { |
| auto acceptInsecureCerts = it->value->asBoolean(); |
| if (!acceptInsecureCerts) |
| return nullptr; |
| result->setBoolean(it->key, *acceptInsecureCerts); |
| } else if (it->key == "browserName"_s || it->key == "browserVersion"_s || it->key == "platformName"_s) { |
| auto stringValue = it->value->asString(); |
| if (!stringValue) |
| return nullptr; |
| result->setString(it->key, stringValue); |
| } else if (it->key == "pageLoadStrategy"_s) { |
| auto pageLoadStrategy = it->value->asString(); |
| if (!pageLoadStrategy || !deserializePageLoadStrategy(pageLoadStrategy)) |
| return nullptr; |
| result->setString(it->key, pageLoadStrategy); |
| } else if (it->key == "proxy"_s) { |
| auto proxy = it->value->asObject(); |
| if (!proxy || !deserializeProxy(*proxy)) |
| return nullptr; |
| result->setValue(it->key, *proxy); |
| } else if (it->key == "strictFileInteractability"_s) { |
| auto strictFileInteractability = it->value->asBoolean(); |
| if (!strictFileInteractability) |
| return nullptr; |
| result->setBoolean(it->key, *strictFileInteractability); |
| } else if (it->key == "timeouts"_s) { |
| auto timeouts = it->value->asObject(); |
| if (!timeouts || !deserializeTimeouts(*timeouts, IgnoreUnknownTimeout::No)) |
| return nullptr; |
| result->setValue(it->key, *timeouts); |
| } else if (it->key == "unhandledPromptBehavior"_s) { |
| auto unhandledPromptBehavior = it->value->asString(); |
| if (!unhandledPromptBehavior || !deserializeUnhandledPromptBehavior(unhandledPromptBehavior)) |
| return nullptr; |
| result->setString(it->key, unhandledPromptBehavior); |
| } else if (it->key.find(':') != notFound) { |
| if (!platformValidateCapability(it->key, it->value)) |
| return nullptr; |
| result->setValue(it->key, it->value.copyRef()); |
| } else if (it->key == "webSocketUrl"_s) { |
| auto webSocketURL = it->value->asBoolean(); |
| if (!webSocketURL) |
| return nullptr; |
| result->setBoolean(it->key, *webSocketURL); |
| } else |
| return nullptr; |
| } |
| return result; |
| } |
| |
| RefPtr<JSON::Object> WebDriverService::mergeCapabilities(const JSON::Object& requiredCapabilities, const JSON::Object& firstMatchCapabilities) const |
| { |
| // §7.2 Processing Capabilities. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-merging-capabilities |
| auto result = JSON::Object::create(); |
| auto requiredEnd = requiredCapabilities.end(); |
| for (auto it = requiredCapabilities.begin(); it != requiredEnd; ++it) |
| result->setValue(it->key, it->value.copyRef()); |
| |
| auto firstMatchEnd = firstMatchCapabilities.end(); |
| for (auto it = firstMatchCapabilities.begin(); it != firstMatchEnd; ++it) |
| result->setValue(it->key, it->value.copyRef()); |
| |
| return result; |
| } |
| |
| RefPtr<JSON::Object> WebDriverService::matchCapabilities(const JSON::Object& mergedCapabilities) const |
| { |
| // §7.2 Processing Capabilities. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-matching-capabilities |
| Capabilities platformCapabilities = this->platformCapabilities(); |
| |
| // Some capabilities like browser name and version might need to launch the browser, |
| // so we only reject the known capabilities that don't match. |
| auto matchedCapabilities = JSON::Object::create(); |
| if (platformCapabilities.browserName) |
| matchedCapabilities->setString("browserName"_s, platformCapabilities.browserName.value()); |
| if (platformCapabilities.browserVersion) |
| matchedCapabilities->setString("browserVersion"_s, platformCapabilities.browserVersion.value()); |
| if (platformCapabilities.platformName) |
| matchedCapabilities->setString("platformName"_s, platformCapabilities.platformName.value()); |
| if (platformCapabilities.acceptInsecureCerts) |
| matchedCapabilities->setBoolean("acceptInsecureCerts"_s, platformCapabilities.acceptInsecureCerts.value()); |
| if (platformCapabilities.strictFileInteractability) |
| matchedCapabilities->setBoolean("strictFileInteractability"_s, platformCapabilities.strictFileInteractability.value()); |
| if (platformCapabilities.setWindowRect) |
| matchedCapabilities->setBoolean("setWindowRect"_s, platformCapabilities.setWindowRect.value()); |
| |
| auto end = mergedCapabilities.end(); |
| for (auto it = mergedCapabilities.begin(); it != end; ++it) { |
| if (it->key == "browserName"_s && platformCapabilities.browserName) { |
| auto browserName = it->value->asString(); |
| if (!equalIgnoringASCIICase(platformCapabilities.browserName.value(), browserName)) |
| return nullptr; |
| } else if (it->key == "browserVersion"_s && platformCapabilities.browserVersion) { |
| auto browserVersion = it->value->asString(); |
| if (!platformCompareBrowserVersions(browserVersion, platformCapabilities.browserVersion.value())) |
| return nullptr; |
| } else if (it->key == "platformName"_s && platformCapabilities.platformName) { |
| auto platformName = it->value->asString(); |
| if (!equalLettersIgnoringASCIICase(platformName, "any"_s) && platformCapabilities.platformName.value() != platformName) |
| return nullptr; |
| } else if (it->key == "acceptInsecureCerts"_s && platformCapabilities.acceptInsecureCerts) { |
| auto acceptInsecureCerts = it->value->asBoolean(); |
| if (acceptInsecureCerts && !platformCapabilities.acceptInsecureCerts.value()) |
| return nullptr; |
| } else if (it->key == "proxy"_s) { |
| auto proxyType = it->value->asObject()->getString("proxyType"_s); |
| if (!platformSupportProxyType(proxyType)) |
| return nullptr; |
| } else if (it->key == "webSocketUrl"_s) { |
| auto webSocketURL = it->value->asBoolean(); |
| if (webSocketURL && !platformSupportBidi()) |
| return nullptr; |
| } else if (!platformMatchCapability(it->key, it->value)) |
| return nullptr; |
| matchedCapabilities->setValue(it->key, it->value.copyRef()); |
| } |
| |
| return matchedCapabilities; |
| } |
| |
| Vector<Capabilities> WebDriverService::processCapabilities(const JSON::Object& parameters, Function<void (CommandResult&&)>& completionHandler) const |
| { |
| // §7.2 Processing Capabilities. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#processing-capabilities |
| |
| // 1. Let capabilities request be the result of getting the property "capabilities" from parameters. |
| auto capabilitiesObject = parameters.getObject("capabilities"_s); |
| if (!capabilitiesObject) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return { }; |
| } |
| |
| // 2. Let required capabilities be the result of getting the property "alwaysMatch" from capabilities request. |
| RefPtr<JSON::Object> requiredCapabilities; |
| auto requiredCapabilitiesValue = capabilitiesObject->getValue("alwaysMatch"_s); |
| if (!requiredCapabilitiesValue) { |
| // 2.1. If required capabilities is undefined, set the value to an empty JSON Object. |
| requiredCapabilities = JSON::Object::create(); |
| } else if (!(requiredCapabilities = requiredCapabilitiesValue->asObject())) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, String("alwaysMatch is invalid in capabilities"_s))); |
| return { }; |
| } |
| |
| // 2.2. Let required capabilities be the result of trying to validate capabilities with argument required capabilities. |
| requiredCapabilities = validatedCapabilities(*requiredCapabilities); |
| if (!requiredCapabilities) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, String("Invalid alwaysMatch capabilities"_s))); |
| return { }; |
| } |
| |
| // 3. Let all first match capabilities be the result of getting the property "firstMatch" from capabilities request. |
| RefPtr<JSON::Array> firstMatchCapabilitiesList; |
| auto firstMatchCapabilitiesValue = capabilitiesObject->getValue("firstMatch"_s); |
| if (!firstMatchCapabilitiesValue) { |
| // 3.1. If all first match capabilities is undefined, set the value to a JSON List with a single entry of an empty JSON Object. |
| firstMatchCapabilitiesList = JSON::Array::create(); |
| firstMatchCapabilitiesList->pushObject(JSON::Object::create()); |
| } else { |
| firstMatchCapabilitiesList = firstMatchCapabilitiesValue->asArray(); |
| if (!firstMatchCapabilitiesList) { |
| // 3.2. If all first match capabilities is not a JSON List, return error with error code invalid argument. |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, String("firstMatch is invalid in capabilities"_s))); |
| return { }; |
| } |
| } |
| |
| // 4. Let validated first match capabilities be an empty JSON List. |
| Vector<RefPtr<JSON::Object>> validatedFirstMatchCapabilitiesList; |
| auto firstMatchCapabilitiesListLength = firstMatchCapabilitiesList->length(); |
| validatedFirstMatchCapabilitiesList.reserveInitialCapacity(firstMatchCapabilitiesListLength); |
| // 5. For each first match capabilities corresponding to an indexed property in all first match capabilities. |
| for (unsigned i = 0; i < firstMatchCapabilitiesListLength; ++i) { |
| auto firstMatchCapabilities = firstMatchCapabilitiesList->get(i)->asObject(); |
| if (!firstMatchCapabilities) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, String("Invalid capabilities found in firstMatch"_s))); |
| return { }; |
| } |
| // 5.1. Let validated capabilities be the result of trying to validate capabilities with argument first match capabilities. |
| firstMatchCapabilities = validatedCapabilities(*firstMatchCapabilities); |
| if (!firstMatchCapabilities) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, String("Invalid firstMatch capabilities"_s))); |
| return { }; |
| } |
| |
| // Validate here that firstMatchCapabilities don't shadow alwaysMatchCapabilities. |
| auto requiredEnd = requiredCapabilities->end(); |
| auto firstMatchEnd = firstMatchCapabilities->end(); |
| for (auto it = firstMatchCapabilities->begin(); it != firstMatchEnd; ++it) { |
| if (requiredCapabilities->find(it->key) != requiredEnd) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, |
| makeString("Invalid firstMatch capabilities: key "_s, it->key, " is present in alwaysMatch"_s))); |
| return { }; |
| } |
| } |
| |
| // 5.2. Append validated capabilities to validated first match capabilities. |
| validatedFirstMatchCapabilitiesList.append(WTFMove(firstMatchCapabilities)); |
| } |
| |
| // 6. For each first match capabilities corresponding to an indexed property in validated first match capabilities. |
| Vector<Capabilities> matchedCapabilitiesList; |
| matchedCapabilitiesList.reserveInitialCapacity(validatedFirstMatchCapabilitiesList.size()); |
| for (auto& validatedFirstMatchCapabilies : validatedFirstMatchCapabilitiesList) { |
| // 6.1. Let merged capabilities be the result of trying to merge capabilities with required capabilities and first match capabilities as arguments. |
| auto mergedCapabilities = mergeCapabilities(*requiredCapabilities, *validatedFirstMatchCapabilies); |
| |
| // 6.2. Let matched capabilities be the result of trying to match capabilities with merged capabilities as an argument. |
| if (auto matchedCapabilities = matchCapabilities(*mergedCapabilities)) { |
| // 6.3. If matched capabilities is not null return matched capabilities. |
| Capabilities capabilities; |
| parseCapabilities(*matchedCapabilities, capabilities); |
| matchedCapabilitiesList.append(WTFMove(capabilities)); |
| } |
| } |
| |
| if (matchedCapabilitiesList.isEmpty()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Failed to match capabilities"_s))); |
| return { }; |
| } |
| |
| return matchedCapabilitiesList; |
| } |
| |
| void WebDriverService::newSession(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §8.1 New Session. |
| // https://www.w3.org/TR/webdriver/#new-session |
| auto matchedCapabilitiesList = processCapabilities(*parameters, completionHandler); |
| if (matchedCapabilitiesList.isEmpty()) |
| return; |
| |
| if (!m_session) { |
| // Reverse the vector to always take last item. |
| matchedCapabilitiesList.reverse(); |
| connectToBrowser(WTFMove(matchedCapabilitiesList), WTFMove(completionHandler)); |
| return; |
| } |
| |
| if (m_replaceOnNewSession) { |
| RELEASE_LOG(WebDriverClassic, "WebDriverService::newSession: Replacing existing session."); |
| auto session = std::exchange(m_session, nullptr); |
| session->close([this, session, matchedCapabilitiesList, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| #if ENABLE(WEBDRIVER_BIDI) |
| m_bidiServer.disconnectSession(session->id()); |
| #endif |
| // Ignore unknown errors when closing the session if the session has abeen actually closed. |
| if ((!result.isError()) || (result.errorCode() == CommandResult::ErrorCode::UnknownError && !session->isConnected())) { |
| matchedCapabilitiesList.reverse(); |
| connectToBrowser(WTFMove(matchedCapabilitiesList), WTFMove(completionHandler)); |
| } else |
| completionHandler(WTFMove(result)); |
| }); |
| return; |
| } |
| RELEASE_LOG(WebDriverClassic, "WebDriverService::newSession: Maximum number of active sessions reached. Returning error."); |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Maximum number of active sessions"_s))); |
| } |
| |
| void WebDriverService::connectToBrowser(Vector<Capabilities>&& capabilitiesList, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| if (capabilitiesList.isEmpty()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Failed to match capabilities"_s))); |
| return; |
| } |
| |
| auto sessionHost = SessionHost::create(capabilitiesList.takeLast()); |
| sessionHost->setHostAddress(m_targetAddress, m_targetPort); |
| auto protectedSessionHost = Ref<SessionHost>(sessionHost); |
| protectedSessionHost->connectToBrowser([this, capabilitiesList = WTFMove(capabilitiesList), sessionHost = WTFMove(sessionHost), completionHandler = WTFMove(completionHandler)](std::optional<String> error) mutable { |
| if (error) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, makeString("Failed to connect to browser: "_s, error.value()))); |
| return; |
| } |
| |
| createSession(WTFMove(capabilitiesList), WTFMove(sessionHost), WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::createSession(Vector<Capabilities>&& capabilitiesList, Ref<SessionHost>&& sessionHost, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| auto protectedSessionHost = Ref<SessionHost>(sessionHost); |
| protectedSessionHost->startAutomationSession([this, capabilitiesList = WTFMove(capabilitiesList), sessionHost = WTFMove(sessionHost), completionHandler = WTFMove(completionHandler)](bool capabilitiesDidMatch, std::optional<String> errorMessage) mutable { |
| if (errorMessage) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError, errorMessage.value())); |
| return; |
| } |
| if (!capabilitiesDidMatch) { |
| connectToBrowser(WTFMove(capabilitiesList), WTFMove(completionHandler)); |
| return; |
| } |
| #if ENABLE(WEBDRIVER_BIDI) |
| RefPtr<Session> session = Session::create(WTFMove(sessionHost), m_bidiServer); |
| #else |
| RefPtr<Session> session = Session::create(WTFMove(sessionHost)); |
| #endif |
| session->createTopLevelBrowsingContext([this, session, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, result.errorMessage())); |
| return; |
| } |
| |
| m_session = WTFMove(session); |
| |
| auto resultObject = JSON::Object::create(); |
| resultObject->setString("sessionId"_s, m_session->id()); |
| auto capabilitiesObject = JSON::Object::create(); |
| const auto& capabilities = m_session->capabilities(); |
| capabilitiesObject->setString("browserName"_s, capabilities.browserName.value_or(emptyString())); |
| capabilitiesObject->setString("browserVersion"_s, capabilities.browserVersion.value_or(emptyString())); |
| capabilitiesObject->setString("platformName"_s, capabilities.platformName.value_or(emptyString())); |
| capabilitiesObject->setBoolean("acceptInsecureCerts"_s, capabilities.acceptInsecureCerts.value_or(false)); |
| capabilitiesObject->setBoolean("strictFileInteractability"_s, capabilities.strictFileInteractability.value_or(false)); |
| capabilitiesObject->setBoolean("setWindowRect"_s, capabilities.setWindowRect.value_or(true)); |
| switch (capabilities.unhandledPromptBehavior.value_or(UnhandledPromptBehavior::DismissAndNotify)) { |
| case UnhandledPromptBehavior::Dismiss: |
| capabilitiesObject->setString("unhandledPromptBehavior"_s, "dismiss"_s); |
| break; |
| case UnhandledPromptBehavior::Accept: |
| capabilitiesObject->setString("unhandledPromptBehavior"_s, "accept"_s); |
| break; |
| case UnhandledPromptBehavior::DismissAndNotify: |
| capabilitiesObject->setString("unhandledPromptBehavior"_s, "dismiss and notify"_s); |
| break; |
| case UnhandledPromptBehavior::AcceptAndNotify: |
| capabilitiesObject->setString("unhandledPromptBehavior"_s, "accept and notify"_s); |
| break; |
| case UnhandledPromptBehavior::Ignore: |
| capabilitiesObject->setString("unhandledPromptBehavior"_s, "ignore"_s); |
| break; |
| } |
| switch (capabilities.pageLoadStrategy.value_or(PageLoadStrategy::Normal)) { |
| case PageLoadStrategy::None: |
| capabilitiesObject->setString("pageLoadStrategy"_s, "none"_s); |
| break; |
| case PageLoadStrategy::Normal: |
| capabilitiesObject->setString("pageLoadStrategy"_s, "normal"_s); |
| break; |
| case PageLoadStrategy::Eager: |
| capabilitiesObject->setString("pageLoadStrategy"_s, "eager"_s); |
| break; |
| } |
| if (!capabilities.proxy) |
| capabilitiesObject->setObject("proxy"_s, JSON::Object::create()); |
| auto timeoutsObject = JSON::Object::create(); |
| if (m_session->scriptTimeout() == std::numeric_limits<double>::infinity()) |
| timeoutsObject->setValue("script"_s, JSON::Value::null()); |
| else |
| timeoutsObject->setDouble("script"_s, m_session->scriptTimeout()); |
| timeoutsObject->setDouble("pageLoad"_s, m_session->pageLoadTimeout()); |
| timeoutsObject->setDouble("implicit"_s, m_session->implicitWaitTimeout()); |
| capabilitiesObject->setObject("timeouts"_s, WTFMove(timeoutsObject)); |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| // Extension steps defined by BiDi spec: https://w3c.github.io/webdriver-bidi/#establishing |
| if (!m_session->hasBiDiEnabled() && capabilities.webSocketURL && *capabilities.webSocketURL) { |
| auto listener = m_bidiServer.startListening(m_session->id()); |
| // We need to update the listener host to a visible one so remote clients can connect to it. |
| listener->host = m_server.visibleHost(); |
| |
| auto webSocketURL = m_bidiServer.getWebSocketURL(listener, m_session->id()); |
| capabilitiesObject->setString("webSocketUrl"_s, webSocketURL); |
| m_session->setHasBiDiEnabled(true); |
| } else { |
| RELEASE_LOG(WebDriverBiDi, "BiDi support not enabled for session %s", m_session->id().utf8().data()); |
| if (!m_session->hasBiDiEnabled()) |
| RELEASE_LOG(WebDriverBiDi, "BiDi flag not set for session %s", m_session->id().utf8().data()); |
| if (!capabilities.webSocketURL || !*capabilities.webSocketURL) |
| RELEASE_LOG(WebDriverBiDi, "webSocketURL not set for session %s", m_session->id().utf8().data()); |
| } |
| #endif |
| |
| resultObject->setObject("capabilities"_s, WTFMove(capabilitiesObject)); |
| completionHandler(CommandResult::success(WTFMove(resultObject))); |
| }); |
| }); |
| } |
| |
| void WebDriverService::deleteSession(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §8.2 Delete Session. |
| // https://www.w3.org/TR/webdriver/#delete-session |
| auto sessionID = parameters->getString("sessionId"_s); |
| if (!sessionID) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| if (!m_session || m_session->id() != sessionID) { |
| completionHandler(CommandResult::success()); |
| return; |
| } |
| |
| auto session = std::exchange(m_session, nullptr); |
| session->close([this, session, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| UNUSED_VARIABLE(this); // Conditionally used in ENABLE(WEBDRIVER_BIDI) block. |
| #if ENABLE(WEBDRIVER_BIDI) |
| m_bidiServer.disconnectSession(session->id()); |
| #endif |
| // Ignore unknown errors when closing the session if the session has abeen actually closed. |
| if (result.isError() && result.errorCode() == CommandResult::ErrorCode::UnknownError && !session->isConnected()) |
| completionHandler(CommandResult::success()); |
| else |
| completionHandler(WTFMove(result)); |
| }); |
| } |
| |
| void WebDriverService::status(RefPtr<JSON::Object>&&, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §8.3 Status |
| // https://w3c.github.io/webdriver/webdriver-spec.html#status |
| auto body = JSON::Object::create(); |
| body->setBoolean("ready"_s, !m_session); |
| body->setString("message"_s, m_session ? "A session already exists"_s : "No sessions"_s); |
| completionHandler(CommandResult::success(WTFMove(body))); |
| } |
| |
| void WebDriverService::getTimeouts(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §8.4 Get Timeouts. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-timeouts |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->getTimeouts(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::setTimeouts(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §8.5 Set Timeouts. |
| // https://www.w3.org/TR/webdriver/#set-timeouts |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto timeouts = deserializeTimeouts(*parameters, IgnoreUnknownTimeout::Yes); |
| if (!timeouts) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->setTimeouts(timeouts.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::go(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §9.1 Go. |
| // https://www.w3.org/TR/webdriver/#go |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto url = parameters->getString("url"_s); |
| if (!url) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->waitForNavigationToComplete([this, url = WTFMove(url), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->go(url, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::getCurrentURL(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §9.2 Get Current URL. |
| // https://www.w3.org/TR/webdriver/#get-current-url |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->getCurrentURL(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::back(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §9.3 Back. |
| // https://www.w3.org/TR/webdriver/#back |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->back(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::forward(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §9.4 Forward. |
| // https://www.w3.org/TR/webdriver/#forward |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->forward(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::refresh(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §9.5 Refresh. |
| // https://www.w3.org/TR/webdriver/#refresh |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->refresh(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::getTitle(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §9.6 Get Title. |
| // https://www.w3.org/TR/webdriver/#get-title |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->getTitle(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::getWindowHandle(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.1 Get Window Handle. |
| // https://www.w3.org/TR/webdriver/#get-window-handle |
| if (findSessionOrCompleteWithError(*parameters, completionHandler)) |
| m_session->getWindowHandle(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getWindowRect(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.7.1 Get Window Rect. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-window-rect |
| if (findSessionOrCompleteWithError(*parameters, completionHandler)) |
| m_session->getWindowRect(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::setWindowRect(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.7.2 Set Window Rect. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#set-window-rect |
| std::optional<double> width; |
| if (auto value = parameters->getValue("width"_s)) { |
| if (auto number = valueAsNumberInRange(*value)) |
| width = number; |
| else if (!value->isNull()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| } |
| std::optional<double> height; |
| if (auto value = parameters->getValue("height"_s)) { |
| if (auto number = valueAsNumberInRange(*value)) |
| height = number; |
| else if (!value->isNull()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| } |
| std::optional<double> x; |
| if (auto value = parameters->getValue("x"_s)) { |
| if (auto number = valueAsNumberInRange(*value, INT_MIN)) |
| x = number; |
| else if (!value->isNull()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| } |
| std::optional<double> y; |
| if (auto value = parameters->getValue("y"_s)) { |
| if (auto number = valueAsNumberInRange(*value, INT_MIN)) |
| y = number; |
| else if (!value->isNull()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| } |
| |
| // FIXME: If the remote end does not support the Set Window Rect command for the current |
| // top-level browsing context for any reason, return error with error code unsupported operation. |
| |
| if (findSessionOrCompleteWithError(*parameters, completionHandler)) |
| m_session->setWindowRect(x, y, width, height, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::maximizeWindow(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.7.3 Maximize Window |
| // https://w3c.github.io/webdriver/#maximize-window |
| if (findSessionOrCompleteWithError(*parameters, completionHandler)) |
| m_session->maximizeWindow(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::minimizeWindow(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.7.4 Minimize Window |
| // https://w3c.github.io/webdriver/#minimize-window |
| if (findSessionOrCompleteWithError(*parameters, completionHandler)) |
| m_session->minimizeWindow(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::fullscreenWindow(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.7.5 Fullscreen Window |
| // https://w3c.github.io/webdriver/#fullscreen-window |
| if (findSessionOrCompleteWithError(*parameters, completionHandler)) |
| m_session->fullscreenWindow(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::closeWindow(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.2 Close Window. |
| // https://www.w3.org/TR/webdriver/#close-window |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->closeWindow([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| |
| auto handles = result.result()->asArray(); |
| if (handles && !handles->length()) |
| m_session = nullptr; |
| |
| completionHandler(WTFMove(result)); |
| }); |
| } |
| |
| void WebDriverService::switchToWindow(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.3 Switch To Window. |
| // https://www.w3.org/TR/webdriver/#switch-to-window |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto handle = parameters->getString("handle"_s); |
| if (!handle) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->switchToWindow(handle, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getWindowHandles(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.4 Get Window Handles. |
| // https://www.w3.org/TR/webdriver/#get-window-handles |
| if (findSessionOrCompleteWithError(*parameters, completionHandler)) |
| m_session->getWindowHandles(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::newWindow(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §11.5 New Window |
| // https://w3c.github.io/webdriver/#new-window |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| std::optional<String> typeHint; |
| if (auto value = parameters->getValue("type"_s)) { |
| auto valueString = value->asString(); |
| if (!!valueString) { |
| if (valueString == "window"_s || valueString == "tab"_s) |
| typeHint = valueString; |
| } else if (!value->isNull()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| } |
| |
| m_session->newWindow(typeHint, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::switchToFrame(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.5 Switch To Frame. |
| // https://www.w3.org/TR/webdriver/#switch-to-frame |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto frameID = parameters->getValue("id"_s); |
| if (!frameID) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| switch (frameID->type()) { |
| case JSON::Value::Type::Null: |
| break; |
| case JSON::Value::Type::Double: |
| case JSON::Value::Type::Integer: |
| if (!valueAsNumberInRange(*frameID, 0, std::numeric_limits<unsigned short>::max())) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| break; |
| case JSON::Value::Type::Object: { |
| auto frameIDObject = frameID->asObject(); |
| if (frameIDObject->find(Session::webElementIdentifier()) == frameIDObject->end()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| break; |
| } |
| case JSON::Value::Type::Boolean: |
| case JSON::Value::Type::String: |
| case JSON::Value::Type::Array: |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->waitForNavigationToComplete([this, frameID, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->switchToFrame(WTFMove(frameID), WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::switchToParentFrame(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §10.6 Switch To Parent Frame. |
| // https://www.w3.org/TR/webdriver/#switch-to-parent-frame |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->switchToParentFrame(WTFMove(completionHandler)); |
| }); |
| } |
| |
| static std::optional<String> findElementOrCompleteWithError(JSON::Object& parameters, Function<void (CommandResult&&)>& completionHandler, Session::ElementIsShadowRoot isShadowRoot = Session::ElementIsShadowRoot::No) |
| { |
| auto elementID = parameters.getString(isShadowRoot == Session::ElementIsShadowRoot::Yes ? "shadowId"_s : "elementId"_s); |
| if (elementID.isEmpty()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return std::nullopt; |
| } |
| return elementID; |
| } |
| |
| static inline bool isValidStrategy(const String& strategy) |
| { |
| // §12.1 Locator Strategies. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-table-of-location-strategies |
| return strategy == "css selector"_s |
| || strategy == "link text"_s |
| || strategy == "partial link text"_s |
| || strategy == "tag name"_s |
| || strategy == "xpath"_s; |
| } |
| |
| static bool findStrategyAndSelectorOrCompleteWithError(JSON::Object& parameters, Function<void (CommandResult&&)>& completionHandler, Session::ElementIsShadowRoot isShadowRoot, String& strategy, String& selector) |
| { |
| strategy = parameters.getString("using"_s); |
| if (!isValidStrategy(strategy)) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return false; |
| } |
| selector = parameters.getString("value"_s); |
| if (!selector) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return false; |
| } |
| |
| if (isShadowRoot == Session::ElementIsShadowRoot::Yes) { |
| // Currently there is an opened discussion about if the following values has to be supported for a Shadow Root |
| // because the current implementation doesn't support them. We have them disabled for now. |
| // https://github.com/w3c/webdriver/issues/1610 |
| if (strategy == "tag name"_s || strategy == "xpath"_s) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidSelector)); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| void WebDriverService::findElement(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §12.2 Find Element. |
| // https://www.w3.org/TR/webdriver/#find-element |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| String strategy, selector; |
| if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::No, strategy, selector)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, strategy = WTFMove(strategy), selector = WTFMove(selector), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->findElements(strategy, selector, Session::FindElementsMode::Single, emptyString(), Session::ElementIsShadowRoot::No, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::findElements(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §12.3 Find Elements. |
| // https://www.w3.org/TR/webdriver/#find-elements |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| String strategy, selector; |
| if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::No, strategy, selector)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, strategy = WTFMove(strategy), selector = WTFMove(selector), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->findElements(strategy, selector, Session::FindElementsMode::Multiple, emptyString(), Session::ElementIsShadowRoot::No, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::findElementFromElement(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §12.4 Find Element From Element. |
| // https://www.w3.org/TR/webdriver/#find-element-from-element |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| String strategy, selector; |
| if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::No, strategy, selector)) |
| return; |
| m_session->findElements(strategy, selector, Session::FindElementsMode::Single, elementID.value(), Session::ElementIsShadowRoot::No, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::findElementsFromElement(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §12.5 Find Elements From Element. |
| // https://www.w3.org/TR/webdriver/#find-elements-from-element |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| String strategy, selector; |
| if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::No, strategy, selector)) |
| return; |
| |
| m_session->findElements(strategy, selector, Session::FindElementsMode::Multiple, elementID.value(), Session::ElementIsShadowRoot::No, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::findElementFromShadowRoot(RefPtr<JSON::Object>&& parameters, Function<void(CommandResult&&)>&& completionHandler) |
| { |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto shadowID = findElementOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::Yes); |
| if (!shadowID) |
| return; |
| |
| String strategy, selector; |
| if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::Yes, strategy, selector)) |
| return; |
| |
| m_session->findElements(strategy, selector, Session::FindElementsMode::Single, shadowID.value(), Session::ElementIsShadowRoot::Yes, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::findElementsFromShadowRoot(RefPtr<JSON::Object>&& parameters, Function<void(CommandResult&&)>&& completionHandler) |
| { |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto shadowID = findElementOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::Yes); |
| if (!shadowID) |
| return; |
| |
| String strategy, selector; |
| if (!findStrategyAndSelectorOrCompleteWithError(*parameters, completionHandler, Session::ElementIsShadowRoot::Yes, strategy, selector)) |
| return; |
| |
| m_session->findElements(strategy, selector, Session::FindElementsMode::Multiple, shadowID.value(), Session::ElementIsShadowRoot::Yes, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getActiveElement(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §12.6 Get Active Element. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-active-element |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->getActiveElement(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::getElementShadowRoot(RefPtr<JSON::Object>&& parameters, Function<void(CommandResult&&)>&& completionHandler) |
| { |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->getElementShadowRoot(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::isElementSelected(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.1 Is Element Selected. |
| // https://www.w3.org/TR/webdriver/#is-element-selected |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->isElementSelected(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getElementAttribute(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.2 Get Element Attribute. |
| // https://www.w3.org/TR/webdriver/#get-element-attribute |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| auto attribute = parameters->getString("name"_s); |
| if (!attribute) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->getElementAttribute(elementID.value(), attribute, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getElementProperty(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.3 Get Element Property |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-element-property |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| auto attribute = parameters->getString("name"_s); |
| if (!attribute) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->getElementProperty(elementID.value(), attribute, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getElementCSSValue(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.4 Get Element CSS Value |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-element-css-value |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| auto cssProperty = parameters->getString("name"_s); |
| if (!cssProperty) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->getElementCSSValue(elementID.value(), cssProperty, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getElementText(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.5 Get Element Text. |
| // https://www.w3.org/TR/webdriver/#get-element-text |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->getElementText(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getElementTagName(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.6 Get Element Tag Name. |
| // https://www.w3.org/TR/webdriver/#get-element-tag-name |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->getElementTagName(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getElementRect(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.7 Get Element Rect. |
| // https://www.w3.org/TR/webdriver/#get-element-rect |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->getElementRect(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::isElementEnabled(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §13.8 Is Element Enabled. |
| // https://www.w3.org/TR/webdriver/#is-element-enabled |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->isElementEnabled(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getComputedRole(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §12.4.9 Get Computed Role |
| // https://www.w3.org/TR/webdriver/#get-computed-role |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->getComputedRole(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getComputedLabel(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §12.4.10 Get Computed Role |
| // https://www.w3.org/TR/webdriver/#get-computed-label |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->getComputedLabel(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::isElementDisplayed(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §C. Element Displayedness. |
| // https://www.w3.org/TR/webdriver/#element-displayedness |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->isElementDisplayed(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::elementClick(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §14.1 Element Click. |
| // https://www.w3.org/TR/webdriver/#element-click |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->elementClick(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::elementClear(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §14.2 Element Clear. |
| // https://www.w3.org/TR/webdriver/#element-clear |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->elementClear(elementID.value(), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::elementSendKeys(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §14.3 Element Send Keys. |
| // https://www.w3.org/TR/webdriver/#element-send-keys |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| auto text = parameters->getString("text"_s); |
| if (text.isEmpty()) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->elementSendKeys(elementID.value(), text, WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::getPageSource(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §15.1 Getting Page Source. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#getting-page-source |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->getPageSource(WTFMove(completionHandler)); |
| } |
| |
| static bool findScriptAndArgumentsOrCompleteWithError(JSON::Object& parameters, Function<void (CommandResult&&)>& completionHandler, String& script, RefPtr<JSON::Array>& arguments) |
| { |
| script = parameters.getString("script"_s); |
| if (!script) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return false; |
| } |
| arguments = parameters.getArray("args"_s); |
| if (!arguments) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return false; |
| } |
| return true; |
| } |
| |
| void WebDriverService::executeScript(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §15.2.1 Execute Script. |
| // https://www.w3.org/TR/webdriver/#execute-script |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| String script; |
| RefPtr<JSON::Array> arguments; |
| if (!findScriptAndArgumentsOrCompleteWithError(*parameters, completionHandler, script, arguments)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, script = WTFMove(script), arguments = WTFMove(arguments), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->executeScript(script, WTFMove(arguments), Session::ExecuteScriptMode::Sync, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::executeAsyncScript(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §15.2.2 Execute Async Script. |
| // https://www.w3.org/TR/webdriver/#execute-async-script |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| String script; |
| RefPtr<JSON::Array> arguments; |
| if (!findScriptAndArgumentsOrCompleteWithError(*parameters, completionHandler, script, arguments)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, script = WTFMove(script), arguments = WTFMove(arguments), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->executeScript(script, WTFMove(arguments), Session::ExecuteScriptMode::Async, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::getAllCookies(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §16.1 Get All Cookies. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-all-cookies |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->getAllCookies(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::getNamedCookie(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §16.2 Get Named Cookie. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-named-cookie |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto name = parameters->getString("name"_s); |
| if (!name) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->waitForNavigationToComplete([this, name = WTFMove(name), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->getNamedCookie(name, WTFMove(completionHandler)); |
| }); |
| } |
| |
| static std::optional<Session::Cookie> deserializeCookie(JSON::Object& cookieObject) |
| { |
| Session::Cookie cookie; |
| |
| cookie.name = cookieObject.getString("name"_s); |
| if (cookie.name.isEmpty()) |
| return std::nullopt; |
| |
| cookie.value = cookieObject.getString("value"_s); |
| if (cookie.value.isEmpty()) |
| return std::nullopt; |
| |
| if (auto value = cookieObject.getValue("path"_s)) { |
| auto path = value->asString(); |
| if (!path) |
| return std::nullopt; |
| cookie.path = path; |
| } |
| if (auto value = cookieObject.getValue("domain"_s)) { |
| auto domain = value->asString(); |
| if (!domain) |
| return std::nullopt; |
| cookie.domain = domain; |
| } |
| if (auto value = cookieObject.getValue("secure"_s)) { |
| auto secure = value->asBoolean(); |
| if (!secure) |
| return std::nullopt; |
| cookie.secure = secure; |
| } |
| if (auto value = cookieObject.getValue("httpOnly"_s)) { |
| auto httpOnly = value->asBoolean(); |
| if (!httpOnly) |
| return std::nullopt; |
| cookie.httpOnly = httpOnly; |
| } |
| if (auto value = cookieObject.getValue("expiry"_s)) { |
| auto expiry = unsignedValue(*value); |
| if (!expiry) |
| return std::nullopt; |
| cookie.expiry = expiry.value(); |
| } |
| if (auto value = cookieObject.getValue("sameSite"_s)) { |
| auto sameSite = value->asString(); |
| if (sameSite != "None"_s && sameSite != "Lax"_s && sameSite != "Strict"_s) |
| return std::nullopt; |
| cookie.sameSite = sameSite; |
| } |
| |
| return cookie; |
| } |
| |
| void WebDriverService::addCookie(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §16.3 Add Cookie. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto cookieObject = parameters->getObject("cookie"_s); |
| if (!cookieObject) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| auto cookie = deserializeCookie(*cookieObject); |
| if (!cookie) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->waitForNavigationToComplete([this, cookie = WTFMove(cookie), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->addCookie(cookie.value(), WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::deleteCookie(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §16.4 Delete Cookie. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#delete-cookie |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto name = parameters->getString("name"_s); |
| if (!name) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->waitForNavigationToComplete([this, name = WTFMove(name), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->deleteCookie(name, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::deleteAllCookies(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §16.5 Delete All Cookies. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#delete-all-cookies |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->deleteAllCookies(WTFMove(completionHandler)); |
| }); |
| } |
| |
| static bool processPauseAction(JSON::Object& actionItem, Action& action, std::optional<String>& errorMessage) |
| { |
| auto durationValue = actionItem.getValue("duration"_s); |
| if (!durationValue) |
| return true; |
| |
| auto duration = unsignedValue(*durationValue); |
| if (!duration) { |
| errorMessage = String("The parameter 'duration' is invalid in pause action"_s); |
| return false; |
| } |
| |
| action.duration = duration.value(); |
| return true; |
| } |
| |
| static std::optional<Action> processNullAction(const String& id, JSON::Object& actionItem, std::optional<String>& errorMessage) |
| { |
| auto subtype = actionItem.getString("type"_s); |
| if (subtype != "pause"_s) { |
| errorMessage = String("The parameter 'type' in null action is invalid or missing"_s); |
| return std::nullopt; |
| } |
| |
| Action action(id, Action::Type::None, Action::Subtype::Pause); |
| if (!processPauseAction(actionItem, action, errorMessage)) |
| return std::nullopt; |
| |
| return action; |
| } |
| |
| static std::optional<Action> processKeyAction(const String& id, JSON::Object& actionItem, std::optional<String>& errorMessage) |
| { |
| Action::Subtype actionSubtype; |
| auto subtype = actionItem.getString("type"_s); |
| if (subtype == "pause"_s) |
| actionSubtype = Action::Subtype::Pause; |
| else if (subtype == "keyUp"_s) |
| actionSubtype = Action::Subtype::KeyUp; |
| else if (subtype == "keyDown"_s) |
| actionSubtype = Action::Subtype::KeyDown; |
| else { |
| errorMessage = String("The parameter 'type' of key action is invalid"_s); |
| return std::nullopt; |
| } |
| |
| Action action(id, Action::Type::Key, actionSubtype); |
| |
| switch (actionSubtype) { |
| case Action::Subtype::Pause: |
| if (!processPauseAction(actionItem, action, errorMessage)) |
| return std::nullopt; |
| break; |
| case Action::Subtype::KeyUp: |
| case Action::Subtype::KeyDown: { |
| auto keyValue = actionItem.getValue("value"_s); |
| if (!keyValue) { |
| errorMessage = String("The paramater 'value' is missing for key up/down action"_s); |
| return std::nullopt; |
| } |
| auto key = keyValue->asString(); |
| if (key.isEmpty()) { |
| errorMessage = String("The paramater 'value' is invalid for key up/down action"_s); |
| return std::nullopt; |
| } |
| // FIXME: check single unicode code point. |
| action.key = key; |
| break; |
| } |
| case Action::Subtype::PointerUp: |
| case Action::Subtype::PointerDown: |
| case Action::Subtype::PointerMove: |
| case Action::Subtype::PointerCancel: |
| case Action::Subtype::Scroll: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| return action; |
| } |
| |
| static MouseButton actionMouseButton(unsigned button) |
| { |
| // MouseEvent.button |
| // https://www.w3.org/TR/uievents/#ref-for-dom-mouseevent-button-1 |
| switch (button) { |
| case 0: |
| return MouseButton::Left; |
| case 1: |
| return MouseButton::Middle; |
| case 2: |
| return MouseButton::Right; |
| } |
| |
| return MouseButton::None; |
| } |
| |
| static bool processPointerMoveAction(JSON::Object& actionItem, Action& action, std::optional<String>& errorMessage) |
| { |
| if (auto durationValue = actionItem.getValue("duration"_s)) { |
| auto duration = unsignedValue(*durationValue); |
| if (!duration) { |
| errorMessage = String("The parameter 'duration' is invalid in action"_s); |
| return false; |
| } |
| action.duration = duration.value(); |
| } |
| |
| if (auto originValue = actionItem.getValue("origin"_s)) { |
| if (auto originObject = originValue->asObject()) { |
| auto elementID = originObject->getString(Session::webElementIdentifier()); |
| if (!elementID) { |
| errorMessage = String("The parameter 'origin' is not a valid web element object in action"_s); |
| return false; |
| } |
| action.origin = PointerOrigin { PointerOrigin::Type::Element, elementID }; |
| } else { |
| auto origin = originValue->asString(); |
| if (origin == "viewport"_s) |
| action.origin = PointerOrigin { PointerOrigin::Type::Viewport, std::nullopt }; |
| else if (origin == "pointer"_s) |
| action.origin = PointerOrigin { PointerOrigin::Type::Pointer, std::nullopt }; |
| else { |
| errorMessage = String("The parameter 'origin' is invalid in action"_s); |
| return false; |
| } |
| } |
| } else |
| action.origin = PointerOrigin { PointerOrigin::Type::Viewport, std::nullopt }; |
| |
| if (auto xValue = actionItem.getValue("x"_s)) { |
| auto x = valueAsNumberInRange(*xValue, INT_MIN); |
| if (!x) { |
| errorMessage = String("The paramater 'x' is invalid for action"_s); |
| return false; |
| } |
| action.x = x.value(); |
| } |
| |
| if (auto yValue = actionItem.getValue("y"_s)) { |
| auto y = valueAsNumberInRange(*yValue, INT_MIN); |
| if (!y) { |
| errorMessage = String("The paramater 'y' is invalid for action"_s); |
| return false; |
| } |
| action.y = y.value(); |
| } |
| |
| return true; |
| } |
| |
| static std::optional<Action> processPointerAction(const String& id, PointerParameters& parameters, JSON::Object& actionItem, std::optional<String>& errorMessage) |
| { |
| Action::Subtype actionSubtype; |
| auto subtype = actionItem.getString("type"_s); |
| if (subtype == "pause"_s) |
| actionSubtype = Action::Subtype::Pause; |
| else if (subtype == "pointerUp"_s) |
| actionSubtype = Action::Subtype::PointerUp; |
| else if (subtype == "pointerDown"_s) |
| actionSubtype = Action::Subtype::PointerDown; |
| else if (subtype == "pointerMove"_s) |
| actionSubtype = Action::Subtype::PointerMove; |
| else if (subtype == "pointerCancel"_s) |
| actionSubtype = Action::Subtype::PointerCancel; |
| else { |
| errorMessage = String("The parameter 'type' of pointer action is invalid"_s); |
| return std::nullopt; |
| } |
| |
| Action action(id, Action::Type::Pointer, actionSubtype); |
| action.pointerType = parameters.pointerType; |
| |
| switch (actionSubtype) { |
| case Action::Subtype::Pause: |
| if (!processPauseAction(actionItem, action, errorMessage)) |
| return std::nullopt; |
| break; |
| case Action::Subtype::PointerUp: |
| case Action::Subtype::PointerDown: { |
| auto buttonValue = actionItem.getValue("button"_s); |
| if (!buttonValue) { |
| errorMessage = String("The paramater 'button' is missing for pointer up/down action"_s); |
| return std::nullopt; |
| } |
| auto button = unsignedValue(*buttonValue); |
| if (!button) { |
| errorMessage = String("The paramater 'button' is invalid for pointer up/down action"_s); |
| return std::nullopt; |
| } |
| action.button = actionMouseButton(button.value()); |
| break; |
| } |
| case Action::Subtype::PointerMove: |
| if (!processPointerMoveAction(actionItem, action, errorMessage)) |
| return std::nullopt; |
| break; |
| case Action::Subtype::PointerCancel: |
| break; |
| case Action::Subtype::KeyUp: |
| case Action::Subtype::KeyDown: |
| case Action::Subtype::Scroll: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| return action; |
| } |
| |
| static std::optional<Action> processWheelAction(const String& id, JSON::Object& actionItem, std::optional<String>& errorMessage) |
| { |
| Action::Subtype actionSubtype; |
| auto subtype = actionItem.getString("type"_s); |
| if (subtype == "pause"_s) |
| actionSubtype = Action::Subtype::Pause; |
| else if (subtype == "scroll"_s) |
| actionSubtype = Action::Subtype::Scroll; |
| else { |
| errorMessage = String("The parameter 'type' of wheel action is invalid"_s); |
| return std::nullopt; |
| } |
| |
| Action action(id, Action::Type::Wheel, actionSubtype); |
| |
| switch (actionSubtype) { |
| case Action::Subtype::Pause: |
| if (!processPauseAction(actionItem, action, errorMessage)) |
| return std::nullopt; |
| break; |
| case Action::Subtype::Scroll: |
| if (!processPointerMoveAction(actionItem, action, errorMessage)) |
| return std::nullopt; |
| |
| if (auto deltaXValue = actionItem.getValue("deltaX"_s)) { |
| auto deltaX = valueAsNumberInRange(*deltaXValue, INT_MIN); |
| if (!deltaX) { |
| errorMessage = String("The paramater 'deltaX' is invalid for action"_s); |
| return std::nullopt; |
| } |
| action.deltaX = deltaX.value(); |
| } |
| |
| if (auto deltaYValue = actionItem.getValue("deltaY"_s)) { |
| auto deltaY = valueAsNumberInRange(*deltaYValue, INT_MIN); |
| if (!deltaY) { |
| errorMessage = String("The paramater 'deltaY' is invalid for action"_s); |
| return std::nullopt; |
| } |
| action.deltaY = deltaY.value(); |
| } |
| break; |
| case Action::Subtype::KeyUp: |
| case Action::Subtype::KeyDown: |
| case Action::Subtype::PointerUp: |
| case Action::Subtype::PointerDown: |
| case Action::Subtype::PointerMove: |
| case Action::Subtype::PointerCancel: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| return action; |
| } |
| |
| static std::optional<PointerParameters> processPointerParameters(JSON::Object& actionSequence, std::optional<String>& errorMessage) |
| { |
| PointerParameters parameters; |
| |
| auto parametersDataValue = actionSequence.getValue("parameters"_s); |
| if (!parametersDataValue) |
| return parameters; |
| |
| auto parametersData = parametersDataValue->asObject(); |
| if (!parametersData) { |
| errorMessage = String("Action sequence pointer parameters is not an object"_s); |
| return std::nullopt; |
| } |
| |
| auto pointerType = parametersData->getString("pointerType"_s); |
| if (!pointerType) |
| return parameters; |
| |
| if (pointerType == "mouse"_s) |
| parameters.pointerType = PointerType::Mouse; |
| else if (pointerType == "pen"_s) |
| parameters.pointerType = PointerType::Pen; |
| else if (pointerType == "touch"_s) |
| parameters.pointerType = PointerType::Touch; |
| else { |
| errorMessage = String("The parameter 'pointerType' in action sequence pointer parameters is invalid"_s); |
| return std::nullopt; |
| } |
| |
| return parameters; |
| } |
| |
| static std::optional<Vector<Action>> processInputActionSequence(Session& session, JSON::Value& actionSequenceValue, std::optional<String>& errorMessage) |
| { |
| auto actionSequence = actionSequenceValue.asObject(); |
| if (!actionSequence) { |
| errorMessage = String("The action sequence is not an object"_s); |
| return std::nullopt; |
| } |
| |
| auto type = actionSequence->getString("type"_s); |
| InputSource::Type inputSourceType; |
| if (type == "key"_s) |
| inputSourceType = InputSource::Type::Key; |
| else if (type == "pointer"_s) |
| inputSourceType = InputSource::Type::Pointer; |
| else if (type == "wheel"_s) |
| inputSourceType = InputSource::Type::Wheel; |
| else if (type == "none"_s) |
| inputSourceType = InputSource::Type::None; |
| else { |
| errorMessage = String("The parameter 'type' is invalid or missing in action sequence"_s); |
| return std::nullopt; |
| } |
| |
| auto id = actionSequence->getString("id"_s); |
| if (!id) { |
| errorMessage = String("The parameter 'id' is invalid or missing in action sequence"_s); |
| return std::nullopt; |
| } |
| |
| std::optional<PointerParameters> parameters; |
| std::optional<PointerType> pointerType; |
| if (inputSourceType == InputSource::Type::Pointer) { |
| parameters = processPointerParameters(*actionSequence, errorMessage); |
| if (!parameters) |
| return std::nullopt; |
| |
| pointerType = parameters->pointerType; |
| } |
| |
| auto& inputSource = session.getOrCreateInputSource(id, inputSourceType, pointerType); |
| if (inputSource.type != inputSourceType) { |
| errorMessage = String("Action sequence type doesn't match input source type"_s); |
| return std::nullopt; |
| } |
| |
| if (inputSource.type == InputSource::Type::Pointer && inputSource.pointerType != pointerType) { |
| errorMessage = String("Action sequence pointer type doesn't match input source pointer type"_s); |
| return std::nullopt; |
| } |
| |
| auto actionItems = actionSequence->getArray("actions"_s); |
| if (!actionItems) { |
| errorMessage = String("The parameter 'actions' is invalid or not present in action sequence"_s); |
| return std::nullopt; |
| } |
| |
| Vector<Action> actions; |
| unsigned actionItemsLength = actionItems->length(); |
| for (unsigned i = 0; i < actionItemsLength; ++i) { |
| auto actionItem = actionItems->get(i)->asObject(); |
| if (!actionItem) { |
| errorMessage = String("An action in action sequence is not an object"_s); |
| return std::nullopt; |
| } |
| |
| std::optional<Action> action; |
| if (inputSourceType == InputSource::Type::None) |
| action = processNullAction(id, *actionItem, errorMessage); |
| else if (inputSourceType == InputSource::Type::Key) |
| action = processKeyAction(id, *actionItem, errorMessage); |
| else if (inputSourceType == InputSource::Type::Pointer) |
| action = processPointerAction(id, parameters.value(), *actionItem, errorMessage); |
| else if (inputSourceType == InputSource::Type::Wheel) |
| action = processWheelAction(id, *actionItem, errorMessage); |
| if (!action) |
| return std::nullopt; |
| |
| actions.append(action.value()); |
| } |
| |
| return actions; |
| } |
| |
| void WebDriverService::performActions(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §17.5 Perform Actions. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#perform-actions |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto actionsArray = parameters->getArray("actions"_s); |
| if (!actionsArray) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, String("The paramater 'actions' is invalid or not present"_s))); |
| return; |
| } |
| |
| std::optional<String> errorMessage; |
| Vector<Vector<Action>> actionsByTick; |
| unsigned actionsArrayLength = actionsArray->length(); |
| for (unsigned i = 0; i < actionsArrayLength; ++i) { |
| auto actionSequence = actionsArray->get(i); |
| auto inputSourceActions = processInputActionSequence(*m_session, actionSequence, errorMessage); |
| if (!inputSourceActions) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument, errorMessage.value())); |
| return; |
| } |
| for (unsigned i = 0; i < inputSourceActions->size(); ++i) { |
| if (actionsByTick.size() < i + 1) |
| actionsByTick.append({ }); |
| actionsByTick[i].append(inputSourceActions.value()[i]); |
| } |
| } |
| |
| m_session->performActions(WTFMove(actionsByTick), WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::releaseActions(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §17.5 Release Actions. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#release-actions |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->releaseActions(WTFMove(completionHandler)); |
| } |
| |
| void WebDriverService::dismissAlert(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §18.1 Dismiss Alert. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#dismiss-alert |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->dismissAlert(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::acceptAlert(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §18.2 Accept Alert. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#accept-alert |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->acceptAlert(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::getAlertText(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §18.3 Get Alert Text. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#get-alert-text |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->getAlertText(WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::sendAlertText(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §18.4 Send Alert Text. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#send-alert-text |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto text = parameters->getString("text"_s); |
| if (!text) { |
| completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); |
| return; |
| } |
| |
| m_session->waitForNavigationToComplete([this, text = WTFMove(text), completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->sendAlertText(text, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::takeScreenshot(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §19.1 Take Screenshot. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#take-screenshot |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->takeScreenshot(std::nullopt, std::nullopt, WTFMove(completionHandler)); |
| }); |
| } |
| |
| void WebDriverService::takeElementScreenshot(RefPtr<JSON::Object>&& parameters, Function<void (CommandResult&&)>&& completionHandler) |
| { |
| // §19.2 Take Element Screenshot. |
| // https://w3c.github.io/webdriver/webdriver-spec.html#take-element-screenshot |
| if (!findSessionOrCompleteWithError(*parameters, completionHandler)) |
| return; |
| |
| auto elementID = findElementOrCompleteWithError(*parameters, completionHandler); |
| if (!elementID) |
| return; |
| |
| m_session->waitForNavigationToComplete([this, elementID, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { |
| if (result.isError()) { |
| completionHandler(WTFMove(result)); |
| return; |
| } |
| m_session->takeScreenshot(elementID.value(), true, WTFMove(completionHandler)); |
| }); |
| } |
| |
| #if ENABLE(WEBDRIVER_BIDI) |
| void WebDriverService::bidiSessionStatus(unsigned id, RefPtr<JSON::Object>&&, Function<void(WebSocketMessageHandler::Message&&)>&& completionHandler) |
| { |
| auto result = JSON::Object::create(); |
| bool ready = !m_session; |
| result->setBoolean("ready"_s, ready); |
| if (ready) |
| result->setString("message"_s, "Ready for new sessions"_s); |
| else |
| result->setString("message"_s, "Maximum number of sessions created"_s); |
| |
| completionHandler(WebSocketMessageHandler::Message::reply("success"_s, id, WTFMove(result))); |
| } |
| |
| void WebDriverService::bidiSessionSubscribe(unsigned id, RefPtr<JSON::Object>&¶meters, Function<void(WebSocketMessageHandler::Message&&)>&& completionHandler) |
| { |
| // https://w3c.github.io/webdriver-bidi/#command-session-subscribe |
| auto eventNames = parameters->getArray("events"_s); |
| |
| if (!eventNames) { |
| completionHandler(WebSocketMessageHandler::Message::fail(CommandResult::ErrorCode::InvalidArgument, std::nullopt, "Missing 'events' parameter"_s, id)); |
| return; |
| } |
| |
| // FIXME: Support event priorities. |
| // https://bugs.webkit.org/show_bug.cgi?id=282436 |
| // FIXME: Support by-context subscriptions. |
| // https://bugs.webkit.org/show_bug.cgi?id=282981 |
| for (auto& eventName : *eventNames) { |
| auto event = eventName->asString(); |
| m_session->enableGlobalEvent(event); |
| } |
| |
| completionHandler(WebSocketMessageHandler::Message::reply("success"_s, id, JSON::Value::null())); |
| } |
| |
| void WebDriverService::bidiSessionUnsubscribe(unsigned id, RefPtr<JSON::Object>&¶meters, Function<void(WebSocketMessageHandler::Message&&)>&& completionHandler) |
| { |
| // https://w3c.github.io/webdriver-bidi/#command-session-unsubscribe |
| auto eventNames = parameters->getArray("events"_s); |
| |
| if (!eventNames) { |
| completionHandler(WebSocketMessageHandler::Message::fail(CommandResult::ErrorCode::InvalidArgument, std::nullopt, "Missing 'events' parameter"_s, id)); |
| return; |
| } |
| |
| // FIXME: Support by-context unsubscriptions. |
| // https://bugs.webkit.org/show_bug.cgi?id=282981 |
| for (auto& eventName : *eventNames) { |
| auto event = eventName->asString(); |
| m_session->disableGlobalEvent(event); |
| } |
| |
| completionHandler(WebSocketMessageHandler::Message::reply("success"_s, id, JSON::Value::null())); |
| } |
| |
| void WebDriverService::clientDisconnected(const WebSocketMessageHandler::Connection& connection) |
| { |
| // https://w3c.github.io/webdriver-bidi/#handle-a-connection-closing |
| if (m_bidiServer.session(connection)) |
| m_bidiServer.removeConnection(connection); |
| else if (m_bidiServer.isStaticConnection(connection)) |
| m_bidiServer.removeStaticConnection(connection); |
| // Note from spec: This does not end any session. |
| } |
| |
| void WebDriverService::onBrowserTerminated(const String& sessionID) |
| { |
| if (m_session && m_session->id() == sessionID) { |
| auto connection = m_bidiServer.connection(sessionID); |
| m_bidiServer.disconnectSession(sessionID); |
| if (connection) |
| clientDisconnected(*connection); |
| } |
| } |
| |
| #endif // ENABLE(WEBDRIVER_BIDI) |
| |
| } // namespace WebDriver |