blob: 5c9931c1573300e00af5f986f99faf5de08f8d59 [file] [log] [blame]
[email protected]257ab712009-04-14 17:16:241// Copyright (c) 2009 The Chromium Authors. All rights reserved.
license.botbf09a502008-08-24 00:55:552// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
initial.commit09911bf2008-07-26 23:55:294
5#include "chrome/browser/autocomplete/search_provider.h"
6
[email protected]a92b8642009-05-05 23:38:567#include "app/l10n_util.h"
initial.commit09911bf2008-07-26 23:55:298#include "base/message_loop.h"
9#include "base/string_util.h"
[email protected]257ab712009-04-14 17:16:2410#include "chrome/browser/autocomplete/keyword_provider.h"
initial.commit09911bf2008-07-26 23:55:2911#include "chrome/browser/browser_process.h"
12#include "chrome/browser/google_util.h"
[email protected]ce560f82009-06-03 09:39:4413#include "chrome/browser/history/history.h"
[email protected]f870a322009-01-16 21:47:2714#include "chrome/browser/net/url_fixer_upper.h"
initial.commit09911bf2008-07-26 23:55:2915#include "chrome/browser/profile.h"
[email protected]d54e03a52009-01-16 00:31:0416#include "chrome/browser/search_engines/template_url_model.h"
initial.commit09911bf2008-07-26 23:55:2917#include "chrome/common/json_value_serializer.h"
initial.commit09911bf2008-07-26 23:55:2918#include "chrome/common/pref_names.h"
19#include "chrome/common/pref_service.h"
[email protected]dcf7d352009-02-26 01:56:0220#include "chrome/common/url_constants.h"
initial.commit09911bf2008-07-26 23:55:2921#include "googleurl/src/url_util.h"
[email protected]34ac8f32009-02-22 23:03:2722#include "grit/generated_resources.h"
initial.commit09911bf2008-07-26 23:55:2923#include "net/base/escape.h"
[email protected]319d9e6f2009-02-18 19:47:2124#include "net/http/http_response_headers.h"
25#include "net/url_request/url_request_status.h"
initial.commit09911bf2008-07-26 23:55:2926
[email protected]e1acf6f2008-10-27 20:43:3327using base::Time;
28using base::TimeDelta;
29
[email protected]b547666d2009-04-23 16:37:5830// static
31const int SearchProvider::kDefaultProviderURLFetcherID = 1;
32// static
33const int SearchProvider::kKeywordProviderURLFetcherID = 2;
34
35// static
36bool SearchProvider::query_suggest_immediately_ = false;
37
[email protected]257ab712009-04-14 17:16:2438void SearchProvider::Providers::Set(const TemplateURL* default_provider,
39 const TemplateURL* keyword_provider) {
40 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy
41 // this. Nor should we need |default_provider_| and |keyword_provider_|
42 // just to know whether the provider changed.
43 default_provider_ = default_provider;
44 if (default_provider)
45 cached_default_provider_ = *default_provider;
46 keyword_provider_ = keyword_provider;
47 if (keyword_provider)
48 cached_keyword_provider_ = *keyword_provider;
49}
50
initial.commit09911bf2008-07-26 23:55:2951void SearchProvider::Start(const AutocompleteInput& input,
[email protected]8deeb952008-10-09 18:21:2752 bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:2953 matches_.clear();
54
[email protected]6c85aa02009-02-27 12:08:0955 // Can't return search/suggest results for bogus input or without a profile.
initial.commit09911bf2008-07-26 23:55:2956 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
57 Stop();
58 return;
59 }
60
[email protected]257ab712009-04-14 17:16:2461 keyword_input_text_.clear();
62 const TemplateURL* keyword_provider =
63 KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input,
64 &keyword_input_text_);
65 if (!TemplateURL::SupportsReplacement(keyword_provider) ||
66 keyword_input_text_.empty()) {
67 keyword_provider = NULL;
68 }
69
70 const TemplateURL* default_provider =
initial.commit09911bf2008-07-26 23:55:2971 profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
[email protected]257ab712009-04-14 17:16:2472 if (!TemplateURL::SupportsReplacement(default_provider))
73 default_provider = NULL;
74
75 if (keyword_provider == default_provider)
76 keyword_provider = NULL; // No use in querying the same provider twice.
77
78 if (!default_provider && !keyword_provider) {
79 // No valid providers.
initial.commit09911bf2008-07-26 23:55:2980 Stop();
81 return;
82 }
83
84 // If we're still running an old query but have since changed the query text
[email protected]257ab712009-04-14 17:16:2485 // or the providers, abort the query.
initial.commit09911bf2008-07-26 23:55:2986 if (!done_ && (!minimal_changes ||
[email protected]257ab712009-04-14 17:16:2487 !providers_.equals(default_provider, keyword_provider))) {
initial.commit09911bf2008-07-26 23:55:2988 Stop();
[email protected]257ab712009-04-14 17:16:2489 }
initial.commit09911bf2008-07-26 23:55:2990
[email protected]257ab712009-04-14 17:16:2491 providers_.Set(default_provider, keyword_provider);
initial.commit09911bf2008-07-26 23:55:2992
93 if (input.text().empty()) {
94 // User typed "?" alone. Give them a placeholder result indicating what
95 // this syntax does.
[email protected]257ab712009-04-14 17:16:2496 if (default_provider) {
97 AutocompleteMatch match(this, 0, false,
98 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED);
99 static const std::wstring kNoQueryInput(
100 l10n_util::GetString(IDS_AUTOCOMPLETE_NO_QUERY));
101 match.contents.assign(l10n_util::GetStringF(
[email protected]bed9bd6c2009-04-21 17:27:47102 IDS_AUTOCOMPLETE_SEARCH_CONTENTS,
103 default_provider->AdjustedShortNameForLocaleDirection(),
[email protected]257ab712009-04-14 17:16:24104 kNoQueryInput));
105 match.contents_class.push_back(
106 ACMatchClassification(0, ACMatchClassification::DIM));
107 matches_.push_back(match);
108 }
initial.commit09911bf2008-07-26 23:55:29109 Stop();
110 return;
111 }
112
113 input_ = input;
114
[email protected]8deeb952008-10-09 18:21:27115 StartOrStopHistoryQuery(minimal_changes);
116 StartOrStopSuggestQuery(minimal_changes);
initial.commit09911bf2008-07-26 23:55:29117 ConvertResultsToAutocompleteMatches();
118}
119
120void SearchProvider::Run() {
121 // Start a new request with the current input.
122 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24123 suggest_results_pending_ = 0;
124 if (providers_.valid_suggest_for_keyword_provider()) {
125 suggest_results_pending_++;
126 keyword_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58127 CreateSuggestFetcher(kKeywordProviderURLFetcherID,
128 providers_.keyword_provider(),
[email protected]257ab712009-04-14 17:16:24129 keyword_input_text_));
130 }
131 if (providers_.valid_suggest_for_default_provider()) {
132 suggest_results_pending_++;
133 default_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58134 CreateSuggestFetcher(kDefaultProviderURLFetcherID,
135 providers_.default_provider(), input_.text()));
[email protected]257ab712009-04-14 17:16:24136 }
137 // We should only get here if we have a suggest url for the keyword or default
138 // providers.
139 DCHECK(suggest_results_pending_ > 0);
initial.commit09911bf2008-07-26 23:55:29140}
141
142void SearchProvider::Stop() {
143 StopHistory();
144 StopSuggest();
145 done_ = true;
146}
147
148void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
149 const GURL& url,
150 const URLRequestStatus& status,
151 int response_code,
152 const ResponseCookies& cookie,
153 const std::string& data) {
154 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24155 suggest_results_pending_--;
156 DCHECK(suggest_results_pending_ >= 0); // Should never go negative.
[email protected]ec9207d32008-09-26 00:51:06157 const net::HttpResponseHeaders* const response_headers =
158 source->response_headers();
159 std::string json_data(data);
[email protected]6c85aa02009-02-27 12:08:09160 // JSON is supposed to be UTF-8, but some suggest service providers send JSON
161 // files in non-UTF-8 encodings. The actual encoding is usually specified in
162 // the Content-Type header field.
[email protected]ec9207d32008-09-26 00:51:06163 if (response_headers) {
164 std::string charset;
165 if (response_headers->GetCharset(&charset)) {
166 std::wstring wide_data;
167 // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
168 if (CodepageToWide(data, charset.c_str(),
169 OnStringUtilConversionError::FAIL, &wide_data))
[email protected]f0a51fb52009-03-05 12:46:38170 json_data = WideToUTF8(wide_data);
[email protected]ec9207d32008-09-26 00:51:06171 }
172 }
173
[email protected]257ab712009-04-14 17:16:24174 bool is_keyword_results = (source == keyword_fetcher_.get());
175 SuggestResults* suggest_results = is_keyword_results ?
176 &keyword_suggest_results_ : &default_suggest_results_;
177
[email protected]b4cebf82008-12-29 19:59:08178 if (status.is_success() && response_code == 200) {
179 JSONStringValueSerializer deserializer(json_data);
180 deserializer.set_allow_trailing_comma(true);
181 scoped_ptr<Value> root_val(deserializer.Deserialize(NULL));
[email protected]257ab712009-04-14 17:16:24182 const std::wstring& input_text =
183 is_keyword_results ? keyword_input_text_ : input_.text();
[email protected]b4cebf82008-12-29 19:59:08184 have_suggest_results_ =
[email protected]257ab712009-04-14 17:16:24185 root_val.get() &&
186 ParseSuggestResults(root_val.get(), is_keyword_results, input_text,
187 suggest_results);
[email protected]b4cebf82008-12-29 19:59:08188 }
189
initial.commit09911bf2008-07-26 23:55:29190 ConvertResultsToAutocompleteMatches();
[email protected]257ab712009-04-14 17:16:24191 listener_->OnProviderUpdate(!suggest_results->empty());
initial.commit09911bf2008-07-26 23:55:29192}
193
[email protected]8deeb952008-10-09 18:21:27194void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:29195 // For the minimal_changes case, if we finished the previous query and still
196 // have its results, or are allowed to keep running it, just do that, rather
197 // than starting a new query.
198 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27199 (have_history_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29200 return;
201
202 // We can't keep running any previous query, so halt it.
203 StopHistory();
204
205 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27206 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29207 return;
208
[email protected]257ab712009-04-14 17:16:24209 // Request history for both the keyword and default provider.
210 if (providers_.valid_keyword_provider()) {
211 ScheduleHistoryQuery(providers_.keyword_provider().id(),
212 keyword_input_text_);
213 }
214 if (providers_.valid_default_provider()) {
215 ScheduleHistoryQuery(providers_.default_provider().id(),
216 input_.text());
217 }
initial.commit09911bf2008-07-26 23:55:29218}
219
[email protected]8deeb952008-10-09 18:21:27220void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) {
[email protected]6c85aa02009-02-27 12:08:09221 // Don't send any queries to the server until some time has elapsed after
222 // the last keypress, to avoid flooding the server with requests we are
223 // likely to end up throwing away anyway.
224 static const int kQueryDelayMs = 200;
225
[email protected]83c726482008-09-10 06:36:34226 if (!IsQuerySuitableForSuggest()) {
initial.commit09911bf2008-07-26 23:55:29227 StopSuggest();
228 return;
229 }
230
231 // For the minimal_changes case, if we finished the previous query and still
232 // have its results, or are allowed to keep running it, just do that, rather
233 // than starting a new query.
234 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27235 (have_suggest_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29236 return;
237
238 // We can't keep running any previous query, so halt it.
239 StopSuggest();
240
241 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27242 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29243 return;
244
[email protected]257ab712009-04-14 17:16:24245 // We'll have at least one pending fetch. Set it to 1 now, but the value is
246 // correctly set in Run. As Run isn't invoked immediately we need to set this
247 // now, else we won't think we're waiting on results from the server when we
248 // really are.
249 suggest_results_pending_ = 1;
250
initial.commit09911bf2008-07-26 23:55:29251 // Kick off a timer that will start the URL fetch if it completes before
252 // the user types another character.
[email protected]b547666d2009-04-23 16:37:58253 int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs;
254 timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run);
initial.commit09911bf2008-07-26 23:55:29255}
256
[email protected]83c726482008-09-10 06:36:34257bool SearchProvider::IsQuerySuitableForSuggest() const {
258 // Don't run Suggest when off the record, the engine doesn't support it, or
259 // the user has disabled it.
260 if (profile_->IsOffTheRecord() ||
[email protected]257ab712009-04-14 17:16:24261 (!providers_.valid_suggest_for_keyword_provider() &&
262 !providers_.valid_suggest_for_default_provider()) ||
[email protected]83c726482008-09-10 06:36:34263 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
264 return false;
265
266 // If the input type is URL, we take extra care so that private data in URL
267 // isn't sent to the server.
268 if (input_.type() == AutocompleteInput::URL) {
269 // Don't query the server for URLs that aren't http/https/ftp. Sending
270 // things like file: and data: is both a waste of time and a disclosure of
271 // potentially private, local data.
272 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") &&
273 (input_.scheme() != L"ftp"))
274 return false;
275
276 // Don't leak private data in URL
277 const url_parse::Parsed& parts = input_.parts();
278
279 // Don't send URLs with usernames, queries or refs. Some of these are
280 // private, and the Suggest server is unlikely to have any useful results
281 // for any of them.
282 // Password is optional and may be omitted. Checking username is
283 // sufficient.
284 if (parts.username.is_nonempty() || parts.query.is_nonempty() ||
285 parts.ref.is_nonempty())
286 return false;
287 // Don't send anything for https except hostname and port number.
288 // Hostname and port number are OK because they are visible when TCP
289 // connection is established and the Suggest server may provide some
290 // useful completed URL.
291 if (input_.scheme() == L"https" && parts.path.is_nonempty())
292 return false;
293 }
294
295 return true;
296}
297
initial.commit09911bf2008-07-26 23:55:29298void SearchProvider::StopHistory() {
299 history_request_consumer_.CancelAllRequests();
300 history_request_pending_ = false;
[email protected]257ab712009-04-14 17:16:24301 keyword_history_results_.clear();
302 default_history_results_.clear();
initial.commit09911bf2008-07-26 23:55:29303 have_history_results_ = false;
304}
305
306void SearchProvider::StopSuggest() {
[email protected]257ab712009-04-14 17:16:24307 suggest_results_pending_ = 0;
[email protected]2d316662008-09-03 18:18:14308 timer_.Stop();
[email protected]257ab712009-04-14 17:16:24309 // Stop any in-progress URL fetches.
310 keyword_fetcher_.reset();
311 default_fetcher_.reset();
312 keyword_suggest_results_.clear();
313 default_suggest_results_.clear();
314 keyword_navigation_results_.clear();
315 default_navigation_results_.clear();
initial.commit09911bf2008-07-26 23:55:29316 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29317}
318
[email protected]257ab712009-04-14 17:16:24319void SearchProvider::ScheduleHistoryQuery(TemplateURL::IDType search_id,
320 const std::wstring& text) {
321 DCHECK(!text.empty());
322 HistoryService* const history_service =
323 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
324 HistoryService::Handle request_handle =
325 history_service->GetMostRecentKeywordSearchTerms(
326 search_id, text, static_cast<int>(max_matches()),
327 &history_request_consumer_,
328 NewCallback(this,
329 &SearchProvider::OnGotMostRecentKeywordSearchTerms));
330 history_request_consumer_.SetClientData(history_service, request_handle,
331 search_id);
332 history_request_pending_ = true;
333}
334
initial.commit09911bf2008-07-26 23:55:29335void SearchProvider::OnGotMostRecentKeywordSearchTerms(
336 CancelableRequestProvider::Handle handle,
337 HistoryResults* results) {
[email protected]257ab712009-04-14 17:16:24338 HistoryService* history_service =
339 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
340 DCHECK(history_service);
341 if (providers_.valid_keyword_provider() &&
[email protected]bed9bd6c2009-04-21 17:27:47342 (providers_.keyword_provider().id() ==
[email protected]257ab712009-04-14 17:16:24343 history_request_consumer_.GetClientData(history_service, handle))) {
344 keyword_history_results_ = *results;
345 } else {
346 default_history_results_ = *results;
347 }
[email protected]257ab712009-04-14 17:16:24348
349 if (history_request_consumer_.PendingRequestCount() == 1) {
350 // Requests are removed AFTER the callback is invoked. If the count == 1,
351 // it means no more history requests are pending.
352 history_request_pending_ = false;
353 have_history_results_ = true;
354 }
[email protected]b547666d2009-04-23 16:37:58355
356 ConvertResultsToAutocompleteMatches();
357 listener_->OnProviderUpdate(!results->empty());
initial.commit09911bf2008-07-26 23:55:29358}
359
[email protected]b547666d2009-04-23 16:37:58360URLFetcher* SearchProvider::CreateSuggestFetcher(int id,
361 const TemplateURL& provider,
[email protected]257ab712009-04-14 17:16:24362 const std::wstring& text) {
363 const TemplateURLRef* const suggestions_url = provider.suggestions_url();
364 DCHECK(suggestions_url->SupportsReplacement());
[email protected]b547666d2009-04-23 16:37:58365 URLFetcher* fetcher = URLFetcher::Create(id,
[email protected]7b9f3672009-06-15 18:31:22366 GURL(WideToUTF8(suggestions_url->ReplaceSearchTerms(
[email protected]b547666d2009-04-23 16:37:58367 provider, text, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE,
[email protected]7b9f3672009-06-15 18:31:22368 std::wstring()))),
[email protected]257ab712009-04-14 17:16:24369 URLFetcher::GET, this);
370 fetcher->set_request_context(profile_->GetRequestContext());
371 fetcher->Start();
372 return fetcher;
373}
374
375bool SearchProvider::ParseSuggestResults(Value* root_val,
376 bool is_keyword,
377 const std::wstring& input_text,
378 SuggestResults* suggest_results) {
initial.commit09911bf2008-07-26 23:55:29379 if (!root_val->IsType(Value::TYPE_LIST))
380 return false;
381 ListValue* root_list = static_cast<ListValue*>(root_val);
382
383 Value* query_val;
384 std::wstring query_str;
385 Value* result_val;
386 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
[email protected]257ab712009-04-14 17:16:24387 !query_val->GetAsString(&query_str) || (query_str != input_text) ||
initial.commit09911bf2008-07-26 23:55:29388 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
389 return false;
390
391 ListValue* description_list = NULL;
392 if (root_list->GetSize() > 2) {
393 // 3rd element: Description list.
394 Value* description_val;
395 if (root_list->Get(2, &description_val) &&
396 description_val->IsType(Value::TYPE_LIST))
397 description_list = static_cast<ListValue*>(description_val);
398 }
399
400 // We don't care about the query URL list (the fourth element in the
401 // response) for now.
402
403 // Parse optional data in the results from the Suggest server if any.
404 ListValue* type_list = NULL;
405 // 5th argument: Optional key-value pairs.
406 // TODO: We may iterate the 5th+ arguments of the root_list if any other
407 // optional data are defined.
408 if (root_list->GetSize() > 4) {
409 Value* optional_val;
410 if (root_list->Get(4, &optional_val) &&
411 optional_val->IsType(Value::TYPE_DICTIONARY)) {
412 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
413
414 // Parse Google Suggest specific type extension.
[email protected]8e50b602009-03-03 22:59:43415 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
initial.commit09911bf2008-07-26 23:55:29416 if (dict_val->HasKey(kGoogleSuggestType))
417 dict_val->GetList(kGoogleSuggestType, &type_list);
418 }
419 }
420
421 ListValue* result_list = static_cast<ListValue*>(result_val);
422 for (size_t i = 0; i < result_list->GetSize(); ++i) {
423 Value* suggestion_val;
424 std::wstring suggestion_str;
425 if (!result_list->Get(i, &suggestion_val) ||
426 !suggestion_val->GetAsString(&suggestion_str))
427 return false;
428
429 Value* type_val;
430 std::wstring type_str;
431 if (type_list && type_list->Get(i, &type_val) &&
432 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
433 Value* site_val;
434 std::wstring site_name;
[email protected]257ab712009-04-14 17:16:24435 NavigationResults& navigation_results =
436 is_keyword ? keyword_navigation_results_ :
437 default_navigation_results_;
438 if ((navigation_results.size() < max_matches()) &&
initial.commit09911bf2008-07-26 23:55:29439 description_list && description_list->Get(i, &site_val) &&
440 site_val->IsType(Value::TYPE_STRING) &&
441 site_val->GetAsString(&site_name)) {
[email protected]16afe222009-01-08 18:57:45442 // We can't blindly trust the URL coming from the server to be valid.
443 GURL result_url =
[email protected]1d73faa2009-02-24 19:32:15444 GURL(URLFixerUpper::FixupURL(WideToUTF8(suggestion_str),
445 std::string()));
[email protected]257ab712009-04-14 17:16:24446 if (result_url.is_valid())
447 navigation_results.push_back(NavigationResult(result_url, site_name));
initial.commit09911bf2008-07-26 23:55:29448 }
449 } else {
450 // TODO(kochi): Currently we treat a calculator result as a query, but it
451 // is better to have better presentation for caluculator results.
[email protected]257ab712009-04-14 17:16:24452 if (suggest_results->size() < max_matches())
453 suggest_results->push_back(suggestion_str);
initial.commit09911bf2008-07-26 23:55:29454 }
455 }
456
initial.commit09911bf2008-07-26 23:55:29457 return true;
458}
459
460void SearchProvider::ConvertResultsToAutocompleteMatches() {
461 // Convert all the results to matches and add them to a map, so we can keep
462 // the most relevant match for each result.
463 MatchMap map;
[email protected]257ab712009-04-14 17:16:24464 const Time no_time;
465 int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ?
initial.commit09911bf2008-07-26 23:55:29466 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
467 TemplateURLRef::NO_SUGGESTION_CHOSEN;
[email protected]257ab712009-04-14 17:16:24468 // Keyword what you typed results are handled by the KeywordProvider.
initial.commit09911bf2008-07-26 23:55:29469
[email protected]257ab712009-04-14 17:16:24470 int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
471 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
472 TemplateURLRef::NO_SUGGESTION_CHOSEN;
473 if (providers_.valid_default_provider()) {
474 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
475 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
476 did_not_accept_default_suggestion, false, &map);
initial.commit09911bf2008-07-26 23:55:29477 }
478
[email protected]257ab712009-04-14 17:16:24479 AddHistoryResultsToMap(keyword_history_results_, true,
480 did_not_accept_keyword_suggestion, &map);
481 AddHistoryResultsToMap(default_history_results_, false,
482 did_not_accept_default_suggestion, &map);
483
484 AddSuggestResultsToMap(keyword_suggest_results_, true,
485 did_not_accept_keyword_suggestion, &map);
486 AddSuggestResultsToMap(default_suggest_results_, false,
487 did_not_accept_default_suggestion, &map);
initial.commit09911bf2008-07-26 23:55:29488
489 // Now add the most relevant matches from the map to |matches_|.
490 matches_.clear();
491 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
492 matches_.push_back(i->second);
493
[email protected]257ab712009-04-14 17:16:24494 AddNavigationResultsToMatches(keyword_navigation_results_, true);
495 AddNavigationResultsToMatches(default_navigation_results_, false);
initial.commit09911bf2008-07-26 23:55:29496
497 const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed"
498 std::partial_sort(matches_.begin(),
499 matches_.begin() + std::min(max_total_matches, matches_.size()),
500 matches_.end(), &AutocompleteMatch::MoreRelevant);
501 if (matches_.size() > max_total_matches)
[email protected]a28e95662008-11-12 19:19:02502 matches_.erase(matches_.begin() + max_total_matches, matches_.end());
initial.commit09911bf2008-07-26 23:55:29503
[email protected]cc63dea2008-08-21 20:56:31504 UpdateStarredStateOfMatches();
505
[email protected]6c85aa02009-02-27 12:08:09506 // We're done when both asynchronous subcomponents have finished. We can't
507 // use CancelableRequestConsumer.HasPendingRequests() for history requests
508 // here. A pending request is not cleared until after the completion
509 // callback has returned, but we've reached here from inside that callback.
510 // HasPendingRequests() would therefore return true, and if this is the last
511 // thing left to calculate for this query, we'll never mark the query "done".
[email protected]257ab712009-04-14 17:16:24512 done_ = !history_request_pending_ && !suggest_results_pending_;
513}
514
515void SearchProvider::AddNavigationResultsToMatches(
516 const NavigationResults& navigation_results,
517 bool is_keyword) {
518 if (!navigation_results.empty()) {
519 // TODO(kochi): http://b/1170574 We add only one results for navigational
520 // suggestions. If we can get more useful information about the score,
521 // consider adding more results.
522 matches_.push_back(
523 NavigationToMatch(navigation_results.front(),
524 CalculateRelevanceForNavigation(0, is_keyword),
525 is_keyword));
526 }
527}
528
529void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results,
530 bool is_keyword,
531 int did_not_accept_suggestion,
532 MatchMap* map) {
533 for (HistoryResults::const_iterator i(results.begin()); i != results.end();
534 ++i) {
535 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time, is_keyword),
536 AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion,
537 is_keyword, map);
538 }
539}
540
541void SearchProvider::AddSuggestResultsToMap(
542 const SuggestResults& suggest_results,
543 bool is_keyword,
544 int did_not_accept_suggestion,
545 MatchMap* map) {
546 for (size_t i = 0; i < suggest_results.size(); ++i) {
547 AddMatchToMap(suggest_results[i],
548 CalculateRelevanceForSuggestion(suggest_results, i,
549 is_keyword),
550 AutocompleteMatch::SEARCH_SUGGEST,
551 static_cast<int>(i), is_keyword, map);
552 }
initial.commit09911bf2008-07-26 23:55:29553}
554
555int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
556 switch (input_.type()) {
557 case AutocompleteInput::UNKNOWN:
[email protected]257ab712009-04-14 17:16:24558 return providers_.valid_keyword_provider() ? 250 : 1300;
initial.commit09911bf2008-07-26 23:55:29559
560 case AutocompleteInput::REQUESTED_URL:
[email protected]257ab712009-04-14 17:16:24561 return providers_.valid_keyword_provider() ? 250 : 1200;
initial.commit09911bf2008-07-26 23:55:29562
563 case AutocompleteInput::URL:
[email protected]257ab712009-04-14 17:16:24564 return providers_.valid_keyword_provider() ? 250 : 850;
initial.commit09911bf2008-07-26 23:55:29565
566 case AutocompleteInput::QUERY:
[email protected]257ab712009-04-14 17:16:24567 return providers_.valid_keyword_provider() ? 250 : 1300;
initial.commit09911bf2008-07-26 23:55:29568
569 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24570 return providers_.valid_keyword_provider() ? 250 : 1500;
initial.commit09911bf2008-07-26 23:55:29571
572 default:
573 NOTREACHED();
574 return 0;
575 }
576}
577
[email protected]257ab712009-04-14 17:16:24578int SearchProvider::CalculateRelevanceForHistory(const Time& time,
579 bool is_keyword) const {
initial.commit09911bf2008-07-26 23:55:29580 // The relevance of past searches falls off over time. This curve is chosen
581 // so that the relevance of a search 15 minutes ago is discounted about 50
582 // points, while the relevance of a search two weeks ago is discounted about
583 // 450 points.
584 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
585 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
586
[email protected]6c85aa02009-02-27 12:08:09587 // Don't let scores go below 0. Negative relevance scores are meaningful in
588 // a different way.
initial.commit09911bf2008-07-26 23:55:29589 int base_score;
[email protected]257ab712009-04-14 17:16:24590 bool is_primary = providers_.is_primary_provider(is_keyword);
initial.commit09911bf2008-07-26 23:55:29591 switch (input_.type()) {
592 case AutocompleteInput::UNKNOWN:
593 case AutocompleteInput::REQUESTED_URL:
[email protected]257ab712009-04-14 17:16:24594 base_score = is_primary ? 1050 : 200;
initial.commit09911bf2008-07-26 23:55:29595 break;
596
597 case AutocompleteInput::URL:
[email protected]257ab712009-04-14 17:16:24598 base_score = is_primary ? 750 : 200;
initial.commit09911bf2008-07-26 23:55:29599 break;
600
601 case AutocompleteInput::QUERY:
602 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24603 base_score = is_primary ? 1250 : 200;
initial.commit09911bf2008-07-26 23:55:29604 break;
605
606 default:
607 NOTREACHED();
608 base_score = 0;
609 break;
610 }
611 return std::max(0, base_score - score_discount);
612}
613
614int SearchProvider::CalculateRelevanceForSuggestion(
[email protected]257ab712009-04-14 17:16:24615 const SuggestResults& suggest_results,
616 size_t suggestion_number,
617 bool is_keyword) const {
618 DCHECK(suggestion_number < suggest_results.size());
619 bool is_primary = providers_.is_primary_provider(is_keyword);
initial.commit09911bf2008-07-26 23:55:29620 const int suggestion_value =
[email protected]257ab712009-04-14 17:16:24621 static_cast<int>(suggest_results.size() - 1 - suggestion_number);
initial.commit09911bf2008-07-26 23:55:29622 switch (input_.type()) {
623 case AutocompleteInput::UNKNOWN:
624 case AutocompleteInput::REQUESTED_URL:
[email protected]257ab712009-04-14 17:16:24625 return suggestion_value + (is_primary ? 600 : 100);
initial.commit09911bf2008-07-26 23:55:29626
627 case AutocompleteInput::URL:
[email protected]257ab712009-04-14 17:16:24628 return suggestion_value + (is_primary ? 300 : 100);
initial.commit09911bf2008-07-26 23:55:29629
630 case AutocompleteInput::QUERY:
631 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24632 return suggestion_value + (is_primary ? 800 : 100);
initial.commit09911bf2008-07-26 23:55:29633
634 default:
635 NOTREACHED();
636 return 0;
637 }
638}
639
640int SearchProvider::CalculateRelevanceForNavigation(
[email protected]257ab712009-04-14 17:16:24641 size_t suggestion_number,
642 bool is_keyword) const {
643 DCHECK(
644 (is_keyword && suggestion_number < keyword_navigation_results_.size()) ||
645 (!is_keyword && suggestion_number < default_navigation_results_.size()));
initial.commit09911bf2008-07-26 23:55:29646 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
647 // server if possible.
[email protected]257ab712009-04-14 17:16:24648 bool is_primary = providers_.is_primary_provider(is_keyword);
initial.commit09911bf2008-07-26 23:55:29649 switch (input_.type()) {
650 case AutocompleteInput::QUERY:
651 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24652 return static_cast<int>(suggestion_number) + (is_primary ? 1000 : 150);
initial.commit09911bf2008-07-26 23:55:29653
654 default:
[email protected]257ab712009-04-14 17:16:24655 return static_cast<int>(suggestion_number) + (is_primary ? 800 : 150);
initial.commit09911bf2008-07-26 23:55:29656 }
657}
658
659void SearchProvider::AddMatchToMap(const std::wstring& query_string,
660 int relevance,
[email protected]4c1fb7ec2008-11-13 00:19:00661 AutocompleteMatch::Type type,
initial.commit09911bf2008-07-26 23:55:29662 int accepted_suggestion,
[email protected]257ab712009-04-14 17:16:24663 bool is_keyword,
initial.commit09911bf2008-07-26 23:55:29664 MatchMap* map) {
[email protected]257ab712009-04-14 17:16:24665 const std::wstring& input_text =
666 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00667 AutocompleteMatch match(this, relevance, false, type);
initial.commit09911bf2008-07-26 23:55:29668 std::vector<size_t> content_param_offsets;
[email protected]257ab712009-04-14 17:16:24669 const TemplateURL& provider = is_keyword ? providers_.keyword_provider() :
670 providers_.default_provider();
[email protected]fb5153c52009-07-31 19:40:33671 // We do intra-string highlighting for suggestions - the suggested segment
672 // will be highlighted, e.g. for input_text = "you" the suggestion may be
673 // "youtube", so we'll bold the "tube" section: you*tube*.
674 if (input_text != query_string) {
675 match.contents.assign(query_string);
676 size_t input_position = match.contents.find(input_text);
677 if (input_position == std::wstring::npos) {
678 // The input text is not a substring of the query string, e.g. input
679 // text is "slasdot" and the query string is "slashdot", so we bold the
680 // whole thing.
681 match.contents_class.push_back(
682 ACMatchClassification(0, ACMatchClassification::MATCH));
[email protected]ec2379162009-06-09 23:58:17683 } else {
[email protected]fb5153c52009-07-31 19:40:33684 // TODO(beng): ACMatchClassification::MATCH now seems to just mean
685 // "bold" this. Consider modifying the terminology.
686 // We don't iterate over the string here annotating all matches because
687 // it looks odd to have every occurrence of a substring that may be as
688 // short as a single character highlighted in a query suggestion result,
689 // e.g. for input text "s" and query string "southwest airlines", it
690 // looks odd if both the first and last s are highlighted.
691 if (input_position != 0) {
692 match.contents_class.push_back(
693 ACMatchClassification(0, ACMatchClassification::NONE));
694 }
695 match.contents_class.push_back(
696 ACMatchClassification(input_position, ACMatchClassification::DIM));
697 size_t next_fragment_position = input_position + input_text.length();
698 if (next_fragment_position < query_string.length()) {
699 match.contents_class.push_back(
700 ACMatchClassification(next_fragment_position,
701 ACMatchClassification::NONE));
702 }
[email protected]ec2379162009-06-09 23:58:17703 }
initial.commit09911bf2008-07-26 23:55:29704 } else {
[email protected]fb5153c52009-07-31 19:40:33705 // Otherwise, we're dealing with the "default search" result which has no
706 // completion, but has the search provider name as the description.
707 match.contents.assign(query_string);
708 match.contents_class.push_back(
709 ACMatchClassification(0, ACMatchClassification::NONE));
710 match.description.assign(l10n_util::GetStringF(
711 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
712 provider.short_name()));
713 match.description_class.push_back(
714 ACMatchClassification(0, ACMatchClassification::DIM));
initial.commit09911bf2008-07-26 23:55:29715 }
716
717 // When the user forced a query, we need to make sure all the fill_into_edit
718 // values preserve that property. Otherwise, if the user starts editing a
719 // suggestion, non-Search results will suddenly appear.
720 size_t search_start = 0;
721 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
722 match.fill_into_edit.assign(L"?");
723 ++search_start;
724 }
[email protected]c0048b42009-05-04 21:47:17725 if (is_keyword) {
726 match.fill_into_edit.append(providers_.keyword_provider().keyword() + L" ");
727 match.template_url = &providers_.keyword_provider();
728 }
initial.commit09911bf2008-07-26 23:55:29729 match.fill_into_edit.append(query_string);
730 // NOTE: All Google suggestions currently start with the original input, but
731 // not all Yahoo! suggestions do.
732 if (!input_.prevent_inline_autocomplete() &&
[email protected]257ab712009-04-14 17:16:24733 !match.fill_into_edit.compare(search_start, input_text.length(),
734 input_text))
735 match.inline_autocomplete_offset = search_start + input_text.length();
initial.commit09911bf2008-07-26 23:55:29736
[email protected]257ab712009-04-14 17:16:24737 const TemplateURLRef* const search_url = provider.url();
initial.commit09911bf2008-07-26 23:55:29738 DCHECK(search_url->SupportsReplacement());
[email protected]7b9f3672009-06-15 18:31:22739 match.destination_url =
740 GURL(WideToUTF8(search_url->ReplaceSearchTerms(provider, query_string,
741 accepted_suggestion,
742 input_text)));
initial.commit09911bf2008-07-26 23:55:29743
744 // Search results don't look like URLs.
[email protected]0bfc29a2009-04-27 16:15:44745 match.transition =
746 is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED;
initial.commit09911bf2008-07-26 23:55:29747
748 // Try to add |match| to |map|. If a match for |query_string| is already in
749 // |map|, replace it if |match| is more relevant.
750 // NOTE: Keep this ToLower() call in sync with url_database.cc.
751 const std::pair<MatchMap::iterator, bool> i = map->insert(
752 std::pair<std::wstring, AutocompleteMatch>(
753 l10n_util::ToLower(query_string), match));
754 // NOTE: We purposefully do a direct relevance comparison here instead of
755 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
756 // first" rather than "items alphabetically first" when the scores are equal.
757 // The only case this matters is when a user has results with the same score
758 // that differ only by capitalization; because the history system returns
759 // results sorted by recency, this means we'll pick the most recent such
760 // result even if the precision of our relevance score is too low to
761 // distinguish the two.
762 if (!i.second && (match.relevance > i.first->second.relevance))
763 i.first->second = match;
764}
765
766AutocompleteMatch SearchProvider::NavigationToMatch(
767 const NavigationResult& navigation,
[email protected]257ab712009-04-14 17:16:24768 int relevance,
769 bool is_keyword) {
770 const std::wstring& input_text =
771 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00772 AutocompleteMatch match(this, relevance, false,
773 AutocompleteMatch::NAVSUGGEST);
initial.commit09911bf2008-07-26 23:55:29774 match.destination_url = navigation.url;
[email protected]e7a5b7872008-12-10 23:52:43775 match.contents = StringForURLDisplay(navigation.url, true);
[email protected]45b2e16d2009-05-29 00:10:17776 if (!url_util::FindAndCompareScheme(WideToUTF8(input_text),
777 chrome::kHttpScheme, NULL))
initial.commit09911bf2008-07-26 23:55:29778 TrimHttpPrefix(&match.contents);
[email protected]257ab712009-04-14 17:16:24779 AutocompleteMatch::ClassifyMatchInString(input_text, match.contents,
initial.commit09911bf2008-07-26 23:55:29780 ACMatchClassification::URL,
781 &match.contents_class);
782
783 match.description = navigation.site_name;
[email protected]257ab712009-04-14 17:16:24784 AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name,
initial.commit09911bf2008-07-26 23:55:29785 ACMatchClassification::NONE,
786 &match.description_class);
787
initial.commit09911bf2008-07-26 23:55:29788 // When the user forced a query, we need to make sure all the fill_into_edit
789 // values preserve that property. Otherwise, if the user starts editing a
790 // suggestion, non-Search results will suddenly appear.
791 if (input_.type() == AutocompleteInput::FORCED_QUERY)
792 match.fill_into_edit.assign(L"?");
793 match.fill_into_edit.append(match.contents);
794 // TODO(pkasting): http://b/1112879 These should perhaps be
795 // inline-autocompletable?
796
797 return match;
798}