blob: 0564f40c70b61049c0b1466421185756ab6e0da8 [file] [log] [blame]
// Copyright 2020 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/search/repeatable_queries/repeatable_queries_service.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "base/stl_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
#include "components/history/core/browser/keyword_search_term.h"
#include "components/history/core/browser/url_database.h"
#include "components/search/ntp_features.h"
#include "components/search/search_provider_observer.h"
#include "components/search_engines/template_url_service.h"
#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
namespace {
const char kXSSIResponsePreamble[] = ")]}'";
const size_t kMaxQueries = 2;
bool JsonToRepeatableQueriesData(const base::Value& root_value,
std::vector<RepeatableQuery>* data) {
// 1st element is the query. 2nd element is the list of results.
base::string16 query;
const base::ListValue* root_list = nullptr;
const base::ListValue* results_list = nullptr;
if (!root_value.GetAsList(&root_list) || !root_list->GetString(0, &query) ||
!query.empty() || !root_list->GetList(1, &results_list))
return false;
// Ignore the 3rd and 4th elements. 5th element is the key-value pairs from
// the Suggest server containing the deletion URLs.
const base::DictionaryValue* extras = nullptr;
const base::ListValue* suggestion_details = nullptr;
if (!root_list->GetDictionary(4, &extras) ||
!extras->GetList("google:suggestdetail", &suggestion_details) ||
suggestion_details->GetSize() != results_list->GetSize()) {
return false;
}
base::string16 suggestion;
for (size_t index = 0; results_list->GetString(index, &suggestion); ++index) {
RepeatableQuery result;
result.query = base::CollapseWhitespace(suggestion, false);
if (result.query.empty())
continue;
const base::DictionaryValue* suggestion_detail = nullptr;
if (suggestion_details->GetDictionary(index, &suggestion_detail)) {
suggestion_detail->GetString("du", &result.deletion_url);
}
data->push_back(result);
}
return !data->empty();
}
} // namespace
// static
const char RepeatableQueriesService::kExtractedCountHistogram[] =
"NewTabPage.RepeatableQueries.ExtractedCount";
const char RepeatableQueriesService::kExtractionDurationHistogram[] =
"NewTabPage.RepeatableQueries.ExtractionDuration";
class RepeatableQueriesService::SigninObserver
: public signin::IdentityManager::Observer {
public:
SigninObserver(signin::IdentityManager* identity_manager,
base::RepeatingClosure callback)
: identity_manager_(identity_manager), callback_(std::move(callback)) {
if (identity_manager_) {
identity_manager_observation_.Observe(identity_manager_);
}
}
~SigninObserver() override = default;
bool IsSignedIn() {
return identity_manager_ ? !identity_manager_->GetAccountsInCookieJar()
.signed_in_accounts.empty()
: false;
}
private:
// IdentityManager::Observer implementation.
void OnAccountsInCookieUpdated(
const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
const GoogleServiceAuthError& error) override {
callback_.Run();
}
base::ScopedObservation<signin::IdentityManager,
signin::IdentityManager::Observer>
identity_manager_observation_{this};
// May be nullptr in tests.
signin::IdentityManager* const identity_manager_;
base::RepeatingClosure callback_;
};
RepeatableQueriesService::RepeatableQueriesService(
signin::IdentityManager* identity_manager,
history::HistoryService* history_service,
TemplateURLService* template_url_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const GURL& request_initiator_url)
: history_service_(history_service),
template_url_service_(template_url_service),
url_loader_factory_(url_loader_factory),
request_initiator_url_(request_initiator_url),
signin_observer_(std::make_unique<SigninObserver>(
identity_manager,
base::BindRepeating(&RepeatableQueriesService::SigninStatusChanged,
base::Unretained(this)))),
search_provider_observer_(std::make_unique<SearchProviderObserver>(
template_url_service,
base::BindRepeating(&RepeatableQueriesService::SearchProviderChanged,
base::Unretained(this)))),
deletion_task_runner_(base::ThreadPool::CreateSequencedTaskRunner({})) {
DCHECK(history_service_);
DCHECK(template_url_service_);
DCHECK(url_loader_factory_);
}
RepeatableQueriesService::~RepeatableQueriesService() = default;
void RepeatableQueriesService::Shutdown() {
for (auto& observer : observers_) {
observer.OnRepeatableQueriesServiceShuttingDown();
}
}
const std::vector<RepeatableQuery>&
RepeatableQueriesService::repeatable_queries() const {
return repeatable_queries_;
}
void RepeatableQueriesService::Refresh() {
if (!search_provider_observer()->is_google()) {
NotifyObservers();
return;
}
if (signin_observer()->IsSignedIn()) {
GetRepeatableQueriesFromServer();
} else {
GetRepeatableQueriesFromURLDatabase();
}
}
void RepeatableQueriesService::DeleteQueryWithDestinationURL(const GURL& url) {
auto it = std::find_if(repeatable_queries_.begin(), repeatable_queries_.end(),
[&url](const auto& repeatable_query) {
return repeatable_query.destination_url == url;
});
// Return if no repeatable query with a matching destination URL exists.
if (it == repeatable_queries_.end()) {
// Still notify observers of the deletion attempt.
NotifyObservers();
return;
}
if (it->deletion_url.empty()) {
DeleteRepeatableQueryFromURLDatabase(it->query);
} else {
DeleteRepeatableQueryFromServer(it->deletion_url);
}
// Delete all the Google search URLs for the given query from history.
const TemplateURL* default_provider =
template_url_service_->GetDefaultSearchProvider();
if (default_provider) {
history_service_->DeleteMatchingURLsForKeyword(default_provider->id(),
it->query);
}
// Make sure the query is not suggested again.
MarkQueryAsDeleted(it->query);
// Update the repeatable queries and notify the observers.
repeatable_queries_.erase(it);
NotifyObservers();
}
void RepeatableQueriesService::AddObserver(
RepeatableQueriesServiceObserver* observer) {
observers_.AddObserver(observer);
}
void RepeatableQueriesService::RemoveObserver(
RepeatableQueriesServiceObserver* observer) {
observers_.RemoveObserver(observer);
}
RepeatableQueriesService::SigninObserver*
RepeatableQueriesService::signin_observer() {
return signin_observer_.get();
}
SearchProviderObserver* RepeatableQueriesService::search_provider_observer() {
return search_provider_observer_.get();
}
void RepeatableQueriesService::SearchProviderChanged() {
// If we have cached data, clear it.
repeatable_queries_.clear();
Refresh();
}
void RepeatableQueriesService::SigninStatusChanged() {
// If we have cached data, clear it.
repeatable_queries_.clear();
Refresh();
}
GURL RepeatableQueriesService::GetQueryDestinationURL(
const base::string16& query,
const TemplateURL* search_provider) {
DCHECK(search_provider);
TemplateURLRef::SearchTermsArgs search_terms_args(query);
const TemplateURLRef& search_url_ref = search_provider->url_ref();
const SearchTermsData& search_terms_data =
template_url_service_->search_terms_data();
DCHECK(search_url_ref.SupportsReplacement(search_terms_data));
return GURL(
search_url_ref.ReplaceSearchTerms(search_terms_args, search_terms_data));
}
GURL RepeatableQueriesService::GetQueryDeletionURL(
const std::string& deletion_url) {
const auto* default_provider =
template_url_service_->GetDefaultSearchProvider();
if (!default_provider)
return GURL();
const SearchTermsData& search_terms_data =
template_url_service_->search_terms_data();
GURL request_url = default_provider->GenerateSearchURL(search_terms_data);
return request_url.GetOrigin().Resolve(deletion_url);
}
GURL RepeatableQueriesService::GetRequestURL() {
TemplateURLRef::SearchTermsArgs search_terms_args;
search_terms_args.request_source = TemplateURLRef::NON_SEARCHBOX_NTP;
const TemplateURLRef& suggestion_url_ref =
template_url_service_->GetDefaultSearchProvider()->suggestions_url_ref();
const SearchTermsData& search_terms_data =
template_url_service_->search_terms_data();
DCHECK(suggestion_url_ref.SupportsReplacement(search_terms_data));
return GURL(suggestion_url_ref.ReplaceSearchTerms(search_terms_args,
search_terms_data));
}
void RepeatableQueriesService::FlushForTesting(base::OnceClosure flushed) {
deletion_task_runner_->PostTaskAndReply(FROM_HERE, base::DoNothing(),
std::move(flushed));
}
void RepeatableQueriesService::GetRepeatableQueriesFromServer() {
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("repeatable_queries_service", R"(
semantics {
sender: "Repeatable Queries Service"
description:
"Downloads search queries to be shown on the Most Visited "
"section of New Tab Page to signed-in users based on their "
"previous search history."
trigger:
"Displaying the new tab page, if Google is the "
"configured search provider, and the user is signed in."
data: "Google credentials if user is signed in."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"Users can control this feature by selecting a non-Google default "
"search engine in Chrome settings under 'Search Engine', or by "
"signing out of the browser on the New Tab Page. Users can opt "
"out of this feature by switching to custom shortcuts."
chrome_policy {
DefaultSearchProviderEnabled {
policy_options {mode: MANDATORY}
DefaultSearchProviderEnabled: false
}
BrowserSignin {
policy_options {mode: MANDATORY}
BrowserSignin: 0
}
}
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
const GURL& request_url = GetRequestURL();
variations::AppendVariationsHeaderUnknownSignedIn(
request_url, variations::InIncognito::kNo, resource_request.get());
resource_request->url = request_url;
resource_request->request_initiator =
url::Origin::Create(request_initiator_url_);
loaders_.push_back(network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation));
loaders_.back()->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&RepeatableQueriesService::RepeatableQueriesResponseLoaded,
weak_ptr_factory_.GetWeakPtr(), loaders_.back().get()),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void RepeatableQueriesService::RepeatableQueriesResponseLoaded(
network::SimpleURLLoader* loader,
std::unique_ptr<std::string> response) {
auto net_error = loader->NetError();
base::EraseIf(loaders_, [loader](const auto& loader_ptr) {
return loader == loader_ptr.get();
});
if (net_error != net::OK || !response) {
// In the case of network errors, keep the cached data, if any, but still
// notify observers of the finished load attempt.
NotifyObservers();
return;
}
if (base::StartsWith(*response, kXSSIResponsePreamble,
base::CompareCase::SENSITIVE)) {
*response = response->substr(strlen(kXSSIResponsePreamble));
}
data_decoder::DataDecoder::ParseJsonIsolated(
*response,
base::BindOnce(&RepeatableQueriesService::RepeatableQueriesParsed,
weak_ptr_factory_.GetWeakPtr()));
}
void RepeatableQueriesService::RepeatableQueriesParsed(
data_decoder::DataDecoder::ValueOrError result) {
const TemplateURL* default_provider =
template_url_service_->GetDefaultSearchProvider();
if (!default_provider)
return;
repeatable_queries_.clear();
std::vector<RepeatableQuery> queries;
if (result.value && JsonToRepeatableQueriesData(*result.value, &queries)) {
for (auto& query : queries) {
if (IsQueryDeleted(query.query))
continue;
query.destination_url =
GetQueryDestinationURL(query.query, default_provider);
repeatable_queries_.push_back(query);
if (repeatable_queries_.size() >= kMaxQueries)
break;
}
}
NotifyObservers();
}
void RepeatableQueriesService::GetRepeatableQueriesFromURLDatabase() {
const TemplateURL* default_provider =
template_url_service_->GetDefaultSearchProvider();
if (!default_provider)
return;
repeatable_queries_.clear();
// Fail if the in-memory URLDatabase is not available.
history::URLDatabase* url_db = history_service_->InMemoryDatabase();
if (!url_db)
return;
const base::TimeTicks db_query_time = base::TimeTicks::Now();
auto results = url_db->GetMostRecentNormalizedKeywordSearchTerms(
template_url_service_->GetDefaultSearchProvider()->id(),
ntp_features::GetLocalHistoryRepeatableQueriesAgeThreshold());
const base::Time now = base::Time::Now();
const int kRecencyDecayUnitSec =
ntp_features::GetLocalHistoryRepeatableQueriesRecencyHalfLifeSeconds();
const double kFrequencyExponent =
ntp_features::GetLocalHistoryRepeatableQueriesFrequencyExponent();
auto CompareByFrecency = [&](const auto& a, const auto& b) {
return a.GetFrecency(now, kRecencyDecayUnitSec, kFrequencyExponent) >
b.GetFrecency(now, kRecencyDecayUnitSec, kFrequencyExponent);
};
std::sort(results.begin(), results.end(), CompareByFrecency);
for (const auto& result : results) {
RepeatableQuery repeatable_query;
repeatable_query.query = result.normalized_term;
if (IsQueryDeleted(repeatable_query.query))
continue;
repeatable_query.destination_url =
GetQueryDestinationURL(repeatable_query.query, default_provider);
repeatable_queries_.push_back(repeatable_query);
if (repeatable_queries_.size() >= kMaxQueries)
break;
}
base::UmaHistogramTimes(kExtractionDurationHistogram,
base::TimeTicks::Now() - db_query_time);
base::UmaHistogramCounts10000(kExtractedCountHistogram, results.size());
NotifyObservers();
}
void RepeatableQueriesService::DeleteRepeatableQueryFromServer(
const std::string& deletion_url) {
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("repeatable_queries_deletion", R"(
semantics {
sender: "Repeatable Queries Service"
description:
"When users attempt to delete a server-provided repeatable search "
"query from the Most Visited section of New Tab Page, Chrome sends "
"a request to the server requesting deletion of that suggestion."
trigger:
"User attempts to delete a server-provided repeatable search "
"query for which the server provided a custom deletion URL from "
"the Most Visited section of New Tab Page, if Google is the "
"configured search provider, and the user is signed in."
data: "Google credentials if user is signed in."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"Users can control this feature by selecting a non-Google default "
"search engine in Chrome settings under 'Search Engine', or by "
"signing out of the browser on the New Tab Page. Users can opt "
"out of this feature by switching to custom shortcuts."
chrome_policy {
DefaultSearchProviderEnabled {
policy_options {mode: MANDATORY}
DefaultSearchProviderEnabled: false
}
BrowserSignin {
policy_options {mode: MANDATORY}
BrowserSignin: 0
}
}
})");
GURL request_url = GetQueryDeletionURL(deletion_url);
if (!request_url.is_valid())
return;
auto deletion_request = std::make_unique<network::ResourceRequest>();
variations::AppendVariationsHeaderUnknownSignedIn(
request_url, variations::InIncognito::kNo, deletion_request.get());
deletion_request->url = request_url;
deletion_request->request_initiator =
url::Origin::Create(request_initiator_url_);
loaders_.push_back(network::SimpleURLLoader::Create(
std::move(deletion_request), traffic_annotation));
loaders_.back()->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&RepeatableQueriesService::DeletionResponseLoaded,
weak_ptr_factory_.GetWeakPtr(), loaders_.back().get()),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void RepeatableQueriesService::DeletionResponseLoaded(
network::SimpleURLLoader* loader,
std::unique_ptr<std::string> response) {
base::EraseIf(loaders_, [loader](const auto& loader_ptr) {
return loader == loader_ptr.get();
});
}
void RepeatableQueriesService::DeleteRepeatableQueryFromURLDatabase(
const base::string16& query) {
deletion_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&RepeatableQueriesService::DeleteRepeatableQueryFromURLDatabaseTask,
weak_ptr_factory_.GetWeakPtr(), query,
history_service_->InMemoryDatabase()));
}
void RepeatableQueriesService::DeleteRepeatableQueryFromURLDatabaseTask(
const base::string16& query,
history::URLDatabase* url_db) {
// Fail if the in-memory URLDatabase is not available.
if (!url_db)
return;
// Delete all the search terms matching the repeatable query suggestion from
// the in-memory URLDatabase.
url_db->DeleteKeywordSearchTermForNormalizedTerm(
template_url_service_->GetDefaultSearchProvider()->id(), query);
}
void RepeatableQueriesService::NotifyObservers() {
for (auto& observer : observers_) {
observer.OnRepeatableQueriesUpdated();
}
}
bool RepeatableQueriesService::IsQueryDeleted(const base::string16& query) {
return base::Contains(deleted_repeatable_queries_, query);
}
void RepeatableQueriesService::MarkQueryAsDeleted(const base::string16& query) {
deleted_repeatable_queries_.insert(query);
}