blob: 9e47d898e417a36e422e55371c30efc2af8f7717 [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/script_executor.h"
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/autofill/core/browser/credit_card.h"
#include "components/autofill_assistant/browser/batch_element_checker.h"
#include "components/autofill_assistant/browser/client_memory.h"
#include "components/autofill_assistant/browser/protocol_utils.h"
#include "components/autofill_assistant/browser/self_delete_full_card_requester.h"
#include "components/autofill_assistant/browser/service.h"
#include "components/autofill_assistant/browser/ui_controller.h"
#include "components/autofill_assistant/browser/web_controller.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
namespace autofill_assistant {
namespace {
// Maximum amount of time normal actions should implicitly wait for a selector
// to show up.
constexpr base::TimeDelta kShortWaitForElementDeadline =
base::TimeDelta::FromSeconds(2);
} // namespace
ScriptExecutor::ScriptExecutor(
const std::string& script_path,
const std::string& global_payload,
const std::string& script_payload,
ScriptExecutor::Listener* listener,
std::map<std::string, ScriptStatusProto>* scripts_state,
const std::vector<Script*>* ordered_interrupts,
ScriptExecutorDelegate* delegate)
: script_path_(script_path),
last_global_payload_(global_payload),
initial_script_payload_(script_payload),
last_script_payload_(script_payload),
listener_(listener),
delegate_(delegate),
at_end_(CONTINUE),
should_stop_script_(false),
should_clean_contextual_ui_on_finish_(false),
previous_action_type_(ActionProto::ACTION_INFO_NOT_SET),
scripts_state_(scripts_state),
ordered_interrupts_(ordered_interrupts),
weak_ptr_factory_(this) {
DCHECK(delegate_);
DCHECK(ordered_interrupts_);
}
ScriptExecutor::~ScriptExecutor() {}
ScriptExecutor::Result::Result() = default;
ScriptExecutor::Result::~Result() = default;
void ScriptExecutor::Run(RunScriptCallback callback) {
(*scripts_state_)[script_path_] = SCRIPT_STATUS_RUNNING;
callback_ = std::move(callback);
DCHECK(delegate_->GetService());
delegate_->GetService()->GetActions(
script_path_, delegate_->GetWebController()->GetUrl(),
delegate_->GetParameters(), last_global_payload_, last_script_payload_,
base::BindOnce(&ScriptExecutor::OnGetActions,
weak_ptr_factory_.GetWeakPtr()));
}
std::unique_ptr<BatchElementChecker>
ScriptExecutor::CreateBatchElementChecker() {
return delegate_->GetWebController()->CreateBatchElementChecker();
}
void ScriptExecutor::ShortWaitForElementExist(
const Selector& selector,
base::OnceCallback<void(bool)> callback) {
WaitForElement(kShortWaitForElementDeadline, kExistenceCheck, selector,
std::move(callback));
}
void ScriptExecutor::WaitForElementVisible(
base::TimeDelta max_wait_time,
bool allow_interrupt,
const Selector& selector,
base::OnceCallback<void(ProcessedActionStatusProto)> callback) {
if (!allow_interrupt || ordered_interrupts_->empty()) {
// No interrupts to worry about. Just run normal wait.
WaitForElement(
max_wait_time, kVisibilityCheck, selector,
base::BindOnce(&ScriptExecutor::OnWaitForElementVisibleNoInterrupts,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
return;
}
wait_with_interrupts_ = std::make_unique<WaitWithInterrupts>(
this, max_wait_time, kVisibilityCheck, selector,
base::BindOnce(&ScriptExecutor::OnWaitForElementVisibleWithInterrupts,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
wait_with_interrupts_->Run();
}
void ScriptExecutor::SetStatusMessage(const std::string& message) {
delegate_->SetStatusMessage(message);
}
std::string ScriptExecutor::GetStatusMessage() {
return delegate_->GetStatusMessage();
}
void ScriptExecutor::ClickOrTapElement(
const Selector& selector,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->ClickOrTapElement(selector,
std::move(callback));
}
void ScriptExecutor::GetPaymentInformation(
payments::mojom::PaymentOptionsPtr payment_options,
base::OnceCallback<void(std::unique_ptr<PaymentInformation>)> callback,
const std::string& title,
const std::vector<std::string>& supported_basic_card_networks) {
delegate_->EnterState(AutofillAssistantState::PROMPT);
delegate_->GetUiController()->GetPaymentInformation(
std::move(payment_options),
base::BindOnce(&ScriptExecutor::OnGetPaymentInformation,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
title, supported_basic_card_networks);
}
void ScriptExecutor::OnGetPaymentInformation(
base::OnceCallback<void(std::unique_ptr<PaymentInformation>)> callback,
std::unique_ptr<PaymentInformation> result) {
delegate_->EnterState(AutofillAssistantState::RUNNING);
std::move(callback).Run(std::move(result));
}
void ScriptExecutor::GetFullCard(GetFullCardCallback callback) {
DCHECK(GetClientMemory()->selected_card());
// User might be asked to provide the cvc.
delegate_->EnterState(AutofillAssistantState::MODAL_DIALOG);
// TODO(crbug.com/806868): Consider refactoring SelfDeleteFullCardRequester
// so as to unit test it.
(new SelfDeleteFullCardRequester())
->GetFullCard(
GetWebContents(), GetClientMemory()->selected_card(),
base::BindOnce(&ScriptExecutor::OnGetFullCard,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ScriptExecutor::OnGetFullCard(GetFullCardCallback callback,
std::unique_ptr<autofill::CreditCard> card,
const base::string16& cvc) {
delegate_->EnterState(AutofillAssistantState::RUNNING);
std::move(callback).Run(std::move(card), cvc);
}
void ScriptExecutor::Prompt(std::unique_ptr<std::vector<Chip>> chips) {
if (touchable_element_area_) {
// SetChips reproduces the end-of-script appearance and behavior during
// script execution. This includes allowing access to touchable elements,
// set through a previous call to the focus action with touchable_elements
// set.
delegate_->SetTouchableElementArea(*touchable_element_area_);
// The touchable_elements_ currently set in the script is reset, so that it
// won't affect the real end of the script.
touchable_element_area_.reset();
// The touchable element and overlays are cleared again in
// ScriptExecutor::OnChosen or ScriptExecutor::ClearChips
}
// We change the chips callback with a callback that cleans up the state
// before calling the initial callback.
for (auto& chip : *chips) {
chip.callback = base::BindOnce(&ScriptExecutor::OnChosen,
weak_ptr_factory_.GetWeakPtr(),
std::move(chip.callback));
}
delegate_->EnterState(AutofillAssistantState::PROMPT);
delegate_->GetUiController()->SetChips(std::move(chips));
}
void ScriptExecutor::CancelPrompt() {
delegate_->GetUiController()->ClearChips();
CleanUpAfterPrompt();
}
void ScriptExecutor::CleanUpAfterPrompt() {
delegate_->ClearTouchableElementArea();
delegate_->EnterState(AutofillAssistantState::RUNNING);
}
void ScriptExecutor::OnChosen(base::OnceClosure callback) {
CleanUpAfterPrompt();
std::move(callback).Run();
}
void ScriptExecutor::FillAddressForm(const autofill::AutofillProfile* profile,
const Selector& selector,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->FillAddressForm(profile, selector,
std::move(callback));
}
void ScriptExecutor::FillCardForm(std::unique_ptr<autofill::CreditCard> card,
const base::string16& cvc,
const Selector& selector,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->FillCardForm(std::move(card), cvc, selector,
std::move(callback));
}
void ScriptExecutor::SelectOption(const Selector& selector,
const std::string& selected_option,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->SelectOption(selector, selected_option,
std::move(callback));
}
void ScriptExecutor::HighlightElement(const Selector& selector,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->HighlightElement(selector,
std::move(callback));
}
void ScriptExecutor::FocusElement(const Selector& selector,
base::OnceCallback<void(bool)> callback) {
last_focused_element_selector_ = selector;
delegate_->GetWebController()->FocusElement(selector, std::move(callback));
}
void ScriptExecutor::SetTouchableElementArea(
const ElementAreaProto& touchable_element_area) {
touchable_element_area_ =
std::make_unique<ElementAreaProto>(touchable_element_area);
}
void ScriptExecutor::ShowProgressBar(int progress) {
delegate_->GetUiController()->ShowProgressBar(progress);
}
void ScriptExecutor::HideProgressBar() {
delegate_->GetUiController()->HideProgressBar();
}
void ScriptExecutor::SetFieldValue(const Selector& selector,
const std::string& value,
bool simulate_key_presses,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->SetFieldValue(
selector, value, simulate_key_presses, std::move(callback));
}
void ScriptExecutor::SetAttribute(const Selector& selector,
const std::vector<std::string>& attribute,
const std::string& value,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->SetAttribute(selector, attribute, value,
std::move(callback));
}
void ScriptExecutor::SendKeyboardInput(
const Selector& selector,
const std::vector<std::string>& text_parts,
base::OnceCallback<void(bool)> callback) {
delegate_->GetWebController()->SendKeyboardInput(selector, text_parts,
std::move(callback));
}
void ScriptExecutor::GetOuterHtml(
const Selector& selector,
base::OnceCallback<void(bool, const std::string&)> callback) {
delegate_->GetWebController()->GetOuterHtml(selector, std::move(callback));
}
void ScriptExecutor::LoadURL(const GURL& url) {
delegate_->GetWebController()->LoadURL(url);
}
void ScriptExecutor::Shutdown() {
// The following handles the case where scripts end with tell + stop
// differently from just stop. TODO(b/806868): Make that difference explicit:
// add an optional message to stop and update the scripts to use that.
if (previous_action_type_ == ActionProto::kTell) {
at_end_ = SHUTDOWN_GRACEFULLY;
} else {
at_end_ = SHUTDOWN;
}
}
void ScriptExecutor::Terminate() {
if (wait_with_interrupts_)
wait_with_interrupts_->Terminate();
at_end_ = TERMINATE;
should_stop_script_ = true;
}
void ScriptExecutor::Close() {
at_end_ = CLOSE_CUSTOM_TAB;
should_stop_script_ = true;
}
void ScriptExecutor::Restart() {
at_end_ = RESTART;
}
void ScriptExecutor::StopCurrentScriptAndShutdown(const std::string& message) {
// Use a default message when |message| is empty.
delegate_->SetStatusMessage(
message.empty() ? l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_GIVE_UP)
: message);
at_end_ = SHUTDOWN_GRACEFULLY;
should_stop_script_ = true;
}
ClientMemory* ScriptExecutor::GetClientMemory() {
return delegate_->GetClientMemory();
}
autofill::PersonalDataManager* ScriptExecutor::GetPersonalDataManager() {
return delegate_->GetPersonalDataManager();
}
content::WebContents* ScriptExecutor::GetWebContents() {
return delegate_->GetWebContents();
}
void ScriptExecutor::ClearDetails() {
delegate_->ClearDetails();
}
void ScriptExecutor::SetDetails(const Details& details) {
return delegate_->SetDetails(details);
}
void ScriptExecutor::OnGetActions(bool result, const std::string& response) {
bool success = result && ProcessNextActionResponse(response);
if (should_stop_script_) {
// The last action forced the script to stop. Sending the result of the
// action is considered best effort in this situation. Report a successful
// run to the caller no matter what, so we don't confuse users with an error
// message.
RunCallback(true);
return;
}
if (!success) {
RunCallback(false);
return;
}
if (!actions_.empty()) {
ProcessNextAction();
return;
}
RunCallback(true);
}
bool ScriptExecutor::ProcessNextActionResponse(const std::string& response) {
processed_actions_.clear();
actions_.clear();
bool should_update_scripts = false;
std::vector<std::unique_ptr<Script>> scripts;
bool parse_result = ProtocolUtils::ParseActions(
response, &last_global_payload_, &last_script_payload_, &actions_,
&scripts, &should_update_scripts);
if (!parse_result) {
return false;
}
ReportPayloadsToListener();
if (should_update_scripts) {
ReportScriptsUpdateToListener(std::move(scripts));
}
return true;
}
void ScriptExecutor::ReportPayloadsToListener() {
if (!listener_)
return;
listener_->OnServerPayloadChanged(last_global_payload_, last_script_payload_);
}
void ScriptExecutor::ReportScriptsUpdateToListener(
std::vector<std::unique_ptr<Script>> scripts) {
if (!listener_)
return;
listener_->OnScriptListChanged(std::move(scripts));
}
void ScriptExecutor::RunCallback(bool success) {
DCHECK(callback_);
if (should_clean_contextual_ui_on_finish_ || !success) {
ClearDetails();
should_clean_contextual_ui_on_finish_ = false;
}
Result result;
result.success = success;
result.at_end = at_end_;
result.touchable_element_area = std::move(touchable_element_area_);
RunCallbackWithResult(result);
}
void ScriptExecutor::RunCallbackWithResult(const Result& result) {
(*scripts_state_)[script_path_] =
result.success ? SCRIPT_STATUS_SUCCESS : SCRIPT_STATUS_FAILURE;
std::move(callback_).Run(result);
}
void ScriptExecutor::ProcessNextAction() {
// We could get into a strange situation if ProcessNextAction is called before
// the action was reported as processed, which should not happen. In that case
// we could have more |processed_actions| than |actions_|.
if (actions_.size() <= processed_actions_.size()) {
DCHECK_EQ(actions_.size(), processed_actions_.size());
// Request more actions to execute.
GetNextActions();
return;
}
Action* action = actions_[processed_actions_.size()].get();
should_clean_contextual_ui_on_finish_ = action->proto().clean_contextual_ui();
int delay_ms = action->proto().action_delay_ms();
if (delay_ms > 0) {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ScriptExecutor::ProcessAction,
weak_ptr_factory_.GetWeakPtr(), action),
base::TimeDelta::FromMilliseconds(delay_ms));
} else {
ProcessAction(action);
}
}
void ScriptExecutor::ProcessAction(Action* action) {
action->ProcessAction(this, base::BindOnce(&ScriptExecutor::OnProcessedAction,
weak_ptr_factory_.GetWeakPtr()));
}
void ScriptExecutor::GetNextActions() {
delegate_->GetService()->GetNextActions(
last_global_payload_, last_script_payload_, processed_actions_,
base::BindOnce(&ScriptExecutor::OnGetActions,
weak_ptr_factory_.GetWeakPtr()));
}
void ScriptExecutor::OnProcessedAction(
std::unique_ptr<ProcessedActionProto> processed_action_proto) {
previous_action_type_ = processed_action_proto->action().action_info_case();
processed_actions_.emplace_back(*processed_action_proto);
auto& processed_action = processed_actions_.back();
if (at_end_ == TERMINATE) {
// Let the backend know that the script has been terminated. The original
// action status doesn't matter.
processed_action.set_status(
ProcessedActionStatusProto::USER_ABORTED_ACTION);
}
if (processed_action.status() != ProcessedActionStatusProto::ACTION_APPLIED) {
// Report error immediately, interrupting action processing.
GetNextActions();
return;
}
ProcessNextAction();
}
void ScriptExecutor::WaitForElement(base::TimeDelta max_wait_time,
ElementCheckType check_type,
const Selector& selector,
base::OnceCallback<void(bool)> callback) {
DCHECK(!batch_element_checker_);
batch_element_checker_ = CreateBatchElementChecker();
batch_element_checker_->AddElementCheck(check_type, selector,
base::DoNothing());
batch_element_checker_->Run(
max_wait_time, /* try_done= */ base::DoNothing(), /* all_done= */
base::BindOnce(&ScriptExecutor::OnWaitForElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ScriptExecutor::OnWaitForElement(base::OnceCallback<void(bool)> callback) {
bool all_found = batch_element_checker_->all_found();
batch_element_checker_.reset();
std::move(callback).Run(all_found);
}
void ScriptExecutor::OnWaitForElementVisibleWithInterrupts(
base::OnceCallback<void(ProcessedActionStatusProto)> callback,
bool element_found,
const Result* interrupt_result,
const std::set<std::string>& interrupt_paths) {
ran_interrupts_.insert(interrupt_paths.begin(), interrupt_paths.end());
if (interrupt_result) {
if (!interrupt_result->success) {
std::move(callback).Run(INTERRUPT_FAILED);
return;
}
if (interrupt_result->at_end != CONTINUE) {
at_end_ = interrupt_result->at_end;
should_stop_script_ = true;
std::move(callback).Run(MANUAL_FALLBACK);
return;
}
}
OnWaitForElementVisibleNoInterrupts(std::move(callback), element_found);
}
void ScriptExecutor::OnWaitForElementVisibleNoInterrupts(
base::OnceCallback<void(ProcessedActionStatusProto)> callback,
bool element_found) {
std::move(callback).Run(element_found ? ACTION_APPLIED
: ELEMENT_RESOLUTION_FAILED);
}
ScriptExecutor::WaitWithInterrupts::WaitWithInterrupts(
ScriptExecutor* main_script,
base::TimeDelta max_wait_time,
ElementCheckType check_type,
const Selector& selector,
WaitWithInterrupts::Callback callback)
: main_script_(main_script),
max_wait_time_(max_wait_time),
check_type_(check_type),
selector_(selector),
callback_(std::move(callback)),
element_found_(false),
weak_ptr_factory_(this) {}
ScriptExecutor::WaitWithInterrupts::~WaitWithInterrupts() = default;
void ScriptExecutor::WaitWithInterrupts::Run() {
// Reset state possibly left over from previous runs.
element_found_ = false;
runnable_interrupts_.clear();
batch_element_checker_ =
main_script_->delegate_->GetWebController()->CreateBatchElementChecker();
batch_element_checker_->AddElementCheck(
check_type_, selector_,
base::BindOnce(&WaitWithInterrupts::OnElementCheckDone,
base::Unretained(this)));
for (const auto* interrupt : *main_script_->ordered_interrupts_) {
if (ran_interrupts_.find(interrupt->handle.path) != ran_interrupts_.end()) {
// Only run an interrupt once in a WaitWithInterrupts, to avoid loops.
continue;
}
interrupt->precondition->Check(
main_script_->delegate_->GetWebController()->GetUrl(),
batch_element_checker_.get(), main_script_->delegate_->GetParameters(),
*main_script_->scripts_state_,
base::BindOnce(&WaitWithInterrupts::OnPreconditionCheckDone,
weak_ptr_factory_.GetWeakPtr(),
base::Unretained(interrupt)));
}
// The base::Unretained(this) above are safe, since the pointers belong to the
// main script, which own this instance.
batch_element_checker_->Run(
max_wait_time_,
base::BindRepeating(&WaitWithInterrupts::OnTryDone,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&WaitWithInterrupts::OnAllDone,
weak_ptr_factory_.GetWeakPtr()));
}
void ScriptExecutor::WaitWithInterrupts::OnServerPayloadChanged(
const std::string& global_payload,
const std::string& script_payload) {
// Interrupts and main scripts share global payloads, but not script payloads.
main_script_->last_global_payload_ = global_payload;
main_script_->ReportPayloadsToListener();
}
void ScriptExecutor::WaitWithInterrupts::OnScriptListChanged(
std::vector<std::unique_ptr<Script>> scripts) {
main_script_->ReportScriptsUpdateToListener(std::move(scripts));
}
void ScriptExecutor::WaitWithInterrupts::OnPreconditionCheckDone(
const Script* interrupt,
bool precondition_match) {
if (precondition_match)
runnable_interrupts_.insert(interrupt);
}
void ScriptExecutor::WaitWithInterrupts::OnElementCheckDone(bool found) {
element_found_ = found;
// Wait for all checks to run before reporting that the element was found to
// the caller, so interrupts have a chance to run.
}
void ScriptExecutor::WaitWithInterrupts::OnTryDone() {
if (!runnable_interrupts_.empty()) {
// We must go through runnable_interrupts_ to make sure priority order is
// respected in case more than one interrupt is ready to run.
for (const auto* interrupt : *main_script_->ordered_interrupts_) {
if (runnable_interrupts_.find(interrupt) != runnable_interrupts_.end()) {
RunInterrupt(interrupt);
return;
}
}
}
if (element_found_)
RunCallback(true, nullptr);
}
void ScriptExecutor::WaitWithInterrupts::OnAllDone() {
// This means that we've reached the end of the timeout. Report whether we
// found the element unless an interrupt has just been started by OnTryDone.
if (!interrupt_executor_)
RunCallback(element_found_, nullptr);
}
void ScriptExecutor::WaitWithInterrupts::RunInterrupt(const Script* interrupt) {
batch_element_checker_.reset();
SavePreInterruptState();
ran_interrupts_.insert(interrupt->handle.path);
interrupt_executor_ = std::make_unique<ScriptExecutor>(
interrupt->handle.path, main_script_->last_global_payload_,
main_script_->initial_script_payload_,
/* listener= */ this, main_script_->scripts_state_, &no_interrupts_,
main_script_->delegate_);
interrupt_executor_->Run(
base::BindOnce(&ScriptExecutor::WaitWithInterrupts::OnInterruptDone,
base::Unretained(this)));
// base::Unretained(this) is safe because interrupt_executor_ belongs to this
}
void ScriptExecutor::WaitWithInterrupts::OnInterruptDone(
const ScriptExecutor::Result& result) {
interrupt_executor_.reset();
if (!result.success || result.at_end != ScriptExecutor::CONTINUE) {
RunCallback(false, &result);
return;
}
RestorePreInterruptUiState();
// Restart. We use the original wait time since the interruption could have
// triggered any kind of actions, including actions that wait on the user. We
// don't trust a previous element_found_ result, since it could have changed.
Run();
}
void ScriptExecutor::WaitWithInterrupts::RunCallback(
bool found,
const ScriptExecutor::Result* result) {
// stop element checking if one is still in progress
batch_element_checker_.reset();
if (!callback_)
return;
RestorePreInterruptScroll(found);
std::move(callback_).Run(found, result, ran_interrupts_);
}
void ScriptExecutor::WaitWithInterrupts::SavePreInterruptState() {
if (saved_pre_interrupt_state_)
return;
pre_interrupt_status_ = main_script_->delegate_->GetStatusMessage();
saved_pre_interrupt_state_ = true;
}
void ScriptExecutor::WaitWithInterrupts::RestorePreInterruptUiState() {
if (!saved_pre_interrupt_state_)
return;
main_script_->delegate_->SetStatusMessage(pre_interrupt_status_);
}
void ScriptExecutor::WaitWithInterrupts::RestorePreInterruptScroll(
bool element_found) {
if (!saved_pre_interrupt_state_)
return;
auto* delegate = main_script_->delegate_;
if (element_found) {
delegate->GetWebController()->FocusElement(selector_, base::DoNothing());
} else if (!main_script_->last_focused_element_selector_.empty()) {
delegate->GetWebController()->FocusElement(
main_script_->last_focused_element_selector_, base::DoNothing());
}
}
void ScriptExecutor::WaitWithInterrupts::Terminate() {
if (interrupt_executor_)
interrupt_executor_->Terminate();
}
} // namespace autofill_assistant