• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2011 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/search_provider.h"
6 
7 #include <algorithm>
8 #include <cmath>
9 
10 #include "base/callback.h"
11 #include "base/i18n/icu_string_conversions.h"
12 #include "base/message_loop.h"
13 #include "base/string16.h"
14 #include "base/utf_string_conversions.h"
15 #include "chrome/browser/autocomplete/autocomplete_classifier.h"
16 #include "chrome/browser/autocomplete/keyword_provider.h"
17 #include "chrome/browser/autocomplete/autocomplete_match.h"
18 #include "chrome/browser/google/google_util.h"
19 #include "chrome/browser/history/history.h"
20 #include "chrome/browser/instant/instant_controller.h"
21 #include "chrome/browser/net/url_fixer_upper.h"
22 #include "chrome/browser/prefs/pref_service.h"
23 #include "chrome/browser/profiles/profile.h"
24 #include "chrome/browser/history/in_memory_database.h"
25 #include "chrome/browser/search_engines/template_url_model.h"
26 #include "chrome/common/pref_names.h"
27 #include "chrome/common/url_constants.h"
28 #include "content/common/json_value_serializer.h"
29 #include "googleurl/src/url_util.h"
30 #include "grit/generated_resources.h"
31 #include "net/base/escape.h"
32 #include "net/http/http_response_headers.h"
33 #include "net/url_request/url_request_status.h"
34 #include "ui/base/l10n/l10n_util.h"
35 
36 using base::Time;
37 using base::TimeDelta;
38 
39 // static
40 const int SearchProvider::kDefaultProviderURLFetcherID = 1;
41 // static
42 const int SearchProvider::kKeywordProviderURLFetcherID = 2;
43 
44 // static
45 bool SearchProvider::query_suggest_immediately_ = false;
46 
Set(const TemplateURL * default_provider,const TemplateURL * keyword_provider)47 void SearchProvider::Providers::Set(const TemplateURL* default_provider,
48                                     const TemplateURL* keyword_provider) {
49   // TODO(pkasting): http://b/1162970  We shouldn't need to structure-copy
50   // this. Nor should we need |default_provider_| and |keyword_provider_|
51   // just to know whether the provider changed.
52   default_provider_ = default_provider;
53   if (default_provider)
54     cached_default_provider_ = *default_provider;
55   keyword_provider_ = keyword_provider;
56   if (keyword_provider)
57     cached_keyword_provider_ = *keyword_provider;
58 }
59 
SearchProvider(ACProviderListener * listener,Profile * profile)60 SearchProvider::SearchProvider(ACProviderListener* listener, Profile* profile)
61     : AutocompleteProvider(listener, profile, "Search"),
62       suggest_results_pending_(0),
63       have_suggest_results_(false),
64       instant_finalized_(false) {
65 }
66 
FinalizeInstantQuery(const string16 & input_text,const string16 & suggest_text)67 void SearchProvider::FinalizeInstantQuery(const string16& input_text,
68                                           const string16& suggest_text) {
69   if (done_ || instant_finalized_)
70     return;
71 
72   instant_finalized_ = true;
73   UpdateDone();
74 
75   if (input_text.empty()) {
76     // We only need to update the listener if we're actually done.
77     if (done_)
78       listener_->OnProviderUpdate(false);
79     return;
80   }
81 
82   default_provider_suggest_text_ = suggest_text;
83 
84   string16 adjusted_input_text(input_text);
85   AutocompleteInput::RemoveForcedQueryStringIfNecessary(input_.type(),
86                                                         &adjusted_input_text);
87 
88   const string16 text = adjusted_input_text + suggest_text;
89   // Remove any matches that are identical to |text|. We don't use the
90   // destination_url for comparison as it varies depending upon the index passed
91   // to TemplateURL::ReplaceSearchTerms.
92   for (ACMatches::iterator i = matches_.begin(); i != matches_.end();) {
93     // Reset the description/description_class of all searches. We'll set the
94     // description of the new first match in the call to
95     // UpdateFirstSearchMatchDescription() below.
96     if ((i->type == AutocompleteMatch::SEARCH_HISTORY) ||
97         (i->type == AutocompleteMatch::SEARCH_SUGGEST) ||
98         (i->type == AutocompleteMatch::SEARCH_WHAT_YOU_TYPED)) {
99       i->description.clear();
100       i->description_class.clear();
101     }
102 
103     if (((i->type == AutocompleteMatch::SEARCH_HISTORY) ||
104          (i->type == AutocompleteMatch::SEARCH_SUGGEST)) &&
105         (i->fill_into_edit == text)) {
106       i = matches_.erase(i);
107     } else {
108       ++i;
109     }
110   }
111 
112   // Add the new suggest result. We give it a rank higher than
113   // SEARCH_WHAT_YOU_TYPED so that it gets autocompleted.
114   int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
115         TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
116         TemplateURLRef::NO_SUGGESTION_CHOSEN;
117   MatchMap match_map;
118   AddMatchToMap(text, adjusted_input_text,
119                 CalculateRelevanceForWhatYouTyped() + 1,
120                 AutocompleteMatch::SEARCH_SUGGEST,
121                 did_not_accept_default_suggestion, false,
122                 input_.initial_prevent_inline_autocomplete(), &match_map);
123   DCHECK_EQ(1u, match_map.size());
124   matches_.push_back(match_map.begin()->second);
125   // Sort the results so that UpdateFirstSearchDescription does the right thing.
126   std::sort(matches_.begin(), matches_.end(), &AutocompleteMatch::MoreRelevant);
127 
128   UpdateFirstSearchMatchDescription();
129 
130   listener_->OnProviderUpdate(true);
131 }
132 
Start(const AutocompleteInput & input,bool minimal_changes)133 void SearchProvider::Start(const AutocompleteInput& input,
134                            bool minimal_changes) {
135   matches_.clear();
136 
137   instant_finalized_ =
138       (input.matches_requested() != AutocompleteInput::ALL_MATCHES);
139 
140   // Can't return search/suggest results for bogus input or without a profile.
141   if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
142     Stop();
143     return;
144   }
145 
146   keyword_input_text_.clear();
147   const TemplateURL* keyword_provider =
148       KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input,
149                                                           &keyword_input_text_);
150   if (keyword_input_text_.empty())
151     keyword_provider = NULL;
152 
153   const TemplateURL* default_provider =
154       profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
155   if (!TemplateURL::SupportsReplacement(default_provider))
156     default_provider = NULL;
157 
158   if (keyword_provider == default_provider)
159     keyword_provider = NULL;  // No use in querying the same provider twice.
160 
161   if (!default_provider && !keyword_provider) {
162     // No valid providers.
163     Stop();
164     return;
165   }
166 
167   // If we're still running an old query but have since changed the query text
168   // or the providers, abort the query.
169   if (!minimal_changes ||
170       !providers_.equals(default_provider, keyword_provider)) {
171     if (done_)
172       default_provider_suggest_text_.clear();
173     else
174       Stop();
175   } else if (minimal_changes &&
176              (input_.original_text() != input.original_text())) {
177     default_provider_suggest_text_.clear();
178   }
179 
180   providers_.Set(default_provider, keyword_provider);
181 
182   if (input.text().empty()) {
183     // User typed "?" alone.  Give them a placeholder result indicating what
184     // this syntax does.
185     if (default_provider) {
186       AutocompleteMatch match;
187       match.provider = this;
188       match.contents.assign(l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE));
189       match.contents_class.push_back(
190           ACMatchClassification(0, ACMatchClassification::NONE));
191       matches_.push_back(match);
192       UpdateFirstSearchMatchDescription();
193     }
194     Stop();
195     return;
196   }
197 
198   input_ = input;
199 
200   DoHistoryQuery(minimal_changes);
201   StartOrStopSuggestQuery(minimal_changes);
202   ConvertResultsToAutocompleteMatches();
203 }
204 
Run()205 void SearchProvider::Run() {
206   // Start a new request with the current input.
207   DCHECK(!done_);
208   suggest_results_pending_ = 0;
209   if (providers_.valid_suggest_for_keyword_provider()) {
210     suggest_results_pending_++;
211     keyword_fetcher_.reset(
212         CreateSuggestFetcher(kKeywordProviderURLFetcherID,
213                              providers_.keyword_provider(),
214                              keyword_input_text_));
215   }
216   if (providers_.valid_suggest_for_default_provider()) {
217     suggest_results_pending_++;
218     default_fetcher_.reset(
219         CreateSuggestFetcher(kDefaultProviderURLFetcherID,
220                              providers_.default_provider(), input_.text()));
221   }
222   // We should only get here if we have a suggest url for the keyword or default
223   // providers.
224   DCHECK_GT(suggest_results_pending_, 0);
225 }
226 
Stop()227 void SearchProvider::Stop() {
228   StopSuggest();
229   done_ = true;
230   default_provider_suggest_text_.clear();
231 }
232 
OnURLFetchComplete(const URLFetcher * source,const GURL & url,const net::URLRequestStatus & status,int response_code,const ResponseCookies & cookie,const std::string & data)233 void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
234                                         const GURL& url,
235                                         const net::URLRequestStatus& status,
236                                         int response_code,
237                                         const ResponseCookies& cookie,
238                                         const std::string& data) {
239   DCHECK(!done_);
240   suggest_results_pending_--;
241   DCHECK_GE(suggest_results_pending_, 0);  // Should never go negative.
242   const net::HttpResponseHeaders* const response_headers =
243       source->response_headers();
244   std::string json_data(data);
245   // JSON is supposed to be UTF-8, but some suggest service providers send JSON
246   // files in non-UTF-8 encodings.  The actual encoding is usually specified in
247   // the Content-Type header field.
248   if (response_headers) {
249     std::string charset;
250     if (response_headers->GetCharset(&charset)) {
251       string16 data_16;
252       // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
253       if (base::CodepageToUTF16(data, charset.c_str(),
254                                 base::OnStringConversionError::FAIL,
255                                 &data_16))
256         json_data = UTF16ToUTF8(data_16);
257     }
258   }
259 
260   bool is_keyword_results = (source == keyword_fetcher_.get());
261   SuggestResults* suggest_results = is_keyword_results ?
262       &keyword_suggest_results_ : &default_suggest_results_;
263 
264   if (status.is_success() && response_code == 200) {
265     JSONStringValueSerializer deserializer(json_data);
266     deserializer.set_allow_trailing_comma(true);
267     scoped_ptr<Value> root_val(deserializer.Deserialize(NULL, NULL));
268     const string16& input_text =
269         is_keyword_results ? keyword_input_text_ : input_.text();
270     have_suggest_results_ =
271         root_val.get() &&
272         ParseSuggestResults(root_val.get(), is_keyword_results, input_text,
273                             suggest_results);
274   }
275 
276   ConvertResultsToAutocompleteMatches();
277   listener_->OnProviderUpdate(!suggest_results->empty());
278 }
279 
~SearchProvider()280 SearchProvider::~SearchProvider() {
281 }
282 
DoHistoryQuery(bool minimal_changes)283 void SearchProvider::DoHistoryQuery(bool minimal_changes) {
284   // The history query results are synchronous, so if minimal_changes is true,
285   // we still have the last results and don't need to do anything.
286   if (minimal_changes)
287     return;
288 
289   keyword_history_results_.clear();
290   default_history_results_.clear();
291 
292   HistoryService* const history_service =
293       profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
294   history::URLDatabase* url_db = history_service ?
295       history_service->InMemoryDatabase() : NULL;
296   if (!url_db)
297     return;
298 
299   // Request history for both the keyword and default provider.
300   if (providers_.valid_keyword_provider()) {
301     url_db->GetMostRecentKeywordSearchTerms(
302         providers_.keyword_provider().id(),
303         keyword_input_text_,
304         static_cast<int>(kMaxMatches),
305         &keyword_history_results_);
306   }
307   if (providers_.valid_default_provider()) {
308     url_db->GetMostRecentKeywordSearchTerms(
309         providers_.default_provider().id(),
310         input_.text(),
311         static_cast<int>(kMaxMatches),
312         &default_history_results_);
313   }
314 }
315 
StartOrStopSuggestQuery(bool minimal_changes)316 void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) {
317   // Don't send any queries to the server until some time has elapsed after
318   // the last keypress, to avoid flooding the server with requests we are
319   // likely to end up throwing away anyway.
320   static const int kQueryDelayMs = 200;
321 
322   if (!IsQuerySuitableForSuggest()) {
323     StopSuggest();
324     return;
325   }
326 
327   // For the minimal_changes case, if we finished the previous query and still
328   // have its results, or are allowed to keep running it, just do that, rather
329   // than starting a new query.
330   if (minimal_changes &&
331       (have_suggest_results_ ||
332        (!done_ &&
333         input_.matches_requested() == AutocompleteInput::ALL_MATCHES)))
334     return;
335 
336   // We can't keep running any previous query, so halt it.
337   StopSuggest();
338 
339   // We can't start a new query if we're only allowed synchronous results.
340   if (input_.matches_requested() != AutocompleteInput::ALL_MATCHES)
341     return;
342 
343   // We'll have at least one pending fetch. Set it to 1 now, but the value is
344   // correctly set in Run. As Run isn't invoked immediately we need to set this
345   // now, else we won't think we're waiting on results from the server when we
346   // really are.
347   suggest_results_pending_ = 1;
348 
349   // Kick off a timer that will start the URL fetch if it completes before
350   // the user types another character.
351   int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs;
352   timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run);
353 }
354 
IsQuerySuitableForSuggest() const355 bool SearchProvider::IsQuerySuitableForSuggest() const {
356   // Don't run Suggest in incognito mode, the engine doesn't support it, or
357   // the user has disabled it.
358   if (profile_->IsOffTheRecord() ||
359       (!providers_.valid_suggest_for_keyword_provider() &&
360        !providers_.valid_suggest_for_default_provider()) ||
361       !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
362     return false;
363 
364   // If the input type might be a URL, we take extra care so that private data
365   // isn't sent to the server.
366 
367   // FORCED_QUERY means the user is explicitly asking us to search for this, so
368   // we assume it isn't a URL and/or there isn't private data.
369   if (input_.type() == AutocompleteInput::FORCED_QUERY)
370     return true;
371 
372   // Next we check the scheme.  If this is UNKNOWN/REQUESTED_URL/URL with a
373   // scheme that isn't http/https/ftp, we shouldn't send it.  Sending things
374   // like file: and data: is both a waste of time and a disclosure of
375   // potentially private, local data.  Other "schemes" may actually be
376   // usernames, and we don't want to send passwords.  If the scheme is OK, we
377   // still need to check other cases below.  If this is QUERY, then the presence
378   // of these schemes means the user explicitly typed one, and thus this is
379   // probably a URL that's being entered and happens to currently be invalid --
380   // in which case we again want to run our checks below.  Other QUERY cases are
381   // less likely to be URLs and thus we assume we're OK.
382   if (!LowerCaseEqualsASCII(input_.scheme(), chrome::kHttpScheme) &&
383       !LowerCaseEqualsASCII(input_.scheme(), chrome::kHttpsScheme) &&
384       !LowerCaseEqualsASCII(input_.scheme(), chrome::kFtpScheme))
385     return (input_.type() == AutocompleteInput::QUERY);
386 
387   // Don't send URLs with usernames, queries or refs.  Some of these are
388   // private, and the Suggest server is unlikely to have any useful results
389   // for any of them.  Also don't send URLs with ports, as we may initially
390   // think that a username + password is a host + port (and we don't want to
391   // send usernames/passwords), and even if the port really is a port, the
392   // server is once again unlikely to have and useful results.
393   const url_parse::Parsed& parts = input_.parts();
394   if (parts.username.is_nonempty() || parts.port.is_nonempty() ||
395       parts.query.is_nonempty() || parts.ref.is_nonempty())
396     return false;
397 
398   // Don't send anything for https except the hostname.  Hostnames are OK
399   // because they are visible when the TCP connection is established, but the
400   // specific path may reveal private information.
401   if (LowerCaseEqualsASCII(input_.scheme(), chrome::kHttpsScheme) &&
402       parts.path.is_nonempty())
403     return false;
404 
405   return true;
406 }
407 
StopSuggest()408 void SearchProvider::StopSuggest() {
409   suggest_results_pending_ = 0;
410   timer_.Stop();
411   // Stop any in-progress URL fetches.
412   keyword_fetcher_.reset();
413   default_fetcher_.reset();
414   keyword_suggest_results_.clear();
415   default_suggest_results_.clear();
416   keyword_navigation_results_.clear();
417   default_navigation_results_.clear();
418   have_suggest_results_ = false;
419 }
420 
CreateSuggestFetcher(int id,const TemplateURL & provider,const string16 & text)421 URLFetcher* SearchProvider::CreateSuggestFetcher(int id,
422                                                  const TemplateURL& provider,
423                                                  const string16& text) {
424   const TemplateURLRef* const suggestions_url = provider.suggestions_url();
425   DCHECK(suggestions_url->SupportsReplacement());
426   URLFetcher* fetcher = URLFetcher::Create(id,
427       GURL(suggestions_url->ReplaceSearchTerms(
428            provider, text,
429            TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, string16())),
430       URLFetcher::GET, this);
431   fetcher->set_request_context(profile_->GetRequestContext());
432   fetcher->Start();
433   return fetcher;
434 }
435 
ParseSuggestResults(Value * root_val,bool is_keyword,const string16 & input_text,SuggestResults * suggest_results)436 bool SearchProvider::ParseSuggestResults(Value* root_val,
437                                          bool is_keyword,
438                                          const string16& input_text,
439                                          SuggestResults* suggest_results) {
440   if (!root_val->IsType(Value::TYPE_LIST))
441     return false;
442   ListValue* root_list = static_cast<ListValue*>(root_val);
443 
444   Value* query_val;
445   string16 query_str;
446   Value* result_val;
447   if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
448       !query_val->GetAsString(&query_str) ||
449       (query_str != input_text) ||
450       !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
451     return false;
452 
453   ListValue* description_list = NULL;
454   if (root_list->GetSize() > 2) {
455     // 3rd element: Description list.
456     Value* description_val;
457     if (root_list->Get(2, &description_val) &&
458         description_val->IsType(Value::TYPE_LIST))
459       description_list = static_cast<ListValue*>(description_val);
460   }
461 
462   // We don't care about the query URL list (the fourth element in the
463   // response) for now.
464 
465   // Parse optional data in the results from the Suggest server if any.
466   ListValue* type_list = NULL;
467   // 5th argument: Optional key-value pairs.
468   // TODO: We may iterate the 5th+ arguments of the root_list if any other
469   // optional data are defined.
470   if (root_list->GetSize() > 4) {
471     Value* optional_val;
472     if (root_list->Get(4, &optional_val) &&
473         optional_val->IsType(Value::TYPE_DICTIONARY)) {
474       DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
475 
476       // Parse Google Suggest specific type extension.
477       static const std::string kGoogleSuggestType("google:suggesttype");
478       if (dict_val->HasKey(kGoogleSuggestType))
479         dict_val->GetList(kGoogleSuggestType, &type_list);
480     }
481   }
482 
483   ListValue* result_list = static_cast<ListValue*>(result_val);
484   for (size_t i = 0; i < result_list->GetSize(); ++i) {
485     Value* suggestion_val;
486     string16 suggestion_str;
487     if (!result_list->Get(i, &suggestion_val) ||
488         !suggestion_val->GetAsString(&suggestion_str))
489       return false;
490 
491     // Google search may return empty suggestions for weird input characters,
492     // they make no sense at all and can cause problem in our code.
493     // See http://crbug.com/56214
494     if (!suggestion_str.length())
495       continue;
496 
497     Value* type_val;
498     std::string type_str;
499     if (type_list && type_list->Get(i, &type_val) &&
500         type_val->GetAsString(&type_str) && (type_str == "NAVIGATION")) {
501       Value* site_val;
502       string16 site_name;
503       NavigationResults& navigation_results =
504           is_keyword ? keyword_navigation_results_ :
505                        default_navigation_results_;
506       if ((navigation_results.size() < kMaxMatches) &&
507           description_list && description_list->Get(i, &site_val) &&
508           site_val->IsType(Value::TYPE_STRING) &&
509           site_val->GetAsString(&site_name)) {
510         // We can't blindly trust the URL coming from the server to be valid.
511         GURL result_url(URLFixerUpper::FixupURL(UTF16ToUTF8(suggestion_str),
512                                                 std::string()));
513         if (result_url.is_valid()) {
514           navigation_results.push_back(NavigationResult(result_url, site_name));
515         }
516       }
517     } else {
518       // TODO(kochi): Currently we treat a calculator result as a query, but it
519       // is better to have better presentation for caluculator results.
520       if (suggest_results->size() < kMaxMatches)
521         suggest_results->push_back(suggestion_str);
522     }
523   }
524 
525   return true;
526 }
527 
ConvertResultsToAutocompleteMatches()528 void SearchProvider::ConvertResultsToAutocompleteMatches() {
529   // Convert all the results to matches and add them to a map, so we can keep
530   // the most relevant match for each result.
531   MatchMap map;
532   const Time no_time;
533   int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ?
534       TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
535       TemplateURLRef::NO_SUGGESTION_CHOSEN;
536   // Keyword what you typed results are handled by the KeywordProvider.
537 
538   int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
539         TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
540         TemplateURLRef::NO_SUGGESTION_CHOSEN;
541   if (providers_.valid_default_provider()) {
542     AddMatchToMap(input_.text(), input_.text(),
543                   CalculateRelevanceForWhatYouTyped(),
544                   AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
545                   did_not_accept_default_suggestion, false,
546                   input_.initial_prevent_inline_autocomplete(), &map);
547     if (!default_provider_suggest_text_.empty()) {
548       AddMatchToMap(input_.text() + default_provider_suggest_text_,
549                     input_.text(), CalculateRelevanceForWhatYouTyped() + 1,
550                     AutocompleteMatch::SEARCH_SUGGEST,
551                     did_not_accept_default_suggestion, false,
552                     input_.initial_prevent_inline_autocomplete(), &map);
553     }
554   }
555 
556   AddHistoryResultsToMap(keyword_history_results_, true,
557                          did_not_accept_keyword_suggestion, &map);
558   AddHistoryResultsToMap(default_history_results_, false,
559                          did_not_accept_default_suggestion, &map);
560 
561   AddSuggestResultsToMap(keyword_suggest_results_, true,
562                          did_not_accept_keyword_suggestion, &map);
563   AddSuggestResultsToMap(default_suggest_results_, false,
564                          did_not_accept_default_suggestion, &map);
565 
566   // Now add the most relevant matches from the map to |matches_|.
567   matches_.clear();
568   for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
569     matches_.push_back(i->second);
570 
571   AddNavigationResultsToMatches(keyword_navigation_results_, true);
572   AddNavigationResultsToMatches(default_navigation_results_, false);
573 
574   const size_t max_total_matches = kMaxMatches + 1;  // 1 for "what you typed"
575   std::partial_sort(matches_.begin(),
576       matches_.begin() + std::min(max_total_matches, matches_.size()),
577       matches_.end(), &AutocompleteMatch::MoreRelevant);
578   if (matches_.size() > max_total_matches)
579     matches_.erase(matches_.begin() + max_total_matches, matches_.end());
580 
581   UpdateFirstSearchMatchDescription();
582 
583   UpdateStarredStateOfMatches();
584 
585   UpdateDone();
586 }
587 
AddNavigationResultsToMatches(const NavigationResults & navigation_results,bool is_keyword)588 void SearchProvider::AddNavigationResultsToMatches(
589     const NavigationResults& navigation_results,
590     bool is_keyword) {
591   if (!navigation_results.empty()) {
592     // TODO(kochi): http://b/1170574  We add only one results for navigational
593     // suggestions. If we can get more useful information about the score,
594     // consider adding more results.
595     const size_t num_results = is_keyword ?
596         keyword_navigation_results_.size() : default_navigation_results_.size();
597     matches_.push_back(NavigationToMatch(navigation_results.front(),
598         CalculateRelevanceForNavigation(num_results, 0, is_keyword),
599         is_keyword));
600   }
601 }
602 
AddHistoryResultsToMap(const HistoryResults & results,bool is_keyword,int did_not_accept_suggestion,MatchMap * map)603 void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results,
604                                             bool is_keyword,
605                                             int did_not_accept_suggestion,
606                                             MatchMap* map) {
607   int last_relevance = 0;
608   AutocompleteClassifier* classifier = profile_->GetAutocompleteClassifier();
609   for (HistoryResults::const_iterator i(results.begin()); i != results.end();
610        ++i) {
611     // History returns results sorted for us. We force the relevance to decrease
612     // so that the sort from history is honored. We should never end up with a
613     // match having a relevance greater than the previous, but they might be
614     // equal. If we didn't force the relevance to decrease and we ended up in a
615     // situation where the relevance was equal, then which was shown first would
616     // be random.
617     // This uses >= to handle the case where 3 or more results have the same
618     // relevance.
619     bool term_looks_like_url = false;
620     // Don't autocomplete search terms that would normally be treated as URLs
621     // when typed. For example, if the user searched for google.com and types
622     // goog, don't autocomplete to the search term google.com. Otherwise, the
623     // input will look like a URL but act like a search, which is confusing.
624     // NOTE: We don't check this in the following cases:
625     //  * When inline autocomplete is disabled, we won't be inline
626     //    autocompleting this term, so we don't need to worry about confusion as
627     //    much.  This also prevents calling Classify() again from inside the
628     //    classifier (which will corrupt state and likely crash), since the
629     //    classifier always disabled inline autocomplete.
630     //  * When the user has typed the whole term, the "what you typed" history
631     //    match will outrank us for URL-like inputs anyway, so we need not do
632     //    anything special.
633     if (!input_.prevent_inline_autocomplete() && classifier &&
634         i->term != input_.text()) {
635       AutocompleteMatch match;
636       classifier->Classify(i->term, string16(), false, &match, NULL);
637       term_looks_like_url = match.transition == PageTransition::TYPED;
638     }
639     int relevance = CalculateRelevanceForHistory(i->time, term_looks_like_url,
640                                                  is_keyword);
641     if (i != results.begin() && relevance >= last_relevance)
642       relevance = last_relevance - 1;
643     last_relevance = relevance;
644     AddMatchToMap(i->term,
645                   is_keyword ? keyword_input_text_ : input_.text(),
646                   relevance,
647                   AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion,
648                   is_keyword, input_.initial_prevent_inline_autocomplete(),
649                   map);
650   }
651 }
652 
AddSuggestResultsToMap(const SuggestResults & suggest_results,bool is_keyword,int did_not_accept_suggestion,MatchMap * map)653 void SearchProvider::AddSuggestResultsToMap(
654     const SuggestResults& suggest_results,
655     bool is_keyword,
656     int did_not_accept_suggestion,
657     MatchMap* map) {
658   for (size_t i = 0; i < suggest_results.size(); ++i) {
659     AddMatchToMap(suggest_results[i],
660                   is_keyword ? keyword_input_text_ : input_.text(),
661                   CalculateRelevanceForSuggestion(suggest_results.size(), i,
662                                                   is_keyword),
663                   AutocompleteMatch::SEARCH_SUGGEST,
664                   static_cast<int>(i), is_keyword,
665                   input_.initial_prevent_inline_autocomplete(), map);
666   }
667 }
668 
CalculateRelevanceForWhatYouTyped() const669 int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
670   if (providers_.valid_keyword_provider())
671     return 250;
672 
673   switch (input_.type()) {
674     case AutocompleteInput::UNKNOWN:
675     case AutocompleteInput::QUERY:
676     case AutocompleteInput::FORCED_QUERY:
677       return 1300;
678 
679     case AutocompleteInput::REQUESTED_URL:
680       return 1150;
681 
682     case AutocompleteInput::URL:
683       return 850;
684 
685     default:
686       NOTREACHED();
687       return 0;
688   }
689 }
690 
CalculateRelevanceForHistory(const Time & time,bool looks_like_url,bool is_keyword) const691 int SearchProvider::CalculateRelevanceForHistory(const Time& time,
692                                                  bool looks_like_url,
693                                                  bool is_keyword) const {
694   // The relevance of past searches falls off over time. There are two distinct
695   // equations used. If the first equation is used (searches to the primary
696   // provider with a type other than URL that don't autocomplete to a url) the
697   // score starts at 1399 and falls to 1300. If the second equation is used the
698   // relevance of a search 15 minutes ago is discounted about 50 points, while
699   // the relevance of a search two weeks ago is discounted about 450 points.
700   double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
701 
702   if (providers_.is_primary_provider(is_keyword) &&
703       input_.type() != AutocompleteInput::URL &&
704       !input_.prevent_inline_autocomplete() && !looks_like_url) {
705     // Searches with the past two days get a different curve.
706     const double autocomplete_time= 2 * 24 * 60 * 60;
707     if (elapsed_time < autocomplete_time) {
708       return 1399 - static_cast<int>(99 *
709           std::pow(elapsed_time / autocomplete_time, 2.5));
710     }
711     elapsed_time -= autocomplete_time;
712   }
713 
714   const int score_discount =
715       static_cast<int>(6.5 * std::pow(elapsed_time, 0.3));
716 
717   // Don't let scores go below 0.  Negative relevance scores are meaningful in
718   // a different way.
719   int base_score;
720   if (!providers_.is_primary_provider(is_keyword))
721     base_score = 200;
722   else
723     base_score = (input_.type() == AutocompleteInput::URL) ? 750 : 1050;
724   return std::max(0, base_score - score_discount);
725 }
726 
CalculateRelevanceForSuggestion(size_t num_results,size_t result_number,bool is_keyword) const727 int SearchProvider::CalculateRelevanceForSuggestion(size_t num_results,
728                                                     size_t result_number,
729                                                     bool is_keyword) const {
730   DCHECK(result_number < num_results);
731   int base_score;
732   if (!providers_.is_primary_provider(is_keyword))
733     base_score = 100;
734   else
735     base_score = (input_.type() == AutocompleteInput::URL) ? 300 : 600;
736   return base_score +
737       static_cast<int>(num_results - 1 - result_number);
738 }
739 
CalculateRelevanceForNavigation(size_t num_results,size_t result_number,bool is_keyword) const740 int SearchProvider::CalculateRelevanceForNavigation(size_t num_results,
741                                                     size_t result_number,
742                                                     bool is_keyword) const {
743   DCHECK(result_number < num_results);
744   // TODO(kochi): http://b/784900  Use relevance score from the NavSuggest
745   // server if possible.
746   return (providers_.is_primary_provider(is_keyword) ? 800 : 150) +
747       static_cast<int>(num_results - 1 - result_number);
748 }
749 
AddMatchToMap(const string16 & query_string,const string16 & input_text,int relevance,AutocompleteMatch::Type type,int accepted_suggestion,bool is_keyword,bool prevent_inline_autocomplete,MatchMap * map)750 void SearchProvider::AddMatchToMap(const string16& query_string,
751                                    const string16& input_text,
752                                    int relevance,
753                                    AutocompleteMatch::Type type,
754                                    int accepted_suggestion,
755                                    bool is_keyword,
756                                    bool prevent_inline_autocomplete,
757                                    MatchMap* map) {
758   AutocompleteMatch match(this, relevance, false, type);
759   std::vector<size_t> content_param_offsets;
760   const TemplateURL& provider = is_keyword ? providers_.keyword_provider() :
761                                              providers_.default_provider();
762   match.contents.assign(query_string);
763   // We do intra-string highlighting for suggestions - the suggested segment
764   // will be highlighted, e.g. for input_text = "you" the suggestion may be
765   // "youtube", so we'll bold the "tube" section: you*tube*.
766   if (input_text != query_string) {
767     size_t input_position = match.contents.find(input_text);
768     if (input_position == string16::npos) {
769       // The input text is not a substring of the query string, e.g. input
770       // text is "slasdot" and the query string is "slashdot", so we bold the
771       // whole thing.
772       match.contents_class.push_back(
773           ACMatchClassification(0, ACMatchClassification::MATCH));
774     } else {
775       // TODO(beng): ACMatchClassification::MATCH now seems to just mean
776       //             "bold" this. Consider modifying the terminology.
777       // We don't iterate over the string here annotating all matches because
778       // it looks odd to have every occurrence of a substring that may be as
779       // short as a single character highlighted in a query suggestion result,
780       // e.g. for input text "s" and query string "southwest airlines", it
781       // looks odd if both the first and last s are highlighted.
782       if (input_position != 0) {
783         match.contents_class.push_back(
784             ACMatchClassification(0, ACMatchClassification::NONE));
785       }
786       match.contents_class.push_back(
787           ACMatchClassification(input_position, ACMatchClassification::DIM));
788       size_t next_fragment_position = input_position + input_text.length();
789       if (next_fragment_position < query_string.length()) {
790         match.contents_class.push_back(
791             ACMatchClassification(next_fragment_position,
792                                   ACMatchClassification::NONE));
793       }
794     }
795   } else {
796     // Otherwise, we're dealing with the "default search" result which has no
797     // completion.
798     match.contents_class.push_back(
799         ACMatchClassification(0, ACMatchClassification::NONE));
800   }
801 
802   // When the user forced a query, we need to make sure all the fill_into_edit
803   // values preserve that property.  Otherwise, if the user starts editing a
804   // suggestion, non-Search results will suddenly appear.
805   size_t search_start = 0;
806   if (input_.type() == AutocompleteInput::FORCED_QUERY) {
807     match.fill_into_edit.assign(ASCIIToUTF16("?"));
808     ++search_start;
809   }
810   if (is_keyword) {
811     match.fill_into_edit.append(
812         providers_.keyword_provider().keyword() + char16(' '));
813     match.template_url = &providers_.keyword_provider();
814   }
815   match.fill_into_edit.append(query_string);
816   // Not all suggestions start with the original input.
817   if (!prevent_inline_autocomplete &&
818       !match.fill_into_edit.compare(search_start, input_text.length(),
819                                    input_text))
820     match.inline_autocomplete_offset = search_start + input_text.length();
821 
822   const TemplateURLRef* const search_url = provider.url();
823   DCHECK(search_url->SupportsReplacement());
824   match.destination_url =
825       GURL(search_url->ReplaceSearchTerms(provider,
826                                           query_string,
827                                           accepted_suggestion,
828                                           input_text));
829 
830   // Search results don't look like URLs.
831   match.transition =
832       is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED;
833 
834   // Try to add |match| to |map|.  If a match for |query_string| is already in
835   // |map|, replace it if |match| is more relevant.
836   // NOTE: Keep this ToLower() call in sync with url_database.cc.
837   const std::pair<MatchMap::iterator, bool> i = map->insert(
838       std::pair<string16, AutocompleteMatch>(
839           l10n_util::ToLower(query_string), match));
840   // NOTE: We purposefully do a direct relevance comparison here instead of
841   // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
842   // first" rather than "items alphabetically first" when the scores are equal.
843   // The only case this matters is when a user has results with the same score
844   // that differ only by capitalization; because the history system returns
845   // results sorted by recency, this means we'll pick the most recent such
846   // result even if the precision of our relevance score is too low to
847   // distinguish the two.
848   if (!i.second && (match.relevance > i.first->second.relevance))
849     i.first->second = match;
850 }
851 
NavigationToMatch(const NavigationResult & navigation,int relevance,bool is_keyword)852 AutocompleteMatch SearchProvider::NavigationToMatch(
853     const NavigationResult& navigation,
854     int relevance,
855     bool is_keyword) {
856   const string16& input_text =
857       is_keyword ? keyword_input_text_ : input_.text();
858   AutocompleteMatch match(this, relevance, false,
859                           AutocompleteMatch::NAVSUGGEST);
860   match.destination_url = navigation.url;
861   match.contents =
862       StringForURLDisplay(navigation.url, true, !HasHTTPScheme(input_text));
863   AutocompleteMatch::ClassifyMatchInString(input_text, match.contents,
864                                            ACMatchClassification::URL,
865                                            &match.contents_class);
866 
867   match.description = navigation.site_name;
868   AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name,
869                                            ACMatchClassification::NONE,
870                                            &match.description_class);
871 
872   // When the user forced a query, we need to make sure all the fill_into_edit
873   // values preserve that property.  Otherwise, if the user starts editing a
874   // suggestion, non-Search results will suddenly appear.
875   if (input_.type() == AutocompleteInput::FORCED_QUERY)
876     match.fill_into_edit.assign(ASCIIToUTF16("?"));
877   match.fill_into_edit.append(
878       AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url,
879                                                               match.contents));
880   // TODO(pkasting): http://b/1112879 These should perhaps be
881   // inline-autocompletable?
882 
883   return match;
884 }
885 
UpdateDone()886 void SearchProvider::UpdateDone() {
887   // We're done when there are no more suggest queries pending (this is set to 1
888   // when the timer is started) and we're not waiting on instant.
889   done_ = ((suggest_results_pending_ == 0) &&
890            (instant_finalized_ || !InstantController::IsEnabled(profile_)));
891 }
892 
UpdateFirstSearchMatchDescription()893 void SearchProvider::UpdateFirstSearchMatchDescription() {
894   if (!providers_.valid_default_provider() || matches_.empty())
895     return;
896 
897   for (ACMatches::iterator i = matches_.begin(); i != matches_.end(); ++i) {
898     AutocompleteMatch& match = *i;
899     switch (match.type) {
900       case AutocompleteMatch::SEARCH_WHAT_YOU_TYPED:
901       case AutocompleteMatch::SEARCH_HISTORY:
902       case AutocompleteMatch::SEARCH_SUGGEST:
903         match.description.assign(l10n_util::GetStringFUTF16(
904             IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
905             providers_.default_provider().
906                 AdjustedShortNameForLocaleDirection()));
907         match.description_class.push_back(
908             ACMatchClassification(0, ACMatchClassification::DIM));
909         // Only the first search match gets a description.
910         return;
911 
912       default:
913         break;
914     }
915   }
916 }
917