|  | // 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(); | 
|  | } |