blob: 21b1c9132c26665ffbf25daeefce8c73824de64a [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 "chrome/browser/prefetch/search_prefetch/search_prefetch_service.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/util/values/values_util.h"
#include "base/values.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/net/prediction_options.h"
#include "chrome/browser/prefetch/pref_names.h"
#include "chrome/browser/prefetch/search_prefetch/back_forward_search_prefetch_url_loader.h"
#include "chrome/browser/prefetch/search_prefetch/field_trial_settings.h"
#include "chrome/browser/prefetch/search_prefetch/full_body_search_prefetch_request.h"
#include "chrome/browser/prefetch/search_prefetch/search_prefetch_url_loader.h"
#include "chrome/browser/prefetch/search_prefetch/streaming_search_prefetch_request.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/search_engines/ui_thread_search_terms_data.h"
#include "chrome/common/pref_names.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/base_search_provider.h"
#include "components/omnibox/browser/omnibox_event_global_tracker.h"
#include "components/omnibox/browser/omnibox_log.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/search_engines/template_url_service.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/resource_request.h"
#include "url/origin.h"
namespace {
// Recomputes the destination URL with the added prefetch information for
// |match| (does not modify |destination_url|).
GURL GetPrefetchURLFromMatch(const AutocompleteMatch& match,
TemplateURLService* template_url_service) {
// Copy the search term args, so we can modify them for just the prefetch.
auto search_terms_args = *(match.search_terms_args);
search_terms_args.is_prefetch = true;
return GURL(template_url_service->GetDefaultSearchProvider()
->url_ref()
.ReplaceSearchTerms(search_terms_args,
template_url_service->search_terms_data(),
nullptr));
}
struct SearchPrefetchEligibilityReasonRecorder {
public:
SearchPrefetchEligibilityReasonRecorder() = default;
~SearchPrefetchEligibilityReasonRecorder() {
UMA_HISTOGRAM_ENUMERATION(
"Omnibox.SearchPrefetch.PrefetchEligibilityReason", reason_);
}
SearchPrefetchEligibilityReason reason_ =
SearchPrefetchEligibilityReason::kPrefetchStarted;
};
struct SearchPrefetchServingReasonRecorder {
public:
SearchPrefetchServingReasonRecorder() = default;
~SearchPrefetchServingReasonRecorder() {
UMA_HISTOGRAM_ENUMERATION("Omnibox.SearchPrefetch.PrefetchServingReason",
reason_);
}
SearchPrefetchServingReason reason_ = SearchPrefetchServingReason::kServed;
};
void RecordFinalStatus(SearchPrefetchStatus status) {
UMA_HISTOGRAM_ENUMERATION("Omnibox.SearchPrefetch.PrefetchFinalStatus",
status);
}
} // namespace
// static
void SearchPrefetchService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
// Some loss in this pref (especially following a browser crash) is well
// tolerated and helps ensure the pref service isn't slammed.
registry->RegisterDictionaryPref(prefetch::prefs::kCachePrefPath,
PrefRegistry::LOSSY_PREF);
}
SearchPrefetchService::SearchPrefetchService(Profile* profile)
: profile_(profile) {
DCHECK(!profile_->IsOffTheRecord());
if (LoadFromPrefs())
SaveToPrefs();
}
SearchPrefetchService::~SearchPrefetchService() = default;
void SearchPrefetchService::Shutdown() {
observer_.Reset();
}
bool SearchPrefetchService::MaybePrefetchURL(const GURL& url) {
if (!SearchPrefetchServicePrefetchingIsEnabled())
return false;
SearchPrefetchEligibilityReasonRecorder recorder;
if (!chrome_browser_net::CanPreresolveAndPreconnectUI(profile_->GetPrefs())) {
recorder.reason_ = SearchPrefetchEligibilityReason::kPrefetchDisabled;
return false;
}
if (!profile_->GetPrefs() ||
!profile_->GetPrefs()->GetBoolean(prefs::kWebKitJavascriptEnabled)) {
recorder.reason_ = SearchPrefetchEligibilityReason::kJavascriptDisabled;
return false;
}
auto* content_settings =
HostContentSettingsMapFactory::GetForProfile(profile_);
if (!content_settings ||
content_settings->GetContentSetting(
url, url, ContentSettingsType::JAVASCRIPT) == CONTENT_SETTING_BLOCK) {
recorder.reason_ = SearchPrefetchEligibilityReason::kJavascriptDisabled;
return false;
}
auto* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile_);
if (!template_url_service ||
!template_url_service->GetDefaultSearchProvider()) {
recorder.reason_ = SearchPrefetchEligibilityReason::kSearchEngineNotValid;
return false;
}
// Lazily observe Template URL Service.
if (!observer_.IsObserving()) {
observer_.Observe(template_url_service);
const TemplateURL* template_url =
template_url_service->GetDefaultSearchProvider();
if (template_url) {
template_url_service_data_ = template_url->data();
}
omnibox_subscription_ =
OmniboxEventGlobalTracker::GetInstance()->RegisterCallback(
base::BindRepeating(&SearchPrefetchService::OnURLOpenedFromOmnibox,
base::Unretained(this)));
}
std::u16string search_terms;
// Extract the terms directly to make sure this string will match the URL
// interception string logic.
template_url_service->GetDefaultSearchProvider()->ExtractSearchTermsFromURL(
url, template_url_service->search_terms_data(), &search_terms);
if (search_terms.size() == 0) {
recorder.reason_ =
SearchPrefetchEligibilityReason::kNotDefaultSearchWithTerms;
return false;
}
if (last_error_time_ticks_ + SearchPrefetchErrorBackoffDuration() >
base::TimeTicks::Now()) {
recorder.reason_ = SearchPrefetchEligibilityReason::kErrorBackoff;
return false;
}
// Don't prefetch the same search terms twice within the expiry duration.
if (prefetches_.find(search_terms) != prefetches_.end()) {
recorder.reason_ = SearchPrefetchEligibilityReason::kAttemptedQueryRecently;
return false;
}
if (prefetches_.size() >= SearchPrefetchMaxAttemptsPerCachingDuration()) {
recorder.reason_ = SearchPrefetchEligibilityReason::kMaxAttemptsReached;
return false;
}
std::unique_ptr<BaseSearchPrefetchRequest> prefetch_request;
if (StreamSearchPrefetchResponses()) {
prefetch_request = std::make_unique<StreamingSearchPrefetchRequest>(
url, base::BindOnce(&SearchPrefetchService::ReportError,
base::Unretained(this)));
} else {
prefetch_request = std::make_unique<FullBodySearchPrefetchRequest>(
url, base::BindOnce(&SearchPrefetchService::ReportError,
base::Unretained(this)));
}
DCHECK(prefetch_request);
if (!prefetch_request->StartPrefetchRequest(profile_)) {
recorder.reason_ = SearchPrefetchEligibilityReason::kThrottled;
return false;
}
prefetches_.emplace(search_terms, std::move(prefetch_request));
prefetch_expiry_timers_.emplace(search_terms,
std::make_unique<base::OneShotTimer>());
prefetch_expiry_timers_[search_terms]->Start(
FROM_HERE, SearchPrefetchCachingLimit(),
base::BindOnce(&SearchPrefetchService::DeletePrefetch,
base::Unretained(this), search_terms));
return true;
}
void SearchPrefetchService::OnURLOpenedFromOmnibox(OmniboxLog* log) {
if (!log)
return;
const AutocompleteMatch& match = log->result.match_at(log->selected_index);
const GURL& opened_url = match.destination_url;
auto* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile_);
DCHECK(template_url_service);
auto* default_search = template_url_service->GetDefaultSearchProvider();
if (!default_search)
return;
std::u16string match_search_terms;
default_search->ExtractSearchTermsFromURL(
opened_url, template_url_service->search_terms_data(),
&match_search_terms);
if (prefetches_.find(match_search_terms) == prefetches_.end() ||
prefetches_[match_search_terms]->current_status() !=
SearchPrefetchStatus::kCanBeServed) {
return;
}
prefetches_[match_search_terms]->MarkPrefetchAsClicked();
}
base::Optional<SearchPrefetchStatus>
SearchPrefetchService::GetSearchPrefetchStatusForTesting(
std::u16string search_terms) {
if (prefetches_.find(search_terms) == prefetches_.end())
return base::nullopt;
return prefetches_[search_terms]->current_status();
}
std::unique_ptr<SearchPrefetchURLLoader>
SearchPrefetchService::TakePrefetchResponseFromMemoryCache(
const network::ResourceRequest& tentative_resource_request) {
const GURL& navigation_url = tentative_resource_request.url;
SearchPrefetchServingReasonRecorder recorder;
auto* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile_);
if (!template_url_service ||
!template_url_service->GetDefaultSearchProvider()) {
recorder.reason_ = SearchPrefetchServingReason::kSearchEngineNotValid;
return nullptr;
}
// The user may have disabled JS since the prefetch occured.
if (!profile_->GetPrefs() ||
!profile_->GetPrefs()->GetBoolean(prefs::kWebKitJavascriptEnabled)) {
recorder.reason_ = SearchPrefetchServingReason::kJavascriptDisabled;
return nullptr;
}
auto* content_settings =
HostContentSettingsMapFactory::GetForProfile(profile_);
if (!content_settings ||
content_settings->GetContentSetting(navigation_url, navigation_url,
ContentSettingsType::JAVASCRIPT) ==
CONTENT_SETTING_BLOCK) {
recorder.reason_ = SearchPrefetchServingReason::kJavascriptDisabled;
return nullptr;
}
std::u16string search_terms;
template_url_service->GetDefaultSearchProvider()->ExtractSearchTermsFromURL(
navigation_url, template_url_service->search_terms_data(), &search_terms);
if (search_terms.length() == 0) {
recorder.reason_ = SearchPrefetchServingReason::kNotDefaultSearchWithTerms;
return nullptr;
}
const auto& iter = prefetches_.find(search_terms);
if (iter == prefetches_.end()) {
recorder.reason_ = SearchPrefetchServingReason::kNoPrefetch;
return nullptr;
}
// Verify that the URL is the same origin as the prefetch URL. While other
// checks should address this by clearing prefetches on user changes to
// default search, it is paramount to never serve content from one origin to
// another.
if (url::Origin::Create(navigation_url) !=
url::Origin::Create(iter->second->prefetch_url())) {
recorder.reason_ =
SearchPrefetchServingReason::kPrefetchWasForDifferentOrigin;
return nullptr;
}
if (iter->second->current_status() ==
SearchPrefetchStatus::kRequestCancelled) {
recorder.reason_ = SearchPrefetchServingReason::kRequestWasCancelled;
return nullptr;
}
if (iter->second->current_status() == SearchPrefetchStatus::kRequestFailed) {
recorder.reason_ = SearchPrefetchServingReason::kRequestFailed;
return nullptr;
}
// POST requests are not supported since they are non-idempotent. Only support
// GET.
if (tentative_resource_request.method !=
net::HttpRequestHeaders::kGetMethod) {
recorder.reason_ = SearchPrefetchServingReason::kPostReloadOrLink;
return nullptr;
}
// If the client requests disabling, bypassing, or validating cache, don't
// return a prefetch.
// These are used mostly for reloads and dev tools.
if (tentative_resource_request.load_flags & net::LOAD_BYPASS_CACHE ||
tentative_resource_request.load_flags & net::LOAD_DISABLE_CACHE ||
tentative_resource_request.load_flags & net::LOAD_VALIDATE_CACHE) {
recorder.reason_ = SearchPrefetchServingReason::kPostReloadOrLink;
return nullptr;
}
// Link clicks should not be served with a prefetch due to results page nth
// page matching the URL pattern of the DSE.
if (ui::PageTransitionCoreTypeIs(
static_cast<ui::PageTransition>(
tentative_resource_request.transition_type),
ui::PAGE_TRANSITION_LINK)) {
recorder.reason_ = SearchPrefetchServingReason::kPostReloadOrLink;
return nullptr;
}
if (iter->second->current_status() != SearchPrefetchStatus::kComplete &&
iter->second->current_status() !=
SearchPrefetchStatus::kCanBeServedAndUserClicked) {
recorder.reason_ = SearchPrefetchServingReason::kNotServedOtherReason;
return nullptr;
}
std::unique_ptr<SearchPrefetchURLLoader> response =
iter->second->TakeSearchPrefetchURLLoader();
AddCacheEntry(navigation_url, iter->second->prefetch_url());
DeletePrefetch(search_terms);
return response;
}
std::unique_ptr<SearchPrefetchURLLoader>
SearchPrefetchService::TakePrefetchResponseFromDiskCache(
const GURL& navigation_url) {
if (prefetch_cache_.find(navigation_url) == prefetch_cache_.end()) {
return nullptr;
}
return std::make_unique<BackForwardSearchPrefetchURLLoader>(
profile_, BaseSearchPrefetchRequest::NetworkAnnotationForPrefetch(),
prefetch_cache_[navigation_url].first);
}
void SearchPrefetchService::ClearPrefetches() {
prefetches_.clear();
prefetch_expiry_timers_.clear();
prefetch_cache_.clear();
SaveToPrefs();
}
void SearchPrefetchService::DeletePrefetch(std::u16string search_terms) {
DCHECK(prefetches_.find(search_terms) != prefetches_.end());
DCHECK(prefetch_expiry_timers_.find(search_terms) !=
prefetch_expiry_timers_.end());
RecordFinalStatus(prefetches_[search_terms]->current_status());
prefetches_.erase(search_terms);
prefetch_expiry_timers_.erase(search_terms);
}
void SearchPrefetchService::ReportError() {
last_error_time_ticks_ = base::TimeTicks::Now();
}
void SearchPrefetchService::OnResultChanged(
AutocompleteController* controller) {
const auto& result = controller->result();
const auto* default_match = result.default_match();
auto* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile_);
DCHECK(template_url_service);
auto* default_search = template_url_service->GetDefaultSearchProvider();
if (!default_search)
return;
// Cancel Unneeded prefetch requests.
if (SearchPrefetchShouldCancelUneededInflightRequests()) {
// Since we limit the number of prefetches in the map, this should be fast
// despite the two loops.
for (const auto& kv_pair : prefetches_) {
const auto& search_terms = kv_pair.first;
auto& prefetch_request = kv_pair.second;
if (prefetch_request->current_status() !=
SearchPrefetchStatus::kInFlight &&
prefetch_request->current_status() !=
SearchPrefetchStatus::kCanBeServed) {
continue;
}
bool should_cancel_request = true;
for (const auto& match : result) {
std::u16string match_search_terms;
default_search->ExtractSearchTermsFromURL(
match.destination_url, template_url_service->search_terms_data(),
&match_search_terms);
if (search_terms == match_search_terms) {
should_cancel_request = false;
break;
}
}
// Cancel the inflight request and mark it as canceled.
if (should_cancel_request) {
prefetch_request->CancelPrefetch();
}
}
}
// One arm of the experiment only prefetches the top match when it is default.
if (SearchPrefetchOnlyFetchDefaultMatch()) {
if (default_match && BaseSearchProvider::ShouldPrefetch(*default_match)) {
MaybePrefetchURL(
GetPrefetchURLFromMatch(*default_match, template_url_service));
}
return;
}
for (const auto& match : result) {
if (BaseSearchProvider::ShouldPrefetch(match)) {
MaybePrefetchURL(GetPrefetchURLFromMatch(match, template_url_service));
}
}
}
void SearchPrefetchService::OnTemplateURLServiceChanged() {
auto* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile_);
DCHECK(template_url_service);
base::Optional<TemplateURLData> template_url_service_data;
const TemplateURL* template_url =
template_url_service->GetDefaultSearchProvider();
if (template_url) {
template_url_service_data = template_url->data();
}
if (!template_url_service_data_.has_value() &&
!template_url_service_data.has_value()) {
return;
}
UIThreadSearchTermsData search_data;
if (template_url_service_data_.has_value() &&
template_url_service_data.has_value() &&
TemplateURL::MatchesData(
template_url, &(template_url_service_data_.value()), search_data)) {
return;
}
template_url_service_data_ = template_url_service_data;
ClearPrefetches();
}
void SearchPrefetchService::ClearCacheEntry(const GURL& navigation_url) {
if (prefetch_cache_.find(navigation_url) == prefetch_cache_.end()) {
return;
}
prefetch_cache_.erase(navigation_url);
SaveToPrefs();
}
void SearchPrefetchService::UpdateServeTime(const GURL& navigation_url) {
if (prefetch_cache_.find(navigation_url) == prefetch_cache_.end())
return;
prefetch_cache_[navigation_url].second = base::Time::Now();
SaveToPrefs();
}
void SearchPrefetchService::AddCacheEntry(const GURL& navigation_url,
const GURL& prefetch_url) {
if (navigation_url == prefetch_url) {
return;
}
prefetch_cache_.emplace(navigation_url,
std::make_pair(prefetch_url, base::Time::Now()));
if (prefetch_cache_.size() <= SearchPrefetchMaxCacheEntries()) {
SaveToPrefs();
return;
}
GURL url_to_remove;
base::Time earliest_time = base::Time::Now();
for (const auto& entry : prefetch_cache_) {
base::Time last_used_time = entry.second.second;
if (last_used_time < earliest_time) {
earliest_time = last_used_time;
url_to_remove = entry.first;
}
}
ClearCacheEntry(url_to_remove);
SaveToPrefs();
}
bool SearchPrefetchService::LoadFromPrefs() {
prefetch_cache_.clear();
const base::DictionaryValue* dictionary =
profile_->GetPrefs()->GetDictionary(prefetch::prefs::kCachePrefPath);
DCHECK(dictionary);
auto* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile_);
if (!template_url_service ||
!template_url_service->GetDefaultSearchProvider()) {
return dictionary->size() > 0;
}
for (const auto& element : *dictionary) {
GURL navigation_url(element.first);
if (!navigation_url.is_valid()) {
continue;
}
if (!element.second) {
continue;
}
base::Value::ConstListView const prefetch_url_and_time =
base::Value::AsListValue(*element.second).GetList();
if (prefetch_url_and_time.size() != 2 ||
!prefetch_url_and_time[0].is_string() ||
!prefetch_url_and_time[1].is_string()) {
continue;
}
std::string prefetch_url;
if (!prefetch_url_and_time[0].GetAsString(&prefetch_url)) {
continue;
}
// Make sure we are only mapping same origin in case of corrupted prefs.
if (url::Origin::Create(navigation_url) !=
url::Origin::Create(GURL(prefetch_url))) {
continue;
}
// Don't redirect same URL.
if (navigation_url == prefetch_url) {
continue;
}
// Make sure the navigation URL is still a search URL.
std::u16string search_terms;
template_url_service->GetDefaultSearchProvider()->ExtractSearchTermsFromURL(
navigation_url, template_url_service->search_terms_data(),
&search_terms);
if (search_terms.size() == 0) {
continue;
}
base::Optional<base::Time> last_update =
util::ValueToTime(prefetch_url_and_time[1]);
if (!last_update) {
continue;
}
// This time isn't valid.
if (last_update.value() > base::Time::Now()) {
continue;
}
prefetch_cache_.emplace(
navigation_url,
std::make_pair(GURL(prefetch_url), last_update.value()));
}
return dictionary->size() > prefetch_cache_.size();
}
void SearchPrefetchService::SaveToPrefs() const {
base::DictionaryValue dictionary;
for (const auto& element : prefetch_cache_) {
std::string navigation_url = element.first.spec();
std::string prefetch_url = element.second.first.spec();
auto time =
std::make_unique<base::Value>(util::TimeToValue(element.second.second));
base::ListValue value;
value.AppendString(prefetch_url);
value.Append(std::move(time));
dictionary.SetKey(std::move(navigation_url), std::move(value));
}
profile_->GetPrefs()->Set(prefetch::prefs::kCachePrefPath, dictionary);
}
bool SearchPrefetchService::LoadFromPrefsForTesting() {
return LoadFromPrefs();
}