blob: 53043b4968c197cfa88950e23e89499ea3f25761 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/session_commands.h"
#include <algorithm>
#include <list>
#include <memory>
#include <thread>
#include <utility>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/logging.h" // For CHECK macros.
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/waitable_event.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "chrome/test/chromedriver/basic_types.h"
#include "chrome/test/chromedriver/bidimapper/bidimapper.h"
#include "chrome/test/chromedriver/capabilities.h"
#include "chrome/test/chromedriver/chrome/bidi_tracker.h"
#include "chrome/test/chromedriver/chrome/browser_info.h"
#include "chrome/test/chromedriver/chrome/chrome.h"
#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
#include "chrome/test/chromedriver/chrome/chrome_impl.h"
#include "chrome/test/chromedriver/chrome/device_manager.h"
#include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
#include "chrome/test/chromedriver/chrome/devtools_event_listener.h"
#include "chrome/test/chromedriver/chrome/geoposition.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/chrome_launcher.h"
#include "chrome/test/chromedriver/command_listener.h"
#include "chrome/test/chromedriver/constants/version.h"
#include "chrome/test/chromedriver/logging.h"
#include "chrome/test/chromedriver/net/sync_websocket.h"
#include "chrome/test/chromedriver/net/sync_websocket_factory.h"
#include "chrome/test/chromedriver/session.h"
#include "chrome/test/chromedriver/util.h"
#include "services/device/public/cpp/generic_sensor/orientation_util.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
namespace {
const int kWifiMask = 0x2;
const int k4GMask = 0x8;
const int k3GMask = 0x10;
const int k2GMask = 0x20;
const int kAirplaneModeLatency = 0;
const int kAirplaneModeThroughput = 0;
const int kWifiLatency = 2;
const int kWifiThroughput = 30720 * 1024;
const int k4GLatency = 20;
const int k4GThroughput = 4096 * 1024;
const int k3GLatency = 100;
const int k3GThroughput = 750 * 1024;
const int k2GLatency = 300;
const int k2GThroughput = 250 * 1024;
Status EvaluateScriptAndIgnoreResult(Session* session,
std::string expression,
const bool await_promise = false) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError())
return status;
if (!web_view->IsServiceWorker() && web_view->IsDialogOpen()) {
std::string alert_text;
status = web_view->GetDialogMessage(alert_text);
if (status.IsError())
return Status(kUnexpectedAlertOpen);
return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}");
}
std::string frame_id = session->GetCurrentFrameId();
std::unique_ptr<base::Value> result;
return web_view->EvaluateScript(frame_id, expression, await_promise, &result);
}
} // namespace
InitSessionParams::InitSessionParams(
network::mojom::URLLoaderFactory* factory,
const SyncWebSocketFactory& socket_factory,
DeviceManager* device_manager,
const scoped_refptr<base::SingleThreadTaskRunner> cmd_task_runner,
TerminateSessionCallback terminate_on_cmd)
: url_loader_factory(factory),
socket_factory(socket_factory),
device_manager(device_manager),
cmd_task_runner(cmd_task_runner),
terminate_on_cmd(terminate_on_cmd) {}
InitSessionParams::InitSessionParams(const InitSessionParams& other) = default;
InitSessionParams::~InitSessionParams() = default;
// Look for W3C mode setting in InitSession command parameters.
bool GetW3CSetting(const base::Value::Dict& params) {
const base::Value::Dict* options_dict = nullptr;
const base::Value::Dict* caps_dict =
params.FindDictByDottedPath("capabilities.alwaysMatch");
if (caps_dict && GetChromeOptionsDictionary(*caps_dict, &options_dict)) {
std::optional<bool> w3c = options_dict->FindBool("w3c");
if (w3c.has_value())
return *w3c;
}
const base::Value::List* list =
params.FindListByDottedPath("capabilities.firstMatch");
if (list && list->size()) {
const base::Value& caps_dict_ref = (*list)[0];
if (caps_dict_ref.is_dict() &&
GetChromeOptionsDictionary(caps_dict_ref.GetDict(), &options_dict)) {
std::optional<bool> w3c = options_dict->FindBool("w3c");
if (w3c.has_value())
return *w3c;
}
}
caps_dict = params.FindDict("desiredCapabilities");
if (caps_dict && GetChromeOptionsDictionary(*caps_dict, &options_dict)) {
std::optional<bool> w3c = options_dict->FindBool("w3c");
if (w3c.has_value())
return *w3c;
}
if (!params.contains("capabilities") &&
params.contains("desiredCapabilities")) {
return false;
}
return kW3CDefault;
}
namespace {
std::string PlatformNameToW3C(const std::string& platform_name) {
std::string result = base::ToLowerASCII(platform_name);
if (base::StartsWith(result, "mac")) {
result = "mac";
}
return result;
}
// Creates a JSON object (represented by base::Value::Dict) that contains
// the capabilities, for returning to the client app as the result of New
// Session command.
base::Value::Dict CreateCapabilities(Session* session,
const Capabilities& capabilities,
const base::Value::Dict& desired_caps) {
base::Value::Dict caps;
// Capabilities defined by W3C. Some of these capabilities have different
// names in legacy mode.
caps.Set("browserName", session->chrome->GetBrowserInfo()->is_headless_shell
? kHeadlessShellCapabilityName
: kBrowserCapabilityName);
caps.Set(session->w3c_compliant ? "browserVersion" : "version",
session->chrome->GetBrowserInfo()->browser_version);
std::string os_name = session->chrome->GetOperatingSystemName();
if (os_name.find("Windows") != std::string::npos)
os_name = "Windows";
if (session->w3c_compliant) {
caps.Set("platformName", PlatformNameToW3C(os_name));
} else {
caps.Set("platform", os_name);
}
caps.Set("pageLoadStrategy", session->chrome->page_load_strategy());
caps.Set("acceptInsecureCerts", capabilities.accept_insecure_certs);
const base::Value* proxy = desired_caps.Find("proxy");
if (proxy == nullptr || proxy->is_none())
caps.Set("proxy", base::Value::Dict());
else
caps.Set("proxy", proxy->Clone());
// add setWindowRect based on whether we are desktop/android/remote
if (capabilities.IsAndroid() || capabilities.IsRemoteBrowser()) {
caps.Set("setWindowRect", false);
} else {
caps.Set("setWindowRect", true);
}
if (session->script_timeout == base::TimeDelta::Max()) {
caps.SetByDottedPath("timeouts.script", base::Value());
} else {
SetSafeInt(caps, "timeouts.script",
session->script_timeout.InMilliseconds());
}
SetSafeInt(caps, "timeouts.pageLoad",
session->page_load_timeout.InMilliseconds());
SetSafeInt(caps, "timeouts.implicit",
session->implicit_wait.InMilliseconds());
caps.Set("strictFileInteractability", session->strict_file_interactability);
caps.Set(session->w3c_compliant ? "unhandledPromptBehavior"
: "unexpectedAlertBehaviour",
session->unhandled_prompt_behavior.CapabilityView());
// Extensions defined by the W3C.
// See https://w3c.github.io/webauthn/#sctn-automation-webdriver-capability
caps.Set("webauthn:virtualAuthenticators", !capabilities.IsAndroid());
caps.Set("webauthn:extension:largeBlob", !capabilities.IsAndroid());
caps.Set("webauthn:extension:minPinLength", !capabilities.IsAndroid());
caps.Set("webauthn:extension:credBlob", !capabilities.IsAndroid());
caps.Set("webauthn:extension:prf", !capabilities.IsAndroid());
// See https://github.com/fedidcg/FedCM/pull/478
caps.Set("fedcm:accounts", true);
// Chrome-specific extensions.
const std::string chrome_driver_version_key = base::StringPrintf(
"%s.%sVersion", base::ToLowerASCII(kBrowserShortName).c_str(),
base::ToLowerASCII(kChromeDriverProductShortName).c_str());
caps.SetByDottedPath(chrome_driver_version_key, kChromeDriverVersion);
if (session->chrome->GetBrowserInfo()->debugger_endpoint.IsValid()) {
const std::string debugger_address_key = base::StringPrintf(
"%s.debuggerAddress", kChromeDriverOptionsKeyPrefixed);
caps.SetByDottedPath(debugger_address_key, session->chrome->GetBrowserInfo()
->debugger_endpoint.Address()
.ToString());
}
ChromeDesktopImpl* desktop = nullptr;
Status status = session->chrome->GetAsDesktop(&desktop);
if (status.IsOk()) {
const std::string user_data_key = base::StringPrintf(
"%s.userDataDir", base::ToLowerASCII(kBrowserShortName).c_str());
caps.SetByDottedPath(
user_data_key,
desktop->command().GetSwitchValuePath("user-data-dir").AsUTF8Unsafe());
caps.Set("networkConnectionEnabled", desktop->IsNetworkConnectionEnabled());
}
// Legacy capabilities.
if (!session->w3c_compliant) {
caps.Set("javascriptEnabled", true);
caps.Set("takesScreenshot", true);
caps.Set("takesHeapSnapshot", true);
caps.Set("handlesAlerts", true);
caps.Set("databaseEnabled", false);
caps.Set("locationContextEnabled", true);
caps.Set("mobileEmulationEnabled",
session->chrome->IsMobileEmulationEnabled());
caps.Set("browserConnectionEnabled", false);
caps.Set("cssSelectorsEnabled", true);
caps.Set("webStorageEnabled", true);
caps.Set("rotatable", false);
caps.Set("acceptSslCerts", capabilities.accept_insecure_certs);
caps.Set("nativeEvents", true);
caps.Set("hasTouchScreen", session->chrome->HasTouchScreen());
}
if (session->web_socket_url) {
caps.Set("webSocketUrl",
"ws://" + session->host + "/session/" + session->id);
}
return caps;
}
Status InitSessionHelper(const InitSessionParams& bound_params,
Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
session->cmd_task_runner = bound_params.cmd_task_runner;
session->terminate_on_cmd = base::BindPostTask(
session->cmd_task_runner,
base::BindOnce(bound_params.terminate_on_cmd, session->id), FROM_HERE);
if (!bound_params.device_manager) {
return Status{kSessionNotCreated, "device manager cannot be null"};
}
const base::Value::Dict* desired_caps;
base::Value::Dict merged_caps;
Capabilities capabilities;
Status status = internal::ConfigureSession(session, params, desired_caps,
merged_caps, &capabilities);
if (status.IsError())
return status;
// Create Log's and DevToolsEventListener's for ones that are DevTools-based.
// Session will own the Log's, Chrome will own the listeners.
// Also create |CommandListener|s for the appropriate logs.
std::vector<std::unique_ptr<DevToolsEventListener>> devtools_event_listeners;
std::vector<std::unique_ptr<CommandListener>> command_listeners;
status = CreateLogs(capabilities,
session,
&session->devtools_logs,
&devtools_event_listeners,
&command_listeners);
if (status.IsError())
return status;
// |session| will own the |CommandListener|s.
session->command_listeners.swap(command_listeners);
if (session->web_socket_url) {
// Suffixes used with the client channels.
BidiTracker* bidi_tracker = new BidiTracker();
bidi_tracker->SetChannelSuffix(std::move(Session::kChannelSuffix));
bidi_tracker->SetBidiCallback(base::BindRepeating(
&Session::OnBidiResponse, base::Unretained(session)));
devtools_event_listeners.emplace_back(bidi_tracker);
}
status = LaunchChrome(
bound_params.url_loader_factory, bound_params.socket_factory,
*bound_params.device_manager, capabilities,
std::move(devtools_event_listeners),
base::BindRepeating(&Session::HandleMessagesAndTerminateIfNecessary),
session->w3c_compliant, session->chrome);
if (status.IsError())
return status;
if (capabilities.accept_insecure_certs) {
status = session->chrome->SetAcceptInsecureCerts();
if (status.IsError())
return status;
}
status = session->chrome->GetWebViewIdForFirstTab(&session->window,
session->w3c_compliant);
if (status.IsError())
return status;
session->detach = capabilities.detach;
session->capabilities = std::make_unique<base::Value::Dict>(
CreateCapabilities(session, capabilities, *desired_caps));
status = internal::ConfigureHeadlessSession(session, capabilities);
if (status.IsError())
return status;
if (session->w3c_compliant) {
base::Value::Dict body;
body.Set("capabilities", session->capabilities->Clone());
body.Set("sessionId", session->id);
*value = std::make_unique<base::Value>(std::move(body));
} else {
*value = std::make_unique<base::Value>(session->capabilities->Clone());
}
if (session->web_socket_url) {
// The webview that will be visible to the user as the initial one.
WebView* initial_web_view = nullptr;
// The webview that will be used for running BiDi mapper. Can be either
// a hidden target or a visible tab.
WebView* bidi_mapper_web_view = nullptr;
// Get the currently open web view.
status = session->GetTargetWindow(&initial_web_view);
if (status.IsError()) {
return status;
}
// Navigate the initial page to `about:blank` to align with the html spec:
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-browsing-context
Timeout timeout(session->page_load_timeout);
status = initial_web_view->Load("about:blank", &timeout);
if (status.IsError()) {
return status;
}
// Wait until the currently open web view's navigation is over.
status = initial_web_view->WaitForPendingNavigations(
session->GetCurrentFrameId(), Timeout(session->page_load_timeout),
true);
if (status.IsError()) {
return status;
}
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
// Create a hidden target for running BiDi-CDP mapper in it.
status = session->chrome->NewHiddenTarget(
session->window, session->w3c_compliant,
&session->bidi_mapper_web_view_id);
if (status.IsError()) {
return status;
}
// Set the BiDi mapper web view to the new target.
session->chrome->GetWebViewById(session->bidi_mapper_web_view_id,
&bidi_mapper_web_view);
// Wait until the initial navigation is over to prevent the mapper from
// being evicted by the navigation.
status = bidi_mapper_web_view->WaitForPendingNavigations(
session->GetCurrentFrameId(), Timeout(session->page_load_timeout),
true);
if (status.IsError()) {
return status;
}
// Start the mapper.
base::FilePath bidi_mapper_path =
cmd_line->GetSwitchValuePath("bidi-mapper-path");
std::string mapper_script = kMapperScript;
if (!bidi_mapper_path.empty()) {
VLOG(0) << "Custom BiDi mapper path specified: " << bidi_mapper_path;
if (!base::ReadFileToString(bidi_mapper_path, &mapper_script)) {
return Status(StatusCode::kUnknownError,
"Failed to read the specified BiDi mapper path: " +
bidi_mapper_path.AsUTF8Unsafe());
}
}
bool enable_unsafe_extension_debugging =
capabilities.switches.HasSwitch("enable-unsafe-extension-debugging");
status = bidi_mapper_web_view->StartBidiServer(
mapper_script, enable_unsafe_extension_debugging);
if (status.IsError()) {
return status;
}
// Execute session.new for the newly-created mapper instance.
base::Value::Dict bidi_cmd;
bidi_cmd.Set("goog:channel", "/init-bidi-session");
bidi_cmd.Set("id", 1);
bidi_cmd.Set("params", params.Clone());
bidi_cmd.Set("method", "session.new");
base::Value::Dict bidi_response;
status = bidi_mapper_web_view->SendBidiCommand(
std::move(bidi_cmd), Timeout(base::Seconds(20)), bidi_response);
if (status.IsError()) {
return status;
}
} // if (session->web_socket_url)
return status;
}
} // namespace
namespace internal {
Status ConfigureSession(Session* session,
const base::Value::Dict& params,
const base::Value::Dict*& desired_caps,
base::Value::Dict& merged_caps,
Capabilities* capabilities) {
session->driver_log =
std::make_unique<WebDriverLog>(WebDriverLog::kDriverType, Log::kAll);
session->w3c_compliant = GetW3CSetting(params);
if (session->w3c_compliant) {
Status status = ProcessCapabilities(params, merged_caps);
if (status.IsError())
return status;
desired_caps = &merged_caps;
} else {
const base::Value::Dict* caps = params.FindDict("desiredCapabilities");
if (!caps)
return Status(kSessionNotCreated, "Missing or invalid capabilities");
desired_caps = caps;
}
Status status = capabilities->Parse(*desired_caps, session->w3c_compliant);
if (status.IsError())
return status;
if (capabilities->unhandled_prompt_behavior) {
session->unhandled_prompt_behavior =
std::move(capabilities->unhandled_prompt_behavior).value();
} else {
session->unhandled_prompt_behavior = PromptBehavior(session->w3c_compliant);
}
session->implicit_wait = capabilities->implicit_wait_timeout;
session->page_load_timeout = capabilities->page_load_timeout;
session->script_timeout = capabilities->script_timeout;
session->strict_file_interactability =
capabilities->strict_file_interactability;
session->web_socket_url = capabilities->web_socket_url;
Log::Level driver_level = Log::kWarning;
if (capabilities->logging_prefs.count(WebDriverLog::kDriverType))
driver_level = capabilities->logging_prefs[WebDriverLog::kDriverType];
session->driver_log->set_min_level(driver_level);
return Status(kOk);
}
Status ConfigureHeadlessSession(Session* session,
const Capabilities& capabilities) {
if (!session->chrome->GetBrowserInfo()->is_headless_shell) {
return Status(kOk);
}
const std::string* download_directory = nullptr;
if (capabilities.prefs) {
download_directory = capabilities.prefs->FindStringByDottedPath(
"download.default_directory");
if (!download_directory) {
download_directory =
capabilities.prefs->FindString("download.default_directory");
}
}
session->headless_download_directory = std::make_unique<std::string>(
download_directory ? *download_directory : ".");
WebView* first_view;
session->chrome->GetWebViewById(session->window, &first_view);
return first_view->OverrideDownloadDirectoryIfNeeded(
*session->headless_download_directory);
}
} // namespace internal
bool MergeCapabilities(const base::Value::Dict& always_match,
const base::Value::Dict& first_match,
base::Value::Dict& merged) {
merged.clear();
for (auto kv : first_match) {
if (always_match.Find(kv.first)) {
// `first_match` cannot have the same `keys` as `always_match`.
return false;
}
}
// merge the capabilities together since guaranteed no key collisions
merged = always_match.Clone();
merged.Merge(first_match.Clone());
return true;
}
// Implementation of "matching capabilities", as defined in W3C spec at
// https://www.w3.org/TR/webdriver/#dfn-matching-capabilities.
// It checks some requested capabilities and make sure they are supported.
// Currently, we only check "browserName", "platformName", "fedcm:accounts"
// and webauthn capabilities but more can be added as necessary.
bool MatchCapabilities(const base::Value::Dict& capabilities) {
const base::Value* name = capabilities.Find("browserName");
if (name && !name->is_none()) {
if (!name->is_string()) {
return false;
}
if (name->GetString() != kBrowserCapabilityName &&
name->GetString() != kHeadlessShellCapabilityName) {
return false;
}
}
const base::Value::Dict* chrome_options;
const bool has_chrome_options =
GetChromeOptionsDictionary(capabilities, &chrome_options);
bool is_android = has_chrome_options &&
chrome_options->FindString("androidPackage") != nullptr;
const base::Value* platform_name_value = capabilities.Find("platformName");
if (platform_name_value && !platform_name_value->is_none()) {
if (!platform_name_value->is_string())
return false;
std::string requested_platform_name = platform_name_value->GetString();
std::string requested_first_token =
requested_platform_name.substr(0, requested_platform_name.find(' '));
std::string actual_platform_name =
base::ToLowerASCII(base::SysInfo::OperatingSystemName());
std::string actual_first_token =
actual_platform_name.substr(0, actual_platform_name.find(' '));
bool is_remote = has_chrome_options &&
chrome_options->FindString("debuggerAddress") != nullptr;
if (requested_platform_name == "any" || is_remote ||
(is_android && requested_platform_name == "android")) {
// "any" can be used as a wild card for platformName.
// if |is_remote| there is no easy way to know
// target platform. Android check also occurs here.
// If any of the above cases pass, we return true.
} else if (is_android && requested_platform_name != "android") {
return false;
} else if (requested_first_token == "mac" ||
requested_first_token == "windows" ||
requested_first_token == "linux") {
if (actual_first_token != requested_first_token)
return false;
} else if (requested_platform_name != actual_platform_name) {
return false;
}
}
const base::Value* virtual_authenticators_value =
capabilities.Find("webauthn:virtualAuthenticators");
if (virtual_authenticators_value) {
if (!virtual_authenticators_value->is_bool() ||
(virtual_authenticators_value->GetBool() && is_android)) {
return false;
}
}
const base::Value* large_blob_value =
capabilities.Find("webauthn:extension:largeBlob");
if (large_blob_value) {
if (!large_blob_value->is_bool() ||
(large_blob_value->GetBool() && is_android)) {
return false;
}
}
const base::Value* fedcm_accounts_value = capabilities.Find("fedcm:accounts");
if (fedcm_accounts_value) {
if (!fedcm_accounts_value->is_bool() || !fedcm_accounts_value->GetBool()) {
return false;
}
}
return true;
}
// Implementation of "process capabilities", as defined in W3C spec at
// https://www.w3.org/TR/webdriver/#processing-capabilities. Step numbers in
// the comments correspond to the step numbers in the spec.
Status ProcessCapabilities(const base::Value::Dict& params,
base::Value::Dict& result_capabilities) {
// 1. Get the property "capabilities" from parameters.
const base::Value::Dict* capabilities_request =
params.FindDict("capabilities");
if (!capabilities_request)
return Status(kInvalidArgument, "'capabilities' must be a JSON object");
// 2. Get the property "alwaysMatch" from capabilities request.
const base::Value::Dict empty_object;
const base::Value::Dict* required_capabilities;
const base::Value* required_capabilities_value =
capabilities_request->Find("alwaysMatch");
if (required_capabilities_value == nullptr) {
required_capabilities = &empty_object;
} else if (required_capabilities_value->is_dict()) {
required_capabilities = &required_capabilities_value->GetDict();
Capabilities cap;
Status status = cap.Parse(*required_capabilities);
if (status.IsError())
return status;
} else {
return Status(kInvalidArgument, "'alwaysMatch' must be a JSON object");
}
// 3. Get the property "firstMatch" from capabilities request.
base::Value::List default_list;
const base::Value::List* all_first_match_capabilities;
const base::Value* all_first_match_capabilities_value =
capabilities_request->Find("firstMatch");
if (all_first_match_capabilities_value == nullptr) {
default_list.Append(base::Value::Dict());
all_first_match_capabilities = &default_list;
} else if (all_first_match_capabilities_value->is_list()) {
all_first_match_capabilities =
&all_first_match_capabilities_value->GetList();
if (all_first_match_capabilities->size() < 1) {
return Status(kInvalidArgument,
"'firstMatch' must contain at least one entry");
}
} else {
return Status(kInvalidArgument, "'firstMatch' must be a JSON list");
}
// 4. Let validated first match capabilities be an empty JSON List.
std::vector<const base::Value::Dict*> validated_first_match_capabilities;
// 5. Validate all first match capabilities.
for (size_t i = 0; i < all_first_match_capabilities->size(); ++i) {
const base::Value& first_match = (*all_first_match_capabilities)[i];
if (!first_match.is_dict()) {
return Status(kInvalidArgument,
base::StringPrintf(
"entry %zu of 'firstMatch' must be a JSON object", i));
}
Capabilities cap;
Status status = cap.Parse(first_match.GetDict());
if (status.IsError())
return Status(
kInvalidArgument,
base::StringPrintf("entry %zu of 'firstMatch' is invalid", i),
status);
validated_first_match_capabilities.push_back(&first_match.GetDict());
}
// 6. Let merged capabilities be an empty List.
std::vector<base::Value::Dict> merged_capabilities;
// 7. Merge capabilities.
for (size_t i = 0; i < validated_first_match_capabilities.size(); ++i) {
const base::Value::Dict* first_match_capabilities =
validated_first_match_capabilities[i];
base::Value::Dict merged;
if (!MergeCapabilities(*required_capabilities, *first_match_capabilities,
merged)) {
return Status(
kInvalidArgument,
base::StringPrintf(
"unable to merge 'alwaysMatch' with entry %zu of 'firstMatch'",
i));
}
merged_capabilities.emplace_back(std::move(merged));
}
// 8. Match capabilities.
for (auto& capabilities : merged_capabilities) {
if (MatchCapabilities(capabilities)) {
result_capabilities = std::move(capabilities);
return Status(kOk);
}
}
// 9. The spec says "return success with data null", but then the caller is
// instructed to return error when the data is null. Since we don't have a
// convenient way to return data null, we will take a shortcut and return an
// error directly.
return Status(kSessionNotCreated, "No matching capabilities found");
}
Status ExecuteInitSession(const InitSessionParams& bound_params,
Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
Status status = InitSessionHelper(bound_params, session, params, value);
if (status.IsError()) {
session->quit = true;
if (session->chrome != nullptr)
session->chrome->Quit();
return status;
}
return status;
}
Status ExecuteQuit(bool allow_detach,
Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
session->quit = true;
if (allow_detach && session->detach)
return Status(kOk);
return session->chrome->Quit();
}
// Quits a session.
Status ExecuteBidiSessionEnd(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
Status status{kOk};
WebView* web_view = nullptr;
status = session->chrome->GetWebViewById(session->bidi_mapper_web_view_id,
&web_view);
if (status.IsOk()) {
// Attempting to forward any pending BiDi responses / events.
status = web_view->HandleReceivedEvents();
}
if (status.IsError()) {
VLOG(0) << "Ignoring the error while shutting down a BiDi session: "
<< status.message();
}
session->quit = true;
status = session->chrome->Quit();
if (status.IsOk()) {
*value = std::make_unique<base::Value>(base::Value::Type::DICT);
}
return status;
}
Status ExecuteGetSessionCapabilities(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
*value = std::make_unique<base::Value>(session->capabilities->Clone());
return Status(kOk);
}
Status ExecuteGetCurrentWindowHandle(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(web_view->GetId());
return Status(kOk);
}
Status ExecuteClose(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::list<std::string> web_view_ids;
Status status = session->chrome->GetTopLevelWebViewIds(
&web_view_ids, session->w3c_compliant);
if (status.IsError())
return status;
bool is_last_web_view = web_view_ids.size() == 1u;
if (session->web_socket_url) {
is_last_web_view = web_view_ids.size() <= 2u;
}
web_view_ids.clear();
WebView* web_view = nullptr;
status = session->GetTargetWindow(&web_view);
if (status.IsError())
return status;
status = web_view->HandleReceivedEvents();
if (status.IsError())
return status;
if (web_view->IsDialogOpen()) {
std::string alert_text;
status = web_view->GetDialogMessage(alert_text);
if (status.IsError())
return status;
std::string dialog_type;
status = web_view->GetTypeOfDialog(dialog_type);
if (status.IsError()) {
return status;
}
PromptHandlerConfiguration prompt_handler_configuration;
status = session->unhandled_prompt_behavior.GetConfiguration(
dialog_type, prompt_handler_configuration);
if (status.IsError()) {
return status;
}
if (prompt_handler_configuration.type == PromptHandlerType::kAccept ||
prompt_handler_configuration.type == PromptHandlerType::kDismiss) {
status = web_view->HandleDialog(
prompt_handler_configuration.type == PromptHandlerType::kAccept,
session->prompt_text);
if (status.IsError()) {
return status;
}
}
if (prompt_handler_configuration.notify) {
return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}");
}
}
status = session->chrome->CloseWebView(web_view->GetTabId());
if (status.IsError())
return status;
if (is_last_web_view) {
// If there is only one window left, call quit as well.
session->quit = true;
status = session->chrome->Quit();
if (status.IsOk())
*value = std::make_unique<base::Value>(base::Value::Type::LIST);
} else {
status = ExecuteGetWindowHandles(session, base::Value::Dict(), value);
if (status.IsError())
return status;
}
return status;
}
Status ExecuteGetWindowHandles(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::list<std::string> tab_view_ids;
Status status = session->chrome->GetTopLevelWebViewIds(
&tab_view_ids, session->w3c_compliant);
if (status.IsError()) {
return status;
}
base::Value::List window_ids;
for (std::list<std::string>::const_iterator it = tab_view_ids.begin();
it != tab_view_ids.end(); ++it) {
WebView* page = nullptr;
status = session->chrome->GetActivePageByWebViewId(*it, &page,
/*wait_for_page=*/false);
if (status.IsError()) {
if (status.code() == kNoActivePage) {
continue;
}
return status;
}
CHECK(page != nullptr);
window_ids.Append(page->GetId());
}
if (session->web_socket_url) {
const std::string& (base::Value::*get_string)() const =
&base::Value::GetString;
auto it = std::ranges::find(window_ids, session->bidi_mapper_web_view_id,
get_string);
if (it != window_ids.end()) {
window_ids.erase(it);
}
}
*value = std::make_unique<base::Value>(std::move(window_ids));
return Status(kOk);
}
Status ExecuteSwitchToWindow(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
const std::string* name;
if (session->w3c_compliant) {
name = params.FindString("handle");
if (!name)
return Status(kInvalidArgument, "'handle' must be a string");
} else {
name = params.FindString("name");
if (!name)
return Status(kInvalidArgument, "'name' must be a string");
}
std::list<std::string> tab_view_ids;
Status status = session->chrome->GetTopLevelWebViewIds(
&tab_view_ids, session->w3c_compliant);
if (status.IsError())
return status;
// Find active web page view for each tab.
std::unordered_map<std::string, WebView*> tab_to_active_webview;
for (auto& tab_id : tab_view_ids) {
WebView* active_page = nullptr;
status = session->chrome->GetActivePageByWebViewId(tab_id, &active_page,
/*wait_for_page=*/false);
if (status.IsError()) {
if (status.code() == kNoActivePage) {
continue;
}
return status;
}
tab_to_active_webview[tab_id] = active_page;
}
WebView* web_view = nullptr;
std::string web_view_id;
bool found = false;
// Check if any tab_view or web_view matches |name|.
for (auto& it : tab_to_active_webview) {
if (it.first == *name || it.second->GetId() == *name) {
web_view = it.second;
web_view_id = it.first;
found = true;
break;
}
}
if (!found) {
// Check if any of the tab window names match |name|.
const char* kGetWindowNameScript = "function() { return window.name; }";
base::Value::List args;
for (auto& it : tab_to_active_webview) {
std::unique_ptr<base::Value> result;
status = it.second->CallFunction(std::string(), kGetWindowNameScript,
args, &result);
if (status.IsError())
return status;
if (!result->is_string())
return Status(kUnknownError, "failed to get window name");
if (result->GetString() == *name) {
web_view = it.second;
web_view_id = it.first;
found = true;
break;
}
}
}
if (!found)
return Status(kNoSuchWindow);
if (session->overridden_geoposition ||
session->overridden_network_conditions ||
session->headless_download_directory ||
session->chrome->IsMobileEmulationEnabled()) {
// apply type specific configurations:
if (session->overridden_geoposition) {
status = web_view->OverrideGeolocation(*session->overridden_geoposition);
if (status.IsError())
return status;
}
if (session->overridden_network_conditions) {
status = web_view->OverrideNetworkConditions(
*session->overridden_network_conditions);
if (status.IsError())
return status;
}
if (session->headless_download_directory) {
status = web_view->OverrideDownloadDirectoryIfNeeded(
*session->headless_download_directory);
if (status.IsError())
return status;
}
}
status = session->chrome->ActivateWebView(web_view_id);
if (status.IsError())
return status;
session->window = web_view_id;
session->SwitchToTopFrame();
session->mouse_position = WebPoint(0, 0);
return Status(kOk);
}
// Handles legacy format SetTimeout command.
// TODO(crbug.com/chromedriver/2596): Remove when we stop supporting legacy
// protocol.
Status ExecuteSetTimeoutLegacy(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::optional<double> maybe_ms = params.FindDouble("ms");
if (!maybe_ms.has_value())
return Status(kInvalidArgument, "'ms' must be a double");
const std::string* type = params.FindString("type");
if (!type)
return Status(kInvalidArgument, "'type' must be a string");
base::TimeDelta timeout =
base::Milliseconds(static_cast<int>(maybe_ms.value()));
if (*type == "implicit") {
session->implicit_wait = timeout;
} else if (*type == "script") {
session->script_timeout = timeout;
} else if (*type == "page load") {
session->page_load_timeout =
((timeout.is_negative()) ? Session::kDefaultPageLoadTimeout : timeout);
} else {
return Status(kInvalidArgument, "unknown type of timeout:" + *type);
}
return Status(kOk);
}
Status ExecuteSetTimeoutsW3C(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
for (auto setting : params) {
int64_t timeout_ms_int64 = -1;
base::TimeDelta timeout;
const std::string& type = setting.first;
if (setting.second.is_none()) {
if (type != "script")
return Status(kInvalidArgument, "timeout can not be null");
timeout = base::TimeDelta::Max();
} else {
if (!GetOptionalSafeInt(params, setting.first, &timeout_ms_int64) ||
timeout_ms_int64 < 0) {
return Status(kInvalidArgument, "value must be a non-negative integer");
}
timeout = base::Milliseconds(timeout_ms_int64);
}
if (type == "script") {
session->script_timeout = timeout;
} else if (type == "pageLoad") {
session->page_load_timeout = timeout;
} else if (type == "implicit") {
session->implicit_wait = timeout;
}
}
return Status(kOk);
}
Status ExecuteSetTimeouts(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
// TODO(crbug.com/chromedriver/2596): Remove legacy version support when we
// stop supporting non-W3C protocol. At that time, we can delete the legacy
// function and merge the W3C function into this function.
if (params.contains("ms"))
return ExecuteSetTimeoutLegacy(session, params, value);
return ExecuteSetTimeoutsW3C(session, params, value);
}
Status ExecuteGetTimeouts(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
base::Value::Dict timeouts;
if (session->script_timeout == base::TimeDelta::Max())
timeouts.Set("script", base::Value());
else
SetSafeInt(timeouts, "script", session->script_timeout.InMilliseconds());
SetSafeInt(timeouts, "pageLoad", session->page_load_timeout.InMilliseconds());
SetSafeInt(timeouts, "implicit", session->implicit_wait.InMilliseconds());
*value = base::Value::ToUniquePtrValue(base::Value(std::move(timeouts)));
return Status(kOk);
}
Status ExecuteSetScriptTimeout(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::optional<double> maybe_ms = params.FindDouble("ms");
if (!maybe_ms.has_value() || maybe_ms.value() < 0)
return Status(kInvalidArgument, "'ms' must be a non-negative number");
session->script_timeout =
base::Milliseconds(static_cast<int>(maybe_ms.value()));
return Status(kOk);
}
Status ExecuteImplicitlyWait(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::optional<double> maybe_ms = params.FindDouble("ms");
if (!maybe_ms.has_value() || maybe_ms.value() < 0)
return Status(kInvalidArgument, "'ms' must be a non-negative number");
session->implicit_wait =
base::Milliseconds(static_cast<int>(maybe_ms.value()));
return Status(kOk);
}
Status ExecuteIsLoading(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError())
return status;
bool is_pending;
status = web_view->IsPendingNavigation(nullptr, &is_pending);
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(is_pending);
return Status(kOk);
}
Status ExecuteCreateVirtualSensor(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
const std::string* type = params.FindString("type");
if (!type) {
return Status(kInvalidArgument, "'type' must be a string");
}
base::Value::Dict args;
args.Set("enabled", true);
args.Set("type", *type);
base::Value::Dict metadata;
metadata.Set("available", params.FindBool("connected").value_or(true));
if (auto minimum_sampling_frequency =
params.FindDouble("minSamplingFrequency");
minimum_sampling_frequency) {
metadata.Set("minimumFrequency", minimum_sampling_frequency.value());
}
if (auto maximum_sampling_frequency =
params.FindDouble("maxSamplingFrequency");
maximum_sampling_frequency) {
metadata.Set("maximumFrequency", maximum_sampling_frequency.value());
}
args.Set("metadata", std::move(metadata));
return web_view->SendCommand("Emulation.setSensorOverrideEnabled", args);
}
namespace {
bool ParseSingleValue(const std::string& key_name,
const base::Value::Dict& params,
base::Value::Dict* out_params) {
std::optional<double> value = params.FindDouble(key_name);
if (!value.has_value()) {
return false;
}
// Construct a dict that looks like this:
// {
// single: {
// value: VAL
// }
// }
out_params->Set("single", base::Value::Dict().Set("value", *value));
return true;
}
bool ParseXYZValue(const base::Value::Dict& params,
base::Value::Dict* out_params) {
std::optional<double> x = params.FindDouble("x");
if (!x.has_value()) {
return false;
}
std::optional<double> y = params.FindDouble("y");
if (!y.has_value()) {
return false;
}
std::optional<double> z = params.FindDouble("z");
if (!z.has_value()) {
return false;
}
// Construct a dict that looks like this:
// {
// xyz: {
// x: VAL1,
// y: VAL2,
// z: VAL3
// }
// }
out_params->Set("xyz",
base::Value::Dict().Set("x", *x).Set("y", *y).Set("z", *z));
return true;
}
bool ParseOrientationEuler(const base::Value::Dict& params,
base::Value::Dict* out_params) {
if (!params.contains("alpha") || !params.contains("beta") ||
!params.contains("gamma")) {
return false;
}
std::optional<double> alpha = params.FindDouble("alpha");
if (!alpha.has_value()) {
return false;
}
std::optional<double> beta = params.FindDouble("beta");
if (!beta.has_value()) {
return false;
}
std::optional<double> gamma = params.FindDouble("gamma");
if (!gamma.has_value()) {
return false;
}
device::SensorReading quaternion_readings;
if (!device::ComputeQuaternionFromEulerAngles(*alpha, *beta, *gamma,
&quaternion_readings)) {
return false;
}
// Construct a dict that looks like this:
// {
// quaternion: {
// x: VAL1,
// y: VAL2,
// z: VAL3,
// w: VAL4
// }
// }
const double x = quaternion_readings.orientation_quat.x;
const double y = quaternion_readings.orientation_quat.y;
const double z = quaternion_readings.orientation_quat.z;
const double w = quaternion_readings.orientation_quat.w;
out_params->Set(
"quaternion",
base::Value::Dict().Set("x", x).Set("y", y).Set("z", z).Set("w", w));
return true;
}
base::expected<base::Value::Dict, Status> ParseSensorUpdateParams(
const base::Value::Dict& params) {
base::Value::Dict cdp_params;
const std::string* type = params.FindString("type");
if (!type) {
return base::unexpected(
Status(kInvalidArgument, "'type' must be a string"));
}
cdp_params.Set("type", *type);
const base::Value::Dict* reading_dict = params.FindDict("reading");
if (!reading_dict) {
return base::unexpected(
Status(kInvalidArgument, "Missing 'reading' field"));
}
base::Value::Dict reading;
if (*type == "ambient-light") {
if (!ParseSingleValue("illuminance", *reading_dict, &reading)) {
return base::unexpected(
Status(kInvalidArgument, "Could not parse illuminance"));
}
} else if (*type == "accelerometer" || *type == "gravity" ||
*type == "gyroscope" || *type == "linear-acceleration" ||
*type == "magnetometer") {
if (!ParseXYZValue(*reading_dict, &reading)) {
return base::unexpected(
Status(kInvalidArgument, "Could not parse XYZ fields"));
}
} else if (*type == "absolute-orientation" ||
*type == "relative-orientation") {
if (!ParseOrientationEuler(*reading_dict, &reading)) {
return base::unexpected(Status(
kInvalidArgument, "Could not parse " + *type +
" readings. Invalid alpha/beta/gamma values"));
}
} else {
return base::unexpected(Status(
kInvalidArgument, "Unexpected type " + *type + " in 'type' field"));
}
cdp_params.Set("reading", std::move(reading));
return cdp_params;
}
} // namespace
Status ExecuteUpdateVirtualSensor(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>*) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
auto cdp_params = ParseSensorUpdateParams(params);
if (!cdp_params.has_value()) {
return cdp_params.error();
}
return web_view->SendCommand("Emulation.setSensorOverrideReadings",
cdp_params.value());
}
Status ExecuteRemoveVirtualSensor(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
const std::string* type = params.FindString("type");
if (!type) {
return Status(kInvalidArgument, "'type' must be a string");
}
base::Value::Dict args;
args.Set("enabled", false);
args.Set("type", *type);
return web_view->SendCommand("Emulation.setSensorOverrideEnabled", args);
}
Status ExecuteGetVirtualSensorInformation(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
const std::string* type = params.FindString("type");
if (!type) {
return Status(kInvalidArgument, "'type' must be a string");
}
base::Value::Dict args;
args.Set("type", *type);
return web_view->SendCommandAndGetResult(
"Emulation.getOverriddenSensorInformation", args, value);
}
Status ExecuteGetLocation(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
if (!session->overridden_geoposition) {
return Status(kUnknownError,
"Location must be set before it can be retrieved");
}
base::Value::Dict location;
location.Set("latitude", session->overridden_geoposition->latitude);
location.Set("longitude", session->overridden_geoposition->longitude);
location.Set("accuracy", session->overridden_geoposition->accuracy);
// Set a dummy altitude to make WebDriver clients happy.
// https://code.google.com/p/chromedriver/issues/detail?id=281
location.Set("altitude", 0);
*value = base::Value::ToUniquePtrValue(base::Value(std::move(location)));
return Status(kOk);
}
Status ExecuteGetNetworkConnection(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
ChromeDesktopImpl* desktop = nullptr;
Status status = session->chrome->GetAsDesktop(&desktop);
if (status.IsError())
return status;
if (!desktop->IsNetworkConnectionEnabled())
return Status(kUnknownError, "network connection must be enabled");
int connection_type = 0;
connection_type = desktop->GetNetworkConnection();
*value = std::make_unique<base::Value>(connection_type);
return Status(kOk);
}
Status ExecuteGetNetworkConditions(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
if (!session->overridden_network_conditions) {
return Status(kUnknownError,
"network conditions must be set before it can be retrieved");
}
base::Value::Dict conditions =
base::Value::Dict()
.Set("offline", session->overridden_network_conditions->offline)
.Set("latency", session->overridden_network_conditions->latency)
.Set("download_throughput",
session->overridden_network_conditions->download_throughput)
.Set("upload_throughput",
session->overridden_network_conditions->upload_throughput);
*value = base::Value::ToUniquePtrValue(base::Value(std::move(conditions)));
return Status(kOk);
}
Status ExecuteSetNetworkConnection(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
ChromeDesktopImpl* desktop = nullptr;
Status status = session->chrome->GetAsDesktop(&desktop);
if (status.IsError())
return status;
if (!desktop->IsNetworkConnectionEnabled())
return Status(kUnknownError, "network connection must be enabled");
std::optional<int> connection_type =
params.FindIntByDottedPath("parameters.type");
if (!connection_type)
return Status(kInvalidArgument, "invalid connection_type");
desktop->SetNetworkConnection(*connection_type);
std::unique_ptr<NetworkConditions> network_conditions(
new NetworkConditions());
if (*connection_type & kWifiMask) {
network_conditions->latency = kWifiLatency;
network_conditions->upload_throughput = kWifiThroughput;
network_conditions->download_throughput = kWifiThroughput;
network_conditions->offline = false;
} else if (*connection_type & k4GMask) {
network_conditions->latency = k4GLatency;
network_conditions->upload_throughput = k4GThroughput;
network_conditions->download_throughput = k4GThroughput;
network_conditions->offline = false;
} else if (*connection_type & k3GMask) {
network_conditions->latency = k3GLatency;
network_conditions->upload_throughput = k3GThroughput;
network_conditions->download_throughput = k3GThroughput;
network_conditions->offline = false;
} else if (*connection_type & k2GMask) {
network_conditions->latency = k2GLatency;
network_conditions->upload_throughput = k2GThroughput;
network_conditions->download_throughput = k2GThroughput;
network_conditions->offline = false;
} else {
network_conditions->latency = kAirplaneModeLatency;
network_conditions->upload_throughput = kAirplaneModeThroughput;
network_conditions->download_throughput = kAirplaneModeThroughput;
network_conditions->offline = true;
}
session->overridden_network_conditions.reset(
network_conditions.release());
// Applies overridden network connection to all WebViews of the session
// to ensure that network emulation is applied on a per-session basis
// rather than the just to the current WebView.
std::list<std::string> tab_view_ids;
status = session->chrome->GetTopLevelWebViewIds(&tab_view_ids,
session->w3c_compliant);
if (status.IsError())
return status;
for (std::string tab_view_id : tab_view_ids) {
WebView* web_view;
status = session->chrome->GetActivePageByWebViewId(tab_view_id, &web_view,
/*wait_for_page=*/false);
if (status.IsError()) {
if (status.code() == kNoActivePage) {
continue;
}
return status;
}
web_view->OverrideNetworkConditions(
*session->overridden_network_conditions);
}
*value = std::make_unique<base::Value>(*connection_type);
return Status(kOk);
}
Status ExecuteGetWindowPosition(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
Chrome::WindowRect window_rect;
Status status = session->chrome->GetWindowRect(session->window, &window_rect);
if (status.IsError())
return status;
base::Value::Dict position =
base::Value::Dict().Set("x", window_rect.x).Set("y", window_rect.y);
*value = base::Value::ToUniquePtrValue(base::Value(std::move(position)));
return Status(kOk);
}
Status ExecuteSetWindowPosition(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::optional<double> maybe_x = params.FindDouble("x");
std::optional<double> maybe_y = params.FindDouble("y");
if (!maybe_x.has_value() || !maybe_y.has_value())
return Status(kInvalidArgument, "missing or invalid 'x' or 'y'");
base::Value::Dict rect_params;
rect_params.Set("x", static_cast<int>(maybe_x.value()));
rect_params.Set("y", static_cast<int>(maybe_y.value()));
return session->chrome->SetWindowRect(session->window,
std::move(rect_params));
}
Status ExecuteGetWindowSize(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
Chrome::WindowRect window_rect;
Status status = session->chrome->GetWindowRect(session->window, &window_rect);
if (status.IsError())
return status;
base::Value::Dict size = base::Value::Dict()
.Set("width", window_rect.width)
.Set("height", window_rect.height);
*value = base::Value::ToUniquePtrValue(base::Value(std::move(size)));
return Status(kOk);
}
Status ExecuteSetWindowSize(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::optional<double> maybe_width = params.FindDouble("width");
std::optional<double> maybe_height = params.FindDouble("height");
if (!maybe_width.has_value() || !maybe_height.has_value())
return Status(kInvalidArgument, "missing or invalid 'width' or 'height'");
base::Value::Dict rect_params;
rect_params.Set("width", static_cast<int>(maybe_width.value()));
rect_params.Set("height", static_cast<int>(maybe_height.value()));
return session->chrome->SetWindowRect(session->window,
std::move(rect_params));
}
Status ExecuteGetAvailableLogTypes(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
std::unique_ptr<base::Value::List> types(new base::Value::List());
std::vector<WebDriverLog*> logs = session->GetAllLogs();
for (std::vector<WebDriverLog*>::const_iterator log = logs.begin();
log != logs.end();
++log) {
types->Append((*log)->type());
}
*value = std::make_unique<base::Value>(base::Value(std::move(*types)));
return Status(kOk);
}
Status ExecuteGetLog(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
const std::string* log_type = params.FindString("type");
if (!log_type) {
return Status(kInvalidArgument, "missing or invalid 'type'");
}
// Evaluate a JavaScript in the renderer process for the current tab, to flush
// out any pending logging-related events.
Status status = EvaluateScriptAndIgnoreResult(session, "1");
if (status.IsError()) {
// Sometimes a WebDriver client fetches logs to diagnose an error that has
// occurred. It's possible that in the case of an error, the renderer is no
// be longer available, but we should return the logs anyway. So log (but
// don't fail on) any error that we get while evaluating the script.
LOG(WARNING) << "Unable to evaluate script: " << status.message();
}
std::vector<WebDriverLog*> logs = session->GetAllLogs();
for (std::vector<WebDriverLog*>::const_iterator log = logs.begin();
log != logs.end();
++log) {
if (*log_type == (*log)->type()) {
*value = base::Value::ToUniquePtrValue(
base::Value((*log)->GetAndClearEntries()));
return Status(kOk);
}
}
return Status(kInvalidArgument, "log type '" + *log_type + "' not found");
}
Status ExecuteUploadFile(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
const std::string* base64_zip_data = params.FindString("file");
if (!base64_zip_data)
return Status(kInvalidArgument, "missing or invalid 'file'");
std::string zip_data;
if (!Base64Decode(*base64_zip_data, &zip_data))
return Status(kUnknownError, "unable to decode 'file'");
if (!session->temp_dir.IsValid()) {
if (!session->temp_dir.CreateUniqueTempDir())
return Status(kUnknownError, "unable to create temp dir");
}
base::FilePath upload_dir;
if (!base::CreateTemporaryDirInDir(session->temp_dir.GetPath(),
FILE_PATH_LITERAL("upload"),
&upload_dir)) {
return Status(kUnknownError, "unable to create temp dir");
}
std::string error_msg;
base::FilePath upload;
Status status = UnzipSoleFile(upload_dir, zip_data, &upload);
if (status.IsError())
return Status(kUnknownError, "unable to unzip 'file'", status);
*value = std::make_unique<base::Value>(upload.AsUTF8Unsafe());
return Status(kOk);
}
Status ExecuteSetSPCTransactionMode(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError())
return status;
const std::string* mode = params.FindString("mode");
if (!mode)
return Status(kInvalidArgument, "missing parameter 'mode'");
base::Value::Dict body;
body.Set("mode", *mode);
return web_view->SendCommandAndGetResult("Page.setSPCTransactionMode", body,
value);
}
Status ExecuteGenerateTestReport(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError())
return status;
const std::string* message = params.FindString("message");
if (!message)
return Status(kInvalidArgument, "missing parameter 'message'");
const std::string* group = params.FindString("group");
base::Value::Dict body;
body.Set("message", *message);
body.Set("group", group ? *group : "default");
web_view->SendCommandAndGetResult("Page.generateTestReport", body, value);
return Status(kOk);
}
Status ExecuteSetTimeZone(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError())
return status;
const std::string* time_zone = params.FindString("time_zone");
if (!time_zone)
return Status(kInvalidArgument, "missing parameter 'time_zone'");
base::Value::Dict body;
body.Set("timezoneId", *time_zone);
web_view->SendCommandAndGetResult("Emulation.setTimezoneOverride", body,
value);
return Status(kOk);
}
Status ExecuteCreateVirtualPressureSource(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
const std::string* type = params.FindString("type");
if (!type) {
return Status(kInvalidArgument, "'type' must be a string");
}
base::Value::Dict body;
body.Set("enabled", true);
body.Set("source", *type);
base::Value::Dict metadata;
metadata.Set("available", true);
if (params.contains("supported")) {
auto supported = params.FindBool("supported");
if (!supported.has_value()) {
return Status(kInvalidArgument, "'supported' must be a boolean");
}
metadata.Set("available", *supported);
}
body.Set("metadata", std::move(metadata));
return web_view->SendCommand("Emulation.setPressureSourceOverrideEnabled",
body);
}
Status ExecuteUpdateVirtualPressureSource(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
const std::string* type = params.FindString("type");
if (!type) {
return Status(kInvalidArgument, "'type' must be a string");
}
const std::string* state = params.FindString("sample");
if (!state) {
return Status(kInvalidArgument, "'sample' must be a string");
}
base::Value::Dict body;
body.Set("source", *type);
body.Set("state", *state);
std::optional<double> maybe_estimate =
params.FindDouble("own_contribution_estimate");
if (!maybe_estimate.has_value()) {
body.Set("ownContributionEstimate", -1.0);
} else {
body.Set("ownContributionEstimate", maybe_estimate.value());
}
return web_view->SendCommand("Emulation.setPressureDataOverride", body);
}
Status ExecuteRemoveVirtualPressureSource(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
const std::string* type = params.FindString("type");
if (!type) {
return Status(kInvalidArgument, "'type' must be a string");
}
base::Value::Dict body;
body.Set("enabled", false);
body.Set("source", *type);
return web_view->SendCommand("Emulation.setPressureSourceOverrideEnabled",
body);
}
Status ExecuteSetProtectedAudienceKAnonymity(
Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
WebView* web_view = nullptr;
Status status = session->GetTargetWindow(&web_view);
if (status.IsError()) {
return status;
}
const std::string* owner = params.FindString("owner");
if (!owner) {
return Status(kInvalidArgument, "missing parameter 'owner'");
}
const std::string* name = params.FindString("name");
if (!name) {
return Status(kInvalidArgument, "missing parameter 'name'");
}
const base::Value::List* hashes = params.FindList("hashes");
if (!hashes) {
return Status(kInvalidArgument, "missing parameter 'hashes'");
}
for (const auto& hash : *hashes) {
if (!hash.is_string()) {
return Status(kInvalidArgument, "hashes should be base64 strings");
}
}
base::Value::Dict body;
body.Set("owner", *owner);
body.Set("name", *name);
body.Set("hashes", base::Value(hashes->Clone()));
return web_view->SendCommand("Storage.setProtectedAudienceKAnonymity", body);
}
// Run a BiDi command
Status ForwardBidiCommand(Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
// session == nullptr is a valid case: ExecuteQuit has already been handled
// in the session thread but the following
// TerminateSessionThreadOnCommandThread has not yet been executed (the later
// destroys the session thread) The connection has already been accepted by
// the CMD thread but soon it will be closed. We don't need to do anything.
if (session == nullptr) {
return Status{kInvalidArgument, "session not found"};
}
const base::Value::Dict* data = params.FindDict("bidiCommand");
if (!data) {
return Status{kUnknownError, "bidiCommand is missing in params"};
}
std::optional<int> connection_id = params.FindInt("connectionId");
if (!connection_id) {
return Status{kUnknownCommand, "connectionId is missing in params"};
}
WebView* web_view = nullptr;
Status status = session->chrome->GetActivePageByWebViewId(
session->bidi_mapper_web_view_id, &web_view, /*wait_for_page=*/false);
if (status.IsError()) {
return status;
}
base::Value::Dict bidi_cmd = data->Clone();
std::string* user_channel = bidi_cmd.FindString("goog:channel");
std::string channel = (user_channel ? *user_channel : "") + "/" +
base::NumberToString(*connection_id) +
Session::kChannelSuffix;
bidi_cmd.Set("goog:channel", std::move(channel));
status = web_view->PostBidiCommand(std::move(bidi_cmd));
return status;
}