blob: f356c66bf13bcfa8e14060ad1d9cba9e2fe6e1b3 [file] [log] [blame] [edit]
// 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/window_commands.h"
#include <stddef.h>
#include <algorithm>
#include <list>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/adapters.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/test/chromedriver/basic_types.h"
#include "chrome/test/chromedriver/chrome/chrome.h"
#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/geoposition.h"
#include "chrome/test/chromedriver/chrome/mobile_emulation_override_manager.h"
#include "chrome/test/chromedriver/chrome/network_conditions.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/ui_events.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/element_commands.h"
#include "chrome/test/chromedriver/element_util.h"
#include "chrome/test/chromedriver/key_converter.h"
#include "chrome/test/chromedriver/net/command_id.h"
#include "chrome/test/chromedriver/net/timeout.h"
#include "chrome/test/chromedriver/session.h"
#include "chrome/test/chromedriver/util.h"
#include "ui/gfx/geometry/point.h"
#include "url/url_util.h"
namespace {
// The error page URL was renamed in
// https://chromium-review.googlesource.com/c/580169, but because ChromeDriver
// needs to be backward-compatible with older versions of Chrome, it is
// necessary to compare against both the old and new error URL.
static const char kUnreachableWebDataURL[] = "chrome-error://chromewebdata/";
const char kDeprecatedUnreachableWebDataURL[] = "data:text/html,chromewebdata";
// Match to content/browser/devtools/devTools_session const of same name
const char kTargetClosedMessage[] = "Inspected target navigated or closed";
// TODO(crbug.com/chromedriver/2596): Remove when we stop supporting legacy
// protocol.
// Defaults to 20 years into the future when adding a cookie.
const double kDefaultCookieExpiryTime = 20*365*24*60*60;
// for pointer actions
enum class PointerActionType { NOT_INITIALIZED, PRESS, MOVE, RELEASE, IDLE };
const base::flat_set<StatusCode> kNavigationHints = {
kNoSuchExecutionContext,
kAbortedByNavigation,
};
Status GetMouseButton(const base::Value::Dict& params, MouseButton* button) {
// Default to left mouse button.
int button_num = params.FindInt("button").value_or(0);
if (button_num < 0 || button_num > 2) {
return Status(kInvalidArgument,
base::StringPrintf("invalid button: %d", button_num));
}
*button = static_cast<MouseButton>(button_num);
return Status(kOk);
}
Status IntToStringButton(int button, std::string& out) {
if (button == 0) {
out = "left";
} else if (button == 1) {
out = "middle";
} else if (button == 2) {
out = "right";
} else if (button == 3) {
out = "back";
} else if (button == 4) {
out = "forward";
} else {
return Status(kInvalidArgument,
"'button' must be an integer between 0 and 4 inclusive");
}
return Status(kOk);
}
Status GetUrl(WebView* web_view, const std::string& frame, std::string* url) {
std::unique_ptr<base::Value> value;
base::Value::List args;
Status status = web_view->CallFunction(
frame, "function() { return document.URL; }", args, &value);
if (status.IsError())
return status;
if (!value->is_string())
return Status(kUnknownError, "javascript failed to return the url");
*url = value->GetString();
return Status(kOk);
}
MouseEventType StringToMouseEventType(std::string action_type) {
if (action_type == "pointerDown")
return kPressedMouseEventType;
if (action_type == "pointerUp")
return kReleasedMouseEventType;
if (action_type == "pointerMove")
return kMovedMouseEventType;
if (action_type == "scroll")
return kWheelMouseEventType;
if (action_type == "pause")
return kPauseMouseEventType;
return kPressedMouseEventType;
}
MouseButton StringToMouseButton(std::string button_type) {
if (button_type == "left")
return kLeftMouseButton;
if (button_type == "middle")
return kMiddleMouseButton;
if (button_type == "right")
return kRightMouseButton;
if (button_type == "back")
return kBackMouseButton;
if (button_type == "forward")
return kForwardMouseButton;
return kNoneMouseButton;
}
TouchEventType StringToTouchEventType(std::string action_type) {
if (action_type == "pointerDown")
return kTouchStart;
if (action_type == "pointerUp")
return kTouchEnd;
if (action_type == "pointerMove")
return kTouchMove;
if (action_type == "pointerCancel")
return kTouchCancel;
if (action_type == "pause")
return kPause;
return kTouchStart;
}
int StringToModifierMouseButton(std::string button_type) {
if (button_type == "left")
return 1;
if (button_type == "right")
return 2;
if (button_type == "middle")
return 4;
if (button_type == "back")
return 8;
if (button_type == "forward")
return 16;
return 0;
}
int MouseButtonToButtons(MouseButton button) {
switch (button) {
case kLeftMouseButton:
return 1;
case kRightMouseButton:
return 2;
case kMiddleMouseButton:
return 4;
case kBackMouseButton:
return 8;
case kForwardMouseButton:
return 16;
default:
return 0;
}
}
int KeyToKeyModifiers(std::string key) {
if (key == "Shift")
return kShiftKeyModifierMask;
if (key == "Control")
return kControlKeyModifierMask;
if (key == "Alt")
return kAltKeyModifierMask;
if (key == "Meta")
return kMetaKeyModifierMask;
return 0;
}
PointerType StringToPointerType(std::string pointer_type) {
CHECK(pointer_type == "pen" || pointer_type == "mouse");
if (pointer_type == "pen")
return kPen;
return kMouse;
}
struct Cookie {
Cookie(const std::string& name,
const std::string& value,
const std::string& domain,
const std::string& path,
const std::string& samesite,
int64_t expiry,
bool http_only,
bool secure,
bool session)
: name(name),
value(value),
domain(domain),
path(path),
samesite(samesite),
expiry(expiry),
http_only(http_only),
secure(secure),
session(session) {}
std::string name;
std::string value;
std::string domain;
std::string path;
std::string samesite;
int64_t expiry;
bool http_only;
bool secure;
bool session;
};
base::Value::Dict CreateDictionaryFrom(const Cookie& cookie) {
base::Value::Dict dict;
dict.Set("name", cookie.name);
dict.Set("value", cookie.value);
if (!cookie.domain.empty())
dict.Set("domain", cookie.domain);
if (!cookie.path.empty())
dict.Set("path", cookie.path);
if (!cookie.session)
SetSafeInt(dict, "expiry", cookie.expiry);
dict.Set("httpOnly", cookie.http_only);
dict.Set("secure", cookie.secure);
if (!cookie.samesite.empty()) {
dict.Set("sameSite", cookie.samesite);
} else {
// The default in the standard is Lax:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
// Chrome (mostly) treats default cookies as Lax so this seems correct:
// https://chromestatus.com/feature/5088147346030592
dict.Set("sameSite", "Lax");
}
return dict;
}
Status GetVisibleCookies(Session* for_session,
WebView* web_view,
std::list<Cookie>* cookies) {
std::string current_page_url;
Status status =
GetUrl(web_view, for_session->GetCurrentFrameId(), &current_page_url);
if (status.IsError())
return status;
base::Value internal_cookies;
status = web_view->GetCookies(&internal_cookies, current_page_url);
if (status.IsError())
return status;
std::list<Cookie> cookies_tmp;
for (const base::Value& cookie_value : internal_cookies.GetList()) {
if (!cookie_value.is_dict())
return Status(kUnknownError, "DevTools returns a non-dictionary cookie");
const base::Value::Dict& cookie_dict = cookie_value.GetDict();
const std::string* name = cookie_dict.FindString("name");
const std::string* value = cookie_dict.FindString("value");
const std::string* domain = cookie_dict.FindString("domain");
const std::string* path = cookie_dict.FindString("path");
std::string samesite;
GetOptionalString(cookie_dict, "sameSite", &samesite);
int64_t expiry =
static_cast<int64_t>(cookie_dict.FindDouble("expires").value_or(0));
// Truncate & convert the value to an integer as required by W3C spec.
if (expiry >= (1ll << 53) || expiry <= -(1ll << 53))
expiry = 0;
bool http_only = cookie_dict.FindBool("httpOnly").value_or(false);
bool session = cookie_dict.FindBool("session").value_or(false);
bool secure = cookie_dict.FindBool("secure").value_or(false);
cookies_tmp.push_back(Cookie(*name, *value, *domain, *path, samesite,
expiry, http_only, secure, session));
}
cookies->swap(cookies_tmp);
return Status(kOk);
}
Status ScrollCoordinateInToView(
Session* session, WebView* web_view, int x, int y, int* offset_x,
int* offset_y) {
std::unique_ptr<base::Value> value;
base::Value::List args;
args.Append(x);
args.Append(y);
Status status = web_view->CallFunction(
std::string(),
"function(x, y) {"
" if (x < window.pageXOffset ||"
" x >= window.pageXOffset + window.innerWidth ||"
" y < window.pageYOffset ||"
" y >= window.pageYOffset + window.innerHeight) {"
" window.scrollTo(x - window.innerWidth/2, y - window.innerHeight/2);"
" }"
" return {"
" view_x: Math.floor(window.pageXOffset),"
" view_y: Math.floor(window.pageYOffset),"
" view_width: Math.floor(window.innerWidth),"
" view_height: Math.floor(window.innerHeight)};"
"}",
args,
&value);
if (!status.IsOk())
return status;
const base::Value::Dict& view_attrib = value->GetDict();
int view_x = view_attrib.FindInt("view_x").value_or(0);
int view_y = view_attrib.FindInt("view_y").value_or(0);
int view_width = view_attrib.FindInt("view_width").value_or(0);
int view_height = view_attrib.FindInt("view_height").value_or(0);
*offset_x = x - view_x;
*offset_y = y - view_y;
if (*offset_x < 0 || *offset_x >= view_width || *offset_y < 0 ||
*offset_y >= view_height) {
return Status(kUnknownError, "Failed to scroll coordinate into view");
}
return Status(kOk);
}
Status ExecuteTouchEvent(Session* session,
WebView* web_view,
TouchEventType type,
const base::Value::Dict& params) {
std::optional<int> x = params.FindInt("x");
std::optional<int> y = params.FindInt("y");
if (!x)
return Status(kInvalidArgument, "'x' must be an integer");
if (!y)
return Status(kInvalidArgument, "'y' must be an integer");
int relative_x = *x;
int relative_y = *y;
Status status = ScrollCoordinateInToView(session, web_view, *x, *y,
&relative_x, &relative_y);
if (!status.IsOk())
return status;
std::vector<TouchEvent> events;
events.emplace_back(type, relative_x, relative_y);
return web_view->DispatchTouchEvents(events, false);
}
Status WindowViewportSize(Session* session,
WebView* web_view,
int* inner_width,
int* inner_height) {
DCHECK(inner_width);
DCHECK(inner_height);
std::unique_ptr<base::Value> value;
base::Value::List args;
Status status =
web_view->CallFunction(std::string(),
"function() {"
" return {"
" view_width: Math.floor(window.innerWidth),"
" view_height: Math.floor(window.innerHeight)};"
"}",
args, &value);
if (!status.IsOk())
return status;
const base::Value::Dict& view_attrib = value->GetDict();
std::optional<int> maybe_inner_width = view_attrib.FindInt("view_width");
if (maybe_inner_width)
*inner_width = *maybe_inner_width;
std::optional<int> maybe_inner_height = view_attrib.FindInt("view_height");
if (maybe_inner_height)
*inner_height = *maybe_inner_height;
return Status(kOk);
}
Status ProcessPauseAction(const base::Value::Dict& action_item,
base::Value::Dict* action) {
int duration = 0;
bool has_value = false;
if (!GetOptionalInt(action_item, "duration", &duration, &has_value) ||
duration < 0)
return Status(kInvalidArgument, "'duration' must be a non-negative int");
if (has_value)
action->Set("duration", duration);
return Status(kOk);
}
Status ElementInViewCenter(Session* session,
WebView* web_view,
std::string element_id,
int* center_x,
int* center_y) {
WebPoint center_location;
Status status = GetElementLocationInViewCenter(session, web_view, element_id,
&center_location);
if (status.IsError())
return status;
*center_x = center_location.x;
*center_y = center_location.y;
return Status(kOk);
}
int GetMouseClickCount(int last_click_count,
float x,
float y,
float last_x,
float last_y,
int button_id,
int last_button_id,
const base::TimeTicks& timestamp,
const base::TimeTicks& last_mouse_click_time) {
const int kDoubleClickTimeMS = 500;
const int kDoubleClickRange = 4;
if (last_click_count == 0)
return 1;
base::TimeDelta time_difference = timestamp - last_mouse_click_time;
if (time_difference.InMilliseconds() > kDoubleClickTimeMS)
return 1;
if (std::abs(x - last_x) > kDoubleClickRange / 2)
return 1;
if (std::abs(y - last_y) > kDoubleClickRange / 2)
return 1;
if (last_button_id != button_id)
return 1;
#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_WIN)
// On Mac and Windows, we keep increasing the click count, but on the other
// platforms, we reset the count to 1 when it is greater than 3.
if (last_click_count >= 3)
return 1;
#endif
return last_click_count + 1;
}
const char kLandscape[] = "landscape";
const char kPortrait[] = "portrait";
Status ParseOrientation(const base::Value::Dict& params,
std::string* orientation) {
bool has_value;
if (!GetOptionalString(params, "orientation", orientation, &has_value)) {
return Status(kInvalidArgument, "'orientation' must be a string");
}
if (!has_value) {
*orientation = kPortrait;
} else if (*orientation != kPortrait && *orientation != kLandscape) {
return Status(kInvalidArgument, "'orientation' must be '" +
std::string(kPortrait) + "' or '" +
std::string(kLandscape) + "'");
}
return Status(kOk);
}
Status ParseScale(const base::Value::Dict& params, double* scale) {
bool has_value;
if (!GetOptionalDouble(params, "scale", scale, &has_value)) {
return Status(kInvalidArgument, "'scale' must be a double");
}
if (!has_value) {
*scale = 1;
} else if (*scale < 0.1 || *scale > 2) {
return Status(kInvalidArgument, "'scale' must not be < 0.1 or > 2");
}
return Status(kOk);
}
Status ParseBoolean(const base::Value::Dict& params,
const std::string& name,
bool default_value,
bool* b) {
*b = default_value;
if (!GetOptionalBool(params, name, b)) {
return Status(kInvalidArgument, "'" + name + "' must be a boolean");
}
return Status(kOk);
}
Status GetNonNegativeDouble(const base::Value::Dict& dict,
const std::string& parent,
const std::string& child,
double* attribute) {
bool has_value;
std::string attribute_str = "'" + parent + "." + child + "'";
if (!GetOptionalDouble(dict, child, attribute, &has_value)) {
return Status(kInvalidArgument, attribute_str + " must be a double");
}
if (has_value) {
*attribute = ConvertCentimeterToInch(*attribute);
if (*attribute < 0) {
return Status(kInvalidArgument,
attribute_str + " must not be less than 0");
}
}
return Status(kOk);
}
struct Page {
double width;
double height;
};
Status ParsePage(const base::Value::Dict& params, Page* page) {
bool has_value;
const base::Value::Dict* page_dict = nullptr;
if (!GetOptionalDictionary(params, "page", &page_dict, &has_value)) {
return Status(kInvalidArgument, "'page' must be an object");
}
page->width = ConvertCentimeterToInch(21.59);
page->height = ConvertCentimeterToInch(27.94);
if (!has_value)
return Status(kOk);
Status status =
GetNonNegativeDouble(*page_dict, "page", "width", &page->width);
if (status.IsError())
return status;
status = GetNonNegativeDouble(*page_dict, "page", "height", &page->height);
if (status.IsError())
return status;
return Status(kOk);
}
struct Margin {
double top;
double bottom;
double left;
double right;
};
Status ParseMargin(const base::Value::Dict& params, Margin* margin) {
bool has_value;
const base::Value::Dict* margin_dict = nullptr;
if (!GetOptionalDictionary(params, "margin", &margin_dict, &has_value)) {
return Status(kInvalidArgument, "'margin' must be an object");
}
margin->top = ConvertCentimeterToInch(1.0);
margin->bottom = ConvertCentimeterToInch(1.0);
margin->left = ConvertCentimeterToInch(1.0);
margin->right = ConvertCentimeterToInch(1.0);
if (!has_value)
return Status(kOk);
Status status =
GetNonNegativeDouble(*margin_dict, "margin", "top", &margin->top);
if (status.IsError())
return status;
status =
GetNonNegativeDouble(*margin_dict, "margin", "bottom", &margin->bottom);
if (status.IsError())
return status;
status = GetNonNegativeDouble(*margin_dict, "margin", "left", &margin->left);
if (status.IsError())
return status;
status =
GetNonNegativeDouble(*margin_dict, "margin", "right", &margin->right);
if (status.IsError())
return status;
return Status(kOk);
}
Status ParsePageRanges(const base::Value::Dict& params,
std::string* page_ranges) {
bool has_value;
const base::Value::List* page_range_list = nullptr;
if (!GetOptionalList(params, "pageRanges", &page_range_list, &has_value)) {
return Status(kInvalidArgument, "'pageRanges' must be an array");
}
if (!has_value) {
return Status(kOk);
}
std::vector<std::string> ranges;
for (const base::Value& page_range : *page_range_list) {
if (page_range.is_int()) {
if (page_range.GetInt() < 0) {
return Status(kInvalidArgument,
"a Number entry in 'pageRanges' must not be less than 0");
}
ranges.push_back(base::NumberToString(page_range.GetInt()));
} else if (page_range.is_string()) {
ranges.push_back(page_range.GetString());
} else {
return Status(kInvalidArgument,
"an entry in 'pageRanges' must be a Number or String");
}
}
*page_ranges = base::JoinString(ranges, ",");
return Status(kOk);
}
// Returns:
// 1. Optional with the default value, if there is no such a key in the
// dictionary.
// 2. Empty optional, if the key is in the dictionary, but value has
// unexpected type.
// 3. Optional with value from dictionary.
template <typename T>
std::optional<T> ParseIfInDictionary(
const base::Value::Dict& dict,
std::string_view key,
T default_value,
std::optional<T> (base::Value::*getterIfType)() const) {
const auto* val = dict.Find(key);
if (!val)
return std::make_optional(default_value);
return (val->*getterIfType)();
}
std::optional<double> ParseDoubleIfInDictionary(const base::Value::Dict& dict,
std::string_view key,
double default_value) {
return ParseIfInDictionary(dict, key, default_value,
&base::Value::GetIfDouble);
}
std::optional<int> ParseIntIfInDictionary(const base::Value::Dict& dict,
std::string_view key,
int default_value) {
return ParseIfInDictionary(dict, key, default_value, &base::Value::GetIfInt);
}
} // namespace
Status ExecuteWindowCommand(const WindowCommand& command,
Session* session,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value) {
Timeout timeout;
WebView* web_view = nullptr;
Status 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 nav_status(kOk);
for (int attempt = 0; attempt < 3; attempt++) {
if (attempt == 2) {
// Switch to main frame and retry command if subframe no longer exists.
session->SwitchToTopFrame();
}
nav_status = web_view->WaitForPendingNavigations(
session->GetCurrentFrameId(),
Timeout(session->page_load_timeout, &timeout), true);
// Impossible errors:
// * kNoSuchExecutionContext as WebView::WaitForPendingNavigations never
// returns it.
// Some possible errors:
// * kTimeout. The pending navigation has taken too long, the whole command
// has timed out.
// * kDisconnected. The connection was lost. There is no point to retry.
if (nav_status.IsError()) {
return nav_status;
}
status = command.Run(session, web_view, params, value, &timeout);
if (kNavigationHints.contains(status.code())) {
// Navigation was detected while running the command. Retry.
continue;
}
if (status.code() == kTimeout) {
// If the command timed out, let WaitForPendingNavigations cancel
// the navigation if there is any.
continue;
} else if (status.code() == kUnknownError && web_view->IsNonBlocking() &&
status.message().find(kTargetClosedMessage) !=
std::string::npos) {
// When pageload strategy is None, new navigation can occur during
// execution of a command. Retry the command.
continue;
} else if (status.code() == kDisconnected ||
status.code() == kTargetDetached) {
// Some commands, like clicking a button or link which closes the window,
// may result in a kDisconnected or kTargetDetached error code.
// |web_view| may be invalid at this point.
return status;
} else if (status.IsError()) {
// If the command failed while a new page or frame started loading, retry
// the command after the pending navigation has completed.
bool is_pending = false;
nav_status = web_view->IsPendingNavigation(&timeout, &is_pending);
if (nav_status.IsError())
return nav_status;
else if (is_pending)
continue;
}
break;
}
nav_status = web_view->WaitForPendingNavigations(
session->GetCurrentFrameId(),
Timeout(session->page_load_timeout, &timeout), true);
if (status.IsOk() && nav_status.IsError() &&
nav_status.code() != kUnexpectedAlertOpen) {
return nav_status;
}
if (status.code() == kUnexpectedAlertOpen) {
return Status(kOk);
}
if (status.code() == kUnexpectedAlertOpen_Keep) {
return Status(kUnexpectedAlertOpen, status.message());
}
if (kNavigationHints.contains(status.code())) {
// The command has failed to run due to pending navigation three times.
// Returning a "timeout" error because infinite retries would, presumably,
// never end.
return Status{kTimeout, status};
}
return status;
}
Status ExecuteGet(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
timeout->SetDuration(session->page_load_timeout);
const std::string* url = params.FindString("url");
if (!url)
return Status(kInvalidArgument, "'url' must be a string");
Status status = web_view->Load(*url, timeout);
if (status.IsError())
return status;
session->SwitchToTopFrame();
return Status(kOk);
}
Status ExecuteExecuteScript(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* maybe_script = params.FindString("script");
if (!maybe_script)
return Status(kInvalidArgument, "'script' must be a string");
std::string script = *maybe_script;
if (script == ":takeHeapSnapshot")
return web_view->TakeHeapSnapshot(value);
if (script == ":startProfile")
return web_view->StartProfile();
if (script == ":endProfile")
return web_view->EndProfile(value);
const base::Value::List* args = params.FindList("args");
if (args == nullptr) {
return Status(kInvalidArgument, "'args' must be a list");
}
// Need to support line oriented comment
if (script.find("//") != std::string::npos)
script = script + "\n";
Status status =
web_view->CallUserSyncScript(session->GetCurrentFrameId(), script, *args,
session->script_timeout, value);
switch (status.code()) {
case kTimeout:
// If the target has been detached the script will never return
case kTargetDetached:
return Status(kScriptTimeout);
default:
return status;
}
}
Status ExecuteExecuteAsyncScript(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* maybe_script = params.FindString("script");
if (!maybe_script)
return Status(kInvalidArgument, "'script' must be a string");
std::string script = *maybe_script;
const base::Value::List* args = params.FindList("args");
if (args == nullptr) {
return Status(kInvalidArgument, "'args' must be a list");
}
// Need to support line oriented comment
if (script.find("//") != std::string::npos)
script = script + "\n";
Status status = web_view->CallUserAsyncFunction(
session->GetCurrentFrameId(), "async function(){" + script + "}", *args,
session->script_timeout, value);
switch (status.code()) {
case kTimeout:
// Navigation has happened during script execution. Further wait would lead
// to timeout.
case kAbortedByNavigation:
return Status(kScriptTimeout);
default:
return status;
}
}
Status ExecuteNewWindow(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
std::string type;
// "type" can either be None or a string.
auto* type_param = params.Find("type");
if (!type_param || type_param->is_none()) {
// Nothing more to do
} else if (type_param->is_string()) {
type = type_param->GetString();
} else {
return Status(kInvalidArgument, "missing or invalid 'type'");
}
// By default, creates new tab.
Chrome::WindowType window_type = (type == "window")
? Chrome::WindowType::kWindow
: Chrome::WindowType::kTab;
std::string handle;
Status status = session->chrome->NewWindow(session->window, window_type, true,
session->w3c_compliant, &handle);
if (status.IsError())
return status;
base::Value::Dict dict;
dict.Set("handle", handle);
dict.Set("type",
(window_type == Chrome::WindowType::kWindow) ? "window" : "tab");
auto results = std::make_unique<base::Value>(std::move(dict));
*value = std::move(results);
return Status(kOk);
}
Status ExecuteSwitchToFrame(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const base::Value* id = params.Find("id");
if (id == nullptr)
return Status(kInvalidArgument, "missing 'id'");
if (id->is_none()) {
session->SwitchToTopFrame();
return Status(kOk);
}
std::string script;
base::Value::List args;
const base::Value::Dict* id_dict = id->GetIfDict();
if (id_dict) {
const std::string* element_id =
id_dict->FindString(GetElementKey(session->w3c_compliant));
if (!element_id)
return Status(kInvalidArgument, "missing 'ELEMENT'");
bool is_displayed = false;
Status status =
IsElementDisplayed(session, web_view, *element_id, true, &is_displayed);
if (status.IsError())
return status;
script = "function(elem) { return elem; }";
args.Append(id_dict->Clone());
} else {
script =
"function(xpath) {"
" return document.evaluate(xpath, document, null, "
" XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
"}";
std::string xpath = "(/html/body//iframe|/html/frameset//frame)";
if (id->is_string()) {
std::string id_string = id->GetString();
if (session->w3c_compliant)
return Status(kInvalidArgument, "'id' can not be string");
else
xpath += base::StringPrintf(
"[@name=\"%s\" or @id=\"%s\"]", id_string.c_str(), id_string.c_str());
} else if (id->is_int()) {
int id_int = id->GetInt();
const int max_range = 65535; // 2^16 - 1
if (id_int < 0 || id_int > max_range)
return Status(kInvalidArgument, "'id' out of range");
else
xpath += base::StringPrintf("[%d]", id_int + 1);
} else {
return Status(kInvalidArgument, "invalid 'id'");
}
args.Append(xpath);
}
std::string frame;
Status status = web_view->GetFrameByFunction(
session->GetCurrentFrameId(), script, args, &frame);
if (status.IsError())
return status;
std::unique_ptr<base::Value> result;
status = web_view->CallFunction(
session->GetCurrentFrameId(), script, args, &result);
if (status.IsError())
return status;
const base::Value::Dict* element = result->GetIfDict();
if (!element)
return Status(kUnknownError, "fail to locate the sub frame element");
std::string chrome_driver_id = GenerateId();
const char kSetFrameIdentifier[] =
"function(frame, id) {"
" frame.setAttribute('cd_frame_id_', id);"
"}";
base::Value::List new_args;
new_args.Append(element->Clone());
new_args.Append(chrome_driver_id);
result.reset();
status = web_view->CallFunction(
session->GetCurrentFrameId(), kSetFrameIdentifier, new_args, &result);
if (status.IsError())
return status;
session->SwitchToSubFrame(frame, chrome_driver_id);
return Status(kOk);
}
Status ExecuteSwitchToParentFrame(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
session->SwitchToParentFrame();
return Status(kOk);
}
Status ExecuteGetTitle(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const char kGetTitleScript[] = "function() { return document.title;}";
base::Value::List args;
return web_view->CallFunction(std::string(), kGetTitleScript, args, value);
}
Status ExecuteGetPageSource(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const char kGetPageSource[] =
"(document.documentElement || {}).outerHTML || ''";
base::Value::List args;
return web_view->EvaluateScript(session->GetCurrentFrameId(), kGetPageSource,
true, value);
}
Status ExecuteFindElement(int interval_ms,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return FindElement(interval_ms, true, nullptr, session, web_view, params,
value);
}
Status ExecuteFindElements(int interval_ms,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return FindElement(interval_ms, false, nullptr, session, web_view, params,
value);
}
Status ExecuteGetCurrentUrl(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
std::string url;
Status status = GetUrl(web_view, std::string(), &url);
if (status.IsError())
return status;
if (url == kUnreachableWebDataURL ||
url == kDeprecatedUnreachableWebDataURL) {
status = web_view->GetUrl(&url);
if (status.IsError())
return status;
}
*value = std::make_unique<base::Value>(url);
return Status(kOk);
}
Status ExecuteGoBack(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
timeout->SetDuration(session->page_load_timeout);
Status status = web_view->TraverseHistory(-1, timeout);
if (status.IsError())
return status;
session->SwitchToTopFrame();
return Status(kOk);
}
Status ExecuteGoForward(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
timeout->SetDuration(session->page_load_timeout);
Status status = web_view->TraverseHistory(1, timeout);
if (status.IsError())
return status;
session->SwitchToTopFrame();
return Status(kOk);
}
Status ExecuteRefresh(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
timeout->SetDuration(session->page_load_timeout);
Status status = web_view->Reload(timeout);
if (status.IsError())
return status;
session->SwitchToTopFrame();
return Status(kOk);
}
Status ExecuteFreeze(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
timeout->SetDuration(session->page_load_timeout);
Status status = web_view->Freeze(timeout);
return status;
}
Status ExecuteResume(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
timeout->SetDuration(session->page_load_timeout);
Status status = web_view->Resume(timeout);
if (status.IsError())
return status;
return Status(kOk);
}
Status ExecuteMouseMoveTo(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
std::string element_id;
bool has_element = false;
const std::string* maybe_element_id = params.FindString("element");
if (maybe_element_id) {
element_id = *maybe_element_id;
has_element = true;
}
std::optional<int> x_offset = params.FindInt("xoffset");
std::optional<int> y_offset = params.FindInt("yoffset");
bool has_offset = x_offset.has_value() && y_offset.has_value();
if (!has_element && !has_offset)
return Status(kInvalidArgument,
"at least an element or offset should be set");
WebPoint location;
if (has_element) {
WebPoint offset;
if (has_offset)
offset.Offset(*x_offset, *y_offset);
Status status = ScrollElementIntoView(session, web_view, element_id,
has_offset ? &offset : nullptr, &location);
if (status.IsError())
return status;
} else {
location = session->mouse_position;
if (has_offset)
location.Offset(*x_offset, *y_offset);
}
std::vector<MouseEvent> events;
events.push_back(MouseEvent(kMovedMouseEventType,
session->pressed_mouse_button, location.x,
location.y, session->sticky_modifiers, 0, 0));
Status status = web_view->DispatchMouseEvents(
events, session->GetCurrentFrameId(), false);
if (status.IsOk())
session->mouse_position = location;
return status;
}
Status ExecuteMouseClick(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
MouseButton button;
Status status = GetMouseButton(params, &button);
if (status.IsError())
return status;
std::vector<MouseEvent> events;
events.push_back(
MouseEvent(kPressedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers, 0, 1));
events.push_back(
MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers,
MouseButtonToButtons(button), 1));
session->pressed_mouse_button = kNoneMouseButton;
return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(),
false);
}
Status ExecuteMouseButtonDown(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
MouseButton button;
Status status = GetMouseButton(params, &button);
if (status.IsError())
return status;
std::vector<MouseEvent> events;
events.push_back(
MouseEvent(kPressedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers, 0, 1));
session->pressed_mouse_button = button;
return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(),
false);
}
Status ExecuteMouseButtonUp(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
MouseButton button;
Status status = GetMouseButton(params, &button);
if (status.IsError())
return status;
std::vector<MouseEvent> events;
events.push_back(
MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers,
MouseButtonToButtons(button), 1));
session->pressed_mouse_button = kNoneMouseButton;
return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(),
false);
}
Status ExecuteMouseDoubleClick(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
MouseButton button;
Status status = GetMouseButton(params, &button);
if (status.IsError())
return status;
std::vector<MouseEvent> events;
events.push_back(
MouseEvent(kPressedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers, 0, 1));
events.push_back(
MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers,
MouseButtonToButtons(button), 1));
events.push_back(
MouseEvent(kPressedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers, 0, 2));
events.push_back(
MouseEvent(kReleasedMouseEventType, button, session->mouse_position.x,
session->mouse_position.y, session->sticky_modifiers,
MouseButtonToButtons(button), 2));
session->pressed_mouse_button = kNoneMouseButton;
return web_view->DispatchMouseEvents(events, session->GetCurrentFrameId(),
false);
}
Status ExecuteTouchDown(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return ExecuteTouchEvent(session, web_view, kTouchStart, params);
}
Status ExecuteTouchUp(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return ExecuteTouchEvent(session, web_view, kTouchEnd, params);
}
Status ExecuteTouchMove(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return ExecuteTouchEvent(session, web_view, kTouchMove, params);
}
Status ExecuteTouchScroll(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
WebPoint location = session->mouse_position;
const std::string* element = params.FindString("element");
if (element) {
Status status =
GetElementClickableLocation(session, web_view, *element, &location);
if (status.IsError())
return status;
}
std::optional<int> xoffset = params.FindInt("xoffset");
if (!xoffset)
return Status(kInvalidArgument, "'xoffset' must be an integer");
std::optional<int> yoffset = params.FindInt("yoffset");
if (!yoffset)
return Status(kInvalidArgument, "'yoffset' must be an integer");
return web_view->SynthesizeScrollGesture(location.x, location.y, *xoffset,
*yoffset);
}
Status ProcessInputActionSequence(Session* session,
const base::Value::Dict& action_sequence,
std::vector<base::Value::Dict>* action_list) {
const std::string* maybe_type = action_sequence.FindString("type");
std::string pointer_type;
if (!maybe_type || ((*maybe_type != "key") && (*maybe_type != "pointer") &&
(*maybe_type != "wheel") && (*maybe_type != "none"))) {
return Status(kInvalidArgument,
"'type' must be one of the strings 'key', 'pointer', 'wheel' "
"or 'none'");
}
const std::string& type = *maybe_type;
const std::string* maybe_id = action_sequence.FindString("id");
if (!maybe_id)
return Status(kInvalidArgument, "'id' must be a string");
const std::string& id = *maybe_id;
if (type == "pointer") {
const base::Value::Dict* parameters =
action_sequence.FindDict("parameters");
if (parameters) {
const std::string* maybe_pointer_type =
parameters->FindString("pointerType");
// error check arguments
if (!maybe_pointer_type ||
(*maybe_pointer_type != "mouse" && *maybe_pointer_type != "pen" &&
*maybe_pointer_type != "touch")) {
return Status(
kInvalidArgument,
"'pointerType' must be a string and one of mouse, pen or touch");
}
pointer_type = *maybe_pointer_type;
} else {
pointer_type = "mouse";
}
}
bool found = false;
for (const base::Value& source_value : session->active_input_sources) {
DCHECK(source_value.is_dict());
const base::Value::Dict& source = source_value.GetDict();
std::string source_id;
std::string source_type;
const std::string* maybe_source_id = source.FindString("id");
const std::string* maybe_source_type = source.FindString("type");
if (maybe_source_id)
source_id = *maybe_source_id;
if (maybe_source_type)
source_type = *maybe_source_type;
if (source_id == id && source_type == type) {
found = true;
if (type == "pointer") {
const std::string* source_pointer_type =
source.FindString("pointerType");
if (!source_pointer_type || pointer_type != *source_pointer_type) {
return Status(kInvalidArgument,
"'pointerType' must be a string that matches sources "
"pointer type");
}
}
break;
}
}
// if we found no matching active input source
if (!found) {
base::Value::Dict tmp_source;
// create input source
tmp_source.Set("id", id);
tmp_source.Set("type", type);
if (type == "pointer") {
tmp_source.Set("pointerType", pointer_type);
}
session->active_input_sources.Append(std::move(tmp_source));
base::Value::Dict tmp_state;
tmp_state.Set("id", id);
if (type == "key") {
// Initialize a key input state object
// (https://w3c.github.io/webdriver/#dfn-key-input-state).
tmp_state.Set("pressed", base::Value::Dict());
// For convenience, we use one integer property to encode four Boolean
// properties (alt, shift, ctrl, meta) from the spec, using values from
// enum KeyModifierMask.
tmp_state.Set("modifiers", 0);
} else if (type == "pointer") {
int x = 0;
int y = 0;
// "pressed" is stored as a bitmask of pointer buttons.
tmp_state.Set("pressed", 0);
tmp_state.Set("subtype", pointer_type);
tmp_state.Set("x", x);
tmp_state.Set("y", y);
}
session->input_state_table.SetByDottedPath(id, std::move(tmp_state));
}
const base::Value::List* actions = action_sequence.FindList("actions");
if (actions == nullptr) {
return Status(kInvalidArgument, "'actions' in the sequence must be a list");
}
std::unique_ptr<base::Value::List> actions_result(new base::Value::List);
for (const base::Value& action_item_value : *actions) {
base::Value::Dict action_dict;
if (!action_item_value.is_dict()) {
return Status(
kInvalidArgument,
"each argument in the action sequence must be a dictionary");
}
const base::Value::Dict& action_item = action_item_value.GetDict();
action_dict.Set("id", id);
action_dict.Set("type", type);
if (type == "none") {
// process none action
const std::string* subtype = action_item.FindString("type");
if (!subtype || *subtype != "pause")
return Status(kInvalidArgument,
"type of action must be the string 'pause'");
action_dict.Set("subtype", *subtype);
Status status = ProcessPauseAction(action_item, &action_dict);
if (status.IsError())
return status;
} else if (type == "key") {
// process key action
const std::string* subtype = action_item.FindString("type");
if (!subtype ||
(*subtype != "keyUp" && *subtype != "keyDown" && *subtype != "pause"))
return Status(
kInvalidArgument,
"type of action must be the string 'keyUp', 'keyDown' or 'pause'");
action_dict.Set("subtype", *subtype);
if (*subtype == "pause") {
Status status = ProcessPauseAction(action_item, &action_dict);
if (status.IsError())
return status;
} else {
const std::string* key = action_item.FindString("value");
bool valid = (key != nullptr);
if (valid) {
// check if key is a single unicode code point
size_t char_index = 0;
base_icu::UChar32 code_point;
valid = base::ReadUnicodeCharacter(key->c_str(), key->size(),
&char_index, &code_point) &&
char_index + 1 == key->size();
}
if (!valid)
return Status(kInvalidArgument,
"'value' must be a single Unicode code point");
action_dict.Set("value", *key);
}
} else if (type == "pointer" || type == "wheel") {
const std::string* subtype = action_item.FindString("type");
if (type == "pointer") {
if (!subtype || (*subtype != "pointerUp" && *subtype != "pointerDown" &&
*subtype != "pointerMove" &&
*subtype != "pointerCancel" && *subtype != "pause")) {
return Status(kInvalidArgument,
"type of pointer action must be the string "
"'pointerUp', 'pointerDown', 'pointerMove' or "
"'pause'");
}
} else {
if (!subtype || (*subtype != "scroll" && *subtype != "pause")) {
return Status(
kInvalidArgument,
"type of action must be the string 'scroll' or 'pause'");
}
}
action_dict.Set("subtype", *subtype);
action_dict.Set("pointerType", pointer_type);
if (*subtype == "pointerDown" || *subtype == "pointerUp") {
if (pointer_type == "mouse" || pointer_type == "pen") {
int button = action_item.FindInt("button").value_or(-1);
if (button < 0 || button > 4) {
return Status(
kInvalidArgument,
"'button' must be a non-negative int and between 0 and 4");
}
std::string button_str;
Status status = IntToStringButton(button, button_str);
if (status.IsError())
return status;
action_dict.Set("button", button_str);
}
} else if (*subtype == "pointerMove" || *subtype == "scroll") {
if (*subtype == "scroll") {
std::optional<int> x = action_item.FindInt("x");
if (!x.has_value()) {
return Status(kInvalidArgument, "'x' must be an int");
}
std::optional<int> y = action_item.FindInt("y");
if (!y.has_value()) {
return Status(kInvalidArgument, "'y' must be an int");
}
action_dict.Set("x", *x);
action_dict.Set("y", *y);
} else {
std::optional<double> x = action_item.FindDouble("x");
if (!x.has_value()) {
return Status(kInvalidArgument, "'x' must be a number");
}
std::optional<double> y = action_item.FindDouble("y");
if (!y.has_value()) {
return Status(kInvalidArgument, "'y' must be a number");
}
action_dict.Set("x", *x);
action_dict.Set("y", *y);
}
const base::Value* origin_val = action_item.Find("origin");
if (origin_val) {
if (!origin_val->is_string()) {
const base::Value::Dict* origin_dict = origin_val->GetIfDict();
if (!origin_dict)
return Status(kInvalidArgument,
"'origin' must be either a string or a dictionary");
const std::string* element_id =
origin_dict->FindString(GetElementKey(session->w3c_compliant));
if (!element_id)
return Status(kInvalidArgument, "'element' is missing");
base::Value* origin_result =
action_dict.Set("origin", base::Value(base::Value::Type::DICT));
origin_result->GetDict().SetByDottedPath(
GetElementKey(session->w3c_compliant), *element_id);
} else {
const std::string& origin = origin_val->GetString();
if (origin != "viewport" && origin != "pointer")
return Status(kInvalidArgument,
"if 'origin' is a string, it must be either "
"'viewport' or 'pointer'");
action_dict.Set("origin", origin);
}
} else {
action_dict.Set("origin", "viewport");
}
Status status = ProcessPauseAction(action_item, &action_dict);
if (status.IsError())
return status;
if (*subtype == "scroll") {
std::optional<int> delta_x = action_item.FindInt("deltaX");
if (!delta_x)
return Status(kInvalidArgument, "'delta x' must be an int");
std::optional<int> delta_y = action_item.FindInt("deltaY");
if (!delta_y)
return Status(kInvalidArgument, "'delta y' must be an int");
action_dict.Set("deltaX", *delta_x);
action_dict.Set("deltaY", *delta_y);
}
} else if (*subtype == "pause") {
Status status = ProcessPauseAction(action_item, &action_dict);
if (status.IsError())
return status;
}
// Process Pointer Event's properties.
std::optional<double> maybe_double_value;
std::optional<int> maybe_int_value;
maybe_double_value = ParseDoubleIfInDictionary(action_item, "width", 1);
if (!maybe_double_value.has_value() || maybe_double_value.value() < 0)
return Status(kInvalidArgument,
"'width' must be a non-negative number");
action_dict.Set("width", maybe_double_value.value());
maybe_double_value = ParseDoubleIfInDictionary(action_item, "height", 1);
if (!maybe_double_value.has_value() || maybe_double_value.value() < 0)
return Status(kInvalidArgument,
"'height' must be a non-negative number");
action_dict.Set("height", maybe_double_value.value());
maybe_double_value =
ParseDoubleIfInDictionary(action_item, "pressure", 0.5);
if (!maybe_double_value.has_value() || maybe_double_value.value() < 0 ||
maybe_double_value.value() > 1)
return Status(
kInvalidArgument,
"'pressure' must be a non-negative number in the range of [0,1]");
action_dict.Set("pressure", maybe_double_value.value());
maybe_double_value =
ParseDoubleIfInDictionary(action_item, "tangentialPressure", 0);
if (!maybe_double_value.has_value() || maybe_double_value.value() < -1 ||
maybe_double_value.value() > 1)
return Status(
kInvalidArgument,
"'tangentialPressure' must be a number in the range of [-1,1]");
action_dict.Set("tangentialPressure", maybe_double_value.value());
maybe_int_value = ParseIntIfInDictionary(action_item, "tiltX", 0);
if (!maybe_int_value.has_value() || maybe_int_value.value() < -90 ||
maybe_int_value.value() > 90)
return Status(kInvalidArgument,
"'tiltX' must be an integer in the range of [-90,90]");
action_dict.Set("tiltX", maybe_int_value.value());
maybe_int_value = ParseIntIfInDictionary(action_item, "tiltY", 0);
if (!maybe_int_value.has_value() || maybe_int_value.value() < -90 ||
maybe_int_value.value() > 90)
return Status(kInvalidArgument,
"'tiltY' must be an integer in the range of [-90,90]");
action_dict.Set("tiltY", maybe_int_value.value());
maybe_int_value = ParseIntIfInDictionary(action_item, "twist", 0);
if (!maybe_int_value.has_value() || maybe_int_value.value() < 0 ||
maybe_int_value.value() > 359)
return Status(kInvalidArgument,
"'twist' must be an integer in the range of [0,359]");
action_dict.Set("twist", maybe_int_value.value());
}
action_list->push_back(std::move(action_dict));
}
return Status(kOk);
}
Status ExecutePerformActions(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
// extract action sequence
const base::Value::List* actions_input = params.FindList("actions");
if (actions_input == nullptr) {
return Status(kInvalidArgument, "'actions' must be a list");
}
// the processed actions
std::vector<std::vector<base::Value::Dict>> actions_list;
for (const base::Value& action_sequence : *actions_input) {
// process input action sequence
if (!action_sequence.is_dict())
return Status(kInvalidArgument, "each argument must be a dictionary");
std::vector<base::Value::Dict> action_list;
Status status = ProcessInputActionSequence(
session, action_sequence.GetDict(), &action_list);
actions_list.push_back(std::move(action_list));
if (status.IsError())
return Status(kInvalidArgument, status);
}
std::set<std::string> pointer_id_set;
std::vector<base::Value::Dict*> action_input_states;
std::map<std::string, gfx::Point> action_locations;
std::map<std::string, bool> has_touch_start;
std::map<std::string, int> buttons;
std::map<std::string, int> last_pressed_buttons;
std::map<std::string, std::string> button_type;
int viewport_width = 0, viewport_height = 0;
int init_x = 0, init_y = 0;
size_t longest_action_list_size = 0;
for (size_t i = 0; i < actions_list.size(); i++) {
longest_action_list_size =
std::max(longest_action_list_size, actions_list[i].size());
}
for (size_t i = 0; i < longest_action_list_size; i++) {
// Find the last pointer action, and it has to be sent synchronously by
// default.
size_t last_action_index = 0;
size_t last_touch_index = 0;
for (size_t j = 0; j < actions_list.size(); j++) {
if (actions_list[j].size() > i) {
const base::Value::Dict& action = actions_list[j][i];
std::string type;
std::string action_type;
GetOptionalString(action, "type", &type);
GetOptionalString(action, "subtype", &action_type);
if (type != "none" && action_type != "pause")
last_action_index = j;
if (type == "pointer") {
std::string pointer_type;
GetOptionalString(action, "pointerType", &pointer_type);
if (pointer_type == "touch")
last_touch_index = j;
}
}
}
// Implements "compute the tick duration" algorithm from W3C spec
// (https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration).
// This is the duration for actions in one tick.
int tick_duration = 0;
std::vector<TouchEvent> dispatch_touch_events;
for (size_t j = 0; j < actions_list.size(); j++) {
if (actions_list[j].size() > i) {
const base::Value::Dict& action = actions_list[j][i];
std::string id;
std::string type;
std::string action_type;
GetOptionalString(action, "id", &id);
base::Value::Dict* input_state =
session->input_state_table.FindDictByDottedPath(id);
if (!input_state)
return Status(kUnknownError, "missing input state");
GetOptionalString(action, "type", &type);
if (i == 0) {
if (pointer_id_set.find(id) != pointer_id_set.end())
return Status(kInvalidArgument, "'id' already exists");
pointer_id_set.insert(id);
action_input_states.push_back(input_state);
if (type == "pointer" || type == "wheel") {
Status status = WindowViewportSize(
session, web_view, &viewport_width, &viewport_height);
if (status.IsError())
return status;
std::optional<int> maybe_init_x = input_state->FindInt("x");
if (maybe_init_x)
init_x = *maybe_init_x;
std::optional<int> maybe_init_y = input_state->FindInt("y");
if (maybe_init_y)
init_y = *maybe_init_y;
action_locations.insert(
std::make_pair(id, gfx::Point(init_x, init_y)));
std::string pointer_type;
GetOptionalString(action, "pointerType", &pointer_type);
if (pointer_type == "mouse" || pointer_type == "pen") {
buttons[id] = input_state->Find("pressed")->GetInt();
last_pressed_buttons[id] = buttons[id];
} else if (pointer_type == "touch") {
has_touch_start[id] = false;
}
}
}
GetOptionalString(action, "subtype", &action_type);
int duration = 0;
if (action_type == "pause") {
GetOptionalInt(action, "duration", &duration);
tick_duration = std::max(tick_duration, duration);
}
if (type != "none") {
bool async_dispatch_event = true;
if (j == last_action_index) {
async_dispatch_event = false;
GetOptionalBool(action, "asyncDispatch", &async_dispatch_event);
}
if (type == "key") {
if (action_type != "pause") {
std::vector<KeyEvent> dispatch_key_events;
KeyEventBuilder builder;
Status status = ConvertKeyActionToKeyEvent(
action, *input_state, action_type == "keyDown",
&dispatch_key_events);
if (status.IsError())
return status;
if (dispatch_key_events.size() > 0) {
const KeyEvent& event = dispatch_key_events.front();
if (action_type == "keyDown") {
session->input_cancel_list.emplace_back(
action_input_states[j], nullptr, nullptr, &event);
session->sticky_modifiers |= KeyToKeyModifiers(event.key);
} else if (action_type == "keyUp") {
session->sticky_modifiers &= ~KeyToKeyModifiers(event.key);
}
status = web_view->DispatchKeyEvents(dispatch_key_events,
async_dispatch_event);
if (status.IsError())
return status;
}
}
} else if (type == "pointer" || type == "wheel") {
std::string element_id;
if (action_type == "pointerMove" || action_type == "scroll") {
double x = action.FindDouble("x").value_or(0);
double y = action.FindDouble("y").value_or(0);
if (const base::Value* origin_val = action.Find("origin")) {
if (const base::Value::Dict* origin_dict =
origin_val->GetIfDict()) {
GetOptionalString(*origin_dict,
GetElementKey(session->w3c_compliant),
&element_id);
if (!element_id.empty()) {
int center_x = 0, center_y = 0;
Status status = ElementInViewCenter(
session, web_view, element_id, &center_x, &center_y);
if (status.IsError())
return status;
x += center_x;
y += center_y;
}
} else {
std::string origin_str;
GetOptionalString(action, "origin", &origin_str);
if (origin_str == "pointer") {
x += action_locations[id].x();
y += action_locations[id].y();
}
}
}
if (x < 0 || x > viewport_width || y < 0 || y > viewport_height)
return Status(kMoveTargetOutOfBounds);
action_locations[id] = gfx::Point(x, y);
duration = 0;
GetOptionalInt(action, "duration", &duration);
tick_duration = std::max(tick_duration, duration);
if (action_type == "scroll") {
int delta_x = action.FindInt("deltaX").value_or(0);
int delta_y = action.FindInt("deltaY").value_or(0);
std::vector<MouseEvent> dispatch_wheel_events;
MouseEvent event(StringToMouseEventType(action_type),
StringToMouseButton(button_type[id]),
action_locations[id].x(),
action_locations[id].y(), 0, buttons[id], 0);
event.modifiers = session->sticky_modifiers;
event.delta_x = delta_x;
event.delta_y = delta_y;
buttons[id] |= StringToModifierMouseButton(button_type[id]);
last_pressed_buttons[id] =
StringToModifierMouseButton(button_type[id]);
session->mouse_position = WebPoint(event.x, event.y);
dispatch_wheel_events.push_back(event);
Status status = web_view->DispatchMouseEvents(
dispatch_wheel_events, session->GetCurrentFrameId(),
async_dispatch_event);
if (status.IsError())
return status;
}
}
double width = action.FindDouble("width").value_or(1);
double height = action.FindDouble("height").value_or(1);
double pressure = action.FindDouble("pressure").value_or(0.5);
double tangential_pressure =
action.FindDouble("tangentialPressure").value_or(0);
int tilt_x = action.FindInt("tiltX").value_or(0);
int tilt_y = action.FindInt("tiltY").value_or(0);
int twist = action.FindInt("twist").value_or(0);
std::string pointer_type;
GetOptionalString(action, "pointerType", &pointer_type);
if (pointer_type == "mouse" || pointer_type == "pen") {
if (action_type != "pause") {
std::vector<MouseEvent> dispatch_mouse_events;
int click_count = 0;
if (action_type == "pointerDown" ||
action_type == "pointerUp") {
std::string button;
GetOptionalString(action, "button", &button);
button_type[id] = button;
click_count = 1;
} else if (buttons[id] == 0) {
button_type[id].clear();
}
MouseEvent event(StringToMouseEventType(action_type),
StringToMouseButton(button_type[id]),
action_locations[id].x(),
action_locations[id].y(), 0, buttons[id],
click_count);
event.pointer_type = StringToPointerType(pointer_type);
event.modifiers = session->sticky_modifiers;
event.tangentialPressure = tangential_pressure;
event.tiltX = tilt_x;
event.tiltY = tilt_y;
event.twist = twist;
if (event.type == kPressedMouseEventType) {
base::TimeTicks timestamp = base::TimeTicks::Now();
event.click_count = GetMouseClickCount(
session->click_count, event.x, event.y,
session->mouse_position.x, session->mouse_position.y,
StringToModifierMouseButton(button_type[id]),
last_pressed_buttons[id], timestamp,
session->mouse_click_timestamp);
buttons[id] |= StringToModifierMouseButton(button_type[id]);
last_pressed_buttons[id] =
StringToModifierMouseButton(button_type[id]);
session->mouse_position = WebPoint(event.x, event.y);
session->click_count = event.click_count;
session->mouse_click_timestamp = timestamp;
session->input_cancel_list.emplace_back(
action_input_states[j], &event, nullptr, nullptr);
action_input_states[j]->Set(
"pressed",
action_input_states[j]->Find("pressed")->GetInt() |
(1 << event.button));
} else if (event.type == kReleasedMouseEventType) {
pressure = 0;
event.click_count = session->click_count;
buttons[id] &= ~StringToModifierMouseButton(button_type[id]);
action_input_states[j]->Set(
"pressed",
action_input_states[j]->Find("pressed")->GetInt() &
~(1 << event.button));
} else if (event.type == kMovedMouseEventType) {
if (action_input_states[j]->Find("pressed")->GetInt() == 0) {
pressure = 0;
}
}
event.force = pressure;
dispatch_mouse_events.push_back(event);
Status status = web_view->DispatchMouseEvents(
dispatch_mouse_events, session->GetCurrentFrameId(),
async_dispatch_event);
if (status.IsError())
return status;
}
} else if (pointer_type == "touch") {
if (action_type == "pointerDown")
has_touch_start[id] = true;
TouchEvent event(StringToTouchEventType(action_type),
action_locations[id].x(),
action_locations[id].y());
event.radiusX = width / 2.f;
event.radiusY = height / 2.f;
event.force = pressure;
event.tangentialPressure = tangential_pressure;
event.tiltX = tilt_x;
event.tiltY = tilt_y;
event.twist = twist;
if (event.type == kTouchStart) {
session->input_cancel_list.emplace_back(
action_input_states[j], nullptr, &event, nullptr);
action_input_states[j]->Set("pressed", 1);
} else if (event.type == kTouchEnd) {
action_input_states[j]->Set("pressed", 0);
}
if (has_touch_start[id]) {
if (event.type == kPause)
event.type = kTouchMove;
event.id = j;
dispatch_touch_events.push_back(event);
}
if (j == last_touch_index) {
Status status = web_view->DispatchTouchEventWithMultiPoints(
dispatch_touch_events, async_dispatch_event);
if (status.IsError())
return status;
}
if (action_type == "pointerUp")
has_touch_start[id] = false;
}
action_input_states[j]->Set("x", action_locations[id].x());
action_input_states[j]->Set("y", action_locations[id].y());
}
}
}
}
if (tick_duration > 0) {
base::PlatformThread::Sleep(base::Milliseconds(tick_duration));
}
}
return Status(kOk);
}
Status ExecuteReleaseActions(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
for (const InputCancelListEntry& entry :
base::Reversed(session->input_cancel_list)) {
if (entry.key_event) {
base::Value::Dict* pressed = entry.input_state->FindDict("pressed");
if (!pressed->Find(entry.key_event->key))
continue;
web_view->DispatchKeyEvents({*entry.key_event}, false);
pressed->Remove(entry.key_event->key);
} else if (entry.mouse_event) {
int pressed = entry.input_state->Find("pressed")->GetInt();
int button_mask = 1 << entry.mouse_event->button;
if ((pressed & button_mask) == 0)
continue;
web_view->DispatchMouseEvents({*entry.mouse_event},
session->GetCurrentFrameId(), false);
entry.input_state->Set("pressed", pressed & ~button_mask);
} else if (entry.touch_event) {
int pressed = entry.input_state->Find("pressed")->GetInt();
if (pressed == 0)
continue;
web_view->DispatchTouchEvents({*entry.touch_event}, false);
entry.input_state->Set("pressed", 0);
}
}
session->input_cancel_list.clear();
session->input_state_table.clear();
session->active_input_sources.clear();
session->mouse_position = WebPoint(0, 0);
session->click_count = 0;
session->mouse_click_timestamp = base::TimeTicks::Now();
session->sticky_modifiers = 0;
return Status(kOk);
}
Status ExecuteSendCommand(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* cmd = params.FindString("cmd");
if (!cmd) {
return Status(kInvalidArgument, "command not passed");
}
const base::Value::Dict* cmd_params = params.FindDict("params");
if (!cmd_params) {
return Status(kInvalidArgument, "params not passed");
}
return web_view->SendCommand(*cmd, *cmd_params);
}
Status ExecuteSendCommandFromWebSocket(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* cmd = params.FindString("method");
if (!cmd) {
return Status(kInvalidArgument, "command not passed");
}
const base::Value::Dict* cmd_params = params.FindDict("params");
if (!cmd_params) {
return Status(kInvalidArgument, "params not passed");
}
std::optional<int> client_cmd_id = params.FindInt("id");
if (!client_cmd_id || !CommandId::IsClientCommandId(*client_cmd_id)) {
return Status(kInvalidArgument, "command id must be negative");
}
return web_view->SendCommandFromWebSocket(*cmd, *cmd_params, *client_cmd_id);
}
Status ExecuteSendCommandAndGetResult(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* cmd = params.FindString("cmd");
if (!cmd) {
return Status(kInvalidArgument, "command not passed");
}
const base::Value::Dict* cmd_params = params.FindDict("params");
if (!cmd_params) {
return Status(kInvalidArgument, "params not passed");
}
return web_view->SendCommandAndGetResult(*cmd, *cmd_params, value);
}
Status ExecuteGetActiveElement(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return GetActiveElement(session, web_view, value);
}
Status ExecuteSendKeysToActiveElement(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const base::Value::List* key_list = params.FindList("value");
if (key_list == nullptr) {
return Status(kInvalidArgument, "'value' must be a list");
}
return SendKeysOnWindow(
web_view, key_list, false, &session->sticky_modifiers);
}
Status ExecuteGetStorageItem(const char* storage,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* key = params.FindString("key");
if (!key)
return Status(kInvalidArgument, "'key' must be a string");
base::Value::List args;
args.Append(*key);
return web_view->CallFunction(
session->GetCurrentFrameId(),
base::StringPrintf("function(key) { return %s[key]; }", storage),
std::move(args), value);
}
Status ExecuteGetStorageKeys(const char* storage,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const char script[] =
"var keys = [];"
"var storage = %s;"
"for (var i = 0; i < storage.length; i++) {"
" keys.push(storage.key(i));"
"}"
"keys";
return web_view->EvaluateScript(session->GetCurrentFrameId(),
base::StringPrintf(script, storage), false,
value);
}
Status ExecuteSetStorageItem(const char* storage,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* key = params.FindString("key");
if (!key)
return Status(kInvalidArgument, "'key' must be a string");
const std::string* storage_value = params.FindString("value");
if (!storage_value)
return Status(kInvalidArgument, "'value' must be a string");
base::Value::List args;
args.Append(*key);
args.Append(*storage_value);
return web_view->CallFunction(
session->GetCurrentFrameId(),
base::StringPrintf("function(key, value) { %s[key] = value; }", storage),
args,
value);
}
Status ExecuteRemoveStorageItem(const char* storage,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* key = params.FindString("key");
if (!key)
return Status(kInvalidArgument, "'key' must be a string");
base::Value::List args;
args.Append(*key);
return web_view->CallFunction(
session->GetCurrentFrameId(),
base::StringPrintf("function(key) { %s.removeItem(key) }", storage),
args,
value);
}
Status ExecuteClearStorage(const char* storage,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->EvaluateScript(session->GetCurrentFrameId(),
base::StringPrintf("%s.clear()", storage),
false, value);
}
Status ExecuteGetStorageSize(const char* storage,
Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->EvaluateScript(session->GetCurrentFrameId(),
base::StringPrintf("%s.length", storage),
false, value);
}
Status ExecuteScreenshot(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
Status status = session->chrome->ActivateWebView(web_view->GetId());
if (status.IsError())
return status;
std::string screenshot;
status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict());
if (status.IsError()) {
if (status.code() == kUnexpectedAlertOpen) {
LOG(WARNING) << status.message() << ", cancelling screenshot";
// we can't take screenshot in this state
// but we must return kUnexpectedAlertOpen_Keep instead
// see https://crbug.com/chromedriver/2117
return Status(kUnexpectedAlertOpen_Keep);
}
LOG(WARNING) << "screenshot failed, retrying " << status.message();
status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict());
}
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(screenshot);
return Status(kOk);
}
Status ExecuteFullPageScreenshot(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
Status status = session->chrome->ActivateWebView(web_view->GetId());
if (status.IsError())
return status;
std::unique_ptr<base::Value> layout_metrics;
// TODO(crbug.com/40911917): Pass base::Value::Dict* as return param.
status = web_view->SendCommandAndGetResult(
"Page.getLayoutMetrics", base::Value::Dict(), &layout_metrics);
if (status.IsError())
return status;
CHECK(layout_metrics && layout_metrics->is_dict());
const auto& layout_metrics_dict = layout_metrics->GetDict();
const auto width =
layout_metrics_dict.FindDoubleByDottedPath("contentSize.width");
if (!width.has_value())
return Status(kUnknownError, "invalid width type");
int w = ceil(width.value());
if (w == 0)
return Status(kUnknownError, "invalid width 0");
const auto height =
layout_metrics_dict.FindDoubleByDottedPath("contentSize.height");
if (!height.has_value())
return Status(kUnknownError, "invalid height type");
int h = ceil(height.value());
if (h == 0)
return Status(kUnknownError, "invalid height 0");
auto* meom = web_view->GetMobileEmulationOverrideManager();
bool has_override_metrics = meom->HasOverrideMetrics();
base::Value::Dict device_metrics;
device_metrics.Set("width", w);
device_metrics.Set("height", h);
if (has_override_metrics) {
const auto* dm = meom->GetDeviceMetrics();
device_metrics.Set("deviceScaleFactor", dm->device_scale_factor);
device_metrics.Set("mobile", dm->mobile);
} else {
device_metrics.Set("deviceScaleFactor", 1);
device_metrics.Set("mobile", false);
}
std::unique_ptr<base::Value> ignore;
status = web_view->SendCommandAndGetResult(
"Emulation.setDeviceMetricsOverride", device_metrics, &ignore);
if (status.IsError())
return status;
std::string screenshot;
// No need to supply clip as it would be default to the device metrics
// parameters
status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict());
if (status.IsError()) {
if (status.code() == kUnexpectedAlertOpen) {
LOG(WARNING) << status.message() << ", cancelling screenshot";
// we can't take screenshot in this state
// but we must return kUnexpectedAlertOpen_Keep instead
// see https://crbug.com/chromedriver/2117
return Status(kUnexpectedAlertOpen_Keep);
}
LOG(WARNING) << "screenshot failed, retrying " << status.message();
status = web_view->CaptureScreenshot(&screenshot, base::Value::Dict());
}
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(screenshot);
// Check if there is already deviceMetricsOverride in use,
// if so, restore to that instead
if (has_override_metrics) {
status = meom->RestoreOverrideMetrics();
} else {
// The scroll bar disappear after setting device metrics to fullpage
// width and height, this is to clear device metrics and restore
// scroll bars
status = web_view->SendCommandAndGetResult(
"Emulation.clearDeviceMetricsOverride", base::Value::Dict(), &ignore);
}
return status;
}
Status ExecutePrint(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
std::string orientation;
Status status = ParseOrientation(params, &orientation);
if (status.IsError())
return status;
double scale;
status = ParseScale(params, &scale);
if (status.IsError())
return status;
bool background;
status = ParseBoolean(params, "background", false, &background);
if (status.IsError())
return status;
Page page;
status = ParsePage(params, &page);
if (status.IsError())
return status;
Margin margin;
status = ParseMargin(params, &margin);
if (status.IsError())
return status;
bool shrink_to_fit;
status = ParseBoolean(params, "shrinkToFit", true, &shrink_to_fit);
if (status.IsError())
return status;
std::string page_ranges;
status = ParsePageRanges(params, &page_ranges);
if (status.IsError())
return status;
base::Value::Dict print_params;
print_params.Set(kLandscape, orientation == kLandscape);
print_params.Set("scale", scale);
print_params.Set("printBackground", background);
print_params.Set("paperWidth", page.width);
print_params.Set("paperHeight", page.height);
print_params.Set("marginTop", margin.top);
print_params.Set("marginBottom", margin.bottom);
print_params.Set("marginLeft", margin.left);
print_params.Set("marginRight", margin.right);
print_params.Set("preferCSSPageSize", !shrink_to_fit);
print_params.Set("pageRanges", page_ranges);
print_params.Set("transferMode", "ReturnAsBase64");
std::string pdf;
status = web_view->PrintToPDF(print_params, &pdf);
if (status.IsError())
return status;
*value = std::make_unique<base::Value>(pdf);
return Status(kOk);
}
Status ExecuteGetCookies(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
std::list<Cookie> cookies;
Status status = GetVisibleCookies(session, web_view, &cookies);
if (status.IsError())
return status;
auto cookie_list = std::make_unique<base::Value>(base::Value::Type::LIST);
for (std::list<Cookie>::const_iterator it = cookies.begin();
it != cookies.end(); ++it) {
cookie_list->GetList().Append(CreateDictionaryFrom(*it));
}
*value = std::move(cookie_list);
return Status(kOk);
}
Status ExecuteGetNamedCookie(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* name = params.FindString("name");
if (!name)
return Status(kInvalidArgument, "missing 'cookie name'");
std::list<Cookie> cookies;
Status status = GetVisibleCookies(session, web_view, &cookies);
if (status.IsError())
return status;
for (std::list<Cookie>::const_iterator it = cookies.begin();
it != cookies.end(); ++it) {
if (*name == it->name) {
*value =
base::Value::ToUniquePtrValue(base::Value(CreateDictionaryFrom(*it)));
return Status(kOk);
}
}
return Status(kNoSuchCookie);
}
Status ExecuteAddCookie(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const base::Value::Dict* cookie = params.FindDict("cookie");
if (!cookie)
return Status(kInvalidArgument, "missing 'cookie'");
const std::string* name = cookie->FindString("name");
const std::string* cookie_value = cookie->FindString("value");
if (!name)
return Status(kInvalidArgument, "missing 'name'");
if (!cookie_value)
return Status(kInvalidArgument, "missing 'value'");
std::string url;
Status status = GetUrl(web_view, session->GetCurrentFrameId(), &url);
if (status.IsError())
return status;
if (!base::StartsWith(url, "http://", base::CompareCase::INSENSITIVE_ASCII) &&
!base::StartsWith(url, "https://",
base::CompareCase::INSENSITIVE_ASCII) &&
!base::StartsWith(url, "ftp://", base::CompareCase::INSENSITIVE_ASCII))
return Status(kInvalidCookieDomain);
std::string domain;
if (!GetOptionalString(*cookie, "domain", &domain))
return Status(kInvalidArgument, "invalid 'domain'");
if (session->w3c_compliant && !domain.empty() &&
!url::HostIsIPAddress(domain)) {
if (domain[0] == '.')
domain = domain.substr(1);
if (domain.size() < 2)
return Status(kInvalidCookieDomain, "invalid 'domain'");
if (!GURL(url).DomainIs(domain))
return Status(kInvalidCookieDomain, "Cookie 'domain' mismatch");
domain.insert(0, 1, '.');
}
std::string path("/");
if (!GetOptionalString(*cookie, "path", &path))
return Status(kInvalidArgument, "invalid 'path'");
std::string samesite("");
if (!GetOptionalString(*cookie, "sameSite", &samesite))
return Status(kInvalidArgument, "invalid 'sameSite'");
if (!samesite.empty() && samesite != "Strict" && samesite != "Lax" &&
samesite != "None")
return Status(kInvalidArgument, "invalid 'sameSite'");
bool secure = false;
if (!GetOptionalBool(*cookie, "secure", &secure))
return Status(kInvalidArgument, "invalid 'secure'");
bool http_only = false;
if (!GetOptionalBool(*cookie, "httpOnly", &http_only))
return Status(kInvalidArgument, "invalid 'httpOnly'");
double expiry;
bool has_value;
if (session->w3c_compliant) {
// W3C spec says expiry is a safe integer.
int64_t expiry_int64;
if (!GetOptionalSafeInt(*cookie, "expiry", &expiry_int64, &has_value) ||
(has_value && expiry_int64 < 0))
return Status(kInvalidArgument, "invalid 'expiry'");
// Use negative value to indicate expiry not specified.
expiry = has_value ? static_cast<double>(expiry_int64) : -1.0;
} else {
// JSON wire protocol didn't specify the type of expiry, but ChromeDriver
// has always accepted double, so we keep that in legacy mode.
if (!GetOptionalDouble(*cookie, "expiry", &expiry, &has_value) ||
(has_value && expiry < 0))
return Status(kInvalidArgument, "invalid 'expiry'");
if (!has_value)
expiry = (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() +
kDefaultCookieExpiryTime;
}
return web_view->AddCookie(*name, url, *cookie_value, domain, path, samesite,
secure, http_only, expiry);
}
Status ExecuteDeleteCookie(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* name = params.FindString("name");
if (!name)
return Status(kInvalidArgument, "missing 'name'");
std::unique_ptr<base::Value> value_url;
std::string url;
Status status = GetUrl(web_view, session->GetCurrentFrameId(), &url);
if (status.IsError())
return status;
std::list<Cookie> cookies;
status = GetVisibleCookies(session, web_view, &cookies);
if (status.IsError())
return status;
for (std::list<Cookie>::const_iterator it = cookies.begin();
it != cookies.end(); ++it) {
if (*name == it->name) {
status = web_view->DeleteCookie(it->name, url, it->domain, it->path);
if (status.IsError())
return status;
}
}
return Status(kOk);
}
Status ExecuteDeleteAllCookies(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
std::list<Cookie> cookies;
Status status = GetVisibleCookies(session, web_view, &cookies);
if (status.IsError())
return status;
if (!cookies.empty()) {
std::unique_ptr<base::Value> value_url;
std::string url;
status = GetUrl(web_view, session->GetCurrentFrameId(), &url);
if (status.IsError())
return status;
for (std::list<Cookie>::const_iterator it = cookies.begin();
it != cookies.end(); ++it) {
status = web_view->DeleteCookie(it->name, url, it->domain, it->path);
if (status.IsError())
return status;
}
}
return Status(kOk);
}
Status ExecuteRunBounceTrackingMitigations(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
// Run command and get result
auto result = std::make_unique<base::Value>(base::Value::Type::DICT);
Status status = web_view->SendCommandAndGetResult(
"Storage.runBounceTrackingMitigations", base::Value::Dict(), &result);
if (status.IsError()) {
return status;
}
if (result->GetDict().empty()) {
// The result dictionary should only be empty if there is no bounce tracking
// mitigations service (DIPSService) for the current browser context.
return Status(
kUnsupportedOperation,
"current remote end configuration does not support bounce tracking "
"mitigations");
}
const base::Value::List* deleted_sites =
result->GetDict().FindList("deletedSites");
// create copies of items `deleted_sites` and add them to the output list.
auto site_list = std::make_unique<base::Value>(base::Value::Type::LIST);
for (const base::Value& site : *deleted_sites) {
if (!site.is_string()) {
return Status(kUnknownError,
"DevTools returns a non-string bounce tracker site");
}
site_list->GetList().Append(site.GetString());
}
*value = std::move(site_list);
return Status(kOk);
}
Status ExecuteSetRPHRegistrationMode(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
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.setRPHRegistrationMode", body,
value);
}
Status ExecuteSetLocation(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const base::Value::Dict* location = params.FindDict("location");
Geoposition geoposition;
if (!location)
return Status(kInvalidArgument, "missing or invalid 'location'");
std::optional<double> maybe_latitude = location->FindDouble("latitude");
if (!maybe_latitude.has_value())
return Status(kInvalidArgument, "missing or invalid 'location.latitude'");
geoposition.latitude = maybe_latitude.value();
std::optional<double> maybe_longitude = location->FindDouble("longitude");
if (!maybe_longitude.has_value())
return Status(kInvalidArgument, "missing or invalid 'location.longitude'");
geoposition.longitude = maybe_longitude.value();
// |accuracy| is not part of the WebDriver spec yet, so if it is not given
// default to 100 meters accuracy.
std::optional<double> maybe_accuracy =
ParseDoubleIfInDictionary(*location, "accuracy", 100);
if (!maybe_accuracy.has_value())
return Status(kInvalidArgument, "invalid 'accuracy'");
geoposition.accuracy = maybe_accuracy.value();
Status status = web_view->OverrideGeolocation(geoposition);
if (status.IsOk()) {
session->overridden_geoposition =
std::make_unique<Geoposition>(geoposition);
}
return status;
}
Status ExecuteSetNetworkConditions(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* network_name = params.FindString("network_name");
std::unique_ptr<NetworkConditions> network_conditions(
new NetworkConditions());
if (network_name) {
// Get conditions from preset list.
Status status = FindPresetNetwork(*network_name, network_conditions.get());
if (status.IsError())
return status;
} else if (const base::Value::Dict* conditions =
params.FindDict("network_conditions")) {
// |latency| is required.
std::optional<double> maybe_latency = conditions->FindDouble("latency");
if (!maybe_latency.has_value())
return Status(kInvalidArgument,
"invalid 'network_conditions' is missing 'latency'");
network_conditions->latency = maybe_latency.value();
// Either |throughput| or the pair |download_throughput| and
// |upload_throughput| is required.
if (conditions->Find("throughput")) {
std::optional<double> maybe_throughput =
conditions->FindDouble("throughput");
if (!maybe_throughput.has_value())
return Status(kInvalidArgument, "invalid 'throughput'");
network_conditions->upload_throughput = maybe_throughput.value();
network_conditions->download_throughput = maybe_throughput.value();
} else if (conditions->Find("download_throughput") &&
conditions->Find("upload_throughput")) {
std::optional<double> maybe_download_throughput =
conditions->FindDouble("download_throughput");
std::optional<double> maybe_upload_throughput =
conditions->FindDouble("upload_throughput");
if (!maybe_download_throughput.has_value() ||
!maybe_upload_throughput.has_value())
return Status(kInvalidArgument,
"invalid 'download_throughput' or 'upload_throughput'");
network_conditions->download_throughput =
maybe_download_throughput.value();
network_conditions->upload_throughput = maybe_upload_throughput.value();
} else {
return Status(kInvalidArgument,
"invalid 'network_conditions' is missing 'throughput' or "
"'download_throughput'/'upload_throughput' pair");
}
// |offline| is optional.
if (const base::Value* offline = conditions->Find("offline")) {
if (!offline->is_bool())
return Status(kInvalidArgument, "invalid 'offline'");
network_conditions->offline = offline->GetBool();
} else {
network_conditions->offline = false;
}
} else {
return Status(kInvalidArgument,
"either 'network_conditions' or 'network_name' must be "
"supplied");
}
session->overridden_network_conditions.reset(
network_conditions.release());
return web_view->OverrideNetworkConditions(
*session->overridden_network_conditions);
}
Status ExecuteDeleteNetworkConditions(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
// Chrome does not have any command to stop overriding network conditions, so
// we just override the network conditions with the "No throttling" preset.
NetworkConditions network_conditions;
// Get conditions from preset list.
Status status = FindPresetNetwork("No throttling", &network_conditions);
if (status.IsError())
return status;
status = web_view->OverrideNetworkConditions(network_conditions);
if (status.IsError())
return status;
// After we've successfully overridden the network conditions with
// "No throttling", we can delete them from |session|.
session->overridden_network_conditions.reset();
return status;
}
Status ExecuteTakeHeapSnapshot(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->TakeHeapSnapshot(value);
}
Status ExecuteGetWindowRect(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
Chrome::WindowRect window_rect;
Status status = session->chrome->GetWindowRect(session->window, &window_rect);
if (status.IsError())
return status;
base::Value::Dict rect;
rect.Set("x", window_rect.x);
rect.Set("y", window_rect.y);
rect.Set("width", window_rect.width);
rect.Set("height", window_rect.height);
value->reset(new base::Value(std::move(rect)));
return Status(kOk);
}
Status ExecuteSetWindowRect(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const double max_range = 2147483647; // 2^31 - 1
const double min_range = -2147483648; // -2^31
const base::Value* temp;
double width = 0;
double height = 0;
double x = 0;
double y = 0;
temp = params.Find("x");
bool has_x = temp && !temp->is_none();
if (has_x) {
if (!temp->is_double() && !temp->is_int())
return Status(kInvalidArgument, "'x' must be a number");
x = temp->GetDouble();
if (x > max_range || x < min_range)
return Status(kInvalidArgument, "'x' out of range");
}
temp = params.Find("y");
bool has_y = temp && !temp->is_none();
if (has_y) {
if (!temp->is_double() && !temp->is_int())
return Status(kInvalidArgument, "'y' must be a number");
y = temp->GetDouble();
if (y > max_range || y < min_range)
return Status(kInvalidArgument, "'y' out of range");
}
temp = params.Find("width");
bool has_width = temp && !temp->is_none();
if (has_width) {
if (!temp->is_double() && !temp->is_int())
return Status(kInvalidArgument, "'width' must be a number");
width = temp->GetDouble();
if (width > max_range || width < 0)
return Status(kInvalidArgument, "'width' out of range");
}
temp = params.Find("height");
bool has_height = temp && !temp->is_none();
if (has_height) {
if (!temp->is_double() && !temp->is_int())
return Status(kInvalidArgument, "'height' must be a number");
height = temp->GetDouble();
if (height > max_range || height < 0)
return Status(kInvalidArgument, "'height' out of range");
}
// to pass to the set window rect command
base::Value::Dict rect_params;
// only set position if both x and y are given
if (has_x && has_y) {
rect_params.Set("x", static_cast<int>(x));
rect_params.Set("y", static_cast<int>(y));
} // only set size if both height and width are given
if (has_width && has_height) {
rect_params.Set("width", static_cast<int>(width));
rect_params.Set("height", static_cast<int>(height));
}
Status status = session->chrome->SetWindowRect(session->window, rect_params);
if (status.IsError())
return status;
// return the current window rect
return ExecuteGetWindowRect(session, web_view, params, value, timeout);
}
Status ExecuteMaximizeWindow(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
Status status = session->chrome->MaximizeWindow(session->window);
if (status.IsError())
return status;
return ExecuteGetWindowRect(session, web_view, params, value, timeout);
}
Status ExecuteMinimizeWindow(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
Status status = session->chrome->MinimizeWindow(session->window);
if (status.IsError())
return status;
return ExecuteGetWindowRect(session, web_view, params, value, timeout);
}
Status ExecuteFullScreenWindow(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
Status status = session->chrome->FullScreenWindow(session->window);
if (status.IsError())
return status;
return ExecuteGetWindowRect(session, web_view, params, value, timeout);
}
Status ExecuteSetSinkToUse(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->SendCommand("Cast.setSinkToUse", params);
}
Status ExecuteStartDesktopMirroring(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->SendCommand("Cast.startDesktopMirroring", params);
}
Status ExecuteStartTabMirroring(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->SendCommand("Cast.startTabMirroring", params);
}
Status ExecuteStopCasting(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->SendCommand("Cast.stopCasting", params);
}
Status ExecuteGetSinks(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
*value = web_view->GetCastSinks();
return Status(kOk);
}
Status ExecuteGetIssueMessage(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
*value = web_view->GetCastIssueMessage();
return Status(kOk);
}
Status ExecuteSetPermission(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const base::Value::Dict* descriptor = params.FindDict("descriptor");
if (!descriptor)
return Status(kInvalidArgument, "no descriptor dictionary");
const std::string* name = descriptor->FindString("name");
if (!name)
return Status(kInvalidArgument, "no name in descriptor");
const std::string* permission_state = params.FindString("state");
if (!permission_state)
return Status(kInvalidArgument, "no permission state");
Chrome::PermissionState valid_state;
if (*permission_state == "granted")
valid_state = Chrome::PermissionState::kGranted;
else if (*permission_state == "denied")
valid_state = Chrome::PermissionState::kDenied;
else if (*permission_state == "prompt")
valid_state = Chrome::PermissionState::kPrompt;
else
return Status(kInvalidArgument, "unrecognized permission state");
auto dict = std::make_unique<base::Value::Dict>(descriptor->Clone());
return session->chrome->SetPermission(std::move(dict), valid_state, web_view,
session->GetCurrentFrameId());
}
Status ExecuteSetDevicePosture(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
const std::string* posture = params.FindString("posture");
if (!posture) {
return Status(kInvalidArgument, "'posture' must be a string");
}
base::Value::Dict args;
args.Set("posture", base::Value::Dict().Set("type", *posture));
return web_view->SendCommand("Emulation.setDevicePostureOverride", args);
}
Status ExecuteClearDevicePosture(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->SendCommand("Emulation.clearDevicePostureOverride",
base::Value::Dict());
}
Status ExecuteSetDisplayFeatures(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
bool has_value;
const base::Value::List* features_list = nullptr;
if (!GetOptionalList(params, "features", &features_list, &has_value)) {
return Status(kInvalidArgument, "'features' must be an array");
}
if (!has_value) {
return Status(kInvalidArgument, "'features' must have a value");
}
for (const base::Value& feature : *features_list) {
if (!feature.is_dict()) {
return Status(kInvalidArgument, "a feature must be a dictionary");
}
const auto& feature_dict = feature.GetDict();
std::optional<int> mask = feature_dict.FindInt("maskLength");
if (!mask) {
return Status(kInvalidArgument,
"a feature must contain the maskLength attribute");
} else if (mask.value() < 0) {
return Status(kInvalidArgument,
"a feature must have a positive maskLength attribute");
}
std::optional<int> offset = feature_dict.FindInt("offset");
if (!offset) {
return Status(kInvalidArgument,
"a feature must contain the offset attribute");
} else if (offset.value() < 0) {
return Status(kInvalidArgument,
"a feature must have a positive offset attribute");
}
const std::string* orientation = feature_dict.FindString("orientation");
if (!orientation) {
return Status(kInvalidArgument,
"a feature must contain the orientation attribute");
}
}
return web_view->SendCommand("Emulation.setDisplayFeaturesOverride", params);
}
Status ExecuteClearDisplayFeatures(Session* session,
WebView* web_view,
const base::Value::Dict& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
return web_view->SendCommand("Emulation.clearDisplayFeaturesOverride",
base::Value::Dict());
}