blob: e89292dd673481a46c2093de782f2f95eebd5d31 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/autofill_assistant/browser/web_controller.h"
#include <math.h>
#include <algorithm>
#include <ctime>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/post_task.h"
#include "build/build_config.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/core/browser/autofill_manager.h"
#include "components/autofill/core/browser/credit_card.h"
#include "components/autofill/core/common/autofill_constants.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill_assistant/browser/rectf.h"
#include "components/autofill_assistant/browser/string_conversions_util.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
namespace autofill_assistant {
using autofill::ContentAutofillDriver;
namespace {
// Time between two periodic box model and document ready state checks.
static constexpr base::TimeDelta kPeriodicCheckInterval =
base::TimeDelta::FromMilliseconds(200);
// Timeout after roughly 10 seconds (50*200ms).
constexpr int kPeriodicCheckRounds = 50;
// Expiration time for the Autofill Assistant cookie.
constexpr int kCookieExpiresSeconds = 600;
// Name and value used for the static cookie.
const char* const kAutofillAssistantCookieName = "autofill_assistant_cookie";
const char* const kAutofillAssistantCookieValue = "true";
const char* const kGetBoundingClientRectAsList =
R"(function(node) {
const r = node.getBoundingClientRect();
const v = window.visualViewport;
return [r.left, r.top, r.right, r.bottom,
v.offsetLeft, v.offsetTop, v.width, v.height];
})";
const char* const kScrollIntoViewScript =
R"(function(node) {
const rect = node.getBoundingClientRect();
if (rect.height < window.innerHeight) {
window.scrollBy({top: rect.top - window.innerHeight * 0.25,
behavior: 'smooth'});
} else {
window.scrollBy({top: rect.top, behavior: 'smooth'});
}
node.scrollIntoViewIfNeeded();
})";
const char* const kScrollIntoViewIfNeededScript =
R"(function(node) {
node.scrollIntoViewIfNeeded();
})";
// Javascript to select a value from a select box. Also fires a "change" event
// to trigger any listeners. Changing the index directly does not trigger this.
const char* const kSelectOptionScript =
R"(function(value) {
const uppercaseValue = value.toUpperCase();
var found = false;
for (var i = 0; i < this.options.length; ++i) {
const label = this.options[i].label.toUpperCase();
if (label.length > 0 && label.startsWith(uppercaseValue)) {
this.options.selectedIndex = i;
found = true;
break;
}
}
if (!found) {
return false;
}
const e = document.createEvent('HTMLEvents');
e.initEvent('change', true, true);
this.dispatchEvent(e);
return true;
})";
// Javascript to highlight an element.
const char* const kHighlightElementScript =
R"(function() {
this.style.boxShadow = '0px 0px 0px 3px white, ' +
'0px 0px 0px 6px rgb(66, 133, 244)';
return true;
})";
// Javascript code to retrieve the 'value' attribute of a node.
const char* const kGetValueAttributeScript =
"function () { return this.value; }";
// Javascript code to set the 'value' attribute of a node and then fire a
// "change" event to trigger any listeners.
const char* const kSetValueAttributeScript =
R"(function (value) {
this.value = value;
const e = document.createEvent('HTMLEvents');
e.initEvent('change', true, true);
this.dispatchEvent(e);
})";
// Javascript code to set an attribute of a node to a given value.
const char* const kSetAttributeScript =
R"(function (attribute, value) {
let receiver = this;
for (let i = 0; i < attribute.length - 1; i++) {
receiver = receiver[attribute[i]];
}
receiver[attribute[attribute.length - 1]] = value;
})";
// Javascript code to get the outerHTML of a node.
// TODO(crbug.com/806868): Investigate if using DOM.GetOuterHtml would be a
// better solution than injecting Javascript code.
const char* const kGetOuterHtmlScript =
"function () { return this.outerHTML; }";
// Javascript code to get document root element.
const char* const kGetDocumentElement =
R"(
(function() {
return document.documentElement;
}())
)";
// Javascript code to query an elements for a selector, either the first
// (non-strict) or a single (strict) element.
//
// Returns undefined if no elements are found, TOO_MANY_ELEMENTS (18) if too
// many elements were found and strict mode is enabled.
const char* const kQuerySelector =
R"(function (selector, strictMode) {
var found = this.querySelectorAll(selector);
if(found.length == 0) return undefined;
if(found.length > 1 && strictMode) return 18;
return found[0];
})";
// Javascript code to query a visible elements for a selector, either the first
// (non-strict) or a single (strict) visible element.q
//
// Returns undefined if no elements are found, TOO_MANY_ELEMENTS (18) if too
// many elements were found and strict mode is enabled.
const char* const kQuerySelectorWithConditions =
R"(function (selector, strict, visible, inner_text_str, value_str) {
var found = this.querySelectorAll(selector);
var found_index = -1;
var inner_text_re = inner_text_str ? RegExp(inner_text_str) : undefined;
var value_re = value_str ? RegExp(value_str) : undefined;
var match = function(e) {
if (visible && e.getClientRects().length == 0) return false;
if (inner_text_re && !inner_text_re.test(e.innerText)) return false;
if (value_re && !value_re.test(e.value)) return false;
return true;
};
for (let i = 0; i < found.length; i++) {
if (match(found[i])) {
if (found_index != -1) return 18;
found_index = i;
if (!strict) break;
}
}
return found_index == -1 ? undefined : found[found_index];
})";
// Javascript code to query whether the document is ready for interact.
const char* const kIsDocumentReadyForInteract =
R"(function () {
return document.readyState == 'interactive'
|| document.readyState == 'complete';
})";
bool ConvertPseudoType(const PseudoType pseudo_type,
dom::PseudoType* pseudo_type_output) {
switch (pseudo_type) {
case PseudoType::UNDEFINED:
break;
case PseudoType::FIRST_LINE:
*pseudo_type_output = dom::PseudoType::FIRST_LINE;
return true;
case PseudoType::FIRST_LETTER:
*pseudo_type_output = dom::PseudoType::FIRST_LETTER;
return true;
case PseudoType::BEFORE:
*pseudo_type_output = dom::PseudoType::BEFORE;
return true;
case PseudoType::AFTER:
*pseudo_type_output = dom::PseudoType::AFTER;
return true;
case PseudoType::BACKDROP:
*pseudo_type_output = dom::PseudoType::BACKDROP;
return true;
case PseudoType::SELECTION:
*pseudo_type_output = dom::PseudoType::SELECTION;
return true;
case PseudoType::FIRST_LINE_INHERITED:
*pseudo_type_output = dom::PseudoType::FIRST_LINE_INHERITED;
return true;
case PseudoType::SCROLLBAR:
*pseudo_type_output = dom::PseudoType::SCROLLBAR;
return true;
case PseudoType::SCROLLBAR_THUMB:
*pseudo_type_output = dom::PseudoType::SCROLLBAR_THUMB;
return true;
case PseudoType::SCROLLBAR_BUTTON:
*pseudo_type_output = dom::PseudoType::SCROLLBAR_BUTTON;
return true;
case PseudoType::SCROLLBAR_TRACK:
*pseudo_type_output = dom::PseudoType::SCROLLBAR_TRACK;
return true;
case PseudoType::SCROLLBAR_TRACK_PIECE:
*pseudo_type_output = dom::PseudoType::SCROLLBAR_TRACK_PIECE;
return true;
case PseudoType::SCROLLBAR_CORNER:
*pseudo_type_output = dom::PseudoType::SCROLLBAR_CORNER;
return true;
case PseudoType::RESIZER:
*pseudo_type_output = dom::PseudoType::RESIZER;
return true;
case PseudoType::INPUT_LIST_BUTTON:
*pseudo_type_output = dom::PseudoType::INPUT_LIST_BUTTON;
return true;
}
return false;
}
// Builds a ClientStatus appropriate for an unexpected error.
//
// This should only be used in situations where getting an error cannot be
// anything but a bug in the client.
ClientStatus UnexpectedErrorStatus(const std::string& file, int line) {
ClientStatus status(OTHER_ACTION_STATUS);
auto* info = status.mutable_details()->mutable_unexpected_error_info();
info->set_source_file(file);
info->set_source_line_number(line);
return status;
}
// Builds a ClientStatus appropriate for a JavaScript error.
ClientStatus JavaScriptErrorStatus(const std::string& file,
int line,
const runtime::ExceptionDetails* exception) {
ClientStatus status(UNEXPECTED_JS_ERROR);
auto* info = status.mutable_details()->mutable_unexpected_error_info();
info->set_source_file(file);
info->set_source_line_number(line);
if (exception) {
if (exception->HasException() &&
exception->GetException()->HasClassName()) {
info->set_js_exception_classname(
exception->GetException()->GetClassName());
}
info->set_js_exception_line_number(exception->GetLineNumber());
info->set_js_exception_column_number(exception->GetColumnNumber());
}
return status;
}
// Makes sure that the given EvaluateResult exists, is successful and contain a
// result.
template <typename T>
ClientStatus CheckJavaScriptResult(T* result, const char* file, int line) {
if (!result)
return JavaScriptErrorStatus(file, line, nullptr);
if (result->HasExceptionDetails())
return JavaScriptErrorStatus(file, line, result->GetExceptionDetails());
if (!result->GetResult())
return JavaScriptErrorStatus(file, line, nullptr);
return OkClientStatus();
}
// Safely gets an object id from a RemoteObject
bool SafeGetObjectId(const runtime::RemoteObject* result, std::string* out) {
if (result && result->HasObjectId()) {
*out = result->GetObjectId();
return true;
}
return false;
}
// Safely gets a string value from a RemoteObject
bool SafeGetStringValue(const runtime::RemoteObject* result, std::string* out) {
if (result && result->HasValue() && result->GetValue()->is_string()) {
*out = result->GetValue()->GetString();
return true;
}
return false;
}
// Safely gets a int value from a RemoteObject.
bool SafeGetIntValue(const runtime::RemoteObject* result, int* out) {
if (result && result->HasValue() && result->GetValue()->is_int()) {
*out = result->GetValue()->GetInt();
return true;
}
*out = 0;
return false;
}
// Safely gets a boolean value from a RemoteObject
bool SafeGetBool(const runtime::RemoteObject* result, bool* out) {
if (result && result->HasValue() && result->GetValue()->is_bool()) {
*out = result->GetValue()->GetBool();
return true;
}
*out = false;
return false;
}
} // namespace
class WebController::Worker {
public:
Worker();
virtual ~Worker();
private:
DISALLOW_COPY_AND_ASSIGN(Worker);
};
WebController::Worker::Worker() = default;
WebController::Worker::~Worker() = default;
class WebController::ElementPositionGetter : public WebController::Worker {
public:
ElementPositionGetter();
~ElementPositionGetter() override;
// |devtools_client| must be valid for the lifetime of the instance, which is
// guaranteed since workers are owned by WebController.
void Start(content::RenderFrameHost* frame_host,
DevtoolsClient* devtools_client,
std::string element_object_id,
ElementPositionCallback callback);
private:
void OnVisualStateUpdatedCallback(bool success);
void GetAndWaitBoxModelStable();
void OnGetBoxModelForStableCheck(
std::unique_ptr<dom::GetBoxModelResult> result);
void OnScrollIntoView(std::unique_ptr<runtime::CallFunctionOnResult> result);
void OnResult(int x, int y);
void OnError();
DevtoolsClient* devtools_client_ = nullptr;
std::string object_id_;
int remaining_rounds_ = 0;
ElementPositionCallback callback_;
bool visual_state_updated_ = false;
// If |has_point_| is true, |point_x_| and |point_y_| contain the last
// computed center of the element, in viewport coordinates. Note that
// negative coordinates are valid, in case the element is above or to the
// left of the viewport.
bool has_point_ = false;
int point_x_ = 0;
int point_y_ = 0;
base::WeakPtrFactory<ElementPositionGetter> weak_ptr_factory_;
};
WebController::ElementPositionGetter::ElementPositionGetter()
: weak_ptr_factory_(this) {}
WebController::ElementPositionGetter::~ElementPositionGetter() = default;
void WebController::ElementPositionGetter::Start(
content::RenderFrameHost* frame_host,
DevtoolsClient* devtools_client,
std::string element_object_id,
ElementPositionCallback callback) {
devtools_client_ = devtools_client;
object_id_ = element_object_id;
callback_ = std::move(callback);
remaining_rounds_ = kPeriodicCheckRounds;
// TODO(crbug/806868): Consider using autofill_assistant::RetryTimer
// Wait for a roundtrips through the renderer and compositor pipeline,
// otherwise touch event may be dropped because of missing handler.
// Note that mouse left button will always be send to the renderer, but it
// is slightly better to wait for the changes, like scroll, to be visualized
// in compositor as real interaction.
frame_host->InsertVisualStateCallback(base::BindOnce(
&WebController::ElementPositionGetter::OnVisualStateUpdatedCallback,
weak_ptr_factory_.GetWeakPtr()));
GetAndWaitBoxModelStable();
}
void WebController::ElementPositionGetter::OnVisualStateUpdatedCallback(
bool success) {
if (success) {
visual_state_updated_ = true;
return;
}
OnError();
}
void WebController::ElementPositionGetter::GetAndWaitBoxModelStable() {
devtools_client_->GetDOM()->GetBoxModel(
dom::GetBoxModelParams::Builder().SetObjectId(object_id_).Build(),
base::BindOnce(
&WebController::ElementPositionGetter::OnGetBoxModelForStableCheck,
weak_ptr_factory_.GetWeakPtr()));
}
void WebController::ElementPositionGetter::OnGetBoxModelForStableCheck(
std::unique_ptr<dom::GetBoxModelResult> result) {
if (!result || !result->GetModel() || !result->GetModel()->GetContent()) {
DVLOG(1) << __func__ << " Failed to get box model.";
OnError();
return;
}
// Return the center of the element.
const std::vector<double>* content_box = result->GetModel()->GetContent();
DCHECK_EQ(content_box->size(), 8u);
int new_point_x =
round((round((*content_box)[0]) + round((*content_box)[2])) * 0.5);
int new_point_y =
round((round((*content_box)[3]) + round((*content_box)[5])) * 0.5);
// Wait for at least three rounds (~600ms =
// 3*kPeriodicCheckInterval) for visual state update callback since
// it might take longer time to return or never return if no updates.
DCHECK(kPeriodicCheckRounds > 2 && kPeriodicCheckRounds >= remaining_rounds_);
if (has_point_ && new_point_x == point_x_ && new_point_y == point_y_ &&
(visual_state_updated_ || remaining_rounds_ + 2 < kPeriodicCheckRounds)) {
// Note that there is still a chance that the element's position has been
// changed after the last call of GetBoxModel, however, it might be safe
// to assume the element's position will not be changed before issuing
// click or tap event after stable for kPeriodicCheckInterval. In
// addition, checking again after issuing click or tap event doesn't help
// since the change may be expected.
OnResult(new_point_x, new_point_y);
return;
}
if (remaining_rounds_ <= 0) {
OnError();
return;
}
bool is_first_round = !has_point_;
has_point_ = true;
point_x_ = new_point_x;
point_y_ = new_point_y;
// Scroll the element into view again if it was moved out of view, starting
// from the second round.
if (!is_first_round) {
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(
runtime::CallArgument::Builder().SetObjectId(object_id_).Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(object_id_)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(std::string(kScrollIntoViewIfNeededScript))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::ElementPositionGetter::OnScrollIntoView,
weak_ptr_factory_.GetWeakPtr()));
return;
}
--remaining_rounds_;
base::PostDelayedTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(
&WebController::ElementPositionGetter::GetAndWaitBoxModelStable,
weak_ptr_factory_.GetWeakPtr()),
kPeriodicCheckInterval);
}
void WebController::ElementPositionGetter::OnScrollIntoView(
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to scroll the element: " << status;
OnError();
return;
}
--remaining_rounds_;
base::PostDelayedTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(
&WebController::ElementPositionGetter::GetAndWaitBoxModelStable,
weak_ptr_factory_.GetWeakPtr()),
kPeriodicCheckInterval);
}
void WebController::ElementPositionGetter::OnResult(int x, int y) {
if (callback_) {
std::move(callback_).Run(/* success= */ true, x, y);
}
}
void WebController::ElementPositionGetter::OnError() {
if (callback_) {
std::move(callback_).Run(/* success= */ false, /* x= */ 0, /* y= */ 0);
}
}
class WebController::ElementFinder : public WebController::Worker {
public:
// |devtools_client| and |web_contents| must be valid for the lifetime of the
// instance, which is guaranteed since workers are owned by WebController.
ElementFinder(content::WebContents* web_contents_,
DevtoolsClient* devtools_client,
const Selector& selector,
bool strict);
~ElementFinder() override;
// Finds the element and call the callback.
void Start(FindElementCallback callback_);
private:
void SendResult(const ClientStatus& status);
void OnGetDocumentElement(std::unique_ptr<runtime::EvaluateResult> result);
void RecursiveFindElement(const std::string& object_id, size_t index);
void OnQuerySelectorAll(
size_t index,
std::unique_ptr<runtime::CallFunctionOnResult> result);
void OnDescribeNodeForPseudoElement(
dom::PseudoType pseudo_type,
std::unique_ptr<dom::DescribeNodeResult> result);
void OnResolveNodeForPseudoElement(
std::unique_ptr<dom::ResolveNodeResult> result);
void OnDescribeNode(const std::string& object_id,
size_t index,
std::unique_ptr<dom::DescribeNodeResult> result);
void OnResolveNode(size_t index,
std::unique_ptr<dom::ResolveNodeResult> result);
content::RenderFrameHost* FindCorrespondingRenderFrameHost(
std::string name,
std::string document_url);
content::WebContents* const web_contents_;
DevtoolsClient* const devtools_client_;
const Selector selector_;
const bool strict_;
FindElementCallback callback_;
std::unique_ptr<FindElementResult> element_result_;
base::WeakPtrFactory<ElementFinder> weak_ptr_factory_;
};
WebController::ElementFinder::ElementFinder(content::WebContents* web_contents,
DevtoolsClient* devtools_client,
const Selector& selector,
bool strict)
: web_contents_(web_contents),
devtools_client_(devtools_client),
selector_(selector),
strict_(strict),
element_result_(std::make_unique<FindElementResult>()),
weak_ptr_factory_(this) {}
WebController::ElementFinder::~ElementFinder() = default;
void WebController::ElementFinder::Start(FindElementCallback callback) {
callback_ = std::move(callback);
if (selector_.empty()) {
SendResult(ClientStatus(INVALID_SELECTOR));
return;
}
devtools_client_->GetRuntime()->Evaluate(
std::string(kGetDocumentElement),
base::BindOnce(&WebController::ElementFinder::OnGetDocumentElement,
weak_ptr_factory_.GetWeakPtr()));
}
void WebController::ElementFinder::SendResult(const ClientStatus& status) {
DCHECK(callback_);
DCHECK(element_result_);
std::move(callback_).Run(status, std::move(element_result_));
}
void WebController::ElementFinder::OnGetDocumentElement(
std::unique_ptr<runtime::EvaluateResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to get document root element.";
SendResult(status);
return;
}
std::string object_id;
if (!SafeGetObjectId(result->GetResult(), &object_id)) {
DVLOG(1) << __func__ << " Failed to get document root element.";
SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
return;
}
element_result_->container_frame_host = web_contents_->GetMainFrame();
element_result_->container_frame_selector_index = 0;
element_result_->object_id = "";
RecursiveFindElement(object_id, 0);
}
void WebController::ElementFinder::RecursiveFindElement(
const std::string& object_id,
size_t index) {
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(
base::Value(selector_.selectors[index])))
.Build());
// For finding intermediate elements, strict mode would be more appropriate,
// as long as the logic does not support more than one intermediate match.
//
// TODO(b/129387787): first, add logging to figure out whether it matters and
// decide between strict mode and full support for multiple matching
// intermeditate elements.
argument.emplace_back(
runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(base::Value(strict_)))
.Build());
std::string function;
if (index == (selector_.selectors.size() - 1)) {
if (selector_.must_be_visible || !selector_.inner_text_pattern.empty() ||
!selector_.value_pattern.empty()) {
function.assign(kQuerySelectorWithConditions);
argument.emplace_back(runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(
base::Value(selector_.must_be_visible)))
.Build());
argument.emplace_back(runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(
base::Value(selector_.inner_text_pattern)))
.Build());
argument.emplace_back(runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(
base::Value(selector_.value_pattern)))
.Build());
}
}
if (function.empty()) {
function.assign(kQuerySelector);
}
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(object_id)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(function)
.Build(),
base::BindOnce(&WebController::ElementFinder::OnQuerySelectorAll,
weak_ptr_factory_.GetWeakPtr(), index));
}
void WebController::ElementFinder::OnQuerySelectorAll(
size_t index,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
if (!result) {
// It is possible for a document element to already exist, but not be
// available yet to query because the document hasn't been loaded. This
// results in OnQuerySelectorAll getting a nullptr result. For this specific
// call, it is expected.
DVLOG(1) << __func__ << ": Context doesn't exist yet to query selector "
<< index << " of " << selector_;
SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
return;
}
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok()) {
DVLOG(1) << __func__ << ": Failed to query selector " << index << " of "
<< selector_;
SendResult(status);
return;
}
int int_result;
if (SafeGetIntValue(result->GetResult(), &int_result)) {
DCHECK(int_result == TOO_MANY_ELEMENTS);
SendResult(ClientStatus(TOO_MANY_ELEMENTS));
return;
}
std::string object_id;
if (!SafeGetObjectId(result->GetResult(), &object_id)) {
SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
return;
}
if (selector_.selectors.size() == index + 1) {
// The pseudo type is associated to the final element matched by
// |selector_|, which means that we currently don't handle matching an
// element inside a pseudo element.
if (selector_.pseudo_type == PseudoType::UNDEFINED) {
// Return object id of the element.
element_result_->object_id = object_id;
SendResult(OkClientStatus());
return;
}
// We are looking for a pseudo element associated with this element.
dom::PseudoType pseudo_type;
if (!ConvertPseudoType(selector_.pseudo_type, &pseudo_type)) {
// Return empty result.
SendResult(ClientStatus(INVALID_ACTION));
return;
}
devtools_client_->GetDOM()->DescribeNode(
dom::DescribeNodeParams::Builder().SetObjectId(object_id).Build(),
base::BindOnce(
&WebController::ElementFinder::OnDescribeNodeForPseudoElement,
weak_ptr_factory_.GetWeakPtr(), pseudo_type));
return;
}
devtools_client_->GetDOM()->DescribeNode(
dom::DescribeNodeParams::Builder().SetObjectId(object_id).Build(),
base::BindOnce(&WebController::ElementFinder::OnDescribeNode,
weak_ptr_factory_.GetWeakPtr(), object_id, index));
}
void WebController::ElementFinder::OnDescribeNodeForPseudoElement(
dom::PseudoType pseudo_type,
std::unique_ptr<dom::DescribeNodeResult> result) {
if (!result || !result->GetNode()) {
DVLOG(1) << __func__ << " Failed to describe the node for pseudo element.";
SendResult(UnexpectedErrorStatus(__FILE__, __LINE__));
return;
}
auto* node = result->GetNode();
if (node->HasPseudoElements()) {
for (const auto& pseudo_element : *(node->GetPseudoElements())) {
if (pseudo_element->HasPseudoType() &&
pseudo_element->GetPseudoType() == pseudo_type) {
devtools_client_->GetDOM()->ResolveNode(
dom::ResolveNodeParams::Builder()
.SetBackendNodeId(pseudo_element->GetBackendNodeId())
.Build(),
base::BindOnce(
&WebController::ElementFinder::OnResolveNodeForPseudoElement,
weak_ptr_factory_.GetWeakPtr()));
return;
}
}
}
// Failed to find the pseudo element: run the callback with empty result.
SendResult(ClientStatus(ELEMENT_RESOLUTION_FAILED));
}
void WebController::ElementFinder::OnResolveNodeForPseudoElement(
std::unique_ptr<dom::ResolveNodeResult> result) {
if (result && result->GetObject() && result->GetObject()->HasObjectId()) {
element_result_->object_id = result->GetObject()->GetObjectId();
}
SendResult(OkClientStatus());
}
void WebController::ElementFinder::OnDescribeNode(
const std::string& object_id,
size_t index,
std::unique_ptr<dom::DescribeNodeResult> result) {
if (!result || !result->GetNode()) {
DVLOG(1) << __func__ << " Failed to describe the node.";
SendResult(UnexpectedErrorStatus(__FILE__, __LINE__));
return;
}
auto* node = result->GetNode();
std::vector<int> backend_ids;
if (node->HasContentDocument()) {
backend_ids.emplace_back(node->GetContentDocument()->GetBackendNodeId());
element_result_->container_frame_selector_index = index;
// Find out the corresponding render frame host through document url and
// name.
// TODO(crbug.com/806868): Use more attributes to find out the render frame
// host if name and document url are not enough to uniquely identify it.
std::string frame_name;
if (node->HasAttributes()) {
const std::vector<std::string>* attributes = node->GetAttributes();
for (size_t i = 0; i < attributes->size();) {
if ((*attributes)[i] == "name") {
frame_name = (*attributes)[i + 1];
break;
}
// Jump two positions since attribute name and value are always paired.
i = i + 2;
}
}
element_result_->container_frame_host = FindCorrespondingRenderFrameHost(
frame_name, node->GetContentDocument()->GetDocumentURL());
if (!element_result_->container_frame_host) {
DVLOG(1) << __func__ << " Failed to find corresponding owner frame.";
SendResult(UnexpectedErrorStatus(__FILE__, __LINE__));
return;
}
} else if (node->HasFrameId()) {
// TODO(crbug.com/806868): Support out-of-process iframe.
DVLOG(3) << "Warning (unsupported): the element is inside an OOPIF.";
SendResult(ClientStatus(UNSUPPORTED));
return;
}
if (node->HasShadowRoots()) {
// TODO(crbug.com/806868): Support multiple shadow roots.
backend_ids.emplace_back(
node->GetShadowRoots()->front()->GetBackendNodeId());
}
if (!backend_ids.empty()) {
devtools_client_->GetDOM()->ResolveNode(
dom::ResolveNodeParams::Builder()
.SetBackendNodeId(backend_ids[0])
.Build(),
base::BindOnce(&WebController::ElementFinder::OnResolveNode,
weak_ptr_factory_.GetWeakPtr(), index));
return;
}
RecursiveFindElement(object_id, ++index);
}
void WebController::ElementFinder::OnResolveNode(
size_t index,
std::unique_ptr<dom::ResolveNodeResult> result) {
if (!result || !result->GetObject() || !result->GetObject()->HasObjectId()) {
DVLOG(1) << __func__ << " Failed to resolve object id from backend id.";
SendResult(UnexpectedErrorStatus(__FILE__, __LINE__));
return;
}
RecursiveFindElement(result->GetObject()->GetObjectId(), ++index);
}
content::RenderFrameHost*
WebController::ElementFinder::FindCorrespondingRenderFrameHost(
std::string name,
std::string document_url) {
content::RenderFrameHost* ret_frame = nullptr;
for (auto* frame : web_contents_->GetAllFrames()) {
if (frame->GetFrameName() == name &&
frame->GetLastCommittedURL().spec() == document_url) {
DCHECK(!ret_frame);
ret_frame = frame;
}
}
return ret_frame;
}
// static
std::unique_ptr<WebController> WebController::CreateForWebContents(
content::WebContents* web_contents) {
return std::make_unique<WebController>(
web_contents,
std::make_unique<DevtoolsClient>(
content::DevToolsAgentHost::GetOrCreateFor(web_contents)));
}
WebController::WebController(content::WebContents* web_contents,
std::unique_ptr<DevtoolsClient> devtools_client)
: web_contents_(web_contents),
devtools_client_(std::move(devtools_client)),
weak_ptr_factory_(this) {}
WebController::~WebController() {}
WebController::FillFormInputData::FillFormInputData() {}
WebController::FillFormInputData::~FillFormInputData() {}
void WebController::LoadURL(const GURL& url) {
DVLOG(3) << __func__ << " " << url;
web_contents_->GetController().LoadURLWithParams(
content::NavigationController::LoadURLParams(url));
}
void WebController::ClickOrTapElement(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << " " << selector;
#if defined(OS_ANDROID)
TapElement(selector, std::move(callback));
#else
// TODO(crbug.com/806868): Remove 'ClickElement' since this feature is only
// available on Android.
ClickElement(selector, std::move(callback));
#endif
}
void WebController::ClickElement(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback) {
DCHECK(!selector.empty());
FindElement(selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForClickOrTap,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback), /* is_a_click= */ true));
}
void WebController::TapElement(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback) {
DCHECK(!selector.empty());
FindElement(selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForClickOrTap,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback), /* is_a_click= */ false));
}
void WebController::OnFindElementForClickOrTap(
base::OnceCallback<void(const ClientStatus&)> callback,
bool is_a_click,
const ClientStatus& status,
std::unique_ptr<FindElementResult> result) {
// Found element must belong to a frame.
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to find the element to click or tap.";
std::move(callback).Run(status);
return;
}
std::string element_object_id = result->object_id;
WaitForDocumentToBecomeInteractive(
kPeriodicCheckRounds, element_object_id,
base::BindOnce(
&WebController::OnWaitDocumentToBecomeInteractiveForClickOrTap,
weak_ptr_factory_.GetWeakPtr(), std::move(callback), is_a_click,
std::move(result)));
}
void WebController::OnWaitDocumentToBecomeInteractiveForClickOrTap(
base::OnceCallback<void(const ClientStatus&)> callback,
bool is_a_click,
std::unique_ptr<FindElementResult> target_element,
bool result) {
if (!result) {
std::move(callback).Run(ClientStatus(TIMED_OUT));
return;
}
ClickOrTapElement(std::move(target_element), is_a_click, std::move(callback));
}
void WebController::ClickOrTapElement(
std::unique_ptr<FindElementResult> target_element,
bool is_a_click,
base::OnceCallback<void(const ClientStatus&)> callback) {
std::string element_object_id = target_element->object_id;
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(
runtime::CallArgument::Builder().SetObjectId(element_object_id).Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(element_object_id)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(std::string(kScrollIntoViewScript))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnScrollIntoView,
weak_ptr_factory_.GetWeakPtr(), std::move(target_element),
std::move(callback), is_a_click));
}
void WebController::OnScrollIntoView(
std::unique_ptr<FindElementResult> target_element,
base::OnceCallback<void(const ClientStatus&)> callback,
bool is_a_click,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to scroll the element.";
std::move(callback).Run(status);
return;
}
std::unique_ptr<ElementPositionGetter> getter =
std::make_unique<ElementPositionGetter>();
ElementPositionGetter* getter_ptr = getter.get();
pending_workers_[getter_ptr] = std::move(getter);
getter_ptr->Start(target_element->container_frame_host,
devtools_client_.get(), target_element->object_id,
base::BindOnce(&WebController::TapOrClickOnCoordinates,
weak_ptr_factory_.GetWeakPtr(),
base::Unretained(getter_ptr),
std::move(callback), is_a_click));
}
void WebController::TapOrClickOnCoordinates(
ElementPositionGetter* getter_to_release,
base::OnceCallback<void(const ClientStatus&)> callback,
bool is_a_click,
bool has_coordinates,
int x,
int y) {
pending_workers_.erase(getter_to_release);
if (!has_coordinates) {
DVLOG(1) << __func__ << " Failed to get element position.";
std::move(callback).Run(ClientStatus(ELEMENT_UNSTABLE));
return;
}
if (is_a_click) {
devtools_client_->GetInput()->DispatchMouseEvent(
input::DispatchMouseEventParams::Builder()
.SetX(x)
.SetY(y)
.SetClickCount(1)
.SetButton(input::DispatchMouseEventButton::LEFT)
.SetType(input::DispatchMouseEventType::MOUSE_PRESSED)
.Build(),
base::BindOnce(&WebController::OnDispatchPressMouseEvent,
weak_ptr_factory_.GetWeakPtr(), std::move(callback), x,
y));
return;
}
std::vector<std::unique_ptr<::autofill_assistant::input::TouchPoint>>
touch_points;
touch_points.emplace_back(
input::TouchPoint::Builder().SetX(x).SetY(y).Build());
devtools_client_->GetInput()->DispatchTouchEvent(
input::DispatchTouchEventParams::Builder()
.SetType(input::DispatchTouchEventType::TOUCH_START)
.SetTouchPoints(std::move(touch_points))
.Build(),
base::BindOnce(&WebController::OnDispatchTouchEventStart,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnDispatchPressMouseEvent(
base::OnceCallback<void(const ClientStatus&)> callback,
int x,
int y,
std::unique_ptr<input::DispatchMouseEventResult> result) {
if (!result) {
DVLOG(1) << __func__
<< " Failed to dispatch mouse left button pressed event.";
std::move(callback).Run(UnexpectedErrorStatus(__FILE__, __LINE__));
return;
}
devtools_client_->GetInput()->DispatchMouseEvent(
input::DispatchMouseEventParams::Builder()
.SetX(x)
.SetY(y)
.SetClickCount(1)
.SetButton(input::DispatchMouseEventButton::LEFT)
.SetType(input::DispatchMouseEventType::MOUSE_RELEASED)
.Build(),
base::BindOnce(&WebController::OnDispatchReleaseMouseEvent,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnDispatchReleaseMouseEvent(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<input::DispatchMouseEventResult> result) {
std::move(callback).Run(OkClientStatus());
}
void WebController::OnDispatchTouchEventStart(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<input::DispatchTouchEventResult> result) {
if (!result) {
DVLOG(1) << __func__ << " Failed to dispatch touch start event.";
std::move(callback).Run(UnexpectedErrorStatus(__FILE__, __LINE__));
return;
}
std::vector<std::unique_ptr<::autofill_assistant::input::TouchPoint>>
touch_points;
devtools_client_->GetInput()->DispatchTouchEvent(
input::DispatchTouchEventParams::Builder()
.SetType(input::DispatchTouchEventType::TOUCH_END)
.SetTouchPoints(std::move(touch_points))
.Build(),
base::BindOnce(&WebController::OnDispatchTouchEventEnd,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnDispatchTouchEventEnd(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<input::DispatchTouchEventResult> result) {
DCHECK(result);
std::move(callback).Run(OkClientStatus());
}
void WebController::ElementCheck(const Selector& selector,
bool strict,
base::OnceCallback<void(bool)> callback) {
DCHECK(!selector.empty());
FindElement(
selector, strict,
base::BindOnce(&WebController::OnFindElementForCheck,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnFindElementForCheck(
base::OnceCallback<void(bool)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> result) {
DVLOG_IF(1,
!status.ok() && status.proto_status() != ELEMENT_RESOLUTION_FAILED)
<< __func__ << ": " << status;
std::move(callback).Run(status.ok());
}
void WebController::FindElement(const Selector& selector,
bool strict_mode,
FindElementCallback callback) {
auto finder = std::make_unique<ElementFinder>(
web_contents_, devtools_client_.get(), selector, strict_mode);
ElementFinder* ptr = finder.get();
pending_workers_[ptr] = std::move(finder);
ptr->Start(base::BindOnce(&WebController::OnFindElementResult,
base::Unretained(this), ptr, std::move(callback)));
}
void WebController::OnFindElementResult(
ElementFinder* finder_to_release,
FindElementCallback callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> result) {
pending_workers_.erase(finder_to_release);
std::move(callback).Run(status, std::move(result));
}
void WebController::OnFindElementForFocusElement(
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to find the element to focus on.";
std::move(callback).Run(status);
return;
}
std::string element_object_id = element_result->object_id;
WaitForDocumentToBecomeInteractive(
kPeriodicCheckRounds, element_object_id,
base::BindOnce(
&WebController::OnWaitDocumentToBecomeInteractiveForFocusElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
std::move(element_result)));
}
void WebController::OnWaitDocumentToBecomeInteractiveForFocusElement(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<FindElementResult> target_element,
bool result) {
if (!result) {
std::move(callback).Run(ClientStatus(ELEMENT_UNSTABLE));
return;
}
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(runtime::CallArgument::Builder()
.SetObjectId(target_element->object_id)
.Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(target_element->object_id)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(std::string(kScrollIntoViewScript))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnFocusElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnFocusElement(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
DVLOG_IF(1, !status.ok()) << __func__ << " Failed to focus on element.";
std::move(callback).Run(status);
}
void WebController::FillAddressForm(
const autofill::AutofillProfile* profile,
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << selector;
auto data_to_autofill = std::make_unique<FillFormInputData>();
data_to_autofill->profile =
std::make_unique<autofill::AutofillProfile>(*profile);
FindElement(selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForFillingForm,
weak_ptr_factory_.GetWeakPtr(),
std::move(data_to_autofill), selector,
std::move(callback)));
}
void WebController::OnFindElementForFillingForm(
std::unique_ptr<FillFormInputData> data_to_autofill,
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to find the element for filling the form.";
std::move(callback).Run(status);
return;
}
ContentAutofillDriver* driver = ContentAutofillDriver::GetForRenderFrameHost(
element_result->container_frame_host);
DCHECK(!selector.empty());
// TODO(crbug.com/806868): Figure out whether there are cases where we need
// more than one selector, and come up with a solution that can figure out the
// right number of selectors to include.
driver->GetAutofillAgent()->GetElementFormAndFieldData(
std::vector<std::string>(1, selector.selectors.back()),
base::BindOnce(&WebController::OnGetFormAndFieldDataForFillingForm,
weak_ptr_factory_.GetWeakPtr(),
std::move(data_to_autofill), std::move(callback),
element_result->container_frame_host));
}
void WebController::OnGetFormAndFieldDataForFillingForm(
std::unique_ptr<FillFormInputData> data_to_autofill,
base::OnceCallback<void(const ClientStatus&)> callback,
content::RenderFrameHost* container_frame_host,
const autofill::FormData& form_data,
const autofill::FormFieldData& form_field) {
if (form_data.fields.empty()) {
DVLOG(1) << __func__ << " Failed to get form data to fill form.";
std::move(callback).Run(
UnexpectedErrorStatus(__FILE__, __LINE__)); // unexpected
return;
}
ContentAutofillDriver* driver =
ContentAutofillDriver::GetForRenderFrameHost(container_frame_host);
if (!driver) {
DVLOG(1) << __func__ << " Failed to get the autofill driver.";
std::move(callback).Run(
UnexpectedErrorStatus(__FILE__, __LINE__)); // unexpected
return;
}
if (data_to_autofill->card) {
driver->autofill_manager()->FillCreditCardForm(
autofill::kNoQueryId, form_data, form_field, *data_to_autofill->card,
data_to_autofill->cvc);
} else {
driver->autofill_manager()->FillProfileForm(*data_to_autofill->profile,
form_data, form_field);
}
std::move(callback).Run(OkClientStatus());
}
void WebController::FillCardForm(
std::unique_ptr<autofill::CreditCard> card,
const base::string16& cvc,
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << " " << selector;
auto data_to_autofill = std::make_unique<FillFormInputData>();
data_to_autofill->card = std::move(card);
data_to_autofill->cvc = cvc;
FindElement(selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForFillingForm,
weak_ptr_factory_.GetWeakPtr(),
std::move(data_to_autofill), selector,
std::move(callback)));
}
void WebController::SelectOption(
const Selector& selector,
const std::string& selected_option,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << " " << selector << ", option=" << selected_option;
FindElement(selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForSelectOption,
weak_ptr_factory_.GetWeakPtr(), selected_option,
std::move(callback)));
}
void WebController::OnFindElementForSelectOption(
const std::string& selected_option,
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to find the element to select an option.";
std::move(callback).Run(status);
return;
}
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(
runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(base::Value(selected_option)))
.Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(element_result->object_id)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(std::string(kSelectOptionScript))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnSelectOption,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnSelectOption(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to select option.";
std::move(callback).Run(status);
return;
}
bool found;
if (!SafeGetBool(result->GetResult(), &found)) {
std::move(callback).Run(UnexpectedErrorStatus(__FILE__, __LINE__));
return;
}
if (!found) {
DVLOG(1) << __func__ << " Failed to find option.";
std::move(callback).Run(ClientStatus(OPTION_VALUE_NOT_FOUND));
return;
}
std::move(callback).Run(OkClientStatus());
}
void WebController::HighlightElement(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << " " << selector;
FindElement(
selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForHighlightElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnFindElementForHighlightElement(
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
DVLOG(1) << __func__ << " Failed to find the element to highlight.";
std::move(callback).Run(status);
return;
}
const std::string& object_id = element_result->object_id;
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(
runtime::CallArgument::Builder().SetObjectId(object_id).Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(object_id)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(std::string(kHighlightElementScript))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnHighlightElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnHighlightElement(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
DVLOG_IF(1, !status.ok()) << __func__ << " Failed to highlight element.";
std::move(callback).Run(status);
}
void WebController::FocusElement(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << " " << selector;
DCHECK(!selector.empty());
FindElement(
selector,
/* strict_mode= */ false,
base::BindOnce(&WebController::OnFindElementForFocusElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::GetFieldValue(
const Selector& selector,
base::OnceCallback<void(bool, const std::string&)> callback) {
FindElement(
selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForGetFieldValue,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnFindElementForGetFieldValue(
base::OnceCallback<void(bool, const std::string&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
const std::string object_id = element_result->object_id;
if (!status.ok()) {
std::move(callback).Run(/* exists= */ false, "");
return;
}
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(object_id)
.SetFunctionDeclaration(std::string(kGetValueAttributeScript))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnGetValueAttribute,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnGetValueAttribute(
base::OnceCallback<void(bool, const std::string&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
std::string value;
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
// Read the result returned from Javascript code.
DVLOG_IF(1, !status.ok())
<< __func__ << "Failed to get attribute value: " << status;
SafeGetStringValue(result->GetResult(), &value);
std::move(callback).Run(/* exists= */ true, value);
}
void WebController::SetFieldValue(
const Selector& selector,
const std::string& value,
bool simulate_key_presses,
int key_press_delay_in_millisecond,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << " " << selector << ", value=" << value
<< ", simulate_key_presses=" << simulate_key_presses;
if (simulate_key_presses) {
// We first clear the field value, and then simulate the key presses.
// TODO(crbug.com/806868): Disable keyboard during this action and then
// reset to previous state.
InternalSetFieldValue(
selector, "",
base::BindOnce(&WebController::OnClearFieldForSendKeyboardInput,
weak_ptr_factory_.GetWeakPtr(), selector,
UTF8ToUnicode(value), key_press_delay_in_millisecond,
std::move(callback)));
return;
}
InternalSetFieldValue(selector, value, std::move(callback));
}
void WebController::InternalSetFieldValue(
const Selector& selector,
const std::string& value,
base::OnceCallback<void(const ClientStatus&)> callback) {
FindElement(selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForSetFieldValue,
weak_ptr_factory_.GetWeakPtr(), value,
std::move(callback)));
}
void WebController::OnClearFieldForSendKeyboardInput(
const Selector& selector,
const std::vector<UChar32>& codepoints,
int key_press_delay_in_millisecond,
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& clear_status) {
if (!clear_status.ok()) {
std::move(callback).Run(clear_status);
return;
}
SendKeyboardInput(selector, codepoints, key_press_delay_in_millisecond,
std::move(callback));
}
void WebController::OnClickElementForSendKeyboardInput(
const std::vector<UChar32>& codepoints,
int delay_in_millisecond,
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& click_status) {
if (!click_status.ok()) {
std::move(callback).Run(click_status);
return;
}
DispatchKeyboardTextDownEvent(codepoints, 0, /*delay=*/false,
delay_in_millisecond, std::move(callback));
}
void WebController::DispatchKeyboardTextDownEvent(
const std::vector<UChar32>& codepoints,
size_t index,
bool delay,
int delay_in_millisecond,
base::OnceCallback<void(const ClientStatus&)> callback) {
if (index >= codepoints.size()) {
std::move(callback).Run(OkClientStatus());
return;
}
if (delay && delay_in_millisecond > 0) {
base::PostDelayedTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&WebController::DispatchKeyboardTextDownEvent,
weak_ptr_factory_.GetWeakPtr(), codepoints, index,
/*delay=*/false, delay_in_millisecond,
std::move(callback)),
base::TimeDelta::FromMilliseconds(delay_in_millisecond));
return;
}
devtools_client_->GetInput()->DispatchKeyEvent(
CreateKeyEventParamsForCharacter(
autofill_assistant::input::DispatchKeyEventType::KEY_DOWN,
codepoints[index]),
base::BindOnce(&WebController::DispatchKeyboardTextUpEvent,
weak_ptr_factory_.GetWeakPtr(), codepoints, index,
delay_in_millisecond, std::move(callback)));
}
void WebController::DispatchKeyboardTextUpEvent(
const std::vector<UChar32>& codepoints,
size_t index,
int delay_in_millisecond,
base::OnceCallback<void(const ClientStatus&)> callback) {
DCHECK_LT(index, codepoints.size());
devtools_client_->GetInput()->DispatchKeyEvent(
CreateKeyEventParamsForCharacter(
autofill_assistant::input::DispatchKeyEventType::KEY_UP,
codepoints[index]),
base::BindOnce(&WebController::DispatchKeyboardTextDownEvent,
weak_ptr_factory_.GetWeakPtr(), codepoints, index + 1,
/*delay=*/true, delay_in_millisecond,
std::move(callback)));
}
auto WebController::CreateKeyEventParamsForCharacter(
autofill_assistant::input::DispatchKeyEventType type,
UChar32 codepoint) -> DispatchKeyEventParamsPtr {
auto params = input::DispatchKeyEventParams::Builder().SetType(type).Build();
std::string text;
if (AppendUnicodeToUTF8(codepoint, &text)) {
params->SetText(text);
} else {
DVLOG(1) << __func__
<< ": Failed to convert codepoint to UTF-8: " << codepoint;
}
auto dom_key = ui::DomKey::FromCharacter(codepoint);
if (dom_key.IsValid()) {
params->SetKey(ui::KeycodeConverter::DomKeyToKeyString(dom_key));
} else {
DVLOG(1) << __func__
<< ": Failed to set DomKey for codepoint: " << codepoint;
}
return params;
}
void WebController::OnFindElementForSetFieldValue(
const std::string& value,
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
std::move(callback).Run(status);
return;
}
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(
runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(base::Value(value)))
.Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(element_result->object_id)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(std::string(kSetValueAttributeScript))
.Build(),
base::BindOnce(&WebController::OnSetValueAttribute,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnSetValueAttribute(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
std::move(callback).Run(
CheckJavaScriptResult(result.get(), __FILE__, __LINE__));
}
void WebController::SetAttribute(
const Selector& selector,
const std::vector<std::string>& attribute,
const std::string& value,
base::OnceCallback<void(const ClientStatus&)> callback) {
DVLOG(3) << __func__ << " " << selector << ", attribute=["
<< base::JoinString(attribute, ",") << "], value=" << value;
DCHECK(!selector.empty());
DCHECK_GT(attribute.size(), 0u);
FindElement(selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForSetAttribute,
weak_ptr_factory_.GetWeakPtr(), attribute, value,
std::move(callback)));
}
void WebController::OnFindElementForSetAttribute(
const std::vector<std::string>& attribute,
const std::string& value,
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
std::move(callback).Run(status);
return;
}
base::Value::ListStorage attribute_values;
for (const std::string& string : attribute) {
attribute_values.emplace_back(base::Value(string));
}
std::vector<std::unique_ptr<runtime::CallArgument>> arguments;
arguments.emplace_back(runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(
base::Value(attribute_values)))
.Build());
arguments.emplace_back(
runtime::CallArgument::Builder()
.SetValue(base::Value::ToUniquePtrValue(base::Value(value)))
.Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(element_result->object_id)
.SetArguments(std::move(arguments))
.SetFunctionDeclaration(std::string(kSetAttributeScript))
.Build(),
base::BindOnce(&WebController::OnSetAttribute,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnSetAttribute(
base::OnceCallback<void(const ClientStatus&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
std::move(callback).Run(
CheckJavaScriptResult(result.get(), __FILE__, __LINE__));
}
void WebController::SendKeyboardInput(
const Selector& selector,
const std::vector<UChar32>& codepoints,
const int delay_in_millisecond,
base::OnceCallback<void(const ClientStatus&)> callback) {
if (VLOG_IS_ON(3)) {
std::string input_str;
if (!UnicodeToUTF8(codepoints, &input_str)) {
input_str.assign("<invalid input>");
}
DVLOG(3) << __func__ << " " << selector << ", input=" << input_str;
}
DCHECK(!selector.empty());
FindElement(
selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForSendKeyboardInput,
weak_ptr_factory_.GetWeakPtr(), selector, codepoints,
delay_in_millisecond, std::move(callback)));
}
void WebController::OnFindElementForSendKeyboardInput(
const Selector& selector,
const std::vector<UChar32>& codepoints,
const int delay_in_millisecond,
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
std::move(callback).Run(status);
return;
}
ClickElement(selector, base::BindOnce(
&WebController::OnClickElementForSendKeyboardInput,
weak_ptr_factory_.GetWeakPtr(), codepoints,
delay_in_millisecond, std::move(callback)));
}
void WebController::GetOuterHtml(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&, const std::string&)>
callback) {
DVLOG(3) << __func__ << " " << selector;
FindElement(
selector,
/* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForGetOuterHtml,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::GetElementPosition(
const Selector& selector,
base::OnceCallback<void(bool, const RectF&)> callback) {
FindElement(
selector, /* strict_mode= */ true,
base::BindOnce(&WebController::OnFindElementForPosition,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnFindElementForPosition(
base::OnceCallback<void(bool, const RectF&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> result) {
if (!status.ok()) {
RectF empty;
std::move(callback).Run(false, empty);
return;
}
std::vector<std::unique_ptr<runtime::CallArgument>> argument;
argument.emplace_back(
runtime::CallArgument::Builder().SetObjectId(result->object_id).Build());
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(result->object_id)
.SetArguments(std::move(argument))
.SetFunctionDeclaration(std::string(kGetBoundingClientRectAsList))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnGetElementPositionResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnGetElementPositionResult(
base::OnceCallback<void(bool, const RectF&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok() || !result->GetResult()->GetValue() ||
!result->GetResult()->GetValue()->is_list() ||
result->GetResult()->GetValue()->GetList().size() != 8u) {
RectF empty;
std::move(callback).Run(false, empty);
return;
}
const auto& list = result->GetResult()->GetValue()->GetList();
// Value::GetDouble() is safe to call without checking the value type; it'll
// return 0.0 if the value has the wrong type.
// getBoundingClientRect returns coordinates in the layout viewport. They need
// to be transformed into coordinates in the visual viewport, between 0 and 1.
float left_layout = static_cast<float>(list[0].GetDouble());
float top_layout = static_cast<float>(list[1].GetDouble());
float right_layout = static_cast<float>(list[2].GetDouble());
float bottom_layout = static_cast<float>(list[3].GetDouble());
float visual_left_offset = static_cast<float>(list[4].GetDouble());
float visual_top_offset = static_cast<float>(list[5].GetDouble());
float visual_w = static_cast<float>(list[6].GetDouble());
float visual_h = static_cast<float>(list[7].GetDouble());
RectF rect;
rect.left = (left_layout - visual_left_offset) / visual_w;
rect.top = (top_layout - visual_top_offset) / visual_h;
rect.right = (right_layout - visual_left_offset) / visual_w;
rect.bottom = (bottom_layout - visual_top_offset) / visual_h;
std::move(callback).Run(true, rect);
}
void WebController::OnFindElementForGetOuterHtml(
base::OnceCallback<void(const ClientStatus&, const std::string&)> callback,
const ClientStatus& status,
std::unique_ptr<FindElementResult> element_result) {
if (!status.ok()) {
DVLOG(2) << __func__ << " Failed to find element for GetOuterHtml";
std::move(callback).Run(status, "");
return;
}
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(element_result->object_id)
.SetFunctionDeclaration(std::string(kGetOuterHtmlScript))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnGetOuterHtml,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnGetOuterHtml(
base::OnceCallback<void(const ClientStatus&, const std::string&)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok()) {
DVLOG(2) << __func__ << " Failed to get HTML content for GetOuterHtml";
std::move(callback).Run(status, "");
return;
}
std::string value;
SafeGetStringValue(result->GetResult(), &value);
std::move(callback).Run(OkClientStatus(), value);
}
void WebController::SetCookie(const std::string& domain,
base::OnceCallback<void(bool)> callback) {
DVLOG(3) << __func__ << " domain=" << domain;
DCHECK(!domain.empty());
auto expires_seconds =
std::chrono::seconds(std::time(nullptr)).count() + kCookieExpiresSeconds;
devtools_client_->GetNetwork()->SetCookie(
network::SetCookieParams::Builder()
.SetName(kAutofillAssistantCookieName)
.SetValue(kAutofillAssistantCookieValue)
.SetDomain(domain)
.SetExpires(expires_seconds)
.Build(),
base::BindOnce(&WebController::OnSetCookie,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnSetCookie(
base::OnceCallback<void(bool)> callback,
std::unique_ptr<network::SetCookieResult> result) {
std::move(callback).Run(result && result->GetSuccess());
}
void WebController::HasCookie(base::OnceCallback<void(bool)> callback) {
DVLOG(3) << __func__;
devtools_client_->GetNetwork()->GetCookies(
base::BindOnce(&WebController::OnHasCookie,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebController::OnHasCookie(
base::OnceCallback<void(bool)> callback,
std::unique_ptr<network::GetCookiesResult> result) {
if (!result) {
std::move(callback).Run(false);
return;
}
const auto& cookies = *result->GetCookies();
for (const auto& cookie : cookies) {
if (cookie->GetName() == kAutofillAssistantCookieName &&
cookie->GetValue() == kAutofillAssistantCookieValue) {
std::move(callback).Run(true);
return;
}
}
std::move(callback).Run(false);
}
void WebController::ClearCookie() {
DVLOG(3) << __func__;
devtools_client_->GetNetwork()->DeleteCookies(kAutofillAssistantCookieName,
base::DoNothing());
}
void WebController::WaitForDocumentToBecomeInteractive(
int remaining_rounds,
std::string object_id,
base::OnceCallback<void(bool)> callback) {
devtools_client_->GetRuntime()->CallFunctionOn(
runtime::CallFunctionOnParams::Builder()
.SetObjectId(object_id)
.SetFunctionDeclaration(std::string(kIsDocumentReadyForInteract))
.SetReturnByValue(true)
.Build(),
base::BindOnce(&WebController::OnWaitForDocumentToBecomeInteractive,
weak_ptr_factory_.GetWeakPtr(), remaining_rounds,
object_id, std::move(callback)));
}
void WebController::OnWaitForDocumentToBecomeInteractive(
int remaining_rounds,
std::string object_id,
base::OnceCallback<void(bool)> callback,
std::unique_ptr<runtime::CallFunctionOnResult> result) {
ClientStatus status = CheckJavaScriptResult(result.get(), __FILE__, __LINE__);
if (!status.ok() || remaining_rounds <= 0) {
DVLOG(1) << __func__
<< " Failed to wait for the document to become interactive with "
"remaining_rounds: "
<< remaining_rounds;
std::move(callback).Run(false);
return;
}
bool ready;
if (SafeGetBool(result->GetResult(), &ready) && ready) {
std::move(callback).Run(true);
return;
}
base::PostDelayedTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&WebController::WaitForDocumentToBecomeInteractive,
weak_ptr_factory_.GetWeakPtr(), --remaining_rounds,
object_id, std::move(callback)),
kPeriodicCheckInterval);
}
} // namespace autofill_assistant