blob: 2683e2ca1481885e4ccb72625b23a2c61acc84b1 [file] [log] [blame]
[email protected]2c33dd22010-02-11 21:46:351// Copyright (c) 2010 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"
[email protected]2041cf342010-02-19 03:15:598#include "base/callback.h"
[email protected]d6e58c6e2009-10-10 20:40:509#include "base/i18n/icu_string_conversions.h"
initial.commit09911bf2008-07-26 23:55:2910#include "base/message_loop.h"
11#include "base/string_util.h"
[email protected]257ab712009-04-14 17:16:2412#include "chrome/browser/autocomplete/keyword_provider.h"
initial.commit09911bf2008-07-26 23:55:2913#include "chrome/browser/browser_process.h"
14#include "chrome/browser/google_util.h"
[email protected]ce560f82009-06-03 09:39:4415#include "chrome/browser/history/history.h"
[email protected]f870a322009-01-16 21:47:2716#include "chrome/browser/net/url_fixer_upper.h"
[email protected]052313b2010-02-19 09:43:0817#include "chrome/browser/pref_service.h"
initial.commit09911bf2008-07-26 23:55:2918#include "chrome/browser/profile.h"
[email protected]d54e03a52009-01-16 00:31:0419#include "chrome/browser/search_engines/template_url_model.h"
initial.commit09911bf2008-07-26 23:55:2920#include "chrome/common/json_value_serializer.h"
initial.commit09911bf2008-07-26 23:55:2921#include "chrome/common/pref_names.h"
[email protected]dcf7d352009-02-26 01:56:0222#include "chrome/common/url_constants.h"
initial.commit09911bf2008-07-26 23:55:2923#include "googleurl/src/url_util.h"
[email protected]34ac8f32009-02-22 23:03:2724#include "grit/generated_resources.h"
initial.commit09911bf2008-07-26 23:55:2925#include "net/base/escape.h"
[email protected]319d9e6f2009-02-18 19:47:2126#include "net/http/http_response_headers.h"
27#include "net/url_request/url_request_status.h"
initial.commit09911bf2008-07-26 23:55:2928
[email protected]e1acf6f2008-10-27 20:43:3329using base::Time;
30using base::TimeDelta;
31
[email protected]b547666d2009-04-23 16:37:5832// static
33const int SearchProvider::kDefaultProviderURLFetcherID = 1;
34// static
35const int SearchProvider::kKeywordProviderURLFetcherID = 2;
36
37// static
38bool SearchProvider::query_suggest_immediately_ = false;
39
[email protected]257ab712009-04-14 17:16:2440void SearchProvider::Providers::Set(const TemplateURL* default_provider,
41 const TemplateURL* keyword_provider) {
42 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy
43 // this. Nor should we need |default_provider_| and |keyword_provider_|
44 // just to know whether the provider changed.
45 default_provider_ = default_provider;
46 if (default_provider)
47 cached_default_provider_ = *default_provider;
48 keyword_provider_ = keyword_provider;
49 if (keyword_provider)
50 cached_keyword_provider_ = *keyword_provider;
51}
52
initial.commit09911bf2008-07-26 23:55:2953void SearchProvider::Start(const AutocompleteInput& input,
[email protected]8deeb952008-10-09 18:21:2754 bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:2955 matches_.clear();
56
[email protected]6c85aa02009-02-27 12:08:0957 // Can't return search/suggest results for bogus input or without a profile.
initial.commit09911bf2008-07-26 23:55:2958 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
59 Stop();
60 return;
61 }
62
[email protected]257ab712009-04-14 17:16:2463 keyword_input_text_.clear();
64 const TemplateURL* keyword_provider =
65 KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input,
66 &keyword_input_text_);
67 if (!TemplateURL::SupportsReplacement(keyword_provider) ||
68 keyword_input_text_.empty()) {
69 keyword_provider = NULL;
70 }
71
72 const TemplateURL* default_provider =
initial.commit09911bf2008-07-26 23:55:2973 profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
[email protected]257ab712009-04-14 17:16:2474 if (!TemplateURL::SupportsReplacement(default_provider))
75 default_provider = NULL;
76
77 if (keyword_provider == default_provider)
78 keyword_provider = NULL; // No use in querying the same provider twice.
79
80 if (!default_provider && !keyword_provider) {
81 // No valid providers.
initial.commit09911bf2008-07-26 23:55:2982 Stop();
83 return;
84 }
85
86 // If we're still running an old query but have since changed the query text
[email protected]257ab712009-04-14 17:16:2487 // or the providers, abort the query.
initial.commit09911bf2008-07-26 23:55:2988 if (!done_ && (!minimal_changes ||
[email protected]257ab712009-04-14 17:16:2489 !providers_.equals(default_provider, keyword_provider))) {
initial.commit09911bf2008-07-26 23:55:2990 Stop();
[email protected]257ab712009-04-14 17:16:2491 }
initial.commit09911bf2008-07-26 23:55:2992
[email protected]257ab712009-04-14 17:16:2493 providers_.Set(default_provider, keyword_provider);
initial.commit09911bf2008-07-26 23:55:2994
95 if (input.text().empty()) {
96 // User typed "?" alone. Give them a placeholder result indicating what
97 // this syntax does.
[email protected]257ab712009-04-14 17:16:2498 if (default_provider) {
99 AutocompleteMatch match(this, 0, false,
100 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED);
[email protected]2c33dd22010-02-11 21:46:35101 match.contents.assign(l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE));
[email protected]257ab712009-04-14 17:16:24102 match.contents_class.push_back(
[email protected]2c33dd22010-02-11 21:46:35103 ACMatchClassification(0, ACMatchClassification::NONE));
104 match.description.assign(l10n_util::GetStringF(
105 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
106 default_provider->AdjustedShortNameForLocaleDirection()));
107 match.description_class.push_back(
[email protected]257ab712009-04-14 17:16:24108 ACMatchClassification(0, ACMatchClassification::DIM));
109 matches_.push_back(match);
110 }
initial.commit09911bf2008-07-26 23:55:29111 Stop();
112 return;
113 }
114
115 input_ = input;
116
[email protected]8deeb952008-10-09 18:21:27117 StartOrStopHistoryQuery(minimal_changes);
118 StartOrStopSuggestQuery(minimal_changes);
initial.commit09911bf2008-07-26 23:55:29119 ConvertResultsToAutocompleteMatches();
120}
121
122void SearchProvider::Run() {
123 // Start a new request with the current input.
124 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24125 suggest_results_pending_ = 0;
126 if (providers_.valid_suggest_for_keyword_provider()) {
127 suggest_results_pending_++;
128 keyword_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58129 CreateSuggestFetcher(kKeywordProviderURLFetcherID,
130 providers_.keyword_provider(),
[email protected]257ab712009-04-14 17:16:24131 keyword_input_text_));
132 }
133 if (providers_.valid_suggest_for_default_provider()) {
134 suggest_results_pending_++;
135 default_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58136 CreateSuggestFetcher(kDefaultProviderURLFetcherID,
137 providers_.default_provider(), input_.text()));
[email protected]257ab712009-04-14 17:16:24138 }
139 // We should only get here if we have a suggest url for the keyword or default
140 // providers.
141 DCHECK(suggest_results_pending_ > 0);
initial.commit09911bf2008-07-26 23:55:29142}
143
144void SearchProvider::Stop() {
145 StopHistory();
146 StopSuggest();
147 done_ = true;
148}
149
150void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
151 const GURL& url,
152 const URLRequestStatus& status,
153 int response_code,
154 const ResponseCookies& cookie,
155 const std::string& data) {
156 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24157 suggest_results_pending_--;
158 DCHECK(suggest_results_pending_ >= 0); // Should never go negative.
[email protected]ec9207d32008-09-26 00:51:06159 const net::HttpResponseHeaders* const response_headers =
160 source->response_headers();
161 std::string json_data(data);
[email protected]6c85aa02009-02-27 12:08:09162 // JSON is supposed to be UTF-8, but some suggest service providers send JSON
163 // files in non-UTF-8 encodings. The actual encoding is usually specified in
164 // the Content-Type header field.
[email protected]ec9207d32008-09-26 00:51:06165 if (response_headers) {
166 std::string charset;
167 if (response_headers->GetCharset(&charset)) {
168 std::wstring wide_data;
169 // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
[email protected]d6e58c6e2009-10-10 20:40:50170 if (base::CodepageToWide(data, charset.c_str(),
171 base::OnStringConversionError::FAIL,
172 &wide_data))
[email protected]f0a51fb52009-03-05 12:46:38173 json_data = WideToUTF8(wide_data);
[email protected]ec9207d32008-09-26 00:51:06174 }
175 }
176
[email protected]257ab712009-04-14 17:16:24177 bool is_keyword_results = (source == keyword_fetcher_.get());
178 SuggestResults* suggest_results = is_keyword_results ?
179 &keyword_suggest_results_ : &default_suggest_results_;
180
[email protected]b4cebf82008-12-29 19:59:08181 if (status.is_success() && response_code == 200) {
182 JSONStringValueSerializer deserializer(json_data);
183 deserializer.set_allow_trailing_comma(true);
184 scoped_ptr<Value> root_val(deserializer.Deserialize(NULL));
[email protected]257ab712009-04-14 17:16:24185 const std::wstring& input_text =
186 is_keyword_results ? keyword_input_text_ : input_.text();
[email protected]b4cebf82008-12-29 19:59:08187 have_suggest_results_ =
[email protected]257ab712009-04-14 17:16:24188 root_val.get() &&
189 ParseSuggestResults(root_val.get(), is_keyword_results, input_text,
190 suggest_results);
[email protected]b4cebf82008-12-29 19:59:08191 }
192
initial.commit09911bf2008-07-26 23:55:29193 ConvertResultsToAutocompleteMatches();
[email protected]257ab712009-04-14 17:16:24194 listener_->OnProviderUpdate(!suggest_results->empty());
initial.commit09911bf2008-07-26 23:55:29195}
196
[email protected]8deeb952008-10-09 18:21:27197void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:29198 // For the minimal_changes case, if we finished the previous query and still
199 // have its results, or are allowed to keep running it, just do that, rather
200 // than starting a new query.
201 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27202 (have_history_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29203 return;
204
205 // We can't keep running any previous query, so halt it.
206 StopHistory();
207
208 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27209 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29210 return;
211
[email protected]257ab712009-04-14 17:16:24212 // Request history for both the keyword and default provider.
213 if (providers_.valid_keyword_provider()) {
214 ScheduleHistoryQuery(providers_.keyword_provider().id(),
215 keyword_input_text_);
216 }
217 if (providers_.valid_default_provider()) {
218 ScheduleHistoryQuery(providers_.default_provider().id(),
219 input_.text());
220 }
initial.commit09911bf2008-07-26 23:55:29221}
222
[email protected]8deeb952008-10-09 18:21:27223void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) {
[email protected]6c85aa02009-02-27 12:08:09224 // Don't send any queries to the server until some time has elapsed after
225 // the last keypress, to avoid flooding the server with requests we are
226 // likely to end up throwing away anyway.
227 static const int kQueryDelayMs = 200;
228
[email protected]83c726482008-09-10 06:36:34229 if (!IsQuerySuitableForSuggest()) {
initial.commit09911bf2008-07-26 23:55:29230 StopSuggest();
231 return;
232 }
233
234 // For the minimal_changes case, if we finished the previous query and still
235 // have its results, or are allowed to keep running it, just do that, rather
236 // than starting a new query.
237 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27238 (have_suggest_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29239 return;
240
241 // We can't keep running any previous query, so halt it.
242 StopSuggest();
243
244 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27245 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29246 return;
247
[email protected]257ab712009-04-14 17:16:24248 // We'll have at least one pending fetch. Set it to 1 now, but the value is
249 // correctly set in Run. As Run isn't invoked immediately we need to set this
250 // now, else we won't think we're waiting on results from the server when we
251 // really are.
252 suggest_results_pending_ = 1;
253
initial.commit09911bf2008-07-26 23:55:29254 // Kick off a timer that will start the URL fetch if it completes before
255 // the user types another character.
[email protected]b547666d2009-04-23 16:37:58256 int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs;
257 timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run);
initial.commit09911bf2008-07-26 23:55:29258}
259
[email protected]83c726482008-09-10 06:36:34260bool SearchProvider::IsQuerySuitableForSuggest() const {
261 // Don't run Suggest when off the record, the engine doesn't support it, or
262 // the user has disabled it.
263 if (profile_->IsOffTheRecord() ||
[email protected]257ab712009-04-14 17:16:24264 (!providers_.valid_suggest_for_keyword_provider() &&
265 !providers_.valid_suggest_for_default_provider()) ||
[email protected]83c726482008-09-10 06:36:34266 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
267 return false;
268
269 // If the input type is URL, we take extra care so that private data in URL
270 // isn't sent to the server.
271 if (input_.type() == AutocompleteInput::URL) {
272 // Don't query the server for URLs that aren't http/https/ftp. Sending
273 // things like file: and data: is both a waste of time and a disclosure of
274 // potentially private, local data.
275 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") &&
276 (input_.scheme() != L"ftp"))
277 return false;
278
279 // Don't leak private data in URL
280 const url_parse::Parsed& parts = input_.parts();
281
282 // Don't send URLs with usernames, queries or refs. Some of these are
283 // private, and the Suggest server is unlikely to have any useful results
284 // for any of them.
285 // Password is optional and may be omitted. Checking username is
286 // sufficient.
287 if (parts.username.is_nonempty() || parts.query.is_nonempty() ||
288 parts.ref.is_nonempty())
289 return false;
290 // Don't send anything for https except hostname and port number.
291 // Hostname and port number are OK because they are visible when TCP
292 // connection is established and the Suggest server may provide some
293 // useful completed URL.
294 if (input_.scheme() == L"https" && parts.path.is_nonempty())
295 return false;
296 }
297
298 return true;
299}
300
initial.commit09911bf2008-07-26 23:55:29301void SearchProvider::StopHistory() {
302 history_request_consumer_.CancelAllRequests();
303 history_request_pending_ = false;
[email protected]257ab712009-04-14 17:16:24304 keyword_history_results_.clear();
305 default_history_results_.clear();
initial.commit09911bf2008-07-26 23:55:29306 have_history_results_ = false;
307}
308
309void SearchProvider::StopSuggest() {
[email protected]257ab712009-04-14 17:16:24310 suggest_results_pending_ = 0;
[email protected]2d316662008-09-03 18:18:14311 timer_.Stop();
[email protected]257ab712009-04-14 17:16:24312 // Stop any in-progress URL fetches.
313 keyword_fetcher_.reset();
314 default_fetcher_.reset();
315 keyword_suggest_results_.clear();
316 default_suggest_results_.clear();
317 keyword_navigation_results_.clear();
318 default_navigation_results_.clear();
initial.commit09911bf2008-07-26 23:55:29319 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29320}
321
[email protected]257ab712009-04-14 17:16:24322void SearchProvider::ScheduleHistoryQuery(TemplateURL::IDType search_id,
323 const std::wstring& text) {
324 DCHECK(!text.empty());
325 HistoryService* const history_service =
326 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
327 HistoryService::Handle request_handle =
328 history_service->GetMostRecentKeywordSearchTerms(
329 search_id, text, static_cast<int>(max_matches()),
330 &history_request_consumer_,
331 NewCallback(this,
332 &SearchProvider::OnGotMostRecentKeywordSearchTerms));
333 history_request_consumer_.SetClientData(history_service, request_handle,
334 search_id);
335 history_request_pending_ = true;
336}
337
initial.commit09911bf2008-07-26 23:55:29338void SearchProvider::OnGotMostRecentKeywordSearchTerms(
339 CancelableRequestProvider::Handle handle,
340 HistoryResults* results) {
[email protected]257ab712009-04-14 17:16:24341 HistoryService* history_service =
342 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
343 DCHECK(history_service);
344 if (providers_.valid_keyword_provider() &&
[email protected]bed9bd6c2009-04-21 17:27:47345 (providers_.keyword_provider().id() ==
[email protected]257ab712009-04-14 17:16:24346 history_request_consumer_.GetClientData(history_service, handle))) {
347 keyword_history_results_ = *results;
348 } else {
349 default_history_results_ = *results;
350 }
[email protected]257ab712009-04-14 17:16:24351
352 if (history_request_consumer_.PendingRequestCount() == 1) {
353 // Requests are removed AFTER the callback is invoked. If the count == 1,
354 // it means no more history requests are pending.
355 history_request_pending_ = false;
356 have_history_results_ = true;
357 }
[email protected]b547666d2009-04-23 16:37:58358
359 ConvertResultsToAutocompleteMatches();
360 listener_->OnProviderUpdate(!results->empty());
initial.commit09911bf2008-07-26 23:55:29361}
362
[email protected]b547666d2009-04-23 16:37:58363URLFetcher* SearchProvider::CreateSuggestFetcher(int id,
364 const TemplateURL& provider,
[email protected]257ab712009-04-14 17:16:24365 const std::wstring& text) {
366 const TemplateURLRef* const suggestions_url = provider.suggestions_url();
367 DCHECK(suggestions_url->SupportsReplacement());
[email protected]b547666d2009-04-23 16:37:58368 URLFetcher* fetcher = URLFetcher::Create(id,
[email protected]7b9f3672009-06-15 18:31:22369 GURL(WideToUTF8(suggestions_url->ReplaceSearchTerms(
[email protected]b547666d2009-04-23 16:37:58370 provider, text, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE,
[email protected]7b9f3672009-06-15 18:31:22371 std::wstring()))),
[email protected]257ab712009-04-14 17:16:24372 URLFetcher::GET, this);
373 fetcher->set_request_context(profile_->GetRequestContext());
374 fetcher->Start();
375 return fetcher;
376}
377
378bool SearchProvider::ParseSuggestResults(Value* root_val,
379 bool is_keyword,
380 const std::wstring& input_text,
381 SuggestResults* suggest_results) {
initial.commit09911bf2008-07-26 23:55:29382 if (!root_val->IsType(Value::TYPE_LIST))
383 return false;
384 ListValue* root_list = static_cast<ListValue*>(root_val);
385
386 Value* query_val;
387 std::wstring query_str;
388 Value* result_val;
389 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
[email protected]257ab712009-04-14 17:16:24390 !query_val->GetAsString(&query_str) || (query_str != input_text) ||
initial.commit09911bf2008-07-26 23:55:29391 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
392 return false;
393
394 ListValue* description_list = NULL;
395 if (root_list->GetSize() > 2) {
396 // 3rd element: Description list.
397 Value* description_val;
398 if (root_list->Get(2, &description_val) &&
399 description_val->IsType(Value::TYPE_LIST))
400 description_list = static_cast<ListValue*>(description_val);
401 }
402
403 // We don't care about the query URL list (the fourth element in the
404 // response) for now.
405
406 // Parse optional data in the results from the Suggest server if any.
407 ListValue* type_list = NULL;
408 // 5th argument: Optional key-value pairs.
409 // TODO: We may iterate the 5th+ arguments of the root_list if any other
410 // optional data are defined.
411 if (root_list->GetSize() > 4) {
412 Value* optional_val;
413 if (root_list->Get(4, &optional_val) &&
414 optional_val->IsType(Value::TYPE_DICTIONARY)) {
415 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
416
417 // Parse Google Suggest specific type extension.
[email protected]8e50b602009-03-03 22:59:43418 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
initial.commit09911bf2008-07-26 23:55:29419 if (dict_val->HasKey(kGoogleSuggestType))
420 dict_val->GetList(kGoogleSuggestType, &type_list);
421 }
422 }
423
424 ListValue* result_list = static_cast<ListValue*>(result_val);
425 for (size_t i = 0; i < result_list->GetSize(); ++i) {
426 Value* suggestion_val;
427 std::wstring suggestion_str;
428 if (!result_list->Get(i, &suggestion_val) ||
429 !suggestion_val->GetAsString(&suggestion_str))
430 return false;
431
432 Value* type_val;
433 std::wstring type_str;
434 if (type_list && type_list->Get(i, &type_val) &&
435 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
436 Value* site_val;
437 std::wstring site_name;
[email protected]257ab712009-04-14 17:16:24438 NavigationResults& navigation_results =
439 is_keyword ? keyword_navigation_results_ :
440 default_navigation_results_;
441 if ((navigation_results.size() < max_matches()) &&
initial.commit09911bf2008-07-26 23:55:29442 description_list && description_list->Get(i, &site_val) &&
443 site_val->IsType(Value::TYPE_STRING) &&
444 site_val->GetAsString(&site_name)) {
[email protected]16afe222009-01-08 18:57:45445 // We can't blindly trust the URL coming from the server to be valid.
446 GURL result_url =
[email protected]1d73faa2009-02-24 19:32:15447 GURL(URLFixerUpper::FixupURL(WideToUTF8(suggestion_str),
448 std::string()));
[email protected]257ab712009-04-14 17:16:24449 if (result_url.is_valid())
450 navigation_results.push_back(NavigationResult(result_url, site_name));
initial.commit09911bf2008-07-26 23:55:29451 }
452 } else {
453 // TODO(kochi): Currently we treat a calculator result as a query, but it
454 // is better to have better presentation for caluculator results.
[email protected]257ab712009-04-14 17:16:24455 if (suggest_results->size() < max_matches())
456 suggest_results->push_back(suggestion_str);
initial.commit09911bf2008-07-26 23:55:29457 }
458 }
459
initial.commit09911bf2008-07-26 23:55:29460 return true;
461}
462
463void SearchProvider::ConvertResultsToAutocompleteMatches() {
464 // Convert all the results to matches and add them to a map, so we can keep
465 // the most relevant match for each result.
466 MatchMap map;
[email protected]257ab712009-04-14 17:16:24467 const Time no_time;
468 int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ?
initial.commit09911bf2008-07-26 23:55:29469 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
470 TemplateURLRef::NO_SUGGESTION_CHOSEN;
[email protected]257ab712009-04-14 17:16:24471 // Keyword what you typed results are handled by the KeywordProvider.
initial.commit09911bf2008-07-26 23:55:29472
[email protected]257ab712009-04-14 17:16:24473 int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
474 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
475 TemplateURLRef::NO_SUGGESTION_CHOSEN;
476 if (providers_.valid_default_provider()) {
477 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
478 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
479 did_not_accept_default_suggestion, false, &map);
initial.commit09911bf2008-07-26 23:55:29480 }
481
[email protected]257ab712009-04-14 17:16:24482 AddHistoryResultsToMap(keyword_history_results_, true,
483 did_not_accept_keyword_suggestion, &map);
484 AddHistoryResultsToMap(default_history_results_, false,
485 did_not_accept_default_suggestion, &map);
486
487 AddSuggestResultsToMap(keyword_suggest_results_, true,
488 did_not_accept_keyword_suggestion, &map);
489 AddSuggestResultsToMap(default_suggest_results_, false,
490 did_not_accept_default_suggestion, &map);
initial.commit09911bf2008-07-26 23:55:29491
492 // Now add the most relevant matches from the map to |matches_|.
493 matches_.clear();
494 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
495 matches_.push_back(i->second);
496
[email protected]257ab712009-04-14 17:16:24497 AddNavigationResultsToMatches(keyword_navigation_results_, true);
498 AddNavigationResultsToMatches(default_navigation_results_, false);
initial.commit09911bf2008-07-26 23:55:29499
500 const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed"
501 std::partial_sort(matches_.begin(),
502 matches_.begin() + std::min(max_total_matches, matches_.size()),
503 matches_.end(), &AutocompleteMatch::MoreRelevant);
504 if (matches_.size() > max_total_matches)
[email protected]a28e95662008-11-12 19:19:02505 matches_.erase(matches_.begin() + max_total_matches, matches_.end());
initial.commit09911bf2008-07-26 23:55:29506
[email protected]cc63dea2008-08-21 20:56:31507 UpdateStarredStateOfMatches();
508
[email protected]6c85aa02009-02-27 12:08:09509 // We're done when both asynchronous subcomponents have finished. We can't
510 // use CancelableRequestConsumer.HasPendingRequests() for history requests
511 // here. A pending request is not cleared until after the completion
512 // callback has returned, but we've reached here from inside that callback.
513 // HasPendingRequests() would therefore return true, and if this is the last
514 // thing left to calculate for this query, we'll never mark the query "done".
[email protected]257ab712009-04-14 17:16:24515 done_ = !history_request_pending_ && !suggest_results_pending_;
516}
517
518void SearchProvider::AddNavigationResultsToMatches(
519 const NavigationResults& navigation_results,
520 bool is_keyword) {
521 if (!navigation_results.empty()) {
522 // TODO(kochi): http://b/1170574 We add only one results for navigational
523 // suggestions. If we can get more useful information about the score,
524 // consider adding more results.
[email protected]52d08b12009-10-19 18:42:36525 const size_t num_results = is_keyword ?
526 keyword_navigation_results_.size() : default_navigation_results_.size();
527 matches_.push_back(NavigationToMatch(navigation_results.front(),
528 CalculateRelevanceForNavigation(num_results, 0, is_keyword),
529 is_keyword));
[email protected]257ab712009-04-14 17:16:24530 }
531}
532
533void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results,
534 bool is_keyword,
535 int did_not_accept_suggestion,
536 MatchMap* map) {
537 for (HistoryResults::const_iterator i(results.begin()); i != results.end();
538 ++i) {
539 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time, is_keyword),
540 AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion,
541 is_keyword, map);
542 }
543}
544
545void SearchProvider::AddSuggestResultsToMap(
546 const SuggestResults& suggest_results,
547 bool is_keyword,
548 int did_not_accept_suggestion,
549 MatchMap* map) {
550 for (size_t i = 0; i < suggest_results.size(); ++i) {
551 AddMatchToMap(suggest_results[i],
[email protected]52d08b12009-10-19 18:42:36552 CalculateRelevanceForSuggestion(suggest_results.size(), i,
[email protected]257ab712009-04-14 17:16:24553 is_keyword),
554 AutocompleteMatch::SEARCH_SUGGEST,
555 static_cast<int>(i), is_keyword, map);
556 }
initial.commit09911bf2008-07-26 23:55:29557}
558
559int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
[email protected]52d08b12009-10-19 18:42:36560 if (providers_.valid_keyword_provider())
561 return 250;
562
initial.commit09911bf2008-07-26 23:55:29563 switch (input_.type()) {
564 case AutocompleteInput::UNKNOWN:
[email protected]52d08b12009-10-19 18:42:36565 case AutocompleteInput::QUERY:
566 case AutocompleteInput::FORCED_QUERY:
567 return 1300;
initial.commit09911bf2008-07-26 23:55:29568
569 case AutocompleteInput::REQUESTED_URL:
[email protected]52d08b12009-10-19 18:42:36570 return 1150;
initial.commit09911bf2008-07-26 23:55:29571
572 case AutocompleteInput::URL:
[email protected]52d08b12009-10-19 18:42:36573 return 850;
initial.commit09911bf2008-07-26 23:55:29574
575 default:
576 NOTREACHED();
577 return 0;
578 }
579}
580
[email protected]257ab712009-04-14 17:16:24581int SearchProvider::CalculateRelevanceForHistory(const Time& time,
582 bool is_keyword) const {
initial.commit09911bf2008-07-26 23:55:29583 // The relevance of past searches falls off over time. This curve is chosen
584 // so that the relevance of a search 15 minutes ago is discounted about 50
585 // points, while the relevance of a search two weeks ago is discounted about
586 // 450 points.
587 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
588 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
589
[email protected]6c85aa02009-02-27 12:08:09590 // Don't let scores go below 0. Negative relevance scores are meaningful in
591 // a different way.
initial.commit09911bf2008-07-26 23:55:29592 int base_score;
[email protected]52d08b12009-10-19 18:42:36593 if (!providers_.is_primary_provider(is_keyword))
594 base_score = 200;
595 else
596 base_score = (input_.type() == AutocompleteInput::URL) ? 750 : 1050;
initial.commit09911bf2008-07-26 23:55:29597 return std::max(0, base_score - score_discount);
598}
599
[email protected]52d08b12009-10-19 18:42:36600int SearchProvider::CalculateRelevanceForSuggestion(size_t num_results,
601 size_t result_number,
602 bool is_keyword) const {
603 DCHECK(result_number < num_results);
604 int base_score;
605 if (!providers_.is_primary_provider(is_keyword))
606 base_score = 100;
607 else
608 base_score = (input_.type() == AutocompleteInput::URL) ? 300 : 600;
609 return base_score +
610 static_cast<int>(num_results - 1 - result_number);
initial.commit09911bf2008-07-26 23:55:29611}
612
[email protected]52d08b12009-10-19 18:42:36613int SearchProvider::CalculateRelevanceForNavigation(size_t num_results,
614 size_t result_number,
615 bool is_keyword) const {
616 DCHECK(result_number < num_results);
initial.commit09911bf2008-07-26 23:55:29617 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
618 // server if possible.
[email protected]52d08b12009-10-19 18:42:36619 return (providers_.is_primary_provider(is_keyword) ? 800 : 150) +
620 static_cast<int>(num_results - 1 - result_number);
initial.commit09911bf2008-07-26 23:55:29621}
622
623void SearchProvider::AddMatchToMap(const std::wstring& query_string,
624 int relevance,
[email protected]4c1fb7ec2008-11-13 00:19:00625 AutocompleteMatch::Type type,
initial.commit09911bf2008-07-26 23:55:29626 int accepted_suggestion,
[email protected]257ab712009-04-14 17:16:24627 bool is_keyword,
initial.commit09911bf2008-07-26 23:55:29628 MatchMap* map) {
[email protected]257ab712009-04-14 17:16:24629 const std::wstring& input_text =
630 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00631 AutocompleteMatch match(this, relevance, false, type);
initial.commit09911bf2008-07-26 23:55:29632 std::vector<size_t> content_param_offsets;
[email protected]257ab712009-04-14 17:16:24633 const TemplateURL& provider = is_keyword ? providers_.keyword_provider() :
634 providers_.default_provider();
[email protected]fb5153c52009-07-31 19:40:33635 // We do intra-string highlighting for suggestions - the suggested segment
636 // will be highlighted, e.g. for input_text = "you" the suggestion may be
637 // "youtube", so we'll bold the "tube" section: you*tube*.
638 if (input_text != query_string) {
639 match.contents.assign(query_string);
640 size_t input_position = match.contents.find(input_text);
641 if (input_position == std::wstring::npos) {
642 // The input text is not a substring of the query string, e.g. input
643 // text is "slasdot" and the query string is "slashdot", so we bold the
644 // whole thing.
645 match.contents_class.push_back(
646 ACMatchClassification(0, ACMatchClassification::MATCH));
[email protected]ec2379162009-06-09 23:58:17647 } else {
[email protected]fb5153c52009-07-31 19:40:33648 // TODO(beng): ACMatchClassification::MATCH now seems to just mean
649 // "bold" this. Consider modifying the terminology.
650 // We don't iterate over the string here annotating all matches because
651 // it looks odd to have every occurrence of a substring that may be as
652 // short as a single character highlighted in a query suggestion result,
653 // e.g. for input text "s" and query string "southwest airlines", it
654 // looks odd if both the first and last s are highlighted.
655 if (input_position != 0) {
656 match.contents_class.push_back(
657 ACMatchClassification(0, ACMatchClassification::NONE));
658 }
659 match.contents_class.push_back(
660 ACMatchClassification(input_position, ACMatchClassification::DIM));
661 size_t next_fragment_position = input_position + input_text.length();
662 if (next_fragment_position < query_string.length()) {
663 match.contents_class.push_back(
664 ACMatchClassification(next_fragment_position,
665 ACMatchClassification::NONE));
666 }
[email protected]ec2379162009-06-09 23:58:17667 }
initial.commit09911bf2008-07-26 23:55:29668 } else {
[email protected]fb5153c52009-07-31 19:40:33669 // Otherwise, we're dealing with the "default search" result which has no
670 // completion, but has the search provider name as the description.
671 match.contents.assign(query_string);
672 match.contents_class.push_back(
673 ACMatchClassification(0, ACMatchClassification::NONE));
674 match.description.assign(l10n_util::GetStringF(
675 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
[email protected]2c33dd22010-02-11 21:46:35676 provider.AdjustedShortNameForLocaleDirection()));
[email protected]fb5153c52009-07-31 19:40:33677 match.description_class.push_back(
678 ACMatchClassification(0, ACMatchClassification::DIM));
initial.commit09911bf2008-07-26 23:55:29679 }
680
681 // When the user forced a query, we need to make sure all the fill_into_edit
682 // values preserve that property. Otherwise, if the user starts editing a
683 // suggestion, non-Search results will suddenly appear.
684 size_t search_start = 0;
685 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
686 match.fill_into_edit.assign(L"?");
687 ++search_start;
688 }
[email protected]c0048b42009-05-04 21:47:17689 if (is_keyword) {
690 match.fill_into_edit.append(providers_.keyword_provider().keyword() + L" ");
691 match.template_url = &providers_.keyword_provider();
692 }
initial.commit09911bf2008-07-26 23:55:29693 match.fill_into_edit.append(query_string);
[email protected]2c33dd22010-02-11 21:46:35694 // Not all suggestions start with the original input.
initial.commit09911bf2008-07-26 23:55:29695 if (!input_.prevent_inline_autocomplete() &&
[email protected]257ab712009-04-14 17:16:24696 !match.fill_into_edit.compare(search_start, input_text.length(),
697 input_text))
698 match.inline_autocomplete_offset = search_start + input_text.length();
initial.commit09911bf2008-07-26 23:55:29699
[email protected]257ab712009-04-14 17:16:24700 const TemplateURLRef* const search_url = provider.url();
initial.commit09911bf2008-07-26 23:55:29701 DCHECK(search_url->SupportsReplacement());
[email protected]7b9f3672009-06-15 18:31:22702 match.destination_url =
703 GURL(WideToUTF8(search_url->ReplaceSearchTerms(provider, query_string,
704 accepted_suggestion,
705 input_text)));
initial.commit09911bf2008-07-26 23:55:29706
707 // Search results don't look like URLs.
[email protected]0bfc29a2009-04-27 16:15:44708 match.transition =
709 is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED;
initial.commit09911bf2008-07-26 23:55:29710
711 // Try to add |match| to |map|. If a match for |query_string| is already in
712 // |map|, replace it if |match| is more relevant.
713 // NOTE: Keep this ToLower() call in sync with url_database.cc.
714 const std::pair<MatchMap::iterator, bool> i = map->insert(
715 std::pair<std::wstring, AutocompleteMatch>(
716 l10n_util::ToLower(query_string), match));
717 // NOTE: We purposefully do a direct relevance comparison here instead of
718 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
719 // first" rather than "items alphabetically first" when the scores are equal.
720 // The only case this matters is when a user has results with the same score
721 // that differ only by capitalization; because the history system returns
722 // results sorted by recency, this means we'll pick the most recent such
723 // result even if the precision of our relevance score is too low to
724 // distinguish the two.
725 if (!i.second && (match.relevance > i.first->second.relevance))
726 i.first->second = match;
727}
728
729AutocompleteMatch SearchProvider::NavigationToMatch(
730 const NavigationResult& navigation,
[email protected]257ab712009-04-14 17:16:24731 int relevance,
732 bool is_keyword) {
733 const std::wstring& input_text =
734 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00735 AutocompleteMatch match(this, relevance, false,
736 AutocompleteMatch::NAVSUGGEST);
initial.commit09911bf2008-07-26 23:55:29737 match.destination_url = navigation.url;
[email protected]e7a5b7872008-12-10 23:52:43738 match.contents = StringForURLDisplay(navigation.url, true);
[email protected]45b2e16d2009-05-29 00:10:17739 if (!url_util::FindAndCompareScheme(WideToUTF8(input_text),
740 chrome::kHttpScheme, NULL))
initial.commit09911bf2008-07-26 23:55:29741 TrimHttpPrefix(&match.contents);
[email protected]257ab712009-04-14 17:16:24742 AutocompleteMatch::ClassifyMatchInString(input_text, match.contents,
initial.commit09911bf2008-07-26 23:55:29743 ACMatchClassification::URL,
744 &match.contents_class);
745
746 match.description = navigation.site_name;
[email protected]257ab712009-04-14 17:16:24747 AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name,
initial.commit09911bf2008-07-26 23:55:29748 ACMatchClassification::NONE,
749 &match.description_class);
750
initial.commit09911bf2008-07-26 23:55:29751 // When the user forced a query, we need to make sure all the fill_into_edit
752 // values preserve that property. Otherwise, if the user starts editing a
753 // suggestion, non-Search results will suddenly appear.
754 if (input_.type() == AutocompleteInput::FORCED_QUERY)
755 match.fill_into_edit.assign(L"?");
756 match.fill_into_edit.append(match.contents);
757 // TODO(pkasting): http://b/1112879 These should perhaps be
758 // inline-autocompletable?
759
760 return match;
761}