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 "chrome/browser/autocomplete/base_search_provider.h" 6 7 #include "base/i18n/case_conversion.h" 8 #include "base/i18n/icu_string_conversions.h" 9 #include "base/json/json_string_value_serializer.h" 10 #include "base/json/json_writer.h" 11 #include "base/prefs/pref_registry_simple.h" 12 #include "base/prefs/pref_service.h" 13 #include "base/strings/string_util.h" 14 #include "base/strings/utf_string_conversions.h" 15 #include "chrome/browser/autocomplete/autocomplete_provider_listener.h" 16 #include "chrome/browser/bitmap_fetcher/bitmap_fetcher_service.h" 17 #include "chrome/browser/bitmap_fetcher/bitmap_fetcher_service_factory.h" 18 #include "chrome/browser/history/history_service.h" 19 #include "chrome/browser/history/history_service_factory.h" 20 #include "chrome/browser/omnibox/omnibox_field_trial.h" 21 #include "chrome/browser/profiles/profile.h" 22 #include "chrome/browser/search/instant_service.h" 23 #include "chrome/browser/search/instant_service_factory.h" 24 #include "chrome/browser/search/search.h" 25 #include "chrome/browser/search_engines/template_url.h" 26 #include "chrome/browser/search_engines/template_url_prepopulate_data.h" 27 #include "chrome/browser/search_engines/template_url_service.h" 28 #include "chrome/browser/search_engines/template_url_service_factory.h" 29 #include "chrome/browser/search_engines/ui_thread_search_terms_data.h" 30 #include "chrome/browser/sync/profile_sync_service.h" 31 #include "chrome/browser/sync/profile_sync_service_factory.h" 32 #include "chrome/common/pref_names.h" 33 #include "components/autocomplete/url_prefix.h" 34 #include "components/metrics/proto/omnibox_event.pb.h" 35 #include "components/metrics/proto/omnibox_input_type.pb.h" 36 #include "components/sync_driver/sync_prefs.h" 37 #include "components/url_fixer/url_fixer.h" 38 #include "content/public/common/url_constants.h" 39 #include "net/base/escape.h" 40 #include "net/base/net_util.h" 41 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" 42 #include "net/http/http_response_headers.h" 43 #include "net/url_request/url_fetcher.h" 44 #include "net/url_request/url_fetcher_delegate.h" 45 #include "url/gurl.h" 46 47 using metrics::OmniboxEventProto; 48 49 namespace { 50 51 AutocompleteMatchType::Type GetAutocompleteMatchType(const std::string& type) { 52 if (type == "ENTITY") 53 return AutocompleteMatchType::SEARCH_SUGGEST_ENTITY; 54 if (type == "INFINITE") 55 return AutocompleteMatchType::SEARCH_SUGGEST_INFINITE; 56 if (type == "PERSONALIZED_QUERY") 57 return AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED; 58 if (type == "PROFILE") 59 return AutocompleteMatchType::SEARCH_SUGGEST_PROFILE; 60 if (type == "NAVIGATION") 61 return AutocompleteMatchType::NAVSUGGEST; 62 if (type == "PERSONALIZED_NAVIGATION") 63 return AutocompleteMatchType::NAVSUGGEST_PERSONALIZED; 64 return AutocompleteMatchType::SEARCH_SUGGEST; 65 } 66 67 } // namespace 68 69 // SuggestionDeletionHandler ------------------------------------------------- 70 71 // This class handles making requests to the server in order to delete 72 // personalized suggestions. 73 class SuggestionDeletionHandler : public net::URLFetcherDelegate { 74 public: 75 typedef base::Callback<void(bool, SuggestionDeletionHandler*)> 76 DeletionCompletedCallback; 77 78 SuggestionDeletionHandler( 79 const std::string& deletion_url, 80 Profile* profile, 81 const DeletionCompletedCallback& callback); 82 83 virtual ~SuggestionDeletionHandler(); 84 85 private: 86 // net::URLFetcherDelegate: 87 virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; 88 89 scoped_ptr<net::URLFetcher> deletion_fetcher_; 90 DeletionCompletedCallback callback_; 91 92 DISALLOW_COPY_AND_ASSIGN(SuggestionDeletionHandler); 93 }; 94 95 SuggestionDeletionHandler::SuggestionDeletionHandler( 96 const std::string& deletion_url, 97 Profile* profile, 98 const DeletionCompletedCallback& callback) : callback_(callback) { 99 GURL url(deletion_url); 100 DCHECK(url.is_valid()); 101 102 deletion_fetcher_.reset(net::URLFetcher::Create( 103 BaseSearchProvider::kDeletionURLFetcherID, 104 url, 105 net::URLFetcher::GET, 106 this)); 107 deletion_fetcher_->SetRequestContext(profile->GetRequestContext()); 108 deletion_fetcher_->Start(); 109 } 110 111 SuggestionDeletionHandler::~SuggestionDeletionHandler() { 112 } 113 114 void SuggestionDeletionHandler::OnURLFetchComplete( 115 const net::URLFetcher* source) { 116 DCHECK(source == deletion_fetcher_.get()); 117 callback_.Run( 118 source->GetStatus().is_success() && (source->GetResponseCode() == 200), 119 this); 120 } 121 122 // BaseSearchProvider --------------------------------------------------------- 123 124 // static 125 const int BaseSearchProvider::kDefaultProviderURLFetcherID = 1; 126 const int BaseSearchProvider::kKeywordProviderURLFetcherID = 2; 127 const int BaseSearchProvider::kDeletionURLFetcherID = 3; 128 129 BaseSearchProvider::BaseSearchProvider(AutocompleteProviderListener* listener, 130 Profile* profile, 131 AutocompleteProvider::Type type) 132 : AutocompleteProvider(listener, profile, type), 133 field_trial_triggered_(false), 134 field_trial_triggered_in_session_(false), 135 suggest_results_pending_(0), 136 in_app_list_(false) { 137 } 138 139 // static 140 bool BaseSearchProvider::ShouldPrefetch(const AutocompleteMatch& match) { 141 return match.GetAdditionalInfo(kShouldPrefetchKey) == kTrue; 142 } 143 144 // static 145 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion( 146 const base::string16& suggestion, 147 AutocompleteMatchType::Type type, 148 bool from_keyword_provider, 149 const TemplateURL* template_url, 150 const SearchTermsData& search_terms_data) { 151 return CreateSearchSuggestion( 152 NULL, AutocompleteInput(), BaseSearchProvider::SuggestResult( 153 suggestion, type, suggestion, base::string16(), base::string16(), 154 base::string16(), base::string16(), std::string(), std::string(), 155 from_keyword_provider, 0, false, false, base::string16()), 156 template_url, search_terms_data, 0, 0, false, false); 157 } 158 159 void BaseSearchProvider::Stop(bool clear_cached_results) { 160 StopSuggest(); 161 done_ = true; 162 163 if (clear_cached_results) 164 ClearAllResults(); 165 } 166 167 void BaseSearchProvider::DeleteMatch(const AutocompleteMatch& match) { 168 DCHECK(match.deletable); 169 if (!match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey).empty()) { 170 deletion_handlers_.push_back(new SuggestionDeletionHandler( 171 match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey), 172 profile_, 173 base::Bind(&BaseSearchProvider::OnDeletionComplete, 174 base::Unretained(this)))); 175 } 176 177 HistoryService* const history_service = 178 HistoryServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS); 179 TemplateURL* template_url = match.GetTemplateURL(profile_, false); 180 // This may be NULL if the template corresponding to the keyword has been 181 // deleted or there is no keyword set. 182 if (template_url != NULL) { 183 history_service->DeleteMatchingURLsForKeyword(template_url->id(), 184 match.contents); 185 } 186 187 // Immediately update the list of matches to show the match was deleted, 188 // regardless of whether the server request actually succeeds. 189 DeleteMatchFromMatches(match); 190 } 191 192 void BaseSearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const { 193 provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo()); 194 metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back(); 195 new_entry.set_provider(AsOmniboxEventProviderType()); 196 new_entry.set_provider_done(done_); 197 std::vector<uint32> field_trial_hashes; 198 OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes); 199 for (size_t i = 0; i < field_trial_hashes.size(); ++i) { 200 if (field_trial_triggered_) 201 new_entry.mutable_field_trial_triggered()->Add(field_trial_hashes[i]); 202 if (field_trial_triggered_in_session_) { 203 new_entry.mutable_field_trial_triggered_in_session()->Add( 204 field_trial_hashes[i]); 205 } 206 } 207 ModifyProviderInfo(&new_entry); 208 } 209 210 // static 211 const char BaseSearchProvider::kRelevanceFromServerKey[] = 212 "relevance_from_server"; 213 const char BaseSearchProvider::kShouldPrefetchKey[] = "should_prefetch"; 214 const char BaseSearchProvider::kSuggestMetadataKey[] = "suggest_metadata"; 215 const char BaseSearchProvider::kDeletionUrlKey[] = "deletion_url"; 216 const char BaseSearchProvider::kTrue[] = "true"; 217 const char BaseSearchProvider::kFalse[] = "false"; 218 219 BaseSearchProvider::~BaseSearchProvider() {} 220 221 // BaseSearchProvider::Result -------------------------------------------------- 222 223 BaseSearchProvider::Result::Result(bool from_keyword_provider, 224 int relevance, 225 bool relevance_from_server, 226 AutocompleteMatchType::Type type, 227 const std::string& deletion_url) 228 : from_keyword_provider_(from_keyword_provider), 229 type_(type), 230 relevance_(relevance), 231 relevance_from_server_(relevance_from_server), 232 deletion_url_(deletion_url) {} 233 234 BaseSearchProvider::Result::~Result() {} 235 236 // BaseSearchProvider::SuggestResult ------------------------------------------- 237 238 BaseSearchProvider::SuggestResult::SuggestResult( 239 const base::string16& suggestion, 240 AutocompleteMatchType::Type type, 241 const base::string16& match_contents, 242 const base::string16& match_contents_prefix, 243 const base::string16& annotation, 244 const base::string16& answer_contents, 245 const base::string16& answer_type, 246 const std::string& suggest_query_params, 247 const std::string& deletion_url, 248 bool from_keyword_provider, 249 int relevance, 250 bool relevance_from_server, 251 bool should_prefetch, 252 const base::string16& input_text) 253 : Result(from_keyword_provider, 254 relevance, 255 relevance_from_server, 256 type, 257 deletion_url), 258 suggestion_(suggestion), 259 match_contents_prefix_(match_contents_prefix), 260 annotation_(annotation), 261 suggest_query_params_(suggest_query_params), 262 answer_contents_(answer_contents), 263 answer_type_(answer_type), 264 should_prefetch_(should_prefetch) { 265 match_contents_ = match_contents; 266 DCHECK(!match_contents_.empty()); 267 ClassifyMatchContents(true, input_text); 268 } 269 270 BaseSearchProvider::SuggestResult::~SuggestResult() {} 271 272 void BaseSearchProvider::SuggestResult::ClassifyMatchContents( 273 const bool allow_bolding_all, 274 const base::string16& input_text) { 275 if (input_text.empty()) { 276 // In case of zero-suggest results, do not highlight matches. 277 match_contents_class_.push_back( 278 ACMatchClassification(0, ACMatchClassification::NONE)); 279 return; 280 } 281 282 base::string16 lookup_text = input_text; 283 if (type_ == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) { 284 const size_t contents_index = 285 suggestion_.length() - match_contents_.length(); 286 // Ensure the query starts with the input text, and ends with the match 287 // contents, and the input text has an overlap with contents. 288 if (StartsWith(suggestion_, input_text, true) && 289 EndsWith(suggestion_, match_contents_, true) && 290 (input_text.length() > contents_index)) { 291 lookup_text = input_text.substr(contents_index); 292 } 293 } 294 size_t lookup_position = match_contents_.find(lookup_text); 295 if (!allow_bolding_all && (lookup_position == base::string16::npos)) { 296 // Bail if the code below to update the bolding would bold the whole 297 // string. Note that the string may already be entirely bolded; if 298 // so, leave it as is. 299 return; 300 } 301 match_contents_class_.clear(); 302 // We do intra-string highlighting for suggestions - the suggested segment 303 // will be highlighted, e.g. for input_text = "you" the suggestion may be 304 // "youtube", so we'll bold the "tube" section: you*tube*. 305 if (input_text != match_contents_) { 306 if (lookup_position == base::string16::npos) { 307 // The input text is not a substring of the query string, e.g. input 308 // text is "slasdot" and the query string is "slashdot", so we bold the 309 // whole thing. 310 match_contents_class_.push_back( 311 ACMatchClassification(0, ACMatchClassification::MATCH)); 312 } else { 313 // We don't iterate over the string here annotating all matches because 314 // it looks odd to have every occurrence of a substring that may be as 315 // short as a single character highlighted in a query suggestion result, 316 // e.g. for input text "s" and query string "southwest airlines", it 317 // looks odd if both the first and last s are highlighted. 318 if (lookup_position != 0) { 319 match_contents_class_.push_back( 320 ACMatchClassification(0, ACMatchClassification::MATCH)); 321 } 322 match_contents_class_.push_back( 323 ACMatchClassification(lookup_position, ACMatchClassification::NONE)); 324 size_t next_fragment_position = lookup_position + lookup_text.length(); 325 if (next_fragment_position < match_contents_.length()) { 326 match_contents_class_.push_back(ACMatchClassification( 327 next_fragment_position, ACMatchClassification::MATCH)); 328 } 329 } 330 } else { 331 // Otherwise, match_contents_ is a verbatim (what-you-typed) match, either 332 // for the default provider or a keyword search provider. 333 match_contents_class_.push_back( 334 ACMatchClassification(0, ACMatchClassification::NONE)); 335 } 336 } 337 338 bool BaseSearchProvider::SuggestResult::IsInlineable( 339 const base::string16& input) const { 340 return StartsWith(suggestion_, input, false); 341 } 342 343 int BaseSearchProvider::SuggestResult::CalculateRelevance( 344 const AutocompleteInput& input, 345 bool keyword_provider_requested) const { 346 if (!from_keyword_provider_ && keyword_provider_requested) 347 return 100; 348 return ((input.type() == metrics::OmniboxInputType::URL) ? 300 : 600); 349 } 350 351 // BaseSearchProvider::NavigationResult ---------------------------------------- 352 353 BaseSearchProvider::NavigationResult::NavigationResult( 354 const AutocompleteProvider& provider, 355 const GURL& url, 356 AutocompleteMatchType::Type type, 357 const base::string16& description, 358 const std::string& deletion_url, 359 bool from_keyword_provider, 360 int relevance, 361 bool relevance_from_server, 362 const base::string16& input_text, 363 const std::string& languages) 364 : Result(from_keyword_provider, 365 relevance, 366 relevance_from_server, 367 type, 368 deletion_url), 369 url_(url), 370 formatted_url_(AutocompleteInput::FormattedStringWithEquivalentMeaning( 371 url, 372 provider.StringForURLDisplay(url, true, false))), 373 description_(description) { 374 DCHECK(url_.is_valid()); 375 CalculateAndClassifyMatchContents(true, input_text, languages); 376 } 377 378 BaseSearchProvider::NavigationResult::~NavigationResult() {} 379 380 void BaseSearchProvider::NavigationResult::CalculateAndClassifyMatchContents( 381 const bool allow_bolding_nothing, 382 const base::string16& input_text, 383 const std::string& languages) { 384 if (input_text.empty()) { 385 // In case of zero-suggest results, do not highlight matches. 386 match_contents_class_.push_back( 387 ACMatchClassification(0, ACMatchClassification::NONE)); 388 return; 389 } 390 391 // First look for the user's input inside the formatted url as it would be 392 // without trimming the scheme, so we can find matches at the beginning of the 393 // scheme. 394 const URLPrefix* prefix = 395 URLPrefix::BestURLPrefix(formatted_url_, input_text); 396 size_t match_start = (prefix == NULL) ? 397 formatted_url_.find(input_text) : prefix->prefix.length(); 398 bool trim_http = !AutocompleteInput::HasHTTPScheme(input_text) && 399 (!prefix || (match_start != 0)); 400 const net::FormatUrlTypes format_types = 401 net::kFormatUrlOmitAll & ~(trim_http ? 0 : net::kFormatUrlOmitHTTP); 402 403 base::string16 match_contents = net::FormatUrl(url_, languages, format_types, 404 net::UnescapeRule::SPACES, NULL, NULL, &match_start); 405 // If the first match in the untrimmed string was inside a scheme that we 406 // trimmed, look for a subsequent match. 407 if (match_start == base::string16::npos) 408 match_start = match_contents.find(input_text); 409 // Update |match_contents_| and |match_contents_class_| if it's allowed. 410 if (allow_bolding_nothing || (match_start != base::string16::npos)) { 411 match_contents_ = match_contents; 412 // Safe if |match_start| is npos; also safe if the input is longer than the 413 // remaining contents after |match_start|. 414 AutocompleteMatch::ClassifyLocationInString(match_start, 415 input_text.length(), match_contents_.length(), 416 ACMatchClassification::URL, &match_contents_class_); 417 } 418 } 419 420 bool BaseSearchProvider::NavigationResult::IsInlineable( 421 const base::string16& input) const { 422 return 423 URLPrefix::BestURLPrefix(base::UTF8ToUTF16(url_.spec()), input) != NULL; 424 } 425 426 int BaseSearchProvider::NavigationResult::CalculateRelevance( 427 const AutocompleteInput& input, 428 bool keyword_provider_requested) const { 429 return (from_keyword_provider_ || !keyword_provider_requested) ? 800 : 150; 430 } 431 432 // BaseSearchProvider::Results ------------------------------------------------- 433 434 BaseSearchProvider::Results::Results() : verbatim_relevance(-1) {} 435 436 BaseSearchProvider::Results::~Results() {} 437 438 void BaseSearchProvider::Results::Clear() { 439 suggest_results.clear(); 440 navigation_results.clear(); 441 verbatim_relevance = -1; 442 metadata.clear(); 443 } 444 445 bool BaseSearchProvider::Results::HasServerProvidedScores() const { 446 if (verbatim_relevance >= 0) 447 return true; 448 449 // Right now either all results of one type will be server-scored or they will 450 // all be locally scored, but in case we change this later, we'll just check 451 // them all. 452 for (SuggestResults::const_iterator i(suggest_results.begin()); 453 i != suggest_results.end(); ++i) { 454 if (i->relevance_from_server()) 455 return true; 456 } 457 for (NavigationResults::const_iterator i(navigation_results.begin()); 458 i != navigation_results.end(); ++i) { 459 if (i->relevance_from_server()) 460 return true; 461 } 462 463 return false; 464 } 465 466 void BaseSearchProvider::SetDeletionURL(const std::string& deletion_url, 467 AutocompleteMatch* match) { 468 if (deletion_url.empty()) 469 return; 470 TemplateURLService* template_service = 471 TemplateURLServiceFactory::GetForProfile(profile_); 472 if (!template_service) 473 return; 474 GURL url = template_service->GetDefaultSearchProvider()->GenerateSearchURL( 475 template_service->search_terms_data()); 476 url = url.GetOrigin().Resolve(deletion_url); 477 if (url.is_valid()) { 478 match->RecordAdditionalInfo(BaseSearchProvider::kDeletionUrlKey, 479 url.spec()); 480 match->deletable = true; 481 } 482 } 483 484 // BaseSearchProvider --------------------------------------------------------- 485 486 // static 487 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion( 488 AutocompleteProvider* autocomplete_provider, 489 const AutocompleteInput& input, 490 const SuggestResult& suggestion, 491 const TemplateURL* template_url, 492 const SearchTermsData& search_terms_data, 493 int accepted_suggestion, 494 int omnibox_start_margin, 495 bool append_extra_query_params, 496 bool from_app_list) { 497 AutocompleteMatch match(autocomplete_provider, suggestion.relevance(), false, 498 suggestion.type()); 499 500 if (!template_url) 501 return match; 502 match.keyword = template_url->keyword(); 503 match.contents = suggestion.match_contents(); 504 match.contents_class = suggestion.match_contents_class(); 505 match.answer_contents = suggestion.answer_contents(); 506 match.answer_type = suggestion.answer_type(); 507 if (suggestion.type() == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) { 508 match.RecordAdditionalInfo( 509 kACMatchPropertyInputText, base::UTF16ToUTF8(input.text())); 510 match.RecordAdditionalInfo( 511 kACMatchPropertyContentsPrefix, 512 base::UTF16ToUTF8(suggestion.match_contents_prefix())); 513 match.RecordAdditionalInfo( 514 kACMatchPropertyContentsStartIndex, 515 static_cast<int>( 516 suggestion.suggestion().length() - match.contents.length())); 517 } 518 519 if (!suggestion.annotation().empty()) 520 match.description = suggestion.annotation(); 521 522 // suggestion.match_contents() should have already been collapsed. 523 match.allowed_to_be_default_match = 524 (base::CollapseWhitespace(input.text(), false) == 525 suggestion.match_contents()); 526 527 // When the user forced a query, we need to make sure all the fill_into_edit 528 // values preserve that property. Otherwise, if the user starts editing a 529 // suggestion, non-Search results will suddenly appear. 530 if (input.type() == metrics::OmniboxInputType::FORCED_QUERY) 531 match.fill_into_edit.assign(base::ASCIIToUTF16("?")); 532 if (suggestion.from_keyword_provider()) 533 match.fill_into_edit.append(match.keyword + base::char16(' ')); 534 if (!input.prevent_inline_autocomplete() && 535 StartsWith(suggestion.suggestion(), input.text(), false)) { 536 match.inline_autocompletion = 537 suggestion.suggestion().substr(input.text().length()); 538 match.allowed_to_be_default_match = true; 539 } 540 match.fill_into_edit.append(suggestion.suggestion()); 541 542 const TemplateURLRef& search_url = template_url->url_ref(); 543 DCHECK(search_url.SupportsReplacement(search_terms_data)); 544 match.search_terms_args.reset( 545 new TemplateURLRef::SearchTermsArgs(suggestion.suggestion())); 546 match.search_terms_args->original_query = input.text(); 547 match.search_terms_args->accepted_suggestion = accepted_suggestion; 548 match.search_terms_args->omnibox_start_margin = omnibox_start_margin; 549 match.search_terms_args->suggest_query_params = 550 suggestion.suggest_query_params(); 551 match.search_terms_args->append_extra_query_params = 552 append_extra_query_params; 553 match.search_terms_args->from_app_list = from_app_list; 554 // This is the destination URL sans assisted query stats. This must be set 555 // so the AutocompleteController can properly de-dupe; the controller will 556 // eventually overwrite it before it reaches the user. 557 match.destination_url = 558 GURL(search_url.ReplaceSearchTerms(*match.search_terms_args.get(), 559 search_terms_data)); 560 561 // Search results don't look like URLs. 562 match.transition = suggestion.from_keyword_provider() ? 563 content::PAGE_TRANSITION_KEYWORD : content::PAGE_TRANSITION_GENERATED; 564 565 return match; 566 } 567 568 // static 569 scoped_ptr<base::Value> BaseSearchProvider::DeserializeJsonData( 570 std::string json_data) { 571 // The JSON response should be an array. 572 for (size_t response_start_index = json_data.find("["), i = 0; 573 response_start_index != std::string::npos && i < 5; 574 response_start_index = json_data.find("[", 1), i++) { 575 // Remove any XSSI guards to allow for JSON parsing. 576 if (response_start_index > 0) 577 json_data.erase(0, response_start_index); 578 579 JSONStringValueSerializer deserializer(json_data); 580 deserializer.set_allow_trailing_comma(true); 581 int error_code = 0; 582 scoped_ptr<base::Value> data(deserializer.Deserialize(&error_code, NULL)); 583 if (error_code == 0) 584 return data.Pass(); 585 } 586 return scoped_ptr<base::Value>(); 587 } 588 589 // static 590 bool BaseSearchProvider::ZeroSuggestEnabled( 591 const GURL& suggest_url, 592 const TemplateURL* template_url, 593 OmniboxEventProto::PageClassification page_classification, 594 Profile* profile) { 595 if (!OmniboxFieldTrial::InZeroSuggestFieldTrial()) 596 return false; 597 598 // Make sure we are sending the suggest request through HTTPS to prevent 599 // exposing the current page URL or personalized results without encryption. 600 if (!suggest_url.SchemeIs(url::kHttpsScheme)) 601 return false; 602 603 // Don't show zero suggest on the NTP. 604 // TODO(hfung): Experiment with showing MostVisited zero suggest on NTP 605 // under the conditions described in crbug.com/305366. 606 if ((page_classification == 607 OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS) || 608 (page_classification == 609 OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS)) 610 return false; 611 612 // Don't run if there's no profile or in incognito mode. 613 if (profile == NULL || profile->IsOffTheRecord()) 614 return false; 615 616 // Don't run if we can't get preferences or search suggest is not enabled. 617 PrefService* prefs = profile->GetPrefs(); 618 if (!prefs->GetBoolean(prefs::kSearchSuggestEnabled)) 619 return false; 620 621 // Only make the request if we know that the provider supports zero suggest 622 // (currently only the prepopulated Google provider). 623 UIThreadSearchTermsData search_terms_data(profile); 624 if (template_url == NULL || 625 !template_url->SupportsReplacement(search_terms_data) || 626 TemplateURLPrepopulateData::GetEngineType( 627 *template_url, search_terms_data) != SEARCH_ENGINE_GOOGLE) 628 return false; 629 630 return true; 631 } 632 633 // static 634 bool BaseSearchProvider::CanSendURL( 635 const GURL& current_page_url, 636 const GURL& suggest_url, 637 const TemplateURL* template_url, 638 OmniboxEventProto::PageClassification page_classification, 639 Profile* profile) { 640 if (!ZeroSuggestEnabled(suggest_url, template_url, page_classification, 641 profile)) 642 return false; 643 644 if (!current_page_url.is_valid()) 645 return false; 646 647 // Only allow HTTP URLs or HTTPS URLs for the same domain as the search 648 // provider. 649 if ((current_page_url.scheme() != url::kHttpScheme) && 650 ((current_page_url.scheme() != url::kHttpsScheme) || 651 !net::registry_controlled_domains::SameDomainOrHost( 652 current_page_url, suggest_url, 653 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES))) 654 return false; 655 656 // Check field trials and settings allow sending the URL on suggest requests. 657 ProfileSyncService* service = 658 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); 659 sync_driver::SyncPrefs sync_prefs(profile->GetPrefs()); 660 if (service == NULL || 661 !service->IsSyncEnabledAndLoggedIn() || 662 !sync_prefs.GetPreferredDataTypes(syncer::UserTypes()).Has( 663 syncer::PROXY_TABS) || 664 service->GetEncryptedDataTypes().Has(syncer::SESSIONS)) 665 return false; 666 667 return true; 668 } 669 670 void BaseSearchProvider::OnURLFetchComplete(const net::URLFetcher* source) { 671 DCHECK(!done_); 672 suggest_results_pending_--; 673 DCHECK_GE(suggest_results_pending_, 0); // Should never go negative. 674 675 const bool is_keyword = IsKeywordFetcher(source); 676 677 // Ensure the request succeeded and that the provider used is still available. 678 // A verbatim match cannot be generated without this provider, causing errors. 679 const bool request_succeeded = 680 source->GetStatus().is_success() && (source->GetResponseCode() == 200) && 681 GetTemplateURL(is_keyword); 682 683 LogFetchComplete(request_succeeded, is_keyword); 684 685 bool results_updated = false; 686 if (request_succeeded) { 687 const net::HttpResponseHeaders* const response_headers = 688 source->GetResponseHeaders(); 689 std::string json_data; 690 source->GetResponseAsString(&json_data); 691 692 // JSON is supposed to be UTF-8, but some suggest service providers send 693 // JSON files in non-UTF-8 encodings. The actual encoding is usually 694 // specified in the Content-Type header field. 695 if (response_headers) { 696 std::string charset; 697 if (response_headers->GetCharset(&charset)) { 698 base::string16 data_16; 699 // TODO(jungshik): Switch to CodePageToUTF8 after it's added. 700 if (base::CodepageToUTF16(json_data, charset.c_str(), 701 base::OnStringConversionError::FAIL, 702 &data_16)) 703 json_data = base::UTF16ToUTF8(data_16); 704 } 705 } 706 707 scoped_ptr<base::Value> data(DeserializeJsonData(json_data)); 708 if (data && StoreSuggestionResponse(json_data, *data.get())) 709 return; 710 711 results_updated = data.get() && ParseSuggestResults( 712 *data.get(), is_keyword, GetResultsToFill(is_keyword)); 713 } 714 715 UpdateMatches(); 716 if (done_ || results_updated) 717 listener_->OnProviderUpdate(results_updated); 718 } 719 720 void BaseSearchProvider::AddMatchToMap(const SuggestResult& result, 721 const std::string& metadata, 722 int accepted_suggestion, 723 bool mark_as_deletable, 724 MatchMap* map) { 725 InstantService* instant_service = 726 InstantServiceFactory::GetForProfile(profile_); 727 // Android and iOS have no InstantService. 728 const int omnibox_start_margin = instant_service ? 729 instant_service->omnibox_start_margin() : chrome::kDisableStartMargin; 730 731 AutocompleteMatch match = CreateSearchSuggestion( 732 this, GetInput(result.from_keyword_provider()), result, 733 GetTemplateURL(result.from_keyword_provider()), 734 UIThreadSearchTermsData(profile_), accepted_suggestion, 735 omnibox_start_margin, ShouldAppendExtraParams(result), 736 in_app_list_); 737 if (!match.destination_url.is_valid()) 738 return; 739 match.search_terms_args->bookmark_bar_pinned = 740 profile_->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); 741 match.RecordAdditionalInfo(kRelevanceFromServerKey, 742 result.relevance_from_server() ? kTrue : kFalse); 743 match.RecordAdditionalInfo(kShouldPrefetchKey, 744 result.should_prefetch() ? kTrue : kFalse); 745 SetDeletionURL(result.deletion_url(), &match); 746 if (mark_as_deletable) 747 match.deletable = true; 748 // Metadata is needed only for prefetching queries. 749 if (result.should_prefetch()) 750 match.RecordAdditionalInfo(kSuggestMetadataKey, metadata); 751 752 // Try to add |match| to |map|. If a match for this suggestion is 753 // already in |map|, replace it if |match| is more relevant. 754 // NOTE: Keep this ToLower() call in sync with url_database.cc. 755 MatchKey match_key( 756 std::make_pair(base::i18n::ToLower(result.suggestion()), 757 match.search_terms_args->suggest_query_params)); 758 const std::pair<MatchMap::iterator, bool> i( 759 map->insert(std::make_pair(match_key, match))); 760 761 bool should_prefetch = result.should_prefetch(); 762 if (!i.second) { 763 // NOTE: We purposefully do a direct relevance comparison here instead of 764 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items 765 // added first" rather than "items alphabetically first" when the scores 766 // are equal. The only case this matters is when a user has results with 767 // the same score that differ only by capitalization; because the history 768 // system returns results sorted by recency, this means we'll pick the most 769 // recent such result even if the precision of our relevance score is too 770 // low to distinguish the two. 771 if (match.relevance > i.first->second.relevance) { 772 match.duplicate_matches.insert(match.duplicate_matches.end(), 773 i.first->second.duplicate_matches.begin(), 774 i.first->second.duplicate_matches.end()); 775 i.first->second.duplicate_matches.clear(); 776 match.duplicate_matches.push_back(i.first->second); 777 i.first->second = match; 778 } else { 779 i.first->second.duplicate_matches.push_back(match); 780 if (match.keyword == i.first->second.keyword) { 781 // Old and new matches are from the same search provider. It is okay to 782 // record one match's prefetch data onto a different match (for the same 783 // query string) for the following reasons: 784 // 1. Because the suggest server only sends down a query string from 785 // which we construct a URL, rather than sending a full URL, and because 786 // we construct URLs from query strings in the same way every time, the 787 // URLs for the two matches will be the same. Therefore, we won't end up 788 // prefetching something the server didn't intend. 789 // 2. Presumably the server sets the prefetch bit on a match it things 790 // is sufficiently relevant that the user is likely to choose it. 791 // Surely setting the prefetch bit on a match of even higher relevance 792 // won't violate this assumption. 793 should_prefetch |= ShouldPrefetch(i.first->second); 794 i.first->second.RecordAdditionalInfo(kShouldPrefetchKey, 795 should_prefetch ? kTrue : kFalse); 796 if (should_prefetch) 797 i.first->second.RecordAdditionalInfo(kSuggestMetadataKey, metadata); 798 } 799 } 800 } 801 } 802 803 bool BaseSearchProvider::ParseSuggestResults(const base::Value& root_val, 804 bool is_keyword_result, 805 Results* results) { 806 base::string16 query; 807 const base::ListValue* root_list = NULL; 808 const base::ListValue* results_list = NULL; 809 const AutocompleteInput& input = GetInput(is_keyword_result); 810 811 if (!root_val.GetAsList(&root_list) || !root_list->GetString(0, &query) || 812 query != input.text() || !root_list->GetList(1, &results_list)) 813 return false; 814 815 // 3rd element: Description list. 816 const base::ListValue* descriptions = NULL; 817 root_list->GetList(2, &descriptions); 818 819 // 4th element: Disregard the query URL list for now. 820 821 // Reset suggested relevance information. 822 results->verbatim_relevance = -1; 823 824 // 5th element: Optional key-value pairs from the Suggest server. 825 const base::ListValue* types = NULL; 826 const base::ListValue* relevances = NULL; 827 const base::ListValue* suggestion_details = NULL; 828 const base::DictionaryValue* extras = NULL; 829 int prefetch_index = -1; 830 if (root_list->GetDictionary(4, &extras)) { 831 extras->GetList("google:suggesttype", &types); 832 833 // Discard this list if its size does not match that of the suggestions. 834 if (extras->GetList("google:suggestrelevance", &relevances) && 835 (relevances->GetSize() != results_list->GetSize())) 836 relevances = NULL; 837 extras->GetInteger("google:verbatimrelevance", 838 &results->verbatim_relevance); 839 840 // Check if the active suggest field trial (if any) has triggered either 841 // for the default provider or keyword provider. 842 bool triggered = false; 843 extras->GetBoolean("google:fieldtrialtriggered", &triggered); 844 field_trial_triggered_ |= triggered; 845 field_trial_triggered_in_session_ |= triggered; 846 847 const base::DictionaryValue* client_data = NULL; 848 if (extras->GetDictionary("google:clientdata", &client_data) && client_data) 849 client_data->GetInteger("phi", &prefetch_index); 850 851 if (extras->GetList("google:suggestdetail", &suggestion_details) && 852 suggestion_details->GetSize() != results_list->GetSize()) 853 suggestion_details = NULL; 854 855 // Store the metadata that came with the response in case we need to pass it 856 // along with the prefetch query to Instant. 857 JSONStringValueSerializer json_serializer(&results->metadata); 858 json_serializer.Serialize(*extras); 859 } 860 861 // Clear the previous results now that new results are available. 862 results->suggest_results.clear(); 863 results->navigation_results.clear(); 864 865 base::string16 suggestion; 866 std::string type; 867 int relevance = GetDefaultResultRelevance(); 868 // Prohibit navsuggest in FORCED_QUERY mode. Users wants queries, not URLs. 869 const bool allow_navsuggest = 870 input.type() != metrics::OmniboxInputType::FORCED_QUERY; 871 const std::string languages( 872 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)); 873 const base::string16& trimmed_input = 874 base::CollapseWhitespace(input.text(), false); 875 for (size_t index = 0; results_list->GetString(index, &suggestion); ++index) { 876 // Google search may return empty suggestions for weird input characters, 877 // they make no sense at all and can cause problems in our code. 878 if (suggestion.empty()) 879 continue; 880 881 // Apply valid suggested relevance scores; discard invalid lists. 882 if (relevances != NULL && !relevances->GetInteger(index, &relevance)) 883 relevances = NULL; 884 AutocompleteMatchType::Type match_type = 885 AutocompleteMatchType::SEARCH_SUGGEST; 886 if (types && types->GetString(index, &type)) 887 match_type = GetAutocompleteMatchType(type); 888 const base::DictionaryValue* suggestion_detail = NULL; 889 std::string deletion_url; 890 891 if (suggestion_details && 892 suggestion_details->GetDictionary(index, &suggestion_detail)) 893 suggestion_detail->GetString("du", &deletion_url); 894 895 if ((match_type == AutocompleteMatchType::NAVSUGGEST) || 896 (match_type == AutocompleteMatchType::NAVSUGGEST_PERSONALIZED)) { 897 // Do not blindly trust the URL coming from the server to be valid. 898 GURL url( 899 url_fixer::FixupURL(base::UTF16ToUTF8(suggestion), std::string())); 900 if (url.is_valid() && allow_navsuggest) { 901 base::string16 title; 902 if (descriptions != NULL) 903 descriptions->GetString(index, &title); 904 results->navigation_results.push_back(NavigationResult( 905 *this, url, match_type, title, deletion_url, is_keyword_result, 906 relevance, relevances != NULL, input.text(), languages)); 907 } 908 } else { 909 base::string16 match_contents = suggestion; 910 base::string16 match_contents_prefix; 911 base::string16 annotation; 912 base::string16 answer_contents; 913 base::string16 answer_type; 914 std::string suggest_query_params; 915 916 if (suggestion_details) { 917 suggestion_details->GetDictionary(index, &suggestion_detail); 918 if (suggestion_detail) { 919 suggestion_detail->GetString("t", &match_contents); 920 suggestion_detail->GetString("mp", &match_contents_prefix); 921 // Error correction for bad data from server. 922 if (match_contents.empty()) 923 match_contents = suggestion; 924 suggestion_detail->GetString("a", &annotation); 925 suggestion_detail->GetString("q", &suggest_query_params); 926 927 // Extract Answers, if provided. 928 const base::DictionaryValue* answer_json = NULL; 929 if (suggestion_detail->GetDictionary("ansa", &answer_json)) { 930 match_type = AutocompleteMatchType::SEARCH_SUGGEST_ANSWER; 931 PrefetchAnswersImages(answer_json); 932 std::string contents; 933 base::JSONWriter::Write(answer_json, &contents); 934 answer_contents = base::UTF8ToUTF16(contents); 935 suggestion_detail->GetString("ansb", &answer_type); 936 } 937 } 938 } 939 940 bool should_prefetch = static_cast<int>(index) == prefetch_index; 941 // TODO(kochi): Improve calculator suggestion presentation. 942 results->suggest_results.push_back(SuggestResult( 943 base::CollapseWhitespace(suggestion, false), match_type, 944 base::CollapseWhitespace(match_contents, false), 945 match_contents_prefix, annotation, answer_contents, answer_type, 946 suggest_query_params, deletion_url, is_keyword_result, relevance, 947 relevances != NULL, should_prefetch, trimmed_input)); 948 } 949 } 950 SortResults(is_keyword_result, relevances, results); 951 return true; 952 } 953 954 void BaseSearchProvider::PrefetchAnswersImages( 955 const base::DictionaryValue* answer_json) { 956 DCHECK(answer_json); 957 const base::ListValue* lines = NULL; 958 answer_json->GetList("l", &lines); 959 if (!lines || lines->GetSize() == 0) 960 return; 961 962 BitmapFetcherService* image_service = 963 BitmapFetcherServiceFactory::GetForBrowserContext(profile_); 964 DCHECK(image_service); 965 966 for (size_t line = 0; line < lines->GetSize(); ++line) { 967 const base::DictionaryValue* imageLine = NULL; 968 lines->GetDictionary(line, &imageLine); 969 if (!imageLine) 970 continue; 971 const base::DictionaryValue* imageData = NULL; 972 imageLine->GetDictionary("i", &imageData); 973 if (!imageData) 974 continue; 975 std::string imageUrl; 976 imageData->GetString("d", &imageUrl); 977 image_service->Prefetch(GURL(imageUrl)); 978 } 979 } 980 981 void BaseSearchProvider::SortResults(bool is_keyword, 982 const base::ListValue* relevances, 983 Results* results) { 984 } 985 986 bool BaseSearchProvider::StoreSuggestionResponse( 987 const std::string& json_data, 988 const base::Value& parsed_data) { 989 return false; 990 } 991 992 void BaseSearchProvider::ModifyProviderInfo( 993 metrics::OmniboxEventProto_ProviderInfo* provider_info) const { 994 } 995 996 void BaseSearchProvider::DeleteMatchFromMatches( 997 const AutocompleteMatch& match) { 998 for (ACMatches::iterator i(matches_.begin()); i != matches_.end(); ++i) { 999 // Find the desired match to delete by checking the type and contents. 1000 // We can't check the destination URL, because the autocomplete controller 1001 // may have reformulated that. Not that while checking for matching 1002 // contents works for personalized suggestions, if more match types gain 1003 // deletion support, this algorithm may need to be re-examined. 1004 if (i->contents == match.contents && i->type == match.type) { 1005 matches_.erase(i); 1006 break; 1007 } 1008 } 1009 } 1010 1011 void BaseSearchProvider::OnDeletionComplete( 1012 bool success, SuggestionDeletionHandler* handler) { 1013 RecordDeletionResult(success); 1014 SuggestionDeletionHandlers::iterator it = std::find( 1015 deletion_handlers_.begin(), deletion_handlers_.end(), handler); 1016 DCHECK(it != deletion_handlers_.end()); 1017 deletion_handlers_.erase(it); 1018 } 1019