blob: 87cb479d4641e15cde12cd4a00581a965dae0dd3 [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/actions/fallback_handler/required_fields_fallback_handler.h"
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/strings/strcat.h"
#include "components/autofill_assistant/browser/actions/action_delegate.h"
#include "components/autofill_assistant/browser/actions/action_delegate_util.h"
#include "components/autofill_assistant/browser/batch_element_checker.h"
#include "components/autofill_assistant/browser/client_status.h"
#include "components/autofill_assistant/browser/field_formatter.h"
#include "components/autofill_assistant/browser/web/element_finder.h"
#include "components/autofill_assistant/browser/web/web_controller.h"
#include "third_party/re2/src/re2/re2.h"
namespace autofill_assistant {
namespace {
const char kSelectElementTag[] = "SELECT";
AutofillErrorInfoProto::AutofillFieldError* AddAutofillError(
const RequiredField& required_field,
ClientStatus* client_status) {
auto* field_error = client_status->mutable_details()
->mutable_autofill_error_info()
->add_autofill_field_error();
*field_error->mutable_field() = required_field.selector.proto;
field_error->set_value_expression(
field_formatter::GetHumanReadableValueExpression(
required_field.proto.value_expression()));
return field_error;
}
void FillStatusDetailsWithMissingFallbackData(
const RequiredField& required_field,
ClientStatus* client_status) {
auto* field_error = AddAutofillError(required_field, client_status);
field_error->set_no_fallback_value(true);
}
void FillStatusDetailsWithError(const RequiredField& required_field,
ProcessedActionStatusProto error_status,
ClientStatus* client_status) {
auto* field_error = AddAutofillError(required_field, client_status);
field_error->set_status(error_status);
}
void FillStatusDetailsWithEmptyField(const RequiredField& required_field,
ClientStatus* client_status) {
auto* field_error = AddAutofillError(required_field, client_status);
field_error->set_empty_after_fallback(true);
}
void FillStatusDetailsWithNotClearedField(const RequiredField& required_field,
ClientStatus* client_status) {
auto* field_error = AddAutofillError(required_field, client_status);
field_error->set_filled_after_clear(true);
}
ClientStatus& ErrorStatusWithDefault(ClientStatus& status) {
if (status.ok()) {
status.set_proto_status(AUTOFILL_INCOMPLETE);
}
return status;
}
ClientStatus GetRe2Value(const RequiredField& required_field,
const std::map<std::string, std::string>& mappings,
bool use_contains,
std::string* re2_value,
bool* case_sensitive) {
if (required_field.proto.has_option_comparison_value_expression_re2()) {
ClientStatus status = field_formatter::FormatExpression(
required_field.proto.option_comparison_value_expression_re2()
.value_expression(),
mappings,
/* quote_meta= */ true, re2_value);
if (!status.ok()) {
return status;
}
*case_sensitive =
required_field.proto.option_comparison_value_expression_re2()
.case_sensitive();
return OkClientStatus();
}
std::string re2;
ClientStatus status =
field_formatter::FormatExpression(required_field.proto.value_expression(),
mappings, /* quote_meta= */ true, &re2);
if (!status.ok()) {
return status;
}
if (use_contains) {
re2_value->assign(re2);
} else {
switch (required_field.proto.select_strategy()) {
case UNSPECIFIED_SELECT_STRATEGY:
case LABEL_STARTS_WITH:
// This is the legacy default.
re2_value->assign(base::StrCat({"^", re2}));
break;
case VALUE_MATCH:
case LABEL_MATCH:
re2_value->assign(base::StrCat({"^", re2, "$"}));
break;
}
}
*case_sensitive = false;
return OkClientStatus();
}
} // namespace
RequiredFieldsFallbackHandler::~RequiredFieldsFallbackHandler() = default;
RequiredFieldsFallbackHandler::RequiredFieldsFallbackHandler(
const std::vector<RequiredField>& required_fields,
const std::map<std::string, std::string>& fallback_values,
ActionDelegate* delegate)
: required_fields_(required_fields),
fallback_values_(fallback_values),
action_delegate_(delegate) {}
void RequiredFieldsFallbackHandler::CheckAndFallbackRequiredFields(
const ClientStatus& initial_autofill_status,
base::OnceCallback<void(const ClientStatus&)> status_update_callback) {
client_status_ = initial_autofill_status;
status_update_callback_ = std::move(status_update_callback);
if (required_fields_.empty()) {
if (!initial_autofill_status.ok()) {
VLOG(1) << __func__ << " Autofill failed and no fallback provided "
<< initial_autofill_status.proto_status();
}
std::move(status_update_callback_).Run(initial_autofill_status);
return;
}
if (!client_status_.ok()) {
client_status_.mutable_details()
->mutable_autofill_error_info()
->set_autofill_error_status(client_status_.proto_status());
}
CheckAllRequiredFields(/* apply_fallback = */ true);
}
void RequiredFieldsFallbackHandler::CheckAllRequiredFields(
bool apply_fallback) {
DCHECK(!batch_element_checker_);
batch_element_checker_ = std::make_unique<BatchElementChecker>();
for (size_t i = 0; i < required_fields_.size(); i++) {
// First run (with fallback) we skip checking forced fields, since we
// overwrite them anyway. Second run (without fallback) forced fields should
// be checked.
if (required_fields_[i].proto.forced() && apply_fallback) {
continue;
}
// We cannot check the value of elements with custom fallback clicks. Those
// elements are JS driven structures that in most cases lack a "value"
// attribute. We define a successful click on the element as successfully
// filling the form field.
if (required_fields_[i].proto.has_option_element_to_click()) {
continue;
}
batch_element_checker_->AddFieldValueCheck(
required_fields_[i].selector,
base::BindOnce(&RequiredFieldsFallbackHandler::OnGetRequiredFieldValue,
weak_ptr_factory_.GetWeakPtr(), i));
}
batch_element_checker_->AddAllDoneCallback(
base::BindOnce(&RequiredFieldsFallbackHandler::OnCheckRequiredFieldsDone,
weak_ptr_factory_.GetWeakPtr(), apply_fallback));
action_delegate_->RunElementChecks(batch_element_checker_.get());
}
void RequiredFieldsFallbackHandler::OnGetRequiredFieldValue(
size_t required_fields_index,
const ClientStatus& element_status,
const std::string& value) {
required_fields_[required_fields_index].status =
value.empty() ? RequiredField::EMPTY : RequiredField::NOT_EMPTY;
}
void RequiredFieldsFallbackHandler::OnCheckRequiredFieldsDone(
bool apply_fallback) {
batch_element_checker_.reset();
// We process all fields with an empty value in order to perform the fallback
// on all those fields, if any.
bool should_fallback = false;
for (const RequiredField& required_field : required_fields_) {
if (required_field.ShouldFallback(apply_fallback)) {
should_fallback = true;
if (!apply_fallback) {
if (required_field.proto.value_expression().chunk().empty()) {
VLOG(1) << "Field was filled after attempting to clear it: "
<< required_field.selector;
FillStatusDetailsWithNotClearedField(required_field, &client_status_);
} else {
VLOG(1) << "Field was empty after applying fallback: "
<< required_field.selector;
FillStatusDetailsWithEmptyField(required_field, &client_status_);
}
}
}
}
if (!should_fallback) {
std::move(status_update_callback_).Run(client_status_);
return;
}
if (!apply_fallback) {
// Validation failed and we don't want to try the fallback.
std::move(status_update_callback_)
.Run(ErrorStatusWithDefault(client_status_));
return;
}
for (const RequiredField& required_field : required_fields_) {
if (required_field.proto.value_expression().chunk().empty() ||
!required_field.ShouldFallback(/* apply_fallback= */ true)) {
continue;
}
std::string tmp;
if (!field_formatter::FormatExpression(
required_field.proto.value_expression(), fallback_values_,
/* quote_meta= */ false, &tmp)
.ok()) {
DVLOG(3) << "Field has no fallback data: " << required_field.selector
<< " " << required_field.proto.value_expression();
FillStatusDetailsWithMissingFallbackData(required_field, &client_status_);
}
}
// Set the fallback values and check again.
SetFallbackFieldValuesSequentially(0);
}
void RequiredFieldsFallbackHandler::SetFallbackFieldValuesSequentially(
size_t required_fields_index) {
// Skip non-empty fields.
while (required_fields_index < required_fields_.size() &&
!required_fields_[required_fields_index].ShouldFallback(
/* apply_fallback= */ true)) {
required_fields_index++;
}
// If there are no more fields to set, check the required fields again,
// but this time we don't want to try the fallback in case of failure.
if (required_fields_index >= required_fields_.size()) {
DCHECK_EQ(required_fields_index, required_fields_.size());
CheckAllRequiredFields(/* apply_fallback= */ false);
return;
}
// Treat the next field.
const RequiredField& required_field = required_fields_[required_fields_index];
base::OnceCallback<void()> set_next_field = base::BindOnce(
&RequiredFieldsFallbackHandler::SetFallbackFieldValuesSequentially,
weak_ptr_factory_.GetWeakPtr(), required_fields_index + 1);
std::string fallback_value;
ClientStatus format_status = field_formatter::FormatExpression(
required_field.proto.value_expression(), fallback_values_,
/* quote_meta= */ false, &fallback_value);
if (!format_status.ok()) {
// Skip optional field, fail otherwise.
if (required_field.proto.is_optional()) {
std::move(set_next_field).Run();
} else {
std::move(status_update_callback_)
.Run(ErrorStatusWithDefault(client_status_));
}
return;
}
if (required_field.proto.has_option_element_to_click()) {
FillJsDrivenDropdown(fallback_value, required_field,
std::move(set_next_field));
} else {
FillFormField(fallback_value, required_field, std::move(set_next_field));
}
}
void RequiredFieldsFallbackHandler::FillFormField(
const std::string& value,
const RequiredField& required_field,
base::OnceCallback<void()> set_next_field) {
action_delegate_->FindElement(
required_field.selector,
base::BindOnce(&RequiredFieldsFallbackHandler::OnFindElement,
weak_ptr_factory_.GetWeakPtr(), value, required_field,
std::move(set_next_field)));
}
void RequiredFieldsFallbackHandler::OnFindElement(
const std::string& value,
const RequiredField& required_field,
base::OnceCallback<void()> set_next_field,
const ClientStatus& element_status,
std::unique_ptr<ElementFinder::Result> element_result) {
if (!element_status.ok()) {
FillStatusDetailsWithError(required_field, element_status.proto_status(),
&client_status_);
// Element to operate on does not exist. We either continue or stop the
// script without checking the other fields.
if (required_field.proto.is_optional() &&
element_status.proto_status() == ELEMENT_RESOLUTION_FAILED) {
std::move(set_next_field).Run();
} else {
std::move(status_update_callback_)
.Run(ErrorStatusWithDefault(client_status_));
}
return;
}
const ElementFinder::Result* element_result_ptr = element_result.get();
action_delegate_->GetWebController()->GetElementTag(
*element_result_ptr,
base::BindOnce(
&RequiredFieldsFallbackHandler::OnGetFallbackFieldElementTag,
weak_ptr_factory_.GetWeakPtr(), value, required_field,
std::move(set_next_field), std::move(element_result)));
}
void RequiredFieldsFallbackHandler::OnGetFallbackFieldElementTag(
const std::string& value,
const RequiredField& required_field,
base::OnceCallback<void()> set_next_field,
std::unique_ptr<ElementFinder::Result> element,
const ClientStatus& element_tag_status,
const std::string& element_tag) {
if (!element_tag_status.ok()) {
DVLOG(3) << "Status for element tag was "
<< element_tag_status.proto_status();
}
const ElementFinder::Result* element_ptr = element.get();
base::OnceCallback<void(const ClientStatus&)> on_set_field_value =
base::BindOnce(&RequiredFieldsFallbackHandler::OnSetFallbackFieldValue,
weak_ptr_factory_.GetWeakPtr(), required_field,
std::move(set_next_field), std::move(element));
DVLOG(3) << "Setting fallback value for " << required_field.selector << " ("
<< element_tag << ")";
if (element_tag == kSelectElementTag) {
std::string re2_value;
bool case_sensitive = false;
ClientStatus re2_status =
GetRe2Value(required_field, fallback_values_, /* use_contains= */ false,
&re2_value, &case_sensitive);
if (!re2_status.ok() || re2_value.empty()) {
// TODO(b/184814284): Selecting an empty value of a dropdown is somewhat
// undefined behaviour. Set the value attribute as a best guess.
action_delegate_->GetWebController()->SetValueAttribute(
std::string(), *element_ptr, std::move(on_set_field_value));
return;
}
SelectOptionProto::OptionComparisonAttribute option_comparison_attribute =
required_field.proto.option_comparison_attribute();
if (option_comparison_attribute == SelectOptionProto::NOT_SET) {
switch (required_field.proto.select_strategy()) {
case UNSPECIFIED_SELECT_STRATEGY:
case LABEL_STARTS_WITH:
case LABEL_MATCH:
option_comparison_attribute = SelectOptionProto::LABEL;
break;
case VALUE_MATCH:
option_comparison_attribute = SelectOptionProto::VALUE;
break;
}
}
action_delegate_->GetWebController()->SelectOption(
re2_value, case_sensitive, option_comparison_attribute,
/* strict= */ false, *element_ptr, std::move(on_set_field_value));
return;
}
action_delegate_util::PerformSetFieldValue(
action_delegate_, value, required_field.proto.fill_strategy(),
required_field.proto.delay_in_millisecond(), *element_ptr,
std::move(on_set_field_value));
}
void RequiredFieldsFallbackHandler::FillJsDrivenDropdown(
const std::string& value,
const RequiredField& required_field,
base::OnceCallback<void()> set_next_field) {
DCHECK(required_field.proto.has_option_element_to_click());
std::string re2_value;
bool case_sensitive = false;
ClientStatus re2_status =
GetRe2Value(required_field, fallback_values_, /* use_contains= */ true,
&re2_value, &case_sensitive);
if (!re2_status.ok() || re2_value.empty()) {
// TODO(b/184814284): Selecting an empty value in a JS driven dropdown is
// undefined behaviour. Do nothing.
std::move(set_next_field).Run();
return;
}
ClickType click_type = required_field.proto.click_type();
if (click_type == ClickType::NOT_SET) {
// default: TAP
click_type = ClickType::TAP;
}
action_delegate_util::ClickOrTapElement(
action_delegate_, required_field.selector, click_type,
base::BindOnce(
&RequiredFieldsFallbackHandler::OnClickOrTapFallbackElement,
weak_ptr_factory_.GetWeakPtr(), re2_value, case_sensitive,
required_field, std::move(set_next_field)));
}
void RequiredFieldsFallbackHandler::OnClickOrTapFallbackElement(
const std::string& re2_value,
bool case_sensitive,
const RequiredField& required_field,
base::OnceCallback<void()> set_next_field,
const ClientStatus& element_click_status) {
if (!element_click_status.ok()) {
FillStatusDetailsWithError(
required_field, element_click_status.proto_status(), &client_status_);
// Fallback failed: we stop the script without checking the other fields.
std::move(status_update_callback_)
.Run(ErrorStatusWithDefault(client_status_));
return;
}
DCHECK(required_field.proto.has_option_element_to_click());
Selector value_selector =
Selector(required_field.proto.option_element_to_click());
value_selector.MatchingInnerText(re2_value, case_sensitive);
action_delegate_->ShortWaitForElementWithSlowWarning(
value_selector,
base::BindOnce(&RequiredFieldsFallbackHandler::OnShortWaitForElement,
weak_ptr_factory_.GetWeakPtr(), value_selector,
required_field, std::move(set_next_field)));
}
void RequiredFieldsFallbackHandler::OnShortWaitForElement(
const Selector& selector_to_click,
const RequiredField& required_field,
base::OnceCallback<void()> set_next_field,
const ClientStatus& find_element_status,
base::TimeDelta wait_time) {
total_wait_time_ += wait_time;
if (!find_element_status.ok()) {
FillStatusDetailsWithError(
required_field, find_element_status.proto_status(), &client_status_);
// Fallback failed: we stop the script without checking the other fields.
std::move(status_update_callback_)
.Run(ErrorStatusWithDefault(client_status_));
return;
}
ClickType click_type = required_field.proto.click_type();
if (click_type == ClickType::NOT_SET) {
// default: TAP
click_type = ClickType::TAP;
}
action_delegate_util::ClickOrTapElement(
action_delegate_, selector_to_click, click_type,
base::BindOnce(&RequiredFieldsFallbackHandler::OnSetFallbackFieldValue,
weak_ptr_factory_.GetWeakPtr(), required_field,
std::move(set_next_field),
/* element= */ nullptr));
}
void RequiredFieldsFallbackHandler::OnSetFallbackFieldValue(
const RequiredField& required_field,
base::OnceCallback<void()> set_next_field,
std::unique_ptr<ElementFinder::Result> element,
const ClientStatus& set_field_status) {
if (!set_field_status.ok()) {
VLOG(1) << "Error setting value for required_field: "
<< required_field.selector << " " << set_field_status;
FillStatusDetailsWithError(required_field, set_field_status.proto_status(),
&client_status_);
// Fallback failed: we stop the script without checking the other fields.
std::move(status_update_callback_)
.Run(ErrorStatusWithDefault(client_status_));
return;
}
std::move(set_next_field).Run();
}
} // namespace autofill_assistant