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. 4 5 #include "components/omnibox/base_search_provider.h" 6 7 #include "base/i18n/case_conversion.h" 8 #include "base/strings/string_util.h" 9 #include "base/strings/utf_string_conversions.h" 10 #include "components/metrics/proto/omnibox_event.pb.h" 11 #include "components/metrics/proto/omnibox_input_type.pb.h" 12 #include "components/omnibox/autocomplete_provider_client.h" 13 #include "components/omnibox/autocomplete_provider_listener.h" 14 #include "components/omnibox/omnibox_field_trial.h" 15 #include "components/search_engines/template_url.h" 16 #include "components/search_engines/template_url_prepopulate_data.h" 17 #include "components/search_engines/template_url_service.h" 18 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" 19 #include "net/url_request/url_fetcher.h" 20 #include "net/url_request/url_fetcher_delegate.h" 21 #include "url/gurl.h" 22 23 using metrics::OmniboxEventProto; 24 25 // SuggestionDeletionHandler ------------------------------------------------- 26 27 // This class handles making requests to the server in order to delete 28 // personalized suggestions. 29 class SuggestionDeletionHandler : public net::URLFetcherDelegate { 30 public: 31 typedef base::Callback<void(bool, SuggestionDeletionHandler*)> 32 DeletionCompletedCallback; 33 34 SuggestionDeletionHandler( 35 const std::string& deletion_url, 36 net::URLRequestContextGetter* request_context, 37 const DeletionCompletedCallback& callback); 38 39 virtual ~SuggestionDeletionHandler(); 40 41 private: 42 // net::URLFetcherDelegate: 43 virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; 44 45 scoped_ptr<net::URLFetcher> deletion_fetcher_; 46 DeletionCompletedCallback callback_; 47 48 DISALLOW_COPY_AND_ASSIGN(SuggestionDeletionHandler); 49 }; 50 51 SuggestionDeletionHandler::SuggestionDeletionHandler( 52 const std::string& deletion_url, 53 net::URLRequestContextGetter* request_context, 54 const DeletionCompletedCallback& callback) : callback_(callback) { 55 GURL url(deletion_url); 56 DCHECK(url.is_valid()); 57 58 deletion_fetcher_.reset(net::URLFetcher::Create( 59 BaseSearchProvider::kDeletionURLFetcherID, 60 url, 61 net::URLFetcher::GET, 62 this)); 63 deletion_fetcher_->SetRequestContext(request_context); 64 deletion_fetcher_->Start(); 65 } 66 67 SuggestionDeletionHandler::~SuggestionDeletionHandler() { 68 } 69 70 void SuggestionDeletionHandler::OnURLFetchComplete( 71 const net::URLFetcher* source) { 72 DCHECK(source == deletion_fetcher_.get()); 73 callback_.Run( 74 source->GetStatus().is_success() && (source->GetResponseCode() == 200), 75 this); 76 } 77 78 // BaseSearchProvider --------------------------------------------------------- 79 80 // static 81 const int BaseSearchProvider::kDefaultProviderURLFetcherID = 1; 82 const int BaseSearchProvider::kKeywordProviderURLFetcherID = 2; 83 const int BaseSearchProvider::kDeletionURLFetcherID = 3; 84 85 BaseSearchProvider::BaseSearchProvider( 86 TemplateURLService* template_url_service, 87 scoped_ptr<AutocompleteProviderClient> client, 88 AutocompleteProvider::Type type) 89 : AutocompleteProvider(type), 90 template_url_service_(template_url_service), 91 client_(client.Pass()), 92 field_trial_triggered_(false), 93 field_trial_triggered_in_session_(false) { 94 } 95 96 // static 97 bool BaseSearchProvider::ShouldPrefetch(const AutocompleteMatch& match) { 98 return match.GetAdditionalInfo(kShouldPrefetchKey) == kTrue; 99 } 100 101 // static 102 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion( 103 const base::string16& suggestion, 104 AutocompleteMatchType::Type type, 105 bool from_keyword_provider, 106 const TemplateURL* template_url, 107 const SearchTermsData& search_terms_data) { 108 // These calls use a number of default values. For instance, they assume 109 // that if this match is from a keyword provider, then the user is in keyword 110 // mode. They also assume the caller knows what it's doing and we set 111 // this match to look as if it was received/created synchronously. 112 SearchSuggestionParser::SuggestResult suggest_result( 113 suggestion, type, suggestion, base::string16(), base::string16(), 114 base::string16(), base::string16(), std::string(), std::string(), 115 from_keyword_provider, 0, false, false, base::string16()); 116 suggest_result.set_received_after_last_keystroke(false); 117 return CreateSearchSuggestion( 118 NULL, AutocompleteInput(), from_keyword_provider, suggest_result, 119 template_url, search_terms_data, 0, false); 120 } 121 122 void BaseSearchProvider::DeleteMatch(const AutocompleteMatch& match) { 123 DCHECK(match.deletable); 124 if (!match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey).empty()) { 125 deletion_handlers_.push_back(new SuggestionDeletionHandler( 126 match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey), 127 client_->RequestContext(), 128 base::Bind(&BaseSearchProvider::OnDeletionComplete, 129 base::Unretained(this)))); 130 } 131 132 TemplateURL* template_url = 133 match.GetTemplateURL(template_url_service_, false); 134 // This may be NULL if the template corresponding to the keyword has been 135 // deleted or there is no keyword set. 136 if (template_url != NULL) { 137 client_->DeleteMatchingURLsForKeywordFromHistory(template_url->id(), 138 match.contents); 139 } 140 141 // Immediately update the list of matches to show the match was deleted, 142 // regardless of whether the server request actually succeeds. 143 DeleteMatchFromMatches(match); 144 } 145 146 void BaseSearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const { 147 provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo()); 148 metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back(); 149 new_entry.set_provider(AsOmniboxEventProviderType()); 150 new_entry.set_provider_done(done_); 151 std::vector<uint32> field_trial_hashes; 152 OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes); 153 for (size_t i = 0; i < field_trial_hashes.size(); ++i) { 154 if (field_trial_triggered_) 155 new_entry.mutable_field_trial_triggered()->Add(field_trial_hashes[i]); 156 if (field_trial_triggered_in_session_) { 157 new_entry.mutable_field_trial_triggered_in_session()->Add( 158 field_trial_hashes[i]); 159 } 160 } 161 } 162 163 // static 164 const char BaseSearchProvider::kRelevanceFromServerKey[] = 165 "relevance_from_server"; 166 const char BaseSearchProvider::kShouldPrefetchKey[] = "should_prefetch"; 167 const char BaseSearchProvider::kSuggestMetadataKey[] = "suggest_metadata"; 168 const char BaseSearchProvider::kDeletionUrlKey[] = "deletion_url"; 169 const char BaseSearchProvider::kTrue[] = "true"; 170 const char BaseSearchProvider::kFalse[] = "false"; 171 172 BaseSearchProvider::~BaseSearchProvider() {} 173 174 void BaseSearchProvider::SetDeletionURL(const std::string& deletion_url, 175 AutocompleteMatch* match) { 176 if (deletion_url.empty()) 177 return; 178 if (!template_url_service_) 179 return; 180 GURL url = 181 template_url_service_->GetDefaultSearchProvider()->GenerateSearchURL( 182 template_url_service_->search_terms_data()); 183 url = url.GetOrigin().Resolve(deletion_url); 184 if (url.is_valid()) { 185 match->RecordAdditionalInfo(BaseSearchProvider::kDeletionUrlKey, 186 url.spec()); 187 match->deletable = true; 188 } 189 } 190 191 // static 192 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion( 193 AutocompleteProvider* autocomplete_provider, 194 const AutocompleteInput& input, 195 const bool in_keyword_mode, 196 const SearchSuggestionParser::SuggestResult& suggestion, 197 const TemplateURL* template_url, 198 const SearchTermsData& search_terms_data, 199 int accepted_suggestion, 200 bool append_extra_query_params) { 201 AutocompleteMatch match(autocomplete_provider, suggestion.relevance(), false, 202 suggestion.type()); 203 204 if (!template_url) 205 return match; 206 match.keyword = template_url->keyword(); 207 match.contents = suggestion.match_contents(); 208 match.contents_class = suggestion.match_contents_class(); 209 match.answer_contents = suggestion.answer_contents(); 210 match.answer_type = suggestion.answer_type(); 211 if (suggestion.type() == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) { 212 match.RecordAdditionalInfo( 213 kACMatchPropertyInputText, base::UTF16ToUTF8(input.text())); 214 match.RecordAdditionalInfo( 215 kACMatchPropertyContentsPrefix, 216 base::UTF16ToUTF8(suggestion.match_contents_prefix())); 217 match.RecordAdditionalInfo( 218 kACMatchPropertyContentsStartIndex, 219 static_cast<int>( 220 suggestion.suggestion().length() - match.contents.length())); 221 } 222 223 if (!suggestion.annotation().empty()) 224 match.description = suggestion.annotation(); 225 226 // suggestion.match_contents() should have already been collapsed. 227 match.allowed_to_be_default_match = 228 (!in_keyword_mode || suggestion.from_keyword_provider()) && 229 (base::CollapseWhitespace(input.text(), false) == 230 suggestion.match_contents()); 231 232 // When the user forced a query, we need to make sure all the fill_into_edit 233 // values preserve that property. Otherwise, if the user starts editing a 234 // suggestion, non-Search results will suddenly appear. 235 if (input.type() == metrics::OmniboxInputType::FORCED_QUERY) 236 match.fill_into_edit.assign(base::ASCIIToUTF16("?")); 237 if (suggestion.from_keyword_provider()) 238 match.fill_into_edit.append(match.keyword + base::char16(' ')); 239 // We only allow inlinable navsuggestions that were received before the 240 // last keystroke because we don't want asynchronous inline autocompletions. 241 if (!input.prevent_inline_autocomplete() && 242 !suggestion.received_after_last_keystroke() && 243 (!in_keyword_mode || suggestion.from_keyword_provider()) && 244 StartsWith(suggestion.suggestion(), input.text(), false)) { 245 match.inline_autocompletion = 246 suggestion.suggestion().substr(input.text().length()); 247 match.allowed_to_be_default_match = true; 248 } 249 match.fill_into_edit.append(suggestion.suggestion()); 250 251 const TemplateURLRef& search_url = template_url->url_ref(); 252 DCHECK(search_url.SupportsReplacement(search_terms_data)); 253 match.search_terms_args.reset( 254 new TemplateURLRef::SearchTermsArgs(suggestion.suggestion())); 255 match.search_terms_args->original_query = input.text(); 256 match.search_terms_args->accepted_suggestion = accepted_suggestion; 257 match.search_terms_args->enable_omnibox_start_margin = true; 258 match.search_terms_args->suggest_query_params = 259 suggestion.suggest_query_params(); 260 match.search_terms_args->append_extra_query_params = 261 append_extra_query_params; 262 // This is the destination URL sans assisted query stats. This must be set 263 // so the AutocompleteController can properly de-dupe; the controller will 264 // eventually overwrite it before it reaches the user. 265 match.destination_url = 266 GURL(search_url.ReplaceSearchTerms(*match.search_terms_args.get(), 267 search_terms_data)); 268 269 // Search results don't look like URLs. 270 match.transition = suggestion.from_keyword_provider() ? 271 ui::PAGE_TRANSITION_KEYWORD : ui::PAGE_TRANSITION_GENERATED; 272 273 return match; 274 } 275 276 // static 277 bool BaseSearchProvider::ZeroSuggestEnabled( 278 const GURL& suggest_url, 279 const TemplateURL* template_url, 280 OmniboxEventProto::PageClassification page_classification, 281 const SearchTermsData& search_terms_data, 282 AutocompleteProviderClient* client) { 283 if (!OmniboxFieldTrial::InZeroSuggestFieldTrial()) 284 return false; 285 286 // Make sure we are sending the suggest request through HTTPS to prevent 287 // exposing the current page URL or personalized results without encryption. 288 if (!suggest_url.SchemeIs(url::kHttpsScheme)) 289 return false; 290 291 // Don't show zero suggest on the NTP. 292 // TODO(hfung): Experiment with showing MostVisited zero suggest on NTP 293 // under the conditions described in crbug.com/305366. 294 if ((page_classification == 295 OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS) || 296 (page_classification == 297 OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS)) 298 return false; 299 300 // Don't run if in incognito mode. 301 if (client->IsOffTheRecord()) 302 return false; 303 304 // Don't run if we can't get preferences or search suggest is not enabled. 305 if (!client->SearchSuggestEnabled()) 306 return false; 307 308 // Only make the request if we know that the provider supports zero suggest 309 // (currently only the prepopulated Google provider). 310 if (template_url == NULL || 311 !template_url->SupportsReplacement(search_terms_data) || 312 TemplateURLPrepopulateData::GetEngineType( 313 *template_url, search_terms_data) != SEARCH_ENGINE_GOOGLE) 314 return false; 315 316 return true; 317 } 318 319 // static 320 bool BaseSearchProvider::CanSendURL( 321 const GURL& current_page_url, 322 const GURL& suggest_url, 323 const TemplateURL* template_url, 324 OmniboxEventProto::PageClassification page_classification, 325 const SearchTermsData& search_terms_data, 326 AutocompleteProviderClient* client) { 327 if (!ZeroSuggestEnabled(suggest_url, template_url, page_classification, 328 search_terms_data, client)) 329 return false; 330 331 if (!current_page_url.is_valid()) 332 return false; 333 334 // Only allow HTTP URLs or HTTPS URLs for the same domain as the search 335 // provider. 336 if ((current_page_url.scheme() != url::kHttpScheme) && 337 ((current_page_url.scheme() != url::kHttpsScheme) || 338 !net::registry_controlled_domains::SameDomainOrHost( 339 current_page_url, suggest_url, 340 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES))) 341 return false; 342 343 if (!client->TabSyncEnabledAndUnencrypted()) 344 return false; 345 346 return true; 347 } 348 349 void BaseSearchProvider::AddMatchToMap( 350 const SearchSuggestionParser::SuggestResult& result, 351 const std::string& metadata, 352 int accepted_suggestion, 353 bool mark_as_deletable, 354 bool in_keyword_mode, 355 MatchMap* map) { 356 AutocompleteMatch match = CreateSearchSuggestion( 357 this, GetInput(result.from_keyword_provider()), in_keyword_mode, result, 358 GetTemplateURL(result.from_keyword_provider()), 359 template_url_service_->search_terms_data(), accepted_suggestion, 360 ShouldAppendExtraParams(result)); 361 if (!match.destination_url.is_valid()) 362 return; 363 match.search_terms_args->bookmark_bar_pinned = client_->ShowBookmarkBar(); 364 match.RecordAdditionalInfo(kRelevanceFromServerKey, 365 result.relevance_from_server() ? kTrue : kFalse); 366 match.RecordAdditionalInfo(kShouldPrefetchKey, 367 result.should_prefetch() ? kTrue : kFalse); 368 SetDeletionURL(result.deletion_url(), &match); 369 if (mark_as_deletable) 370 match.deletable = true; 371 // Metadata is needed only for prefetching queries. 372 if (result.should_prefetch()) 373 match.RecordAdditionalInfo(kSuggestMetadataKey, metadata); 374 375 // Try to add |match| to |map|. If a match for this suggestion is 376 // already in |map|, replace it if |match| is more relevant. 377 // NOTE: Keep this ToLower() call in sync with url_database.cc. 378 MatchKey match_key( 379 std::make_pair(base::i18n::ToLower(result.suggestion()), 380 match.search_terms_args->suggest_query_params)); 381 const std::pair<MatchMap::iterator, bool> i( 382 map->insert(std::make_pair(match_key, match))); 383 384 bool should_prefetch = result.should_prefetch(); 385 if (!i.second) { 386 // NOTE: We purposefully do a direct relevance comparison here instead of 387 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items 388 // added first" rather than "items alphabetically first" when the scores 389 // are equal. The only case this matters is when a user has results with 390 // the same score that differ only by capitalization; because the history 391 // system returns results sorted by recency, this means we'll pick the most 392 // recent such result even if the precision of our relevance score is too 393 // low to distinguish the two. 394 if (match.relevance > i.first->second.relevance) { 395 match.duplicate_matches.insert(match.duplicate_matches.end(), 396 i.first->second.duplicate_matches.begin(), 397 i.first->second.duplicate_matches.end()); 398 i.first->second.duplicate_matches.clear(); 399 match.duplicate_matches.push_back(i.first->second); 400 i.first->second = match; 401 } else { 402 i.first->second.duplicate_matches.push_back(match); 403 if (match.keyword == i.first->second.keyword) { 404 // Old and new matches are from the same search provider. It is okay to 405 // record one match's prefetch data onto a different match (for the same 406 // query string) for the following reasons: 407 // 1. Because the suggest server only sends down a query string from 408 // which we construct a URL, rather than sending a full URL, and because 409 // we construct URLs from query strings in the same way every time, the 410 // URLs for the two matches will be the same. Therefore, we won't end up 411 // prefetching something the server didn't intend. 412 // 2. Presumably the server sets the prefetch bit on a match it things 413 // is sufficiently relevant that the user is likely to choose it. 414 // Surely setting the prefetch bit on a match of even higher relevance 415 // won't violate this assumption. 416 should_prefetch |= ShouldPrefetch(i.first->second); 417 i.first->second.RecordAdditionalInfo(kShouldPrefetchKey, 418 should_prefetch ? kTrue : kFalse); 419 if (should_prefetch) 420 i.first->second.RecordAdditionalInfo(kSuggestMetadataKey, metadata); 421 } 422 } 423 // Copy over answer data from lower-ranking item, if necessary. 424 // This depends on the lower-ranking item always being added last - see 425 // use of push_back above. 426 AutocompleteMatch& more_relevant_match = i.first->second; 427 const AutocompleteMatch& less_relevant_match = 428 more_relevant_match.duplicate_matches.back(); 429 if (!less_relevant_match.answer_type.empty() && 430 more_relevant_match.answer_type.empty()) { 431 more_relevant_match.answer_type = less_relevant_match.answer_type; 432 more_relevant_match.answer_contents = less_relevant_match.answer_contents; 433 } 434 } 435 } 436 437 bool BaseSearchProvider::ParseSuggestResults( 438 const base::Value& root_val, 439 int default_result_relevance, 440 bool is_keyword_result, 441 SearchSuggestionParser::Results* results) { 442 if (!SearchSuggestionParser::ParseSuggestResults( 443 root_val, GetInput(is_keyword_result), 444 client_->SchemeClassifier(), default_result_relevance, 445 client_->AcceptLanguages(), is_keyword_result, results)) 446 return false; 447 448 for (std::vector<GURL>::const_iterator it = 449 results->answers_image_urls.begin(); 450 it != results->answers_image_urls.end(); ++it) 451 client_->PrefetchImage(*it); 452 453 field_trial_triggered_ |= results->field_trial_triggered; 454 field_trial_triggered_in_session_ |= results->field_trial_triggered; 455 return true; 456 } 457 458 void BaseSearchProvider::DeleteMatchFromMatches( 459 const AutocompleteMatch& match) { 460 for (ACMatches::iterator i(matches_.begin()); i != matches_.end(); ++i) { 461 // Find the desired match to delete by checking the type and contents. 462 // We can't check the destination URL, because the autocomplete controller 463 // may have reformulated that. Not that while checking for matching 464 // contents works for personalized suggestions, if more match types gain 465 // deletion support, this algorithm may need to be re-examined. 466 if (i->contents == match.contents && i->type == match.type) { 467 matches_.erase(i); 468 break; 469 } 470 } 471 } 472 473 void BaseSearchProvider::OnDeletionComplete( 474 bool success, SuggestionDeletionHandler* handler) { 475 RecordDeletionResult(success); 476 SuggestionDeletionHandlers::iterator it = std::find( 477 deletion_handlers_.begin(), deletion_handlers_.end(), handler); 478 DCHECK(it != deletion_handlers_.end()); 479 deletion_handlers_.erase(it); 480 } 481