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