blob: f8fb15438eab951f105265cceeccc48b0bb7c9d5 [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 <cstdio>
#include <ostream>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.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/data_model/credit_card.h"
#include "components/autofill_assistant/browser/actions/action.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/full_card_requester.h"
#include "components/autofill_assistant/browser/protocol_utils.h"
#include "components/autofill_assistant/browser/service/service.h"
#include "components/autofill_assistant/browser/trigger_context.h"
#include "components/autofill_assistant/browser/wait_for_document_operation.h"
#include "components/autofill_assistant/browser/web/element_action_util.h"
#include "components/autofill_assistant/browser/web/element_finder.h"
#include "components/autofill_assistant/browser/web/element_store.h"
#include "components/autofill_assistant/browser/web/web_controller.h"
#include "components/strings/grit/components_strings.h"
#include "net/http/http_status_code.h"
#include "ui/base/l10n/l10n_util.h"
namespace autofill_assistant {
namespace {
std::ostream& operator<<(std::ostream& out,
const ScriptExecutor::AtEnd& at_end) {
#ifdef NDEBUG
out << static_cast<int>(at_end);
return out;
#else
switch (at_end) {
case ScriptExecutor::CONTINUE:
out << "CONTINUE";
break;
case ScriptExecutor::SHUTDOWN:
out << "SHUTDOWN";
break;
case ScriptExecutor::SHUTDOWN_GRACEFULLY:
out << "SHUTDOWN_GRACEFULLY";
break;
case ScriptExecutor::CLOSE_CUSTOM_TAB:
out << "CLOSE_CUSTOM_TAB";
break;
// Intentionally no default case to make compilation fail if a new value
// was added to the enum but not to this list.
}
return out;
#endif // NDEBUG
}
} // namespace
ScriptExecutor::ScriptExecutor(
const std::string& script_path,
std::unique_ptr<TriggerContext> additional_context,
const std::string& global_payload,
const std::string& script_payload,
ScriptExecutor::Listener* listener,
const std::vector<std::unique_ptr<Script>>* ordered_interrupts,
ScriptExecutorDelegate* delegate)
: script_path_(script_path),
additional_context_(std::move(additional_context)),
last_global_payload_(global_payload),
initial_script_payload_(script_payload),
last_script_payload_(script_payload),
listener_(listener),
delegate_(delegate),
ordered_interrupts_(ordered_interrupts),
element_store_(
std::make_unique<ElementStore>(delegate->GetWebContents())) {
DCHECK(delegate_);
DCHECK(ordered_interrupts_);
}
ScriptExecutor::~ScriptExecutor() {
delegate_->RemoveNavigationListener(this);
delegate_->RemoveListener(this);
}
ScriptExecutor::Result::Result() = default;
ScriptExecutor::Result::~Result() = default;
void ScriptExecutor::Run(const UserData* user_data,
RunScriptCallback callback) {
#ifdef NDEBUG
VLOG(2) << "Starting script";
#else
DVLOG(2) << "Starting script " << script_path_;
#endif
DCHECK(user_data);
user_data_ = user_data;
delegate_->AddNavigationListener(this);
delegate_->AddListener(this);
callback_ = std::move(callback);
DCHECK(delegate_->GetService());
#ifdef NDEBUG
VLOG(2) << "GetActions for (redacted)";
#else
VLOG(2) << "GetActions for " << delegate_->GetCurrentURL().host();
#endif
delegate_->GetService()->GetActions(
script_path_, delegate_->GetScriptURL(),
TriggerContext(
{delegate_->GetTriggerContext(), additional_context_.get()}),
last_global_payload_, last_script_payload_,
base::BindOnce(&ScriptExecutor::OnGetActions,
weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now()));
}
const UserData* ScriptExecutor::GetUserData() const {
DCHECK(user_data_);
return user_data_;
}
UserModel* ScriptExecutor::GetUserModel() const {
return delegate_->GetUserModel();
}
void ScriptExecutor::OnNavigationStateChanged() {
NavigationInfoProto& navigation_info = current_action_data_.navigation_info;
if (delegate_->IsNavigatingToNewDocument()) {
navigation_info.set_started(true);
navigation_info.set_unexpected(expected_navigation_step_ !=
ExpectedNavigationStep::EXPECTED);
} else {
navigation_info.set_ended(true);
}
if (delegate_->HasNavigationError()) {
navigation_info.set_has_error(true);
}
switch (expected_navigation_step_) {
case ExpectedNavigationStep::UNEXPECTED:
break;
case ExpectedNavigationStep::EXPECTED:
if (delegate_->IsNavigatingToNewDocument()) {
expected_navigation_step_ = ExpectedNavigationStep::STARTED;
}
break;
case ExpectedNavigationStep::STARTED:
if (!delegate_->IsNavigatingToNewDocument()) {
expected_navigation_step_ = ExpectedNavigationStep::DONE;
if (on_expected_navigation_done_)
std::move(on_expected_navigation_done_)
.Run(!delegate_->HasNavigationError());
}
// Early return since current_action_data_ is no longer valid at this
// point.
return;
case ExpectedNavigationStep::DONE:
// nothing to do
break;
}
// Potentially terminate an ongoing prompt action.
if (navigation_info.ended() &&
current_action_data_.end_prompt_on_navigation_callback) {
std::move(current_action_data_.end_prompt_on_navigation_callback).Run();
}
}
void ScriptExecutor::OnPause(const std::string& message,
const std::string& button_label) {
if (current_action_index_.has_value()) {
DCHECK_LT(*current_action_index_, actions_.size());
if (actions_[*current_action_index_]->ShouldInterruptOnPause()) {
actions_[*current_action_index_] = ProtocolUtils::CreateAction(
this, actions_[*current_action_index_]->proto());
current_action_data_ = CurrentActionData();
current_action_index_.reset();
}
}
delegate_->ClearInfoBox();
delegate_->SetDetails(nullptr, base::TimeDelta());
delegate_->SetCollectUserDataOptions(nullptr);
delegate_->SetForm(nullptr, base::DoNothing(), base::DoNothing());
last_status_message_ = GetStatusMessage();
delegate_->SetStatusMessage(message);
auto user_actions = std::make_unique<std::vector<UserAction>>();
UserAction undo_action;
Chip undo_chip;
undo_chip.type = ChipType::HIGHLIGHTED_ACTION;
undo_chip.text = button_label;
undo_action.chip() = undo_chip;
undo_action.SetCallback(base::BindOnce(&ScriptExecutor::OnResume,
weak_ptr_factory_.GetWeakPtr()));
user_actions->emplace_back(std::move(undo_action));
delegate_->SetUserActions(std::move(user_actions));
delegate_->EnterState(AutofillAssistantState::STOPPED);
is_paused_ = true;
}
void ScriptExecutor::OnResume() {
DCHECK(is_paused_);
is_paused_ = false;
delegate_->EnterState(AutofillAssistantState::RUNNING);
delegate_->SetStatusMessage(last_status_message_);
if (!current_action_index_.has_value()) {
ProcessNextAction();
}
}
void ScriptExecutor::RunElementChecks(BatchElementChecker* checker) {
return checker->Run(delegate_->GetWebController());
}
void ScriptExecutor::ShortWaitForElement(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback) {
current_action_data_.wait_for_dom = std::make_unique<WaitForDomOperation>(
this, delegate_, delegate_->GetSettings().short_wait_for_element_deadline,
/* allow_interrupt= */ false, /* observer= */ nullptr,
base::BindRepeating(&ScriptExecutor::CheckElementMatches,
weak_ptr_factory_.GetWeakPtr(), selector),
base::BindOnce(&ScriptExecutor::OnShortWaitForElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
current_action_data_.wait_for_dom->Run();
}
void ScriptExecutor::ShortWaitForElementWithSlowWarning(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback) {
current_action_data_.wait_for_dom = std::make_unique<WaitForDomOperation>(
this, delegate_, delegate_->GetSettings().short_wait_for_element_deadline,
/* allow_interrupt= */ false, /* observer= */ nullptr,
base::BindRepeating(&ScriptExecutor::CheckElementMatches,
weak_ptr_factory_.GetWeakPtr(), selector),
base::BindOnce(&ScriptExecutor::OnShortWaitForElement,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
current_action_data_.wait_for_dom->SetTimeoutWarningCallback(
base::BindOnce(&ScriptExecutor::MaybeShowSlowWebsiteWarning,
weak_ptr_factory_.GetWeakPtr()));
current_action_data_.wait_for_dom->Run();
}
void ScriptExecutor::WaitForDom(
base::TimeDelta max_wait_time,
bool allow_interrupt,
WaitForDomObserver* observer,
base::RepeatingCallback<void(BatchElementChecker*,
base::OnceCallback<void(const ClientStatus&)>)>
check_elements,
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback) {
current_action_data_.wait_for_dom = std::make_unique<WaitForDomOperation>(
this, delegate_, max_wait_time, allow_interrupt, observer, check_elements,
base::BindOnce(&ScriptExecutor::OnWaitForElementVisibleWithInterrupts,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
current_action_data_.wait_for_dom->Run();
}
void ScriptExecutor::WaitForDomWithSlowWarning(
base::TimeDelta max_wait_time,
bool allow_interrupt,
WaitForDomObserver* observer,
base::RepeatingCallback<void(BatchElementChecker*,
base::OnceCallback<void(const ClientStatus&)>)>
check_elements,
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback) {
current_action_data_.wait_for_dom = std::make_unique<WaitForDomOperation>(
this, delegate_, max_wait_time, allow_interrupt, observer, check_elements,
base::BindOnce(&ScriptExecutor::OnWaitForElementVisibleWithInterrupts,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
current_action_data_.wait_for_dom->SetTimeoutWarningCallback(
base::BindOnce(&ScriptExecutor::MaybeShowSlowWebsiteWarning,
weak_ptr_factory_.GetWeakPtr()));
current_action_data_.wait_for_dom->Run();
}
void ScriptExecutor::SetStatusMessage(const std::string& message) {
delegate_->SetStatusMessage(message);
}
std::string ScriptExecutor::GetStatusMessage() const {
return delegate_->GetStatusMessage();
}
void ScriptExecutor::SetBubbleMessage(const std::string& message) {
delegate_->SetBubbleMessage(message);
}
std::string ScriptExecutor::GetBubbleMessage() const {
return delegate_->GetBubbleMessage();
}
void ScriptExecutor::SetTtsMessage(const std::string& message) {
delegate_->SetTtsMessage(message);
}
TtsButtonState ScriptExecutor::GetTtsButtonState() const {
return delegate_->GetTtsButtonState();
}
void ScriptExecutor::MaybePlayTtsMessage() {
delegate_->MaybePlayTtsMessage();
}
void ScriptExecutor::FindElement(const Selector& selector,
ElementFinder::Callback callback) const {
VLOG(3) << __func__ << " " << selector;
delegate_->GetWebController()->FindElement(selector, /* strict_mode= */ true,
std::move(callback));
}
void ScriptExecutor::FindAllElements(const Selector& selector,
ElementFinder::Callback callback) const {
VLOG(3) << __func__ << " " << selector;
delegate_->GetWebController()->FindAllElements(selector, std::move(callback));
}
void ScriptExecutor::CollectUserData(
CollectUserDataOptions* collect_user_data_options) {
collect_user_data_options->confirm_callback = base::BindOnce(
&ScriptExecutor::OnGetUserData, weak_ptr_factory_.GetWeakPtr(),
std::move(collect_user_data_options->confirm_callback));
collect_user_data_options->additional_actions_callback = base::BindOnce(
&ScriptExecutor::OnAdditionalActionTriggered,
weak_ptr_factory_.GetWeakPtr(),
std::move(collect_user_data_options->additional_actions_callback));
collect_user_data_options->terms_link_callback =
base::BindOnce(&ScriptExecutor::OnTermsAndConditionsLinkClicked,
weak_ptr_factory_.GetWeakPtr(),
std::move(collect_user_data_options->terms_link_callback));
delegate_->SetCollectUserDataOptions(collect_user_data_options);
delegate_->EnterState(AutofillAssistantState::PROMPT);
}
void ScriptExecutor::SetLastSuccessfulUserDataOptions(
std::unique_ptr<CollectUserDataOptions> collect_user_data_options) {
delegate_->SetLastSuccessfulUserDataOptions(
std::move(collect_user_data_options));
}
const CollectUserDataOptions* ScriptExecutor::GetLastSuccessfulUserDataOptions()
const {
return delegate_->GetLastSuccessfulUserDataOptions();
}
void ScriptExecutor::WriteUserData(
base::OnceCallback<void(UserData*, UserData::FieldChange*)>
write_callback) {
delegate_->WriteUserData(std::move(write_callback));
}
void ScriptExecutor::OnGetUserData(
base::OnceCallback<void(UserData*, const UserModel*)> callback,
UserData* user_data,
const UserModel* user_model) {
delegate_->EnterState(AutofillAssistantState::RUNNING);
delegate_->SetUserActions(nullptr);
std::move(callback).Run(user_data, user_model);
}
void ScriptExecutor::OnAdditionalActionTriggered(
base::OnceCallback<void(int, UserData*, const UserModel*)> callback,
int index,
UserData* user_data,
const UserModel* user_model) {
delegate_->EnterState(AutofillAssistantState::RUNNING);
std::move(callback).Run(index, user_data, user_model);
}
void ScriptExecutor::OnTermsAndConditionsLinkClicked(
base::OnceCallback<void(int, UserData*, const UserModel*)> callback,
int link,
UserData* user_data,
const UserModel* user_model) {
delegate_->EnterState(AutofillAssistantState::RUNNING);
std::move(callback).Run(link, user_data, user_model);
}
void ScriptExecutor::GetFullCard(const autofill::CreditCard* credit_card,
GetFullCardCallback callback) {
DCHECK(credit_card);
// User might be asked to provide the cvc.
delegate_->EnterState(AutofillAssistantState::MODAL_DIALOG);
std::unique_ptr<FullCardRequester> full_card_requester =
std::make_unique<FullCardRequester>();
FullCardRequester* full_card_requester_ptr = full_card_requester.get();
full_card_requester_ptr->GetFullCard(
GetWebContents(), credit_card,
base::BindOnce(&ScriptExecutor::OnGetFullCard,
weak_ptr_factory_.GetWeakPtr(),
std::move(full_card_requester), std::move(callback)));
}
void ScriptExecutor::OnGetFullCard(
std::unique_ptr<FullCardRequester> full_card_requester,
GetFullCardCallback callback,
const ClientStatus& status,
std::unique_ptr<autofill::CreditCard> card,
const std::u16string& cvc) {
delegate_->EnterState(AutofillAssistantState::RUNNING);
std::move(callback).Run(status, std::move(card), cvc);
}
void ScriptExecutor::Prompt(
std::unique_ptr<std::vector<UserAction>> user_actions,
bool disable_force_expand_sheet,
base::OnceCallback<void()> end_on_navigation_callback,
bool browse_mode,
bool browse_mode_invisible) {
// First communicate to the delegate that prompt actions should or should not
// expand the sheet intitially.
delegate_->SetExpandSheetForPromptAction(!disable_force_expand_sheet);
delegate_->SetBrowseModeInvisible(browse_mode_invisible);
if (browse_mode) {
delegate_->EnterState(AutofillAssistantState::BROWSE);
} else if (delegate_->EnterState(AutofillAssistantState::PROMPT)) {
if (touchable_element_area_) {
// Prompt() 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 element and overlays are cleared by calling
// ScriptExecutor::CleanUpAfterPrompt
}
if (end_on_navigation_callback) {
current_action_data_.end_prompt_on_navigation_callback =
std::move(end_on_navigation_callback);
}
}
if (user_actions != nullptr) {
for (auto& user_action : *user_actions) {
if (!user_action.HasCallback())
continue;
user_action.AddInterceptor(base::BindOnce(
&ScriptExecutor::OnChosen, weak_ptr_factory_.GetWeakPtr()));
}
delegate_->SetUserActions(std::move(user_actions));
}
}
void ScriptExecutor::CleanUpAfterPrompt() {
delegate_->SetUserActions(nullptr);
// Mark touchable_elements_ as consumed, so that it won't affect the next
// prompt or the end of the script.
touchable_element_area_.reset();
delegate_->ClearTouchableElementArea();
delegate_->SetExpandSheetForPromptAction(true);
delegate_->SetBrowseModeInvisible(false);
delegate_->EnterState(AutofillAssistantState::RUNNING);
}
void ScriptExecutor::SetBrowseDomainsAllowlist(
std::vector<std::string> domains) {
delegate_->SetBrowseDomainsAllowlist(std::move(domains));
}
void ScriptExecutor::OnChosen(UserAction::Callback callback,
std::unique_ptr<TriggerContext> context) {
if (context->GetDirectAction()) {
current_action_data_.direct_action = true;
}
std::move(callback).Run(std::move(context));
}
void ScriptExecutor::RetrieveElementFormAndFieldData(
const Selector& selector,
base::OnceCallback<void(const ClientStatus&,
const autofill::FormData&,
const autofill::FormFieldData&)> callback) {
delegate_->GetWebController()->RetrieveElementFormAndFieldData(
selector, std::move(callback));
}
void ScriptExecutor::StoreScrolledToElement(
const ElementFinder::Result& element) {
last_focused_element_ = element.dom_object;
}
void ScriptExecutor::SetTouchableElementArea(
const ElementAreaProto& touchable_element_area) {
touchable_element_area_ =
std::make_unique<ElementAreaProto>(touchable_element_area);
}
void ScriptExecutor::SetProgress(int progress) {
delegate_->SetProgress(progress);
}
bool ScriptExecutor::SetProgressActiveStepIdentifier(
const std::string& active_step_identifier) {
return delegate_->SetProgressActiveStepIdentifier(active_step_identifier);
}
void ScriptExecutor::SetProgressActiveStep(int active_step) {
delegate_->SetProgressActiveStep(active_step);
}
void ScriptExecutor::SetProgressVisible(bool visible) {
delegate_->SetProgressVisible(visible);
}
void ScriptExecutor::SetProgressBarErrorState(bool error) {
delegate_->SetProgressBarErrorState(error);
}
void ScriptExecutor::SetStepProgressBarConfiguration(
const ShowProgressBarProto::StepProgressBarConfiguration& configuration) {
delegate_->SetStepProgressBarConfiguration(configuration);
}
void ScriptExecutor::ExpectNavigation() {
// TODO(b/160948417): Clean this up such that the logic is not required in
// both |ScriptExecutor| and |Controller|.
delegate_->ExpectNavigation();
expected_navigation_step_ = ExpectedNavigationStep::EXPECTED;
}
bool ScriptExecutor::ExpectedNavigationHasStarted() {
return expected_navigation_step_ != ExpectedNavigationStep::EXPECTED;
}
bool ScriptExecutor::WaitForNavigation(
base::OnceCallback<void(bool)> callback) {
switch (expected_navigation_step_) {
case ExpectedNavigationStep::UNEXPECTED:
return false;
case ExpectedNavigationStep::DONE:
std::move(callback).Run(!delegate_->HasNavigationError());
break;
case ExpectedNavigationStep::EXPECTED:
case ExpectedNavigationStep::STARTED:
on_expected_navigation_done_ = std::move(callback);
break;
// No default to make compilation fail if not all cases are covered
}
return true;
}
void ScriptExecutor::WaitForDocumentReadyState(
base::TimeDelta max_wait_time,
DocumentReadyState min_ready_state,
const ElementFinder::Result& optional_frame_element,
base::OnceCallback<void(const ClientStatus&,
DocumentReadyState,
base::TimeDelta)> callback) {
current_action_data_.wait_for_document =
std::make_unique<WaitForDocumentOperation>(
delegate_, max_wait_time, min_ready_state, optional_frame_element,
std::move(callback));
current_action_data_.wait_for_document->Run();
}
void ScriptExecutor::WaitUntilDocumentIsInReadyState(
base::TimeDelta max_wait_time,
DocumentReadyState min_ready_state,
const ElementFinder::Result& optional_frame_element,
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback) {
WaitForDocumentReadyState(
max_wait_time, min_ready_state, optional_frame_element,
base::BindOnce(&ScriptExecutor::OnWaitForDocumentReadyState,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ScriptExecutor::OnWaitForDocumentReadyState(
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback,
const ClientStatus& status,
DocumentReadyState ready_state,
base::TimeDelta wait_time) {
std::move(callback).Run(status, wait_time);
}
void ScriptExecutor::LoadURL(const GURL& url) {
delegate_->GetWebController()->LoadURL(url);
}
void ScriptExecutor::Shutdown(bool show_feedback_chip) {
// 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) {
delegate_->SetShowFeedbackChip(show_feedback_chip);
at_end_ = SHUTDOWN_GRACEFULLY;
} else {
at_end_ = SHUTDOWN;
}
}
void ScriptExecutor::Close() {
at_end_ = CLOSE_CUSTOM_TAB;
should_stop_script_ = true;
}
autofill::PersonalDataManager* ScriptExecutor::GetPersonalDataManager() const {
return delegate_->GetPersonalDataManager();
}
WebsiteLoginManager* ScriptExecutor::GetWebsiteLoginManager() const {
return delegate_->GetWebsiteLoginManager();
}
content::WebContents* ScriptExecutor::GetWebContents() const {
return delegate_->GetWebContents();
}
ElementStore* ScriptExecutor::GetElementStore() const {
return element_store_.get();
}
WebController* ScriptExecutor::GetWebController() const {
return delegate_->GetWebController();
}
std::string ScriptExecutor::GetEmailAddressForAccessTokenAccount() const {
return delegate_->GetEmailAddressForAccessTokenAccount();
}
void ScriptExecutor::SetDetails(std::unique_ptr<Details> details,
base::TimeDelta delay) {
return delegate_->SetDetails(std::move(details), delay);
}
void ScriptExecutor::AppendDetails(std::unique_ptr<Details> details,
base::TimeDelta delay) {
return delegate_->AppendDetails(std::move(details), delay);
}
void ScriptExecutor::ClearInfoBox() {
delegate_->ClearInfoBox();
}
void ScriptExecutor::SetInfoBox(const InfoBox& info_box) {
delegate_->SetInfoBox(info_box);
}
void ScriptExecutor::SetViewportMode(ViewportMode mode) {
delegate_->SetViewportMode(mode);
}
ViewportMode ScriptExecutor::GetViewportMode() const {
return delegate_->GetViewportMode();
}
void ScriptExecutor::SetPeekMode(
ConfigureBottomSheetProto::PeekMode peek_mode) {
delegate_->SetPeekMode(peek_mode);
}
ConfigureBottomSheetProto::PeekMode ScriptExecutor::GetPeekMode() const {
return delegate_->GetPeekMode();
}
void ScriptExecutor::ExpandBottomSheet() {
return delegate_->ExpandBottomSheet();
}
void ScriptExecutor::CollapseBottomSheet() {
return delegate_->CollapseBottomSheet();
}
void ScriptExecutor::WaitForWindowHeightChange(
base::OnceCallback<void(const ClientStatus&)> callback) {
delegate_->GetWebController()->WaitForWindowHeightChange(std::move(callback));
}
const ClientSettings& ScriptExecutor::GetSettings() const {
return delegate_->GetSettings();
}
void ScriptExecutor::SetClientSettings(
const ClientSettingsProto& client_settings) {
return delegate_->SetClientSettings(client_settings);
}
bool ScriptExecutor::SetForm(
std::unique_ptr<FormProto> form,
base::RepeatingCallback<void(const FormProto::Result*)> changed_callback,
base::OnceCallback<void(const ClientStatus&)> cancel_callback) {
return delegate_->SetForm(std::move(form), std::move(changed_callback),
std::move(cancel_callback));
}
void ScriptExecutor::RequireUI() {
delegate_->RequireUI();
}
void ScriptExecutor::SetGenericUi(
std::unique_ptr<GenericUserInterfaceProto> generic_ui,
base::OnceCallback<void(const ClientStatus&)> end_action_callback,
base::OnceCallback<void(const ClientStatus&)>
view_inflation_finished_callback) {
delegate_->SetGenericUi(std::move(generic_ui), std::move(end_action_callback),
std::move(view_inflation_finished_callback));
}
void ScriptExecutor::SetPersistentGenericUi(
std::unique_ptr<GenericUserInterfaceProto> generic_ui,
base::OnceCallback<void(const ClientStatus&)>
view_inflation_finished_callback) {
delegate_->SetPersistentGenericUi(
std::move(generic_ui), std::move(view_inflation_finished_callback));
}
void ScriptExecutor::ClearGenericUi() {
delegate_->ClearGenericUi();
}
void ScriptExecutor::ClearPersistentGenericUi() {
delegate_->ClearPersistentGenericUi();
}
void ScriptExecutor::SetOverlayBehavior(
ConfigureUiStateProto::OverlayBehavior overlay_behavior) {
delegate_->SetOverlayBehavior(overlay_behavior);
}
void ScriptExecutor::MaybeShowSlowWebsiteWarning(
base::OnceCallback<void(bool)> callback) {
bool should_show_warning =
!delegate_->GetSettings().only_show_website_warning_once ||
!website_warning_already_shown_;
// MaybeShowSlowWarning is only called if should_sown_warning is true.
bool warning_was_shown =
should_show_warning &&
MaybeShowSlowWarning(
delegate_->GetSettings().slow_website_message,
delegate_->GetSettings().enable_slow_website_warnings);
website_warning_already_shown_ |= warning_was_shown;
if (callback) {
std::move(callback).Run(warning_was_shown);
}
}
void ScriptExecutor::MaybeShowSlowConnectionWarning() {
bool should_show_warning =
!delegate_->GetSettings().only_show_connection_warning_once ||
!connection_warning_already_shown_;
base::TimeDelta delay = base::Milliseconds(0);
// MaybeShowSlowWarning is only called if should_sown_warning is true.
bool warning_was_shown =
should_show_warning &&
MaybeShowSlowWarning(
delegate_->GetSettings().slow_connection_message,
delegate_->GetSettings().enable_slow_connection_warnings);
if (warning_was_shown) {
delay = delegate_->GetSettings().minimum_warning_duration;
}
connection_warning_already_shown_ |= warning_was_shown;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ScriptExecutor::ProcessNextAction,
weak_ptr_factory_.GetWeakPtr()),
delay);
}
bool ScriptExecutor::MaybeShowSlowWarning(const std::string& message,
bool enabled) {
if (message.empty() || !enabled || !delegate_->ShouldShowWarning()) {
return false;
}
if (delegate_->GetSettings().only_show_warning_once &&
(connection_warning_already_shown_ || website_warning_already_shown_)) {
return false;
}
const std::string& previous_message = GetStatusMessage();
switch (delegate_->GetSettings().message_mode) {
case ClientSettingsProto::SlowWarningSettings::CONCATENATE:
if (previous_message.find(message) == std::string::npos) {
SetStatusMessage(previous_message + message);
}
break;
case ClientSettingsProto::SlowWarningSettings::REPLACE:
SetStatusMessage(message);
break;
case ClientSettingsProto::SlowWarningSettings::UNKNOWN:
return false;
}
return true;
}
base::WeakPtr<ActionDelegate> ScriptExecutor::GetWeakPtr() const {
return weak_ptr_factory_.GetWeakPtr();
}
void ScriptExecutor::OnGetActions(base::TimeTicks start_time,
int http_status,
const std::string& response) {
VLOG(2) << __func__ << " http-status=" << http_status;
batch_start_time_ = base::TimeTicks::Now();
const base::TimeDelta& roundtrip_duration = batch_start_time_ - start_time;
// Doesn't trigger when the script is completed.
roundtrip_timing_stats_.set_roundtrip_time_ms(
roundtrip_duration.InMilliseconds());
bool success =
http_status == net::HTTP_OK && 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 (roundtrip_duration < delegate_->GetSettings().slow_roundtrip_threshold) {
consecutive_slow_roundtrip_counter_ = 0;
} else {
consecutive_slow_roundtrip_counter_++;
}
if (!actions_.empty()) {
if (consecutive_slow_roundtrip_counter_ >=
delegate_->GetSettings().max_consecutive_slow_roundtrips) {
consecutive_slow_roundtrip_counter_ = 0;
MaybeShowSlowConnectionWarning();
} else {
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(
this, 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) {
if (should_clean_contextual_ui_on_finish_ || !success) {
SetDetails(nullptr, base::TimeDelta());
ClearPersistentGenericUi();
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) {
DCHECK(callback_);
std::move(callback_).Run(result);
}
void ScriptExecutor::ProcessNextAction() {
current_action_index_.reset();
if (is_paused_) {
return;
}
// 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());
VLOG(2) << __func__ << ", get more actions";
GetNextActions();
return;
}
current_action_index_ = processed_actions_.size();
Action* action = actions_[*current_action_index_].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::Milliseconds(delay_ms));
} else {
ProcessAction(action);
}
}
void ScriptExecutor::ProcessAction(Action* action) {
VLOG(2) << "Begin action: " << *action;
current_action_data_ = CurrentActionData();
current_action_data_.navigation_info.set_has_error(
delegate_->HasNavigationError());
action->ProcessAction(base::BindOnce(&ScriptExecutor::OnProcessedAction,
weak_ptr_factory_.GetWeakPtr(),
base::TimeTicks::Now()));
}
void ScriptExecutor::GetNextActions() {
base::TimeTicks get_next_actions_start = base::TimeTicks::Now();
roundtrip_timing_stats_.set_client_time_ms(
(get_next_actions_start - batch_start_time_).InMilliseconds());
VLOG(2) << "Batch timing stats";
VLOG(2) << "Roundtrip time: " << roundtrip_timing_stats_.roundtrip_time_ms();
VLOG(2) << "Client execution time: "
<< roundtrip_timing_stats_.client_time_ms();
delegate_->GetService()->GetNextActions(
TriggerContext(
{delegate_->GetTriggerContext(), additional_context_.get()}),
last_global_payload_, last_script_payload_, processed_actions_,
roundtrip_timing_stats_,
base::BindOnce(&ScriptExecutor::OnGetActions,
weak_ptr_factory_.GetWeakPtr(), get_next_actions_start));
}
void ScriptExecutor::OnProcessedAction(
base::TimeTicks start_time,
std::unique_ptr<ProcessedActionProto> processed_action_proto) {
base::TimeDelta run_time = base::TimeTicks::Now() - start_time;
previous_action_type_ = processed_action_proto->action().action_info_case();
processed_actions_.emplace_back(*processed_action_proto);
#ifdef NDEBUG
VLOG(2) << "Action completed";
#else
VLOG(2) << "Requested delay ms: "
<< processed_action_proto->timing_stats().delay_ms();
VLOG(2) << "Active time ms: "
<< processed_action_proto->timing_stats().active_time_ms();
VLOG(2) << "Wait time ms: "
<< processed_action_proto->timing_stats().wait_time_ms();
VLOG(2) << "Run time: " << run_time;
#endif
auto& processed_action = processed_actions_.back();
processed_action.set_run_time_ms(run_time.InMilliseconds());
processed_action.set_direct_action(current_action_data_.direct_action);
*processed_action.mutable_navigation_info() =
current_action_data_.navigation_info;
if (processed_action.status() != ProcessedActionStatusProto::ACTION_APPLIED) {
VLOG(1) << "Action failed: " << processed_action.status();
// Remove unexecuted actions, this will cause the |ProcessNextActions| call
// to immediately ask for new actions.
actions_.resize(processed_actions_.size());
}
ProcessNextAction();
}
void ScriptExecutor::CheckElementMatches(
const Selector& selector,
BatchElementChecker* checker,
base::OnceCallback<void(const ClientStatus&)> callback) {
checker->AddElementCheck(
selector,
base::BindOnce(&ScriptExecutor::CheckElementMatchesCallback,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ScriptExecutor::CheckElementMatchesCallback(
base::OnceCallback<void(const ClientStatus&)> callback,
const ClientStatus& status,
const ElementFinder::Result& ignored_element) {
std::move(callback).Run(status);
}
void ScriptExecutor::OnShortWaitForElement(
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback,
const ClientStatus& element_status,
const Result* interrupt_result,
base::TimeDelta wait_time) {
// Interrupts cannot run, so should never be reported.
DCHECK(!interrupt_result);
std::move(callback).Run(element_status, wait_time);
}
void ScriptExecutor::OnWaitForElementVisibleWithInterrupts(
base::OnceCallback<void(const ClientStatus&, base::TimeDelta)> callback,
const ClientStatus& element_status,
const Result* interrupt_result,
base::TimeDelta wait_time) {
if (interrupt_result) {
if (!interrupt_result->success) {
std::move(callback).Run(ClientStatus(INTERRUPT_FAILED), wait_time);
return;
}
if (interrupt_result->at_end != CONTINUE) {
at_end_ = interrupt_result->at_end;
should_stop_script_ = true;
std::move(callback).Run(ClientStatus(MANUAL_FALLBACK), wait_time);
return;
}
}
std::move(callback).Run(element_status, wait_time);
}
ScriptExecutor::WaitForDomOperation::WaitForDomOperation(
ScriptExecutor* main_script,
ScriptExecutorDelegate* delegate,
base::TimeDelta max_wait_time,
bool allow_interrupt,
WaitForDomObserver* observer,
base::RepeatingCallback<void(BatchElementChecker*,
base::OnceCallback<void(const ClientStatus&)>)>
check_elements,
WaitForDomOperation::Callback callback)
: main_script_(main_script),
delegate_(delegate),
max_wait_time_(max_wait_time),
allow_interrupt_(allow_interrupt),
observer_(observer),
check_elements_(std::move(check_elements)),
callback_(std::move(callback)),
timeout_warning_delay_(
main_script->delegate_->GetSettings().warning_delay),
retry_timer_(main_script->delegate_->GetSettings()
.periodic_element_check_interval) {}
ScriptExecutor::WaitForDomOperation::~WaitForDomOperation() {
delegate_->RemoveNavigationListener(this);
}
void ScriptExecutor::WaitForDomOperation::Run() {
delegate_->AddNavigationListener(this);
wait_time_stopwatch_.Start();
Start();
}
void ScriptExecutor::WaitForDomOperation::SetTimeoutWarningCallback(
WarningCallback warning_callback) {
warning_callback_ = std::move(warning_callback);
}
void ScriptExecutor::WaitForDomOperation::Start() {
retry_timer_.Start(
max_wait_time_,
base::BindRepeating(&ScriptExecutor::WaitForDomOperation::RunChecks,
// safe since this instance owns retry_timer_
base::Unretained(this)),
base::BindOnce(&ScriptExecutor::WaitForDomOperation::RunCallback,
base::Unretained(this)));
}
void ScriptExecutor::WaitForDomOperation::Pause() {
if (interrupt_executor_) {
// If an interrupt is running, it'll be the one to be paused, if necessary.
return;
}
retry_timer_.Cancel();
}
void ScriptExecutor::WaitForDomOperation::Continue() {
if (retry_timer_.running() || !callback_)
return;
Start();
}
void ScriptExecutor::WaitForDomOperation::OnNavigationStateChanged() {
if (delegate_->IsNavigatingToNewDocument()) {
Pause();
} else {
Continue();
}
}
void ScriptExecutor::WaitForDomOperation::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::WaitForDomOperation::OnScriptListChanged(
std::vector<std::unique_ptr<Script>> scripts) {
main_script_->ReportScriptsUpdateToListener(std::move(scripts));
}
void ScriptExecutor::WaitForDomOperation::TimeoutWarning() {
if (warning_callback_) {
std::move(warning_callback_)
.Run(base::BindOnce(
&ScriptExecutor::WaitForDomOperation::SetSlowWarningStatus,
weak_ptr_factory_.GetWeakPtr()));
}
}
void ScriptExecutor::WaitForDomOperation::SetSlowWarningStatus(bool was_shown) {
if (was_shown) {
warning_status_ = WARNING_SHOWN;
} else {
warning_status_ = WARNING_TRIGGERED;
}
}
void ScriptExecutor::WaitForDomOperation::RunChecks(
base::OnceCallback<void(const ClientStatus&)> report_attempt_result) {
warning_timer_ = std::make_unique<base::OneShotTimer>();
warning_timer_->Start(
FROM_HERE, timeout_warning_delay_,
base::BindOnce(&ScriptExecutor::WaitForDomOperation::TimeoutWarning,
weak_ptr_factory_.GetWeakPtr()));
wait_time_total_ =
(wait_time_stopwatch_.TotalElapsed() < retry_timer_.period())
// It's the first run of the checks, set the total time waited to 0.
? base::Seconds(0)
// If this is not the first run of the checks, in order to estimate
// the real cost of periodic checks, half the duration of the retry
// timer period is removed from the total wait time. This is to
// account for the fact that the conditions could have been satisfied
// at any point between the two consecutive checks.
: wait_time_stopwatch_.TotalElapsed() - retry_timer_.period() / 2;
// Reset state possibly left over from previous runs.
element_check_result_ = ClientStatus();
runnable_interrupts_.clear();
batch_element_checker_ = std::make_unique<BatchElementChecker>();
check_elements_.Run(batch_element_checker_.get(),
base::BindOnce(&WaitForDomOperation::OnElementCheckDone,
base::Unretained(this)));
if (allow_interrupt_) {
for (const std::unique_ptr<Script>& interrupt :
*main_script_->ordered_interrupts_) {
if (ran_interrupts_.find(interrupt->handle.path) !=
ran_interrupts_.end()) {
continue;
}
interrupt->precondition->Check(
delegate_->GetCurrentURL(), batch_element_checker_.get(),
*delegate_->GetTriggerContext(),
base::BindOnce(&WaitForDomOperation::OnPreconditionCheckDone,
weak_ptr_factory_.GetWeakPtr(),
interrupt->handle.path));
}
}
batch_element_checker_->AddAllDoneCallback(
base::BindOnce(&WaitForDomOperation::OnAllChecksDone,
base::Unretained(this), std::move(report_attempt_result)));
batch_element_checker_->Run(delegate_->GetWebController());
}
void ScriptExecutor::WaitForDomOperation::OnPreconditionCheckDone(
const std::string& interrupt_path,
bool precondition_match) {
if (precondition_match)
runnable_interrupts_.insert(interrupt_path);
}
void ScriptExecutor::WaitForDomOperation::OnElementCheckDone(
const ClientStatus& element_status) {
element_check_result_ = element_status;
// 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::WaitForDomOperation::OnAllChecksDone(
base::OnceCallback<void(const ClientStatus&)> report_attempt_result) {
warning_timer_->Stop();
if (runnable_interrupts_.empty()) {
// Since no interrupts fired, allow previously-run interrupts to be run
// again in the next round. This is meant to give elements one round to
// disappear and avoid the simplest form of loops. A round with interrupts
// firing doesn't count as one round here, because an interrupt can run
// quickly and return immediately, without waiting for
// periodic_element_check_interval.
ran_interrupts_.clear();
} else {
// 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 std::unique_ptr<Script>& interrupt :
*main_script_->ordered_interrupts_) {
const std::string& path = interrupt->handle.path;
if (runnable_interrupts_.find(path) != runnable_interrupts_.end()) {
RunInterrupt(path);
return;
}
}
}
std::move(report_attempt_result).Run(element_check_result_);
}
void ScriptExecutor::WaitForDomOperation::RunInterrupt(
const std::string& path) {
batch_element_checker_.reset();
if (observer_)
observer_->OnInterruptStarted();
SavePreInterruptState();
ran_interrupts_.insert(path);
interrupt_executor_ = std::make_unique<ScriptExecutor>(
path,
std::make_unique<TriggerContext>(std::vector<const TriggerContext*>{
main_script_->additional_context_.get()}),
main_script_->last_global_payload_, main_script_->initial_script_payload_,
/* listener= */ this, &no_interrupts_, delegate_);
delegate_->EnterState(AutofillAssistantState::RUNNING);
delegate_->SetUserActions(nullptr);
interrupt_executor_->Run(
main_script_->user_data_,
base::BindOnce(&ScriptExecutor::WaitForDomOperation::OnInterruptDone,
base::Unretained(this)));
// base::Unretained(this) is safe because interrupt_executor_ belongs to this
}
void ScriptExecutor::WaitForDomOperation::OnInterruptDone(
const ScriptExecutor::Result& result) {
interrupt_executor_.reset();
if (!result.success || result.at_end != ScriptExecutor::CONTINUE) {
RunCallbackWithResult(ClientStatus(INTERRUPT_FAILED), &result);
return;
}
if (observer_)
observer_->OnInterruptFinished();
RestoreStatusMessage();
RestorePreInterruptScroll();
// 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.
Start();
}
void ScriptExecutor::WaitForDomOperation::RunCallback(
const ClientStatus& element_status) {
RunCallbackWithResult(element_status, nullptr);
}
void ScriptExecutor::WaitForDomOperation::RunCallbackWithResult(
const ClientStatus& element_status,
const ScriptExecutor::Result* result) {
// stop element checking if one is still in progress
batch_element_checker_.reset();
retry_timer_.Cancel();
warning_timer_->Stop();
if (!callback_)
return;
ClientStatus status(element_status);
status.set_slow_warning_status(warning_status_);
std::move(callback_).Run(status, result, wait_time_total_);
}
void ScriptExecutor::WaitForDomOperation::SavePreInterruptState() {
if (saved_pre_interrupt_state_)
return;
pre_interrupt_status_ = delegate_->GetStatusMessage();
saved_pre_interrupt_state_ = true;
}
void ScriptExecutor::WaitForDomOperation::RestoreStatusMessage() {
if (!saved_pre_interrupt_state_)
return;
delegate_->SetStatusMessage(pre_interrupt_status_);
}
void ScriptExecutor::WaitForDomOperation::RestorePreInterruptScroll() {
if (!saved_pre_interrupt_state_)
return;
if (!main_script_->last_focused_element_.has_value())
return;
auto element = std::make_unique<ElementFinder::Result>();
if (!main_script_->GetElementStore()
->RestoreElement(*main_script_->last_focused_element_, element.get())
.ok()) {
return;
}
auto actions = std::make_unique<element_action_util::ElementActionVector>();
action_delegate_util::AddStepIgnoreTiming(
base::BindOnce(&ActionDelegate::WaitUntilDocumentIsInReadyState,
main_script_->GetWeakPtr(),
delegate_->GetSettings().document_ready_check_timeout,
DOCUMENT_INTERACTIVE),
actions.get());
actions->emplace_back(
base::BindOnce(&WebController::ScrollIntoViewIfNeeded,
main_script_->GetWebController()->GetWeakPtr(),
/* center= */ true));
element_action_util::TakeElementAndPerform(
base::BindOnce(&element_action_util::PerformAll, std::move(actions)),
/* done= */ base::DoNothing(), /* element_status= */ OkClientStatus(),
std::move(element));
}
ScriptExecutor::CurrentActionData::CurrentActionData() = default;
ScriptExecutor::CurrentActionData::~CurrentActionData() = default;
ScriptExecutor::CurrentActionData& ScriptExecutor::CurrentActionData::operator=(
ScriptExecutor::CurrentActionData&& other) = default;
std::ostream& operator<<(std::ostream& out,
const ScriptExecutor::Result& result) {
result.success ? out << "succeeded. " : out << "failed. ";
out << "at_end = " << result.at_end;
return out;
}
} // namespace autofill_assistant