| // 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 <ostream> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.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/actions/action.h" |
| #include "components/autofill_assistant/browser/batch_element_checker.h" |
| #include "components/autofill_assistant/browser/client_memory.h" |
| #include "components/autofill_assistant/browser/client_status.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); |
| |
| // Time between two element checks. |
| static constexpr base::TimeDelta kPeriodicElementCheck = |
| base::TimeDelta::FromSeconds(1); |
| |
| 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; |
| case ScriptExecutor::RESTART: |
| out << "RESTART"; |
| break; |
| case ScriptExecutor::TERMINATE: |
| out << "TERMINATE"; |
| 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, |
| 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), |
| retry_timer_(kPeriodicElementCheck), |
| weak_ptr_factory_(this) { |
| DCHECK(delegate_); |
| DCHECK(ordered_interrupts_); |
| } |
| ScriptExecutor::~ScriptExecutor() {} |
| |
| ScriptExecutor::Result::Result() = default; |
| ScriptExecutor::Result::~Result() = default; |
| |
| void ScriptExecutor::Run(RunScriptCallback callback) { |
| DVLOG(2) << "Starting script " << script_path_; |
| (*scripts_state_)[script_path_] = SCRIPT_STATUS_RUNNING; |
| |
| callback_ = std::move(callback); |
| DCHECK(delegate_->GetService()); |
| |
| DVLOG(2) << "GetActions for " << delegate_->GetCurrentURL().host(); |
| delegate_->GetService()->GetActions( |
| script_path_, delegate_->GetCurrentURL(), delegate_->GetTriggerContext(), |
| last_global_payload_, last_script_payload_, |
| base::BindOnce(&ScriptExecutor::OnGetActions, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ScriptExecutor::RunElementChecks(BatchElementChecker* checker, |
| base::OnceCallback<void()> all_done) { |
| return checker->Run(delegate_->GetWebController(), std::move(all_done)); |
| } |
| |
| void ScriptExecutor::ShortWaitForElement( |
| const Selector& selector, |
| base::OnceCallback<void(bool)> callback) { |
| retry_timer_.Start( |
| kShortWaitForElementDeadline, |
| base::BindRepeating(&ScriptExecutor::CheckForElement, |
| weak_ptr_factory_.GetWeakPtr(), selector), |
| std::move(callback)); |
| } |
| |
| void ScriptExecutor::CheckForElement(const Selector& selector, |
| base::OnceCallback<void(bool)> callback) { |
| delegate_->GetWebController()->ElementCheck(selector, |
| /* strict= */ false, |
| std::move(callback)); |
| } |
| |
| void ScriptExecutor::WaitForDom( |
| base::TimeDelta max_wait_time, |
| bool allow_interrupt, |
| ActionDelegate::SelectorPredicate selector_predicate, |
| const Selector& selector, |
| base::OnceCallback<void(ProcessedActionStatusProto)> callback) { |
| wait_for_dom_ = std::make_unique<WaitForDomOperation>( |
| this, max_wait_time, allow_interrupt, selector_predicate, selector, |
| base::BindOnce(&ScriptExecutor::OnWaitForElementVisibleWithInterrupts, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| wait_for_dom_->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(const ClientStatus&)> callback) { |
| delegate_->GetWebController()->ClickOrTapElement(selector, |
| std::move(callback)); |
| } |
| |
| void ScriptExecutor::GetPaymentInformation( |
| std::unique_ptr<PaymentRequestOptions> options) { |
| options->callback = base::BindOnce(&ScriptExecutor::OnGetPaymentInformation, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(options->callback)); |
| delegate_->SetPaymentRequestOptions(std::move(options)); |
| delegate_->EnterState(AutofillAssistantState::PROMPT); |
| } |
| |
| 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, |
| base::OnceCallback<void()> on_terminate) { |
| 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_->SetChips(std::move(chips)); |
| on_terminate_prompt_ = std::move(on_terminate); |
| } |
| |
| void ScriptExecutor::CancelPrompt() { |
| // Delete on_terminate_prompt_ if necessary, without running. |
| if (on_terminate_prompt_) |
| std::move(on_terminate_prompt_); |
| |
| delegate_->SetChips(nullptr); |
| 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(const ClientStatus&)> 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(const ClientStatus&)> 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(const ClientStatus&)> callback) { |
| delegate_->GetWebController()->SelectOption(selector, selected_option, |
| std::move(callback)); |
| } |
| |
| void ScriptExecutor::HighlightElement( |
| const Selector& selector, |
| base::OnceCallback<void(const ClientStatus&)> callback) { |
| delegate_->GetWebController()->HighlightElement(selector, |
| std::move(callback)); |
| } |
| |
| void ScriptExecutor::FocusElement( |
| const Selector& selector, |
| base::OnceCallback<void(const ClientStatus&)> 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::SetProgress(int progress) { |
| delegate_->SetProgress(progress); |
| } |
| |
| void ScriptExecutor::SetProgressVisible(bool visible) { |
| delegate_->SetProgressVisible(visible); |
| } |
| |
| void ScriptExecutor::GetFieldValue( |
| const Selector& selector, |
| base::OnceCallback<void(bool, const std::string&)> callback) { |
| delegate_->GetWebController()->GetFieldValue(selector, std::move(callback)); |
| } |
| |
| void ScriptExecutor::SetFieldValue( |
| const Selector& selector, |
| const std::string& value, |
| bool simulate_key_presses, |
| base::OnceCallback<void(const ClientStatus&)> 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(const ClientStatus&)> callback) { |
| delegate_->GetWebController()->SetAttribute(selector, attribute, value, |
| std::move(callback)); |
| } |
| |
| void ScriptExecutor::SendKeyboardInput( |
| const Selector& selector, |
| const std::vector<UChar32>& codepoints, |
| base::OnceCallback<void(const ClientStatus&)> callback) { |
| delegate_->GetWebController()->SendKeyboardInput(selector, codepoints, |
| std::move(callback)); |
| } |
| |
| void ScriptExecutor::GetOuterHtml( |
| const Selector& selector, |
| base::OnceCallback<void(const ClientStatus&, 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_for_dom_) |
| wait_for_dom_->Terminate(); |
| at_end_ = TERMINATE; |
| should_stop_script_ = true; |
| |
| // Force PR and other prompt-based actions to end. |
| // |
| // TODO(b/128300038): get rid of this special case. Instead, delete actions |
| // without waiting for them to return. |
| delegate_->CancelPaymentRequest(); |
| if (on_terminate_prompt_) { |
| std::move(on_terminate_prompt_).Run(); |
| CancelPrompt(); |
| } |
| } |
| |
| void ScriptExecutor::Close() { |
| at_end_ = CLOSE_CUSTOM_TAB; |
| should_stop_script_ = true; |
| } |
| |
| void ScriptExecutor::Restart() { |
| at_end_ = RESTART; |
| } |
| |
| ClientMemory* ScriptExecutor::GetClientMemory() { |
| return delegate_->GetClientMemory(); |
| } |
| |
| autofill::PersonalDataManager* ScriptExecutor::GetPersonalDataManager() { |
| return delegate_->GetPersonalDataManager(); |
| } |
| |
| content::WebContents* ScriptExecutor::GetWebContents() { |
| return delegate_->GetWebContents(); |
| } |
| |
| void ScriptExecutor::SetDetails(std::unique_ptr<Details> details) { |
| return delegate_->SetDetails(std::move(details)); |
| } |
| |
| void ScriptExecutor::ClearInfoBox() { |
| delegate_->ClearInfoBox(); |
| } |
| |
| void ScriptExecutor::SetInfoBox(const InfoBox& info_box) { |
| delegate_->SetInfoBox(info_box); |
| } |
| |
| void ScriptExecutor::OnGetActions(bool result, const std::string& response) { |
| bool success = result && ProcessNextActionResponse(response); |
| DVLOG(2) << __func__ << " result=" << result; |
| 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) { |
| if (should_clean_contextual_ui_on_finish_ || !success) { |
| SetDetails(nullptr); |
| 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_); |
| (*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()); |
| DVLOG(2) << __func__ << ", get more actions"; |
| 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) { |
| DVLOG(2) << "Begin action: " << *action; |
| action->ProcessAction(this, base::BindOnce(&ScriptExecutor::OnProcessedAction, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ScriptExecutor::GetNextActions() { |
| delegate_->GetService()->GetNextActions( |
| delegate_->GetTriggerContext(), 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) { |
| DVLOG(1) << "Action failed: " << processed_action.status() |
| << ", get more actions"; |
| // Report error immediately, interrupting action processing. |
| GetNextActions(); |
| return; |
| } |
| ProcessNextAction(); |
| } |
| |
| 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::WaitForDomOperation::WaitForDomOperation( |
| ScriptExecutor* main_script, |
| base::TimeDelta max_wait_time, |
| bool allow_interrupt, |
| ActionDelegate::SelectorPredicate selector_predicate, |
| const Selector& selector, |
| WaitForDomOperation::Callback callback) |
| : main_script_(main_script), |
| max_wait_time_(max_wait_time), |
| allow_interrupt_(allow_interrupt), |
| selector_predicate_(selector_predicate), |
| selector_(selector), |
| callback_(std::move(callback)), |
| retry_timer_(kPeriodicElementCheck), |
| weak_ptr_factory_(this) {} |
| |
| ScriptExecutor::WaitForDomOperation::~WaitForDomOperation() = default; |
| |
| void ScriptExecutor::WaitForDomOperation::Run() { |
| 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::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::RunChecks( |
| base::OnceCallback<void(bool)> report_attempt_result) { |
| // Reset state possibly left over from previous runs. |
| element_check_result_ = false; |
| runnable_interrupts_.clear(); |
| batch_element_checker_ = std::make_unique<BatchElementChecker>(); |
| batch_element_checker_->AddElementCheck( |
| selector_, base::BindOnce(&WaitForDomOperation::OnElementCheckDone, |
| base::Unretained(this))); |
| if (allow_interrupt_) { |
| 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 WaitForDom, to avoid loops. |
| continue; |
| } |
| |
| interrupt->precondition->Check( |
| main_script_->delegate_->GetCurrentURL(), |
| batch_element_checker_.get(), |
| main_script_->delegate_->GetTriggerContext()->script_parameters, |
| *main_script_->scripts_state_, |
| base::BindOnce(&WaitForDomOperation::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( |
| main_script_->delegate_->GetWebController(), |
| base::BindOnce(&WaitForDomOperation::OnAllChecksDone, |
| base::Unretained(this), std::move(report_attempt_result))); |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::OnPreconditionCheckDone( |
| const Script* interrupt, |
| bool precondition_match) { |
| if (precondition_match) |
| runnable_interrupts_.insert(interrupt); |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::OnElementCheckDone(bool found) { |
| switch (selector_predicate_) { |
| case ActionDelegate::SelectorPredicate::kMatches: |
| element_check_result_ = found; |
| break; |
| |
| case ActionDelegate::SelectorPredicate::kDoesntMatch: |
| element_check_result_ = !found; |
| break; |
| |
| // Default intentionally left unset to cause a compilation error if a new |
| // value is added. |
| } |
| |
| // 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(bool)> report_attempt_result) { |
| 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; |
| } |
| } |
| } |
| std::move(report_attempt_result).Run(element_check_result_); |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::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::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(false, &result); |
| return; |
| } |
| RestoreStatusMessage(); |
| |
| // 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_check_result_, since it could have changed. |
| Run(); |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::RunCallback(bool found) { |
| RunCallbackWithResult(found, nullptr); |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::RunCallbackWithResult( |
| bool check_result, |
| const ScriptExecutor::Result* result) { |
| // stop element checking if one is still in progress |
| batch_element_checker_.reset(); |
| retry_timer_.Cancel(); |
| if (!callback_) |
| return; |
| |
| RestorePreInterruptScroll(check_result); |
| std::move(callback_).Run(check_result, result, ran_interrupts_); |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::SavePreInterruptState() { |
| if (saved_pre_interrupt_state_) |
| return; |
| |
| pre_interrupt_status_ = main_script_->delegate_->GetStatusMessage(); |
| saved_pre_interrupt_state_ = true; |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::RestoreStatusMessage() { |
| if (!saved_pre_interrupt_state_) |
| return; |
| |
| main_script_->delegate_->SetStatusMessage(pre_interrupt_status_); |
| } |
| |
| void ScriptExecutor::WaitForDomOperation::RestorePreInterruptScroll( |
| bool check_result) { |
| if (!saved_pre_interrupt_state_) |
| return; |
| |
| auto* delegate = main_script_->delegate_; |
| if (check_result && |
| selector_predicate_ == ActionDelegate::SelectorPredicate::kMatches) { |
| 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::WaitForDomOperation::Terminate() { |
| if (interrupt_executor_) |
| interrupt_executor_->Terminate(); |
| } |
| |
| 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 |