blob: c2a2b00e363ea81fe565bdef6f26203171a29ea6 [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/controller.h"
#include <utility>
#include "base/json/json_writer.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/values.h"
#include "components/autofill_assistant/browser/protocol_utils.h"
#include "components/autofill_assistant/browser/ui_controller.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
namespace autofill_assistant {
namespace {
// Time between two periodic script checks.
static constexpr base::TimeDelta kPeriodicScriptCheckInterval =
base::TimeDelta::FromSeconds(2);
// Number of script checks to run after a call to StartPeriodicScriptChecks.
static constexpr int kPeriodicScriptCheckCount = 10;
// Maximum number of script checks we should do before failing when trying to
// autostart.
static constexpr int kAutostartCheckCountLimit = 5;
// Caller parameter name.
static const char* const kCallerScriptParameterName = "CALLER";
} // namespace
// static
void Controller::CreateForWebContents(
content::WebContents* web_contents,
std::unique_ptr<Client> client,
std::unique_ptr<std::map<std::string, std::string>> parameters,
const std::string& locale,
const std::string& country_code) {
// Get the key early since |client| will be invalidated when moved below.
GURL server_url(client->GetServerUrl());
DCHECK(server_url.is_valid());
std::unique_ptr<Service> service = std::make_unique<Service>(
client->GetApiKey(), server_url, web_contents->GetBrowserContext(),
client->GetAccessTokenFetcher(), locale, country_code);
new Controller(web_contents, std::move(client),
WebController::CreateForWebContents(web_contents),
std::move(service), std::move(parameters));
}
Service* Controller::GetService() {
return service_.get();
}
UiController* Controller::GetUiController() {
return client_->GetUiController();
}
WebController* Controller::GetWebController() {
return web_controller_.get();
}
ClientMemory* Controller::GetClientMemory() {
return memory_.get();
}
const std::map<std::string, std::string>& Controller::GetParameters() {
return *parameters_;
}
autofill::PersonalDataManager* Controller::GetPersonalDataManager() {
return client_->GetPersonalDataManager();
}
content::WebContents* Controller::GetWebContents() {
return web_contents();
}
Controller::Controller(
content::WebContents* web_contents,
std::unique_ptr<Client> client,
std::unique_ptr<WebController> web_controller,
std::unique_ptr<Service> service,
std::unique_ptr<std::map<std::string, std::string>> parameters)
: content::WebContentsObserver(web_contents),
client_(std::move(client)),
web_controller_(std::move(web_controller)),
service_(std::move(service)),
script_tracker_(std::make_unique<ScriptTracker>(/* delegate= */ this,
/* listener= */ this)),
parameters_(std::move(parameters)),
memory_(std::make_unique<ClientMemory>()),
touchable_element_area_(web_controller_.get()),
weak_ptr_factory_(this) {
DCHECK(parameters_);
// Only set the controller as the delegate if web_contents does not yet have
// one.
// TODO(crbug.com/806868): Find a better way to get a loading progress instead
// of using the controller as a web_contents delegate. It may interfere with
// an already existing delegate.
if (web_contents->GetDelegate() == nullptr) {
clear_web_contents_delegate_ = true;
web_contents->SetDelegate(this);
}
GetUiController()->SetUiDelegate(this);
}
Controller::~Controller() {
if (clear_web_contents_delegate_) {
web_contents()->SetDelegate(nullptr);
}
}
void Controller::Start(const GURL& initialUrl) {
started_ = true;
if (initialUrl.is_valid())
GetOrCheckScripts(initialUrl);
if (allow_autostart_) {
auto iter = parameters_->find(kCallerScriptParameterName);
// TODO(crbug.com/806868): Put back an explicit AUTOSTART parameter so we
// don't need to know who calls us.
if (iter != parameters_->end() && iter->second == "1") {
should_fail_after_checking_scripts_ = true;
GetUiController()->ShowOverlay();
// TODO(crbug.com/806868): Find out how to add template string and add
// domain in the "Loading..." message.
GetUiController()->ShowStatusMessage(l10n_util::GetStringFUTF8(
IDS_AUTOFILL_ASSISTANT_LOADING,
base::UTF8ToUTF16(web_contents()->GetVisibleURL().host())));
}
}
touchable_element_area_.SetOnUpdate(base::BindRepeating(
&UiController::UpdateTouchableArea,
// Unretained is safe, since touchable_element_area_ is guaranteed to be
// deleted before the UI controller.
base::Unretained(GetUiController())));
}
void Controller::GetOrCheckScripts(const GURL& url) {
if (!started_) {
return;
}
if (script_tracker_->running())
return;
if (script_domain_ != url.host()) {
StopPeriodicScriptChecks();
script_domain_ = url.host();
service_->GetScriptsForUrl(
url, *parameters_,
base::BindOnce(&Controller::OnGetScripts, base::Unretained(this), url));
} else {
script_tracker_->CheckScripts(kPeriodicScriptCheckInterval);
StartPeriodicScriptChecks();
}
}
void Controller::StartPeriodicScriptChecks() {
periodic_script_check_count_ = kPeriodicScriptCheckCount;
// If periodic checks are running, setting periodic_script_check_count_ keeps
// them running longer.
if (periodic_script_check_scheduled_)
return;
periodic_script_check_scheduled_ = true;
base::PostDelayedTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&Controller::OnPeriodicScriptCheck,
weak_ptr_factory_.GetWeakPtr()),
kPeriodicScriptCheckInterval);
}
void Controller::StopPeriodicScriptChecks() {
periodic_script_check_count_ = 0;
}
void Controller::OnPeriodicScriptCheck() {
if (periodic_script_check_count_ <= 0) {
DCHECK_EQ(0, periodic_script_check_count_);
periodic_script_check_scheduled_ = false;
return;
}
if (should_fail_after_checking_scripts_ &&
++total_script_check_count_ >= kAutostartCheckCountLimit) {
should_fail_after_checking_scripts_ = false;
GetUiController()->HideOverlay();
GetUiController()->ShowStatusMessage(
l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_DEFAULT_ERROR));
GetUiController()->ShutdownGracefully();
return;
}
periodic_script_check_count_--;
script_tracker_->CheckScripts(kPeriodicScriptCheckInterval);
base::PostDelayedTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&Controller::OnPeriodicScriptCheck,
weak_ptr_factory_.GetWeakPtr()),
kPeriodicScriptCheckInterval);
}
void Controller::OnGetScripts(const GURL& url,
bool result,
const std::string& response) {
// If the domain of the current URL changed since the request was sent, the
// response is not relevant anymore and can be safely discarded.
if (url.host() != script_domain_)
return;
if (!result) {
LOG(ERROR) << "Failed to get assistant scripts for URL " << url.spec();
// TODO(crbug.com/806868): Terminate Autofill Assistant.
return;
}
std::vector<std::unique_ptr<Script>> scripts;
bool parse_result = ProtocolUtils::ParseScripts(response, &scripts);
DCHECK(parse_result);
script_tracker_->SetScripts(std::move(scripts));
script_tracker_->CheckScripts(kPeriodicScriptCheckInterval);
StartPeriodicScriptChecks();
}
void Controller::OnScriptExecuted(const std::string& script_path,
ScriptExecutor::Result result) {
GetUiController()->HideOverlay();
if (!result.success) {
LOG(ERROR) << "Failed to execute script " << script_path;
GetUiController()->ShowStatusMessage(
l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_DEFAULT_ERROR));
GetUiController()->ShutdownGracefully();
return;
}
touchable_element_area_.SetElements(result.touchable_elements);
switch (result.at_end) {
case ScriptExecutor::SHUTDOWN:
GetUiController()->Shutdown(); // indirectly deletes this
return;
case ScriptExecutor::SHUTDOWN_GRACEFULLY:
GetUiController()->ShutdownGracefully();
return;
case ScriptExecutor::RESTART:
script_tracker_ = std::make_unique<ScriptTracker>(/* delegate= */ this,
/* listener= */ this);
memory_ = std::make_unique<ClientMemory>();
script_domain_ = "";
break;
case ScriptExecutor::CONTINUE:
break;
default:
DLOG(ERROR) << "Unexpected value for at_end: " << result.at_end;
break;
}
GetOrCheckScripts(web_contents()->GetLastCommittedURL());
}
void Controller::GiveUp() {
GetUiController()->ShowStatusMessage(
l10n_util::GetStringUTF8(IDS_AUTOFILL_ASSISTANT_GIVE_UP));
GetUiController()->ShutdownGracefully();
}
void Controller::OnClickOverlay() {
GetUiController()->HideOverlay();
// TODO(crbug.com/806868): Stop executing scripts.
}
void Controller::OnScriptSelected(const std::string& script_path) {
DCHECK(!script_path.empty());
DCHECK(!script_tracker_->running());
GetUiController()->ShowOverlay();
touchable_element_area_.ClearElements();
StopPeriodicScriptChecks();
// Runnable scripts will be checked and reported if necessary after executing
// the script.
script_tracker_->ClearRunnableScripts();
GetUiController()->UpdateScripts({}); // Clear scripts.
allow_autostart_ = false; // Only ever autostart the very first script.
script_tracker_->ExecuteScript(
script_path, base::BindOnce(&Controller::OnScriptExecuted,
// script_tracker_ is owned by Controller.
base::Unretained(this), script_path));
}
std::string Controller::GetDebugContext() {
base::Value dict(base::Value::Type::DICTIONARY);
std::vector<base::Value> parameters_js;
for (const auto& entry : *parameters_) {
base::Value parameter_js = base::Value(base::Value::Type::DICTIONARY);
parameter_js.SetKey(entry.first, base::Value(entry.second));
parameters_js.push_back(std::move(parameter_js));
}
dict.SetKey("parameters", base::Value(parameters_js));
dict.SetKey("scripts", script_tracker_->GetDebugContext());
std::string output_js;
base::JSONWriter::Write(dict, &output_js);
return output_js;
}
void Controller::OnDestroy() {
delete this;
}
void Controller::ScrollBy(float distanceXRatio, float distanceYRatio) {
GetWebController()->ScrollBy(distanceXRatio, distanceYRatio);
touchable_element_area_.UpdatePositions();
}
void Controller::UpdateTouchableArea() {
touchable_element_area_.UpdatePositions();
}
void Controller::DidAttachInterstitialPage() {
GetUiController()->Shutdown();
}
void Controller::DidGetUserInteraction(const blink::WebInputEvent::Type type) {
switch (type) {
case blink::WebInputEvent::kTouchStart:
case blink::WebInputEvent::kGestureTapDown:
// Disable autostart after interaction with the web page.
allow_autostart_ = false;
if (!script_tracker_->running()) {
script_tracker_->CheckScripts(kPeriodicScriptCheckInterval);
StartPeriodicScriptChecks();
}
break;
default:
break;
}
}
void Controller::OnNoRunnableScriptsAnymore() {
if (script_tracker_->running())
return;
// We're navigated to a page that has no scripts or the scripts have reached a
// state from which they cannot recover through a DOM change.
GiveUp();
return;
}
void Controller::OnRunnableScriptsChanged(
const std::vector<ScriptHandle>& runnable_scripts) {
// Script selection is disabled when a script is already running. We will
// check again and maybe update when the current script has finished.
if (script_tracker_->running())
return;
if (!runnable_scripts.empty()) {
should_fail_after_checking_scripts_ = false;
GetUiController()->HideOverlay();
}
// Under specific conditions, we can directly run a script without first
// displaying it. This is meant to work only at the very beginning, when no
// scripts have run, there has been no interaction with the webpage and only
// if there's exactly one runnable autostartable script.
if (allow_autostart_) {
int autostart_count = 0;
std::string autostart_path;
for (const auto& script : runnable_scripts) {
if (script.autostart) {
autostart_count++;
autostart_path = script.path;
}
}
if (autostart_count == 1) {
OnScriptSelected(autostart_path);
return;
}
}
// Show the initial prompt if available.
for (const auto& script : runnable_scripts) {
// runnable_scripts is ordered by priority.
if (!script.initial_prompt.empty()) {
GetUiController()->ShowStatusMessage(script.initial_prompt);
break;
}
}
// Update the set of scripts. Only scripts that are not marked as autostart
// should be shown.
std::vector<ScriptHandle> scripts_to_update;
for (const auto& script : runnable_scripts) {
if (!script.autostart) {
scripts_to_update.emplace_back(script);
}
}
GetUiController()->UpdateScripts(scripts_to_update);
}
void Controller::DocumentAvailableInMainFrame() {
GetOrCheckScripts(web_contents()->GetLastCommittedURL());
}
void Controller::DidFinishLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
// validated_url might not be the page URL. Ignore it and always check the
// last committed url.
// Note that we also check for scripts in LoadProgressChanged below. This is
// the last attempt and occurs later than a load progress of 1.0.
GetOrCheckScripts(web_contents()->GetLastCommittedURL());
}
void Controller::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
// The following types of navigations are allowed for the main frame:
// - first-time URL load
// - script-directed navigation, while a script is running
// - server redirections, which might happen outside of a script, but
// because of a load triggered by a previously-running script.
// - same-document modifications, which might happen automatically
// - javascript-initiated navigation or refresh
// - navigation by clicking on a link
// In the last two cases, autofill assistant might still give up later on if
// it discovers that the new page has no scripts.
//
// Everything else, such as going back to a previous page, or refreshing the
// page is considered an end condition.
if (navigation_handle->IsInMainFrame() &&
web_contents()->GetLastCommittedURL().is_valid() &&
!script_tracker_->running() && !navigation_handle->WasServerRedirect() &&
!navigation_handle->IsSameDocument() &&
!navigation_handle->IsRendererInitiated()) {
GiveUp();
return;
}
}
void Controller::RenderProcessGone(base::TerminationStatus status) {
GetUiController()->Shutdown();
}
void Controller::WebContentsDestroyed() {
OnDestroy();
}
void Controller::LoadProgressChanged(content::WebContents* source,
double progress) {
int percent = 100 * progress;
// We wait for a page to be at least 40 percent loaded. Then a new
// precondition check is started every additional 20 percent.
if (percent >= 40 && percent % 20 == 0) {
DCHECK(web_contents()->GetLastCommittedURL().is_valid());
// In order to show available scripts as early as possible we start checking
// preconditions when the page has not yet fully loaded. This can lead to
// the behavior where scripts are being added sequentially instead of all
// at the same time. Also, depending on the progress values, we may never
// actually get here. In that case the only check will happen in
// DidFinishLoad.
GetOrCheckScripts(web_contents()->GetLastCommittedURL());
}
}
} // namespace autofill_assistant