1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/autocomplete/base_search_provider.h"
7 #include "base/i18n/case_conversion.h"
8 #include "base/prefs/pref_registry_simple.h"
9 #include "base/prefs/pref_service.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
13 #include "chrome/browser/bitmap_fetcher/bitmap_fetcher_service.h"
14 #include "chrome/browser/bitmap_fetcher/bitmap_fetcher_service_factory.h"
15 #include "chrome/browser/history/history_service.h"
16 #include "chrome/browser/history/history_service_factory.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/sync/profile_sync_service.h"
19 #include "chrome/browser/sync/profile_sync_service_factory.h"
20 #include "chrome/common/pref_names.h"
21 #include "components/metrics/proto/omnibox_event.pb.h"
22 #include "components/metrics/proto/omnibox_input_type.pb.h"
23 #include "components/omnibox/autocomplete_provider_listener.h"
24 #include "components/omnibox/omnibox_field_trial.h"
25 #include "components/search_engines/template_url.h"
26 #include "components/search_engines/template_url_prepopulate_data.h"
27 #include "components/search_engines/template_url_service.h"
28 #include "components/sync_driver/sync_prefs.h"
29 #include "net/base/escape.h"
30 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
31 #include "net/url_request/url_fetcher.h"
32 #include "net/url_request/url_fetcher_delegate.h"
35 using metrics::OmniboxEventProto;
37 // SuggestionDeletionHandler -------------------------------------------------
39 // This class handles making requests to the server in order to delete
40 // personalized suggestions.
41 class SuggestionDeletionHandler : public net::URLFetcherDelegate {
43 typedef base::Callback<void(bool, SuggestionDeletionHandler*)>
44 DeletionCompletedCallback;
46 SuggestionDeletionHandler(
47 const std::string& deletion_url,
49 const DeletionCompletedCallback& callback);
51 virtual ~SuggestionDeletionHandler();
54 // net::URLFetcherDelegate:
55 virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
57 scoped_ptr<net::URLFetcher> deletion_fetcher_;
58 DeletionCompletedCallback callback_;
60 DISALLOW_COPY_AND_ASSIGN(SuggestionDeletionHandler);
63 SuggestionDeletionHandler::SuggestionDeletionHandler(
64 const std::string& deletion_url,
66 const DeletionCompletedCallback& callback) : callback_(callback) {
67 GURL url(deletion_url);
68 DCHECK(url.is_valid());
70 deletion_fetcher_.reset(net::URLFetcher::Create(
71 BaseSearchProvider::kDeletionURLFetcherID,
75 deletion_fetcher_->SetRequestContext(profile->GetRequestContext());
76 deletion_fetcher_->Start();
79 SuggestionDeletionHandler::~SuggestionDeletionHandler() {
82 void SuggestionDeletionHandler::OnURLFetchComplete(
83 const net::URLFetcher* source) {
84 DCHECK(source == deletion_fetcher_.get());
86 source->GetStatus().is_success() && (source->GetResponseCode() == 200),
90 // BaseSearchProvider ---------------------------------------------------------
93 const int BaseSearchProvider::kDefaultProviderURLFetcherID = 1;
94 const int BaseSearchProvider::kKeywordProviderURLFetcherID = 2;
95 const int BaseSearchProvider::kDeletionURLFetcherID = 3;
97 BaseSearchProvider::BaseSearchProvider(TemplateURLService* template_url_service,
99 AutocompleteProvider::Type type)
100 : AutocompleteProvider(type),
101 template_url_service_(template_url_service),
103 field_trial_triggered_(false),
104 field_trial_triggered_in_session_(false) {
108 bool BaseSearchProvider::ShouldPrefetch(const AutocompleteMatch& match) {
109 return match.GetAdditionalInfo(kShouldPrefetchKey) == kTrue;
113 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion(
114 const base::string16& suggestion,
115 AutocompleteMatchType::Type type,
116 bool from_keyword_provider,
117 const TemplateURL* template_url,
118 const SearchTermsData& search_terms_data) {
119 // This call uses a number of default values. For instance, it assumes that
120 // if this match is from a keyword provider than the user is in keyword mode.
121 return CreateSearchSuggestion(
122 NULL, AutocompleteInput(), from_keyword_provider,
123 SearchSuggestionParser::SuggestResult(
124 suggestion, type, suggestion, base::string16(), base::string16(),
125 base::string16(), base::string16(), std::string(), std::string(),
126 from_keyword_provider, 0, false, false, base::string16()),
127 template_url, search_terms_data, 0, false);
130 void BaseSearchProvider::Stop(bool clear_cached_results) {
134 if (clear_cached_results)
138 void BaseSearchProvider::DeleteMatch(const AutocompleteMatch& match) {
139 DCHECK(match.deletable);
140 if (!match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey).empty()) {
141 deletion_handlers_.push_back(new SuggestionDeletionHandler(
142 match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey),
144 base::Bind(&BaseSearchProvider::OnDeletionComplete,
145 base::Unretained(this))));
148 HistoryService* const history_service =
149 HistoryServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS);
150 TemplateURL* template_url =
151 match.GetTemplateURL(template_url_service_, false);
152 // This may be NULL if the template corresponding to the keyword has been
153 // deleted or there is no keyword set.
154 if (template_url != NULL) {
155 history_service->DeleteMatchingURLsForKeyword(template_url->id(),
159 // Immediately update the list of matches to show the match was deleted,
160 // regardless of whether the server request actually succeeds.
161 DeleteMatchFromMatches(match);
164 void BaseSearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const {
165 provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo());
166 metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back();
167 new_entry.set_provider(AsOmniboxEventProviderType());
168 new_entry.set_provider_done(done_);
169 std::vector<uint32> field_trial_hashes;
170 OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes);
171 for (size_t i = 0; i < field_trial_hashes.size(); ++i) {
172 if (field_trial_triggered_)
173 new_entry.mutable_field_trial_triggered()->Add(field_trial_hashes[i]);
174 if (field_trial_triggered_in_session_) {
175 new_entry.mutable_field_trial_triggered_in_session()->Add(
176 field_trial_hashes[i]);
179 ModifyProviderInfo(&new_entry);
183 const char BaseSearchProvider::kRelevanceFromServerKey[] =
184 "relevance_from_server";
185 const char BaseSearchProvider::kShouldPrefetchKey[] = "should_prefetch";
186 const char BaseSearchProvider::kSuggestMetadataKey[] = "suggest_metadata";
187 const char BaseSearchProvider::kDeletionUrlKey[] = "deletion_url";
188 const char BaseSearchProvider::kTrue[] = "true";
189 const char BaseSearchProvider::kFalse[] = "false";
191 BaseSearchProvider::~BaseSearchProvider() {}
193 void BaseSearchProvider::SetDeletionURL(const std::string& deletion_url,
194 AutocompleteMatch* match) {
195 if (deletion_url.empty())
197 if (!template_url_service_)
200 template_url_service_->GetDefaultSearchProvider()->GenerateSearchURL(
201 template_url_service_->search_terms_data());
202 url = url.GetOrigin().Resolve(deletion_url);
203 if (url.is_valid()) {
204 match->RecordAdditionalInfo(BaseSearchProvider::kDeletionUrlKey,
206 match->deletable = true;
211 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion(
212 AutocompleteProvider* autocomplete_provider,
213 const AutocompleteInput& input,
214 const bool in_keyword_mode,
215 const SearchSuggestionParser::SuggestResult& suggestion,
216 const TemplateURL* template_url,
217 const SearchTermsData& search_terms_data,
218 int accepted_suggestion,
219 bool append_extra_query_params) {
220 AutocompleteMatch match(autocomplete_provider, suggestion.relevance(), false,
225 match.keyword = template_url->keyword();
226 match.contents = suggestion.match_contents();
227 match.contents_class = suggestion.match_contents_class();
228 match.answer_contents = suggestion.answer_contents();
229 match.answer_type = suggestion.answer_type();
230 if (suggestion.type() == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) {
231 match.RecordAdditionalInfo(
232 kACMatchPropertyInputText, base::UTF16ToUTF8(input.text()));
233 match.RecordAdditionalInfo(
234 kACMatchPropertyContentsPrefix,
235 base::UTF16ToUTF8(suggestion.match_contents_prefix()));
236 match.RecordAdditionalInfo(
237 kACMatchPropertyContentsStartIndex,
239 suggestion.suggestion().length() - match.contents.length()));
242 if (!suggestion.annotation().empty())
243 match.description = suggestion.annotation();
245 // suggestion.match_contents() should have already been collapsed.
246 match.allowed_to_be_default_match =
247 (!in_keyword_mode || suggestion.from_keyword_provider()) &&
248 (base::CollapseWhitespace(input.text(), false) ==
249 suggestion.match_contents());
251 // When the user forced a query, we need to make sure all the fill_into_edit
252 // values preserve that property. Otherwise, if the user starts editing a
253 // suggestion, non-Search results will suddenly appear.
254 if (input.type() == metrics::OmniboxInputType::FORCED_QUERY)
255 match.fill_into_edit.assign(base::ASCIIToUTF16("?"));
256 if (suggestion.from_keyword_provider())
257 match.fill_into_edit.append(match.keyword + base::char16(' '));
258 if (!input.prevent_inline_autocomplete() &&
259 (!in_keyword_mode || suggestion.from_keyword_provider()) &&
260 StartsWith(suggestion.suggestion(), input.text(), false)) {
261 match.inline_autocompletion =
262 suggestion.suggestion().substr(input.text().length());
263 match.allowed_to_be_default_match = true;
265 match.fill_into_edit.append(suggestion.suggestion());
267 const TemplateURLRef& search_url = template_url->url_ref();
268 DCHECK(search_url.SupportsReplacement(search_terms_data));
269 match.search_terms_args.reset(
270 new TemplateURLRef::SearchTermsArgs(suggestion.suggestion()));
271 match.search_terms_args->original_query = input.text();
272 match.search_terms_args->accepted_suggestion = accepted_suggestion;
273 match.search_terms_args->enable_omnibox_start_margin = true;
274 match.search_terms_args->suggest_query_params =
275 suggestion.suggest_query_params();
276 match.search_terms_args->append_extra_query_params =
277 append_extra_query_params;
278 // This is the destination URL sans assisted query stats. This must be set
279 // so the AutocompleteController can properly de-dupe; the controller will
280 // eventually overwrite it before it reaches the user.
281 match.destination_url =
282 GURL(search_url.ReplaceSearchTerms(*match.search_terms_args.get(),
285 // Search results don't look like URLs.
286 match.transition = suggestion.from_keyword_provider() ?
287 content::PAGE_TRANSITION_KEYWORD : content::PAGE_TRANSITION_GENERATED;
293 bool BaseSearchProvider::ZeroSuggestEnabled(
294 const GURL& suggest_url,
295 const TemplateURL* template_url,
296 OmniboxEventProto::PageClassification page_classification,
297 const SearchTermsData& search_terms_data,
299 if (!OmniboxFieldTrial::InZeroSuggestFieldTrial())
302 // Make sure we are sending the suggest request through HTTPS to prevent
303 // exposing the current page URL or personalized results without encryption.
304 if (!suggest_url.SchemeIs(url::kHttpsScheme))
307 // Don't show zero suggest on the NTP.
308 // TODO(hfung): Experiment with showing MostVisited zero suggest on NTP
309 // under the conditions described in crbug.com/305366.
310 if ((page_classification ==
311 OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS) ||
312 (page_classification ==
313 OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS))
316 // Don't run if there's no profile or in incognito mode.
317 if (profile == NULL || profile->IsOffTheRecord())
320 // Don't run if we can't get preferences or search suggest is not enabled.
321 PrefService* prefs = profile->GetPrefs();
322 if (!prefs->GetBoolean(prefs::kSearchSuggestEnabled))
325 // Only make the request if we know that the provider supports zero suggest
326 // (currently only the prepopulated Google provider).
327 if (template_url == NULL ||
328 !template_url->SupportsReplacement(search_terms_data) ||
329 TemplateURLPrepopulateData::GetEngineType(
330 *template_url, search_terms_data) != SEARCH_ENGINE_GOOGLE)
337 bool BaseSearchProvider::CanSendURL(
338 const GURL& current_page_url,
339 const GURL& suggest_url,
340 const TemplateURL* template_url,
341 OmniboxEventProto::PageClassification page_classification,
342 const SearchTermsData& search_terms_data,
344 if (!ZeroSuggestEnabled(suggest_url, template_url, page_classification,
345 search_terms_data, profile))
348 if (!current_page_url.is_valid())
351 // Only allow HTTP URLs or HTTPS URLs for the same domain as the search
353 if ((current_page_url.scheme() != url::kHttpScheme) &&
354 ((current_page_url.scheme() != url::kHttpsScheme) ||
355 !net::registry_controlled_domains::SameDomainOrHost(
356 current_page_url, suggest_url,
357 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES)))
360 // Check field trials and settings allow sending the URL on suggest requests.
361 ProfileSyncService* service =
362 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
363 sync_driver::SyncPrefs sync_prefs(profile->GetPrefs());
364 if (service == NULL ||
365 !service->IsSyncEnabledAndLoggedIn() ||
366 !sync_prefs.GetPreferredDataTypes(syncer::UserTypes()).Has(
367 syncer::PROXY_TABS) ||
368 service->GetEncryptedDataTypes().Has(syncer::SESSIONS))
374 void BaseSearchProvider::AddMatchToMap(
375 const SearchSuggestionParser::SuggestResult& result,
376 const std::string& metadata,
377 int accepted_suggestion,
378 bool mark_as_deletable,
379 bool in_keyword_mode,
381 AutocompleteMatch match = CreateSearchSuggestion(
382 this, GetInput(result.from_keyword_provider()), in_keyword_mode, result,
383 GetTemplateURL(result.from_keyword_provider()),
384 template_url_service_->search_terms_data(), accepted_suggestion,
385 ShouldAppendExtraParams(result));
386 if (!match.destination_url.is_valid())
388 match.search_terms_args->bookmark_bar_pinned =
389 profile_->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
390 match.RecordAdditionalInfo(kRelevanceFromServerKey,
391 result.relevance_from_server() ? kTrue : kFalse);
392 match.RecordAdditionalInfo(kShouldPrefetchKey,
393 result.should_prefetch() ? kTrue : kFalse);
394 SetDeletionURL(result.deletion_url(), &match);
395 if (mark_as_deletable)
396 match.deletable = true;
397 // Metadata is needed only for prefetching queries.
398 if (result.should_prefetch())
399 match.RecordAdditionalInfo(kSuggestMetadataKey, metadata);
401 // Try to add |match| to |map|. If a match for this suggestion is
402 // already in |map|, replace it if |match| is more relevant.
403 // NOTE: Keep this ToLower() call in sync with url_database.cc.
405 std::make_pair(base::i18n::ToLower(result.suggestion()),
406 match.search_terms_args->suggest_query_params));
407 const std::pair<MatchMap::iterator, bool> i(
408 map->insert(std::make_pair(match_key, match)));
410 bool should_prefetch = result.should_prefetch();
412 // NOTE: We purposefully do a direct relevance comparison here instead of
413 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items
414 // added first" rather than "items alphabetically first" when the scores
415 // are equal. The only case this matters is when a user has results with
416 // the same score that differ only by capitalization; because the history
417 // system returns results sorted by recency, this means we'll pick the most
418 // recent such result even if the precision of our relevance score is too
419 // low to distinguish the two.
420 if (match.relevance > i.first->second.relevance) {
421 match.duplicate_matches.insert(match.duplicate_matches.end(),
422 i.first->second.duplicate_matches.begin(),
423 i.first->second.duplicate_matches.end());
424 i.first->second.duplicate_matches.clear();
425 match.duplicate_matches.push_back(i.first->second);
426 i.first->second = match;
428 i.first->second.duplicate_matches.push_back(match);
429 if (match.keyword == i.first->second.keyword) {
430 // Old and new matches are from the same search provider. It is okay to
431 // record one match's prefetch data onto a different match (for the same
432 // query string) for the following reasons:
433 // 1. Because the suggest server only sends down a query string from
434 // which we construct a URL, rather than sending a full URL, and because
435 // we construct URLs from query strings in the same way every time, the
436 // URLs for the two matches will be the same. Therefore, we won't end up
437 // prefetching something the server didn't intend.
438 // 2. Presumably the server sets the prefetch bit on a match it things
439 // is sufficiently relevant that the user is likely to choose it.
440 // Surely setting the prefetch bit on a match of even higher relevance
441 // won't violate this assumption.
442 should_prefetch |= ShouldPrefetch(i.first->second);
443 i.first->second.RecordAdditionalInfo(kShouldPrefetchKey,
444 should_prefetch ? kTrue : kFalse);
446 i.first->second.RecordAdditionalInfo(kSuggestMetadataKey, metadata);
452 bool BaseSearchProvider::ParseSuggestResults(
453 const base::Value& root_val,
454 int default_result_relevance,
455 bool is_keyword_result,
456 SearchSuggestionParser::Results* results) {
457 if (!SearchSuggestionParser::ParseSuggestResults(
458 root_val, GetInput(is_keyword_result),
459 ChromeAutocompleteSchemeClassifier(profile_), default_result_relevance,
460 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages),
461 is_keyword_result, results))
464 BitmapFetcherService* image_service =
465 BitmapFetcherServiceFactory::GetForBrowserContext(profile_);
466 DCHECK(image_service);
467 for (std::vector<GURL>::const_iterator it =
468 results->answers_image_urls.begin();
469 it != results->answers_image_urls.end(); ++it)
470 image_service->Prefetch(*it);
472 field_trial_triggered_ |= results->field_trial_triggered;
473 field_trial_triggered_in_session_ |= results->field_trial_triggered;
477 void BaseSearchProvider::ModifyProviderInfo(
478 metrics::OmniboxEventProto_ProviderInfo* provider_info) const {
481 void BaseSearchProvider::DeleteMatchFromMatches(
482 const AutocompleteMatch& match) {
483 for (ACMatches::iterator i(matches_.begin()); i != matches_.end(); ++i) {
484 // Find the desired match to delete by checking the type and contents.
485 // We can't check the destination URL, because the autocomplete controller
486 // may have reformulated that. Not that while checking for matching
487 // contents works for personalized suggestions, if more match types gain
488 // deletion support, this algorithm may need to be re-examined.
489 if (i->contents == match.contents && i->type == match.type) {
496 void BaseSearchProvider::OnDeletionComplete(
497 bool success, SuggestionDeletionHandler* handler) {
498 RecordDeletionResult(success);
499 SuggestionDeletionHandlers::iterator it = std::find(
500 deletion_handlers_.begin(), deletion_handlers_.end(), handler);
501 DCHECK(it != deletion_handlers_.end());
502 deletion_handlers_.erase(it);