/* * 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 "SessionHost.h" #include #include #include 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) { } static void printUsageStatement(const char* programName) { printf("Usage: %s options\n", programName); printf(" -h, --help Prints this help message\n"); printf(" -p , --port= Port number the driver will use\n"); printf(" --host= Host IP the driver will use, or either 'local' or 'all' (default: 'local')\n"); #if USE(INSPECTOR_SOCKET_SERVER) printf(" -t --target= [WinCairo] Target IP and port\n"); #endif } int WebDriverService::run(int argc, char** argv) { String portString; std::optional host; #if USE(INSPECTOR_SOCKET_SERVER) String targetString; if (const char* targetEnvVar = getenv("WEBDRIVER_TARGET_ADDR")) targetString = targetEnvVar; #endif for (int i = 1 ; i < argc; ++i) { const char* arg = argv[i]; if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) { printUsageStatement(argv[0]); return EXIT_SUCCESS; } if (!strcmp(arg, "-p") && portString.isNull()) { if (++i == argc) { printUsageStatement(argv[0]); return EXIT_FAILURE; } portString = argv[i]; continue; } static const unsigned portStrLength = strlen("--port="); if (!strncmp(arg, "--port=", portStrLength) && portString.isNull()) { portString = String(arg + portStrLength); continue; } static const unsigned hostStrLength = strlen("--host="); if (!strncmp(arg, "--host=", hostStrLength) && !host) { host = String(arg + hostStrLength); continue; } #if USE(INSPECTOR_SOCKET_SERVER) if (!strcmp(arg, "-t") && targetString.isNull()) { if (++i == argc) { printUsageStatement(argv[0]); return EXIT_FAILURE; } targetString = argv[i]; continue; } static const unsigned targetStrLength = strlen("--target="); if (!strncmp(arg, "--target=", targetStrLength) && targetString.isNull()) { targetString = String(arg + targetStrLength); continue; } #endif } if (portString.isNull()) { printUsageStatement(argv[0]); return EXIT_FAILURE; } #if USE(INSPECTOR_SOCKET_SERVER) if (!targetString.isEmpty()) { auto position = targetString.reverseFind(":"_s); if (position != notFound) { m_targetAddress = targetString.left(position); m_targetPort = parseIntegerAllowingTrailingJunk(StringView { targetString }.substring(position + 1)).value_or(0); } } #endif auto port = parseInteger(portString); if (!port) { fprintf(stderr, "Invalid port %s provided\n", portString.utf8().data()); return EXIT_FAILURE; } WTF::initializeMainThread(); if (!m_server.listen(host, *port)) return EXIT_FAILURE; RunLoop::run(); 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::Get, "/session/$sessionId/element/active", &WebDriverService::getActiveElement }, { 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::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 }, }; std::optional WebDriverService::toCommandHTTPMethod(const String& method) { auto lowerCaseMethod = method.convertToASCIILowercase(); if (lowerCaseMethod == "get") return WebDriverService::HTTPMethod::Get; if (lowerCaseMethod == "post" || lowerCaseMethod == "put") return WebDriverService::HTTPMethod::Post; if (lowerCaseMethod == "delete") return WebDriverService::HTTPMethod::Delete; return std::nullopt; } bool WebDriverService::findCommand(HTTPMethod method, const String& path, CommandHandler* handler, HashMap& parameters) { size_t length = WTF_ARRAY_LENGTH(s_commands); for (size_t i = 0; i < length; ++i) { if (s_commands[i].method != method) continue; Vector pathTokens = path.split('/'); Vector 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&& replyHandler) { auto method = toCommandHTTPMethod(request.method); if (!method) { sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::UnknownCommand, String("Unknown method: " + request.method))); return; } CommandHandler handler; HashMap parameters; if (!findCommand(method.value(), request.path, &handler, parameters)) { sendResponse(WTFMove(replyHandler), CommandResult::fail(CommandResult::ErrorCode::UnknownCommand, String("Unknown command: " + request.path))); return; } RefPtr 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&& replyHandler, CommandResult&& result) const { // §6.3 Processing Model. // https://w3c.github.io/webdriver/webdriver-spec.html#processing-model RefPtr 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 }); } static std::optional valueAsNumberInRange(const JSON::Value& value, double minAllowed = 0, double maxAllowed = std::numeric_limits::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 unsignedValue(JSON::Value& value) { auto number = valueAsNumberInRange(value, 0, maxSafeInteger); if (!number) return std::nullopt; auto intValue = static_cast(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 { No, Yes }; static std::optional 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") continue; if (it->key == "script" && it->value->isNull()) { timeouts.script = std::numeric_limits::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") timeouts.script = timeoutMS.value(); else if (it->key == "pageLoad") timeouts.pageLoad = timeoutMS.value(); else if (it->key == "implicit") timeouts.implicit = timeoutMS.value(); else if (ignoreUnknownTimeout == IgnoreUnknownTimeout::No) return std::nullopt; } return timeouts; } static std::optional 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" || proxy.type == "autodetect" || proxy.type == "system") return proxy; if (proxy.type == "pac") { proxy.autoconfigURL = proxyObject.getString("proxyAutoconfigUrl"_s); if (!proxy.autoconfigURL) return std::nullopt; return proxy; } if (proxy.type == "manual") { if (auto value = proxyObject.getValue("ftpProxy"_s)) { auto ftpProxy = value->asString(); if (!ftpProxy) return std::nullopt; proxy.ftpURL = URL({ }, makeString("ftp://", 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://", 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://", sslProxy)); if (!proxy.httpsURL->isValid()) return std::nullopt; } if (auto value = proxyObject.getValue("socksProxy")) { auto socksProxy = value->asString(); if (!socksProxy) return std::nullopt; proxy.socksURL = URL({ }, makeString("socks://", socksProxy)); if (!proxy.socksURL->isValid()) return std::nullopt; auto socksVersionValue = proxyObject.getValue("socksVersion"); 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 deserializePageLoadStrategy(const String& pageLoadStrategy) { if (pageLoadStrategy == "none") return PageLoadStrategy::None; if (pageLoadStrategy == "normal") return PageLoadStrategy::Normal; if (pageLoadStrategy == "eager") return PageLoadStrategy::Eager; return std::nullopt; } static std::optional deserializeUnhandledPromptBehavior(const String& unhandledPromptBehavior) { if (unhandledPromptBehavior == "dismiss") return UnhandledPromptBehavior::Dismiss; if (unhandledPromptBehavior == "accept") return UnhandledPromptBehavior::Accept; if (unhandledPromptBehavior == "dismiss and notify") return UnhandledPromptBehavior::DismissAndNotify; if (unhandledPromptBehavior == "accept and notify") return UnhandledPromptBehavior::AcceptAndNotify; if (unhandledPromptBehavior == "ignore") 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); platformParseCapabilities(matchedCapabilities, capabilities); } bool WebDriverService::findSessionOrCompleteWithError(JSON::Object& parameters, Function& 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."))); return false; } return true; } RefPtr 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") { auto acceptInsecureCerts = it->value->asBoolean(); if (!acceptInsecureCerts) return nullptr; result->setBoolean(it->key, *acceptInsecureCerts); } else if (it->key == "browserName" || it->key == "browserVersion" || it->key == "platformName") { auto stringValue = it->value->asString(); if (!stringValue) return nullptr; result->setString(it->key, stringValue); } else if (it->key == "pageLoadStrategy") { auto pageLoadStrategy = it->value->asString(); if (!pageLoadStrategy || !deserializePageLoadStrategy(pageLoadStrategy)) return nullptr; result->setString(it->key, pageLoadStrategy); } else if (it->key == "proxy") { auto proxy = it->value->asObject(); if (!proxy || !deserializeProxy(*proxy)) return nullptr; result->setValue(it->key, *proxy); } else if (it->key == "strictFileInteractability") { auto strictFileInteractability = it->value->asBoolean(); if (!strictFileInteractability) return nullptr; result->setBoolean(it->key, *strictFileInteractability); } else if (it->key == "timeouts") { auto timeouts = it->value->asObject(); if (!timeouts || !deserializeTimeouts(*timeouts, IgnoreUnknownTimeout::No)) return nullptr; result->setValue(it->key, *timeouts); } else if (it->key == "unhandledPromptBehavior") { 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 return nullptr; } return result; } RefPtr 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 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" && platformCapabilities.browserName) { auto browserName = it->value->asString(); if (!equalIgnoringASCIICase(platformCapabilities.browserName.value(), browserName)) return nullptr; } else if (it->key == "browserVersion" && platformCapabilities.browserVersion) { auto browserVersion = it->value->asString(); if (!platformCompareBrowserVersions(browserVersion, platformCapabilities.browserVersion.value())) return nullptr; } else if (it->key == "platformName" && platformCapabilities.platformName) { auto platformName = it->value->asString(); if (!equalLettersIgnoringASCIICase(platformName, "any") && platformCapabilities.platformName.value() != platformName) return nullptr; } else if (it->key == "acceptInsecureCerts" && platformCapabilities.acceptInsecureCerts) { auto acceptInsecureCerts = it->value->asBoolean(); if (acceptInsecureCerts && !platformCapabilities.acceptInsecureCerts.value()) return nullptr; } else if (it->key == "proxy") { auto proxyType = it->value->asObject()->getString("proxyType"_s); if (!platformSupportProxyType(proxyType)) return nullptr; } else if (!platformMatchCapability(it->key, it->value)) return nullptr; matchedCapabilities->setValue(it->key, it->value.copyRef()); } return matchedCapabilities; } Vector WebDriverService::processCapabilities(const JSON::Object& parameters, Function& 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 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"))); 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"))); return { }; } // 3. Let all first match capabilities be the result of getting the property "firstMatch" from capabilities request. RefPtr 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"))); return { }; } } // 4. Let validated first match capabilities be an empty JSON List. Vector> 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"))); 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"))); 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 ", it->key, " is present in alwaysMatch"))); return { }; } } // 5.2. Append validated capabilities to validated first match capabilities. validatedFirstMatchCapabilitiesList.uncheckedAppend(WTFMove(firstMatchCapabilities)); } // 6. For each first match capabilities corresponding to an indexed property in validated first match capabilities. Vector 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.uncheckedAppend(WTFMove(capabilities)); } } if (matchedCapabilitiesList.isEmpty()) { completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Failed to match capabilities"))); return { }; } return matchedCapabilitiesList; } void WebDriverService::newSession(RefPtr&& parameters, Function&& completionHandler) { // §8.1 New Session. // https://www.w3.org/TR/webdriver/#new-session if (m_session) { completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Maximum number of active sessions"))); return; } auto matchedCapabilitiesList = processCapabilities(*parameters, completionHandler); if (matchedCapabilitiesList.isEmpty()) return; // Reverse the vector to always take last item. matchedCapabilitiesList.reverse(); connectToBrowser(WTFMove(matchedCapabilitiesList), WTFMove(completionHandler)); } void WebDriverService::connectToBrowser(Vector&& capabilitiesList, Function&& completionHandler) { if (capabilitiesList.isEmpty()) { completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, String("Failed to match capabilities"))); return; } auto sessionHost = makeUnique(capabilitiesList.takeLast()); auto* sessionHostPtr = sessionHost.get(); #if USE(INSPECTOR_SOCKET_SERVER) sessionHostPtr->setHostAddress(m_targetAddress, m_targetPort); #endif sessionHostPtr->connectToBrowser([this, capabilitiesList = WTFMove(capabilitiesList), sessionHost = WTFMove(sessionHost), completionHandler = WTFMove(completionHandler)](std::optional error) mutable { if (error) { completionHandler(CommandResult::fail(CommandResult::ErrorCode::SessionNotCreated, makeString("Failed to connect to browser: ", error.value()))); return; } createSession(WTFMove(capabilitiesList), WTFMove(sessionHost), WTFMove(completionHandler)); }); } void WebDriverService::createSession(Vector&& capabilitiesList, std::unique_ptr&& sessionHost, Function&& completionHandler) { auto* sessionHostPtr = sessionHost.get(); sessionHostPtr->startAutomationSession([this, capabilitiesList = WTFMove(capabilitiesList), sessionHost = WTFMove(sessionHost), completionHandler = WTFMove(completionHandler)](bool capabilitiesDidMatch, std::optional errorMessage) mutable { if (errorMessage) { completionHandler(CommandResult::fail(CommandResult::ErrorCode::UnknownError, errorMessage.value())); return; } if (!capabilitiesDidMatch) { connectToBrowser(WTFMove(capabilitiesList), WTFMove(completionHandler)); return; } RefPtr session = Session::create(WTFMove(sessionHost)); 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"); break; case UnhandledPromptBehavior::Accept: capabilitiesObject->setString("unhandledPromptBehavior"_s, "accept"); break; case UnhandledPromptBehavior::DismissAndNotify: capabilitiesObject->setString("unhandledPromptBehavior"_s, "dismiss and notify"); break; case UnhandledPromptBehavior::AcceptAndNotify: capabilitiesObject->setString("unhandledPromptBehavior"_s, "accept and notify"); break; case UnhandledPromptBehavior::Ignore: capabilitiesObject->setString("unhandledPromptBehavior"_s, "ignore"); break; } switch (capabilities.pageLoadStrategy.value_or(PageLoadStrategy::Normal)) { case PageLoadStrategy::None: capabilitiesObject->setString("pageLoadStrategy"_s, "none"); break; case PageLoadStrategy::Normal: capabilitiesObject->setString("pageLoadStrategy"_s, "normal"); break; case PageLoadStrategy::Eager: capabilitiesObject->setString("pageLoadStrategy"_s, "eager"); break; } if (!capabilities.proxy) capabilitiesObject->setObject("proxy"_s, JSON::Object::create()); auto timeoutsObject = JSON::Object::create(); if (m_session->scriptTimeout() == std::numeric_limits::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)); resultObject->setObject("capabilities"_s, WTFMove(capabilitiesObject)); completionHandler(CommandResult::success(WTFMove(resultObject))); }); }); } void WebDriverService::deleteSession(RefPtr&& parameters, Function&& 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([session, completionHandler = WTFMove(completionHandler)](CommandResult&& result) mutable { // Ignore unknown errors when closing the session if the browser is closed. if (result.isError() && result.errorCode() == CommandResult::ErrorCode::UnknownError && !session->isConnected()) completionHandler(CommandResult::success()); else completionHandler(WTFMove(result)); }); } void WebDriverService::status(RefPtr&&, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& completionHandler) { // §10.7.2 Set Window Rect. // https://w3c.github.io/webdriver/webdriver-spec.html#set-window-rect std::optional 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 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 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 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& completionHandler) { // §11.5 New Window // https://w3c.github.io/webdriver/#new-window if (!findSessionOrCompleteWithError(*parameters, completionHandler)) return; std::optional typeHint; if (auto value = parameters->getValue("type"_s)) { auto valueString = value->asString(); if (!!valueString) { if (valueString == "window" || valueString == "tab") typeHint = valueString; } else if (!value->isNull()) { completionHandler(CommandResult::fail(CommandResult::ErrorCode::InvalidArgument)); return; } } m_session->newWindow(typeHint, WTFMove(completionHandler)); } void WebDriverService::switchToFrame(RefPtr&& parameters, Function&& 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::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&& parameters, Function&& 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 findElementOrCompleteWithError(JSON::Object& parameters, Function& completionHandler) { auto elementID = parameters.getString("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" || strategy == "link text" || strategy == "partial link text" || strategy == "tag name" || strategy == "xpath"; } static bool findStrategyAndSelectorOrCompleteWithError(JSON::Object& parameters, Function& completionHandler, 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; } return true; } void WebDriverService::findElement(RefPtr&& parameters, Function&& 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, 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(), WTFMove(completionHandler)); }); } void WebDriverService::findElements(RefPtr&& parameters, Function&& 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, 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(), WTFMove(completionHandler)); }); } void WebDriverService::findElementFromElement(RefPtr&& parameters, Function&& 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, strategy, selector)) return; m_session->findElements(strategy, selector, Session::FindElementsMode::Single, elementID.value(), WTFMove(completionHandler)); } void WebDriverService::findElementsFromElement(RefPtr&& parameters, Function&& 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, strategy, selector)) return; m_session->findElements(strategy, selector, Session::FindElementsMode::Multiple, elementID.value(), WTFMove(completionHandler)); } void WebDriverService::getActiveElement(RefPtr&& parameters, Function&& 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::isElementSelected(RefPtr&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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::isElementDisplayed(RefPtr&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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& completionHandler, String& script, RefPtr& 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&& parameters, Function&& completionHandler) { // §15.2.1 Execute Script. // https://www.w3.org/TR/webdriver/#execute-script if (!findSessionOrCompleteWithError(*parameters, completionHandler)) return; String script; RefPtr 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&& parameters, Function&& completionHandler) { // §15.2.2 Execute Async Script. // https://www.w3.org/TR/webdriver/#execute-async-script if (!findSessionOrCompleteWithError(*parameters, completionHandler)) return; String script; RefPtr 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&& parameters, Function&& 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&& parameters, Function&& 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 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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& 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"); return false; } action.duration = duration.value(); return true; } static std::optional processNullAction(const String& id, JSON::Object& actionItem, std::optional& errorMessage) { auto subtype = actionItem.getString("type"_s); if (subtype != "pause") { errorMessage = String("The parameter 'type' in null action is invalid or missing"); 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 processKeyAction(const String& id, JSON::Object& actionItem, std::optional& errorMessage) { Action::Subtype actionSubtype; auto subtype = actionItem.getString("type"_s); if (subtype == "pause") actionSubtype = Action::Subtype::Pause; else if (subtype == "keyUp") actionSubtype = Action::Subtype::KeyUp; else if (subtype == "keyDown") actionSubtype = Action::Subtype::KeyDown; else { errorMessage = String("The parameter 'type' of key action is invalid"); 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"); return std::nullopt; } auto key = keyValue->asString(); if (key.isEmpty()) { errorMessage = String("The paramater 'value' is invalid for key up/down action"); 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& errorMessage) { if (auto durationValue = actionItem.getValue("duration"_s)) { auto duration = unsignedValue(*durationValue); if (!duration) { errorMessage = String("The parameter 'duration' is invalid in action"); 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"); return false; } action.origin = PointerOrigin { PointerOrigin::Type::Element, elementID }; } else { auto origin = originValue->asString(); if (origin == "viewport") action.origin = PointerOrigin { PointerOrigin::Type::Viewport, std::nullopt }; else if (origin == "pointer") action.origin = PointerOrigin { PointerOrigin::Type::Pointer, std::nullopt }; else { errorMessage = String("The parameter 'origin' is invalid in action"); 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"); 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"); return false; } action.y = y.value(); } return true; } static std::optional processPointerAction(const String& id, PointerParameters& parameters, JSON::Object& actionItem, std::optional& errorMessage) { Action::Subtype actionSubtype; auto subtype = actionItem.getString("type"_s); if (subtype == "pause") actionSubtype = Action::Subtype::Pause; else if (subtype == "pointerUp") actionSubtype = Action::Subtype::PointerUp; else if (subtype == "pointerDown") actionSubtype = Action::Subtype::PointerDown; else if (subtype == "pointerMove") actionSubtype = Action::Subtype::PointerMove; else if (subtype == "pointerCancel") actionSubtype = Action::Subtype::PointerCancel; else { errorMessage = String("The parameter 'type' of pointer action is invalid"); 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"); return std::nullopt; } auto button = unsignedValue(*buttonValue); if (!button) { errorMessage = String("The paramater 'button' is invalid for pointer up/down action"); 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 processWheelAction(const String& id, JSON::Object& actionItem, std::optional& errorMessage) { Action::Subtype actionSubtype; auto subtype = actionItem.getString("type"_s); if (subtype == "pause") actionSubtype = Action::Subtype::Pause; else if (subtype == "scroll") actionSubtype = Action::Subtype::Scroll; else { errorMessage = String("The parameter 'type' of wheel action is invalid"); 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"); 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"); 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 processPointerParameters(JSON::Object& actionSequence, std::optional& 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"); return std::nullopt; } auto pointerType = parametersData->getString("pointerType"_s); if (!pointerType) return parameters; if (pointerType == "mouse") parameters.pointerType = PointerType::Mouse; else if (pointerType == "pen") parameters.pointerType = PointerType::Pen; else if (pointerType == "touch") parameters.pointerType = PointerType::Touch; else { errorMessage = String("The parameter 'pointerType' in action sequence pointer parameters is invalid"); return std::nullopt; } return parameters; } static std::optional> processInputActionSequence(Session& session, JSON::Value& actionSequenceValue, std::optional& errorMessage) { auto actionSequence = actionSequenceValue.asObject(); if (!actionSequence) { errorMessage = String("The action sequence is not an object"); return std::nullopt; } auto type = actionSequence->getString("type"_s); InputSource::Type inputSourceType; if (type == "key") inputSourceType = InputSource::Type::Key; else if (type == "pointer") inputSourceType = InputSource::Type::Pointer; else if (type == "wheel") inputSourceType = InputSource::Type::Wheel; else if (type == "none") inputSourceType = InputSource::Type::None; else { errorMessage = String("The parameter 'type' is invalid or missing in action sequence"); return std::nullopt; } auto id = actionSequence->getString("id"_s); if (!id) { errorMessage = String("The parameter 'id' is invalid or missing in action sequence"); return std::nullopt; } std::optional parameters; std::optional 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"); 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"); return std::nullopt; } auto actionItems = actionSequence->getArray("actions"_s); if (!actionItems) { errorMessage = String("The parameter 'actions' is invalid or not present in action sequence"); return std::nullopt; } Vector 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"); return std::nullopt; } std::optional 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&& parameters, Function&& 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"))); return; } std::optional errorMessage; Vector> 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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&& parameters, Function&& 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)); }); } } // namespace WebDriver