blob: ba550a6d911d89d20e86f5a8a1fce695b8253681 [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]1cb2dac2010-03-08 21:49:157#include <algorithm>
8
[email protected]a92b8642009-05-05 23:38:569#include "app/l10n_util.h"
[email protected]2041cf342010-02-19 03:15:5910#include "base/callback.h"
[email protected]d6e58c6e2009-10-10 20:40:5011#include "base/i18n/icu_string_conversions.h"
initial.commit09911bf2008-07-26 23:55:2912#include "base/message_loop.h"
[email protected]1cb2dac2010-03-08 21:49:1513#include "base/utf_string_conversions.h"
[email protected]257ab712009-04-14 17:16:2414#include "chrome/browser/autocomplete/keyword_provider.h"
initial.commit09911bf2008-07-26 23:55:2915#include "chrome/browser/browser_process.h"
16#include "chrome/browser/google_util.h"
[email protected]ce560f82009-06-03 09:39:4417#include "chrome/browser/history/history.h"
[email protected]f870a322009-01-16 21:47:2718#include "chrome/browser/net/url_fixer_upper.h"
[email protected]052313b2010-02-19 09:43:0819#include "chrome/browser/pref_service.h"
initial.commit09911bf2008-07-26 23:55:2920#include "chrome/browser/profile.h"
[email protected]d54e03a52009-01-16 00:31:0421#include "chrome/browser/search_engines/template_url_model.h"
initial.commit09911bf2008-07-26 23:55:2922#include "chrome/common/json_value_serializer.h"
initial.commit09911bf2008-07-26 23:55:2923#include "chrome/common/pref_names.h"
[email protected]dcf7d352009-02-26 01:56:0224#include "chrome/common/url_constants.h"
initial.commit09911bf2008-07-26 23:55:2925#include "googleurl/src/url_util.h"
[email protected]34ac8f32009-02-22 23:03:2726#include "grit/generated_resources.h"
initial.commit09911bf2008-07-26 23:55:2927#include "net/base/escape.h"
[email protected]319d9e6f2009-02-18 19:47:2128#include "net/http/http_response_headers.h"
29#include "net/url_request/url_request_status.h"
initial.commit09911bf2008-07-26 23:55:2930
[email protected]e1acf6f2008-10-27 20:43:3331using base::Time;
32using base::TimeDelta;
33
[email protected]b547666d2009-04-23 16:37:5834// static
35const int SearchProvider::kDefaultProviderURLFetcherID = 1;
36// static
37const int SearchProvider::kKeywordProviderURLFetcherID = 2;
38
39// static
40bool SearchProvider::query_suggest_immediately_ = false;
41
[email protected]257ab712009-04-14 17:16:2442void SearchProvider::Providers::Set(const TemplateURL* default_provider,
43 const TemplateURL* keyword_provider) {
44 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy
45 // this. Nor should we need |default_provider_| and |keyword_provider_|
46 // just to know whether the provider changed.
47 default_provider_ = default_provider;
48 if (default_provider)
49 cached_default_provider_ = *default_provider;
50 keyword_provider_ = keyword_provider;
51 if (keyword_provider)
52 cached_keyword_provider_ = *keyword_provider;
53}
54
initial.commit09911bf2008-07-26 23:55:2955void SearchProvider::Start(const AutocompleteInput& input,
[email protected]8deeb952008-10-09 18:21:2756 bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:2957 matches_.clear();
58
[email protected]6c85aa02009-02-27 12:08:0959 // Can't return search/suggest results for bogus input or without a profile.
initial.commit09911bf2008-07-26 23:55:2960 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
61 Stop();
62 return;
63 }
64
[email protected]257ab712009-04-14 17:16:2465 keyword_input_text_.clear();
66 const TemplateURL* keyword_provider =
67 KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input,
68 &keyword_input_text_);
69 if (!TemplateURL::SupportsReplacement(keyword_provider) ||
70 keyword_input_text_.empty()) {
71 keyword_provider = NULL;
72 }
73
74 const TemplateURL* default_provider =
initial.commit09911bf2008-07-26 23:55:2975 profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
[email protected]257ab712009-04-14 17:16:2476 if (!TemplateURL::SupportsReplacement(default_provider))
77 default_provider = NULL;
78
79 if (keyword_provider == default_provider)
80 keyword_provider = NULL; // No use in querying the same provider twice.
81
82 if (!default_provider && !keyword_provider) {
83 // No valid providers.
initial.commit09911bf2008-07-26 23:55:2984 Stop();
85 return;
86 }
87
88 // If we're still running an old query but have since changed the query text
[email protected]257ab712009-04-14 17:16:2489 // or the providers, abort the query.
initial.commit09911bf2008-07-26 23:55:2990 if (!done_ && (!minimal_changes ||
[email protected]257ab712009-04-14 17:16:2491 !providers_.equals(default_provider, keyword_provider))) {
initial.commit09911bf2008-07-26 23:55:2992 Stop();
[email protected]257ab712009-04-14 17:16:2493 }
initial.commit09911bf2008-07-26 23:55:2994
[email protected]257ab712009-04-14 17:16:2495 providers_.Set(default_provider, keyword_provider);
initial.commit09911bf2008-07-26 23:55:2996
97 if (input.text().empty()) {
98 // User typed "?" alone. Give them a placeholder result indicating what
99 // this syntax does.
[email protected]257ab712009-04-14 17:16:24100 if (default_provider) {
[email protected]69c579e2010-04-23 20:01:00101 AutocompleteMatch match;
102 match.provider = this;
[email protected]2c33dd22010-02-11 21:46:35103 match.contents.assign(l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE));
[email protected]257ab712009-04-14 17:16:24104 match.contents_class.push_back(
[email protected]2c33dd22010-02-11 21:46:35105 ACMatchClassification(0, ACMatchClassification::NONE));
106 match.description.assign(l10n_util::GetStringF(
107 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
108 default_provider->AdjustedShortNameForLocaleDirection()));
109 match.description_class.push_back(
[email protected]257ab712009-04-14 17:16:24110 ACMatchClassification(0, ACMatchClassification::DIM));
111 matches_.push_back(match);
112 }
initial.commit09911bf2008-07-26 23:55:29113 Stop();
114 return;
115 }
116
117 input_ = input;
118
[email protected]8deeb952008-10-09 18:21:27119 StartOrStopHistoryQuery(minimal_changes);
120 StartOrStopSuggestQuery(minimal_changes);
initial.commit09911bf2008-07-26 23:55:29121 ConvertResultsToAutocompleteMatches();
122}
123
124void SearchProvider::Run() {
125 // Start a new request with the current input.
126 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24127 suggest_results_pending_ = 0;
128 if (providers_.valid_suggest_for_keyword_provider()) {
129 suggest_results_pending_++;
130 keyword_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58131 CreateSuggestFetcher(kKeywordProviderURLFetcherID,
132 providers_.keyword_provider(),
[email protected]257ab712009-04-14 17:16:24133 keyword_input_text_));
134 }
135 if (providers_.valid_suggest_for_default_provider()) {
136 suggest_results_pending_++;
137 default_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58138 CreateSuggestFetcher(kDefaultProviderURLFetcherID,
139 providers_.default_provider(), input_.text()));
[email protected]257ab712009-04-14 17:16:24140 }
141 // We should only get here if we have a suggest url for the keyword or default
142 // providers.
[email protected]1cb2dac2010-03-08 21:49:15143 DCHECK_GT(suggest_results_pending_, 0);
initial.commit09911bf2008-07-26 23:55:29144}
145
146void SearchProvider::Stop() {
147 StopHistory();
148 StopSuggest();
149 done_ = true;
150}
151
152void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
153 const GURL& url,
154 const URLRequestStatus& status,
155 int response_code,
156 const ResponseCookies& cookie,
157 const std::string& data) {
158 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24159 suggest_results_pending_--;
[email protected]1cb2dac2010-03-08 21:49:15160 DCHECK_GE(suggest_results_pending_, 0); // Should never go negative.
[email protected]ec9207d32008-09-26 00:51:06161 const net::HttpResponseHeaders* const response_headers =
162 source->response_headers();
163 std::string json_data(data);
[email protected]6c85aa02009-02-27 12:08:09164 // JSON is supposed to be UTF-8, but some suggest service providers send JSON
165 // files in non-UTF-8 encodings. The actual encoding is usually specified in
166 // the Content-Type header field.
[email protected]ec9207d32008-09-26 00:51:06167 if (response_headers) {
168 std::string charset;
169 if (response_headers->GetCharset(&charset)) {
170 std::wstring wide_data;
171 // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
[email protected]d6e58c6e2009-10-10 20:40:50172 if (base::CodepageToWide(data, charset.c_str(),
173 base::OnStringConversionError::FAIL,
174 &wide_data))
[email protected]f0a51fb52009-03-05 12:46:38175 json_data = WideToUTF8(wide_data);
[email protected]ec9207d32008-09-26 00:51:06176 }
177 }
178
[email protected]257ab712009-04-14 17:16:24179 bool is_keyword_results = (source == keyword_fetcher_.get());
180 SuggestResults* suggest_results = is_keyword_results ?
181 &keyword_suggest_results_ : &default_suggest_results_;
182
[email protected]b4cebf82008-12-29 19:59:08183 if (status.is_success() && response_code == 200) {
184 JSONStringValueSerializer deserializer(json_data);
185 deserializer.set_allow_trailing_comma(true);
[email protected]ba399672010-04-06 15:42:39186 scoped_ptr<Value> root_val(deserializer.Deserialize(NULL, NULL));
[email protected]257ab712009-04-14 17:16:24187 const std::wstring& input_text =
188 is_keyword_results ? keyword_input_text_ : input_.text();
[email protected]b4cebf82008-12-29 19:59:08189 have_suggest_results_ =
[email protected]257ab712009-04-14 17:16:24190 root_val.get() &&
191 ParseSuggestResults(root_val.get(), is_keyword_results, input_text,
192 suggest_results);
[email protected]b4cebf82008-12-29 19:59:08193 }
194
initial.commit09911bf2008-07-26 23:55:29195 ConvertResultsToAutocompleteMatches();
[email protected]257ab712009-04-14 17:16:24196 listener_->OnProviderUpdate(!suggest_results->empty());
initial.commit09911bf2008-07-26 23:55:29197}
198
[email protected]8deeb952008-10-09 18:21:27199void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:29200 // For the minimal_changes case, if we finished the previous query and still
201 // have its results, or are allowed to keep running it, just do that, rather
202 // than starting a new query.
203 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27204 (have_history_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29205 return;
206
207 // We can't keep running any previous query, so halt it.
208 StopHistory();
209
210 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27211 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29212 return;
213
[email protected]257ab712009-04-14 17:16:24214 // Request history for both the keyword and default provider.
215 if (providers_.valid_keyword_provider()) {
216 ScheduleHistoryQuery(providers_.keyword_provider().id(),
217 keyword_input_text_);
218 }
219 if (providers_.valid_default_provider()) {
220 ScheduleHistoryQuery(providers_.default_provider().id(),
221 input_.text());
222 }
initial.commit09911bf2008-07-26 23:55:29223}
224
[email protected]8deeb952008-10-09 18:21:27225void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) {
[email protected]6c85aa02009-02-27 12:08:09226 // Don't send any queries to the server until some time has elapsed after
227 // the last keypress, to avoid flooding the server with requests we are
228 // likely to end up throwing away anyway.
229 static const int kQueryDelayMs = 200;
230
[email protected]83c726482008-09-10 06:36:34231 if (!IsQuerySuitableForSuggest()) {
initial.commit09911bf2008-07-26 23:55:29232 StopSuggest();
233 return;
234 }
235
236 // For the minimal_changes case, if we finished the previous query and still
237 // have its results, or are allowed to keep running it, just do that, rather
238 // than starting a new query.
239 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27240 (have_suggest_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29241 return;
242
243 // We can't keep running any previous query, so halt it.
244 StopSuggest();
245
246 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27247 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29248 return;
249
[email protected]257ab712009-04-14 17:16:24250 // We'll have at least one pending fetch. Set it to 1 now, but the value is
251 // correctly set in Run. As Run isn't invoked immediately we need to set this
252 // now, else we won't think we're waiting on results from the server when we
253 // really are.
254 suggest_results_pending_ = 1;
255
initial.commit09911bf2008-07-26 23:55:29256 // Kick off a timer that will start the URL fetch if it completes before
257 // the user types another character.
[email protected]b547666d2009-04-23 16:37:58258 int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs;
259 timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run);
initial.commit09911bf2008-07-26 23:55:29260}
261
[email protected]83c726482008-09-10 06:36:34262bool SearchProvider::IsQuerySuitableForSuggest() const {
263 // Don't run Suggest when off the record, the engine doesn't support it, or
264 // the user has disabled it.
265 if (profile_->IsOffTheRecord() ||
[email protected]257ab712009-04-14 17:16:24266 (!providers_.valid_suggest_for_keyword_provider() &&
267 !providers_.valid_suggest_for_default_provider()) ||
[email protected]83c726482008-09-10 06:36:34268 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
269 return false;
270
271 // If the input type is URL, we take extra care so that private data in URL
272 // isn't sent to the server.
273 if (input_.type() == AutocompleteInput::URL) {
274 // Don't query the server for URLs that aren't http/https/ftp. Sending
275 // things like file: and data: is both a waste of time and a disclosure of
276 // potentially private, local data.
277 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") &&
278 (input_.scheme() != L"ftp"))
279 return false;
280
281 // Don't leak private data in URL
282 const url_parse::Parsed& parts = input_.parts();
283
284 // Don't send URLs with usernames, queries or refs. Some of these are
285 // private, and the Suggest server is unlikely to have any useful results
286 // for any of them.
287 // Password is optional and may be omitted. Checking username is
288 // sufficient.
289 if (parts.username.is_nonempty() || parts.query.is_nonempty() ||
290 parts.ref.is_nonempty())
291 return false;
292 // Don't send anything for https except hostname and port number.
293 // Hostname and port number are OK because they are visible when TCP
294 // connection is established and the Suggest server may provide some
295 // useful completed URL.
296 if (input_.scheme() == L"https" && parts.path.is_nonempty())
297 return false;
298 }
299
300 return true;
301}
302
initial.commit09911bf2008-07-26 23:55:29303void SearchProvider::StopHistory() {
304 history_request_consumer_.CancelAllRequests();
305 history_request_pending_ = false;
[email protected]257ab712009-04-14 17:16:24306 keyword_history_results_.clear();
307 default_history_results_.clear();
initial.commit09911bf2008-07-26 23:55:29308 have_history_results_ = false;
309}
310
311void SearchProvider::StopSuggest() {
[email protected]257ab712009-04-14 17:16:24312 suggest_results_pending_ = 0;
[email protected]2d316662008-09-03 18:18:14313 timer_.Stop();
[email protected]257ab712009-04-14 17:16:24314 // Stop any in-progress URL fetches.
315 keyword_fetcher_.reset();
316 default_fetcher_.reset();
317 keyword_suggest_results_.clear();
318 default_suggest_results_.clear();
319 keyword_navigation_results_.clear();
320 default_navigation_results_.clear();
initial.commit09911bf2008-07-26 23:55:29321 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29322}
323
[email protected]257ab712009-04-14 17:16:24324void SearchProvider::ScheduleHistoryQuery(TemplateURL::IDType search_id,
325 const std::wstring& text) {
326 DCHECK(!text.empty());
327 HistoryService* const history_service =
328 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
329 HistoryService::Handle request_handle =
330 history_service->GetMostRecentKeywordSearchTerms(
[email protected]0be9b612010-05-18 01:13:41331 search_id, text, static_cast<int>(kMaxMatches),
[email protected]257ab712009-04-14 17:16:24332 &history_request_consumer_,
333 NewCallback(this,
334 &SearchProvider::OnGotMostRecentKeywordSearchTerms));
335 history_request_consumer_.SetClientData(history_service, request_handle,
336 search_id);
337 history_request_pending_ = true;
338}
339
initial.commit09911bf2008-07-26 23:55:29340void SearchProvider::OnGotMostRecentKeywordSearchTerms(
341 CancelableRequestProvider::Handle handle,
342 HistoryResults* results) {
[email protected]257ab712009-04-14 17:16:24343 HistoryService* history_service =
344 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
345 DCHECK(history_service);
346 if (providers_.valid_keyword_provider() &&
[email protected]bed9bd6c2009-04-21 17:27:47347 (providers_.keyword_provider().id() ==
[email protected]257ab712009-04-14 17:16:24348 history_request_consumer_.GetClientData(history_service, handle))) {
349 keyword_history_results_ = *results;
350 } else {
351 default_history_results_ = *results;
352 }
[email protected]257ab712009-04-14 17:16:24353
354 if (history_request_consumer_.PendingRequestCount() == 1) {
355 // Requests are removed AFTER the callback is invoked. If the count == 1,
356 // it means no more history requests are pending.
357 history_request_pending_ = false;
358 have_history_results_ = true;
359 }
[email protected]b547666d2009-04-23 16:37:58360
361 ConvertResultsToAutocompleteMatches();
362 listener_->OnProviderUpdate(!results->empty());
initial.commit09911bf2008-07-26 23:55:29363}
364
[email protected]b547666d2009-04-23 16:37:58365URLFetcher* SearchProvider::CreateSuggestFetcher(int id,
366 const TemplateURL& provider,
[email protected]257ab712009-04-14 17:16:24367 const std::wstring& text) {
368 const TemplateURLRef* const suggestions_url = provider.suggestions_url();
369 DCHECK(suggestions_url->SupportsReplacement());
[email protected]b547666d2009-04-23 16:37:58370 URLFetcher* fetcher = URLFetcher::Create(id,
[email protected]7b9f3672009-06-15 18:31:22371 GURL(WideToUTF8(suggestions_url->ReplaceSearchTerms(
[email protected]b547666d2009-04-23 16:37:58372 provider, text, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE,
[email protected]7b9f3672009-06-15 18:31:22373 std::wstring()))),
[email protected]257ab712009-04-14 17:16:24374 URLFetcher::GET, this);
375 fetcher->set_request_context(profile_->GetRequestContext());
376 fetcher->Start();
377 return fetcher;
378}
379
380bool SearchProvider::ParseSuggestResults(Value* root_val,
381 bool is_keyword,
382 const std::wstring& input_text,
383 SuggestResults* suggest_results) {
initial.commit09911bf2008-07-26 23:55:29384 if (!root_val->IsType(Value::TYPE_LIST))
385 return false;
386 ListValue* root_list = static_cast<ListValue*>(root_val);
387
388 Value* query_val;
389 std::wstring query_str;
390 Value* result_val;
391 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
[email protected]257ab712009-04-14 17:16:24392 !query_val->GetAsString(&query_str) || (query_str != input_text) ||
initial.commit09911bf2008-07-26 23:55:29393 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
394 return false;
395
396 ListValue* description_list = NULL;
397 if (root_list->GetSize() > 2) {
398 // 3rd element: Description list.
399 Value* description_val;
400 if (root_list->Get(2, &description_val) &&
401 description_val->IsType(Value::TYPE_LIST))
402 description_list = static_cast<ListValue*>(description_val);
403 }
404
405 // We don't care about the query URL list (the fourth element in the
406 // response) for now.
407
408 // Parse optional data in the results from the Suggest server if any.
409 ListValue* type_list = NULL;
410 // 5th argument: Optional key-value pairs.
411 // TODO: We may iterate the 5th+ arguments of the root_list if any other
412 // optional data are defined.
413 if (root_list->GetSize() > 4) {
414 Value* optional_val;
415 if (root_list->Get(4, &optional_val) &&
416 optional_val->IsType(Value::TYPE_DICTIONARY)) {
417 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
418
419 // Parse Google Suggest specific type extension.
[email protected]8e50b602009-03-03 22:59:43420 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
initial.commit09911bf2008-07-26 23:55:29421 if (dict_val->HasKey(kGoogleSuggestType))
422 dict_val->GetList(kGoogleSuggestType, &type_list);
423 }
424 }
425
426 ListValue* result_list = static_cast<ListValue*>(result_val);
427 for (size_t i = 0; i < result_list->GetSize(); ++i) {
428 Value* suggestion_val;
429 std::wstring suggestion_str;
430 if (!result_list->Get(i, &suggestion_val) ||
431 !suggestion_val->GetAsString(&suggestion_str))
432 return false;
433
434 Value* type_val;
435 std::wstring type_str;
436 if (type_list && type_list->Get(i, &type_val) &&
437 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
438 Value* site_val;
439 std::wstring site_name;
[email protected]257ab712009-04-14 17:16:24440 NavigationResults& navigation_results =
441 is_keyword ? keyword_navigation_results_ :
442 default_navigation_results_;
[email protected]0be9b612010-05-18 01:13:41443 if ((navigation_results.size() < kMaxMatches) &&
initial.commit09911bf2008-07-26 23:55:29444 description_list && description_list->Get(i, &site_val) &&
445 site_val->IsType(Value::TYPE_STRING) &&
446 site_val->GetAsString(&site_name)) {
[email protected]16afe222009-01-08 18:57:45447 // We can't blindly trust the URL coming from the server to be valid.
448 GURL result_url =
[email protected]1d73faa2009-02-24 19:32:15449 GURL(URLFixerUpper::FixupURL(WideToUTF8(suggestion_str),
450 std::string()));
[email protected]257ab712009-04-14 17:16:24451 if (result_url.is_valid())
452 navigation_results.push_back(NavigationResult(result_url, site_name));
initial.commit09911bf2008-07-26 23:55:29453 }
454 } else {
455 // TODO(kochi): Currently we treat a calculator result as a query, but it
456 // is better to have better presentation for caluculator results.
[email protected]0be9b612010-05-18 01:13:41457 if (suggest_results->size() < kMaxMatches)
[email protected]257ab712009-04-14 17:16:24458 suggest_results->push_back(suggestion_str);
initial.commit09911bf2008-07-26 23:55:29459 }
460 }
461
initial.commit09911bf2008-07-26 23:55:29462 return true;
463}
464
465void SearchProvider::ConvertResultsToAutocompleteMatches() {
466 // Convert all the results to matches and add them to a map, so we can keep
467 // the most relevant match for each result.
468 MatchMap map;
[email protected]257ab712009-04-14 17:16:24469 const Time no_time;
470 int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ?
initial.commit09911bf2008-07-26 23:55:29471 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
472 TemplateURLRef::NO_SUGGESTION_CHOSEN;
[email protected]257ab712009-04-14 17:16:24473 // Keyword what you typed results are handled by the KeywordProvider.
initial.commit09911bf2008-07-26 23:55:29474
[email protected]257ab712009-04-14 17:16:24475 int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
476 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
477 TemplateURLRef::NO_SUGGESTION_CHOSEN;
478 if (providers_.valid_default_provider()) {
479 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
480 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
481 did_not_accept_default_suggestion, false, &map);
initial.commit09911bf2008-07-26 23:55:29482 }
483
[email protected]257ab712009-04-14 17:16:24484 AddHistoryResultsToMap(keyword_history_results_, true,
485 did_not_accept_keyword_suggestion, &map);
486 AddHistoryResultsToMap(default_history_results_, false,
487 did_not_accept_default_suggestion, &map);
488
489 AddSuggestResultsToMap(keyword_suggest_results_, true,
490 did_not_accept_keyword_suggestion, &map);
491 AddSuggestResultsToMap(default_suggest_results_, false,
492 did_not_accept_default_suggestion, &map);
initial.commit09911bf2008-07-26 23:55:29493
494 // Now add the most relevant matches from the map to |matches_|.
495 matches_.clear();
496 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
497 matches_.push_back(i->second);
498
[email protected]257ab712009-04-14 17:16:24499 AddNavigationResultsToMatches(keyword_navigation_results_, true);
500 AddNavigationResultsToMatches(default_navigation_results_, false);
initial.commit09911bf2008-07-26 23:55:29501
[email protected]0be9b612010-05-18 01:13:41502 const size_t max_total_matches = kMaxMatches + 1; // 1 for "what you typed"
initial.commit09911bf2008-07-26 23:55:29503 std::partial_sort(matches_.begin(),
504 matches_.begin() + std::min(max_total_matches, matches_.size()),
505 matches_.end(), &AutocompleteMatch::MoreRelevant);
506 if (matches_.size() > max_total_matches)
[email protected]a28e95662008-11-12 19:19:02507 matches_.erase(matches_.begin() + max_total_matches, matches_.end());
initial.commit09911bf2008-07-26 23:55:29508
[email protected]cc63dea2008-08-21 20:56:31509 UpdateStarredStateOfMatches();
510
[email protected]6c85aa02009-02-27 12:08:09511 // We're done when both asynchronous subcomponents have finished. We can't
512 // use CancelableRequestConsumer.HasPendingRequests() for history requests
513 // here. A pending request is not cleared until after the completion
514 // callback has returned, but we've reached here from inside that callback.
515 // HasPendingRequests() would therefore return true, and if this is the last
516 // thing left to calculate for this query, we'll never mark the query "done".
[email protected]257ab712009-04-14 17:16:24517 done_ = !history_request_pending_ && !suggest_results_pending_;
518}
519
520void SearchProvider::AddNavigationResultsToMatches(
521 const NavigationResults& navigation_results,
522 bool is_keyword) {
523 if (!navigation_results.empty()) {
524 // TODO(kochi): http://b/1170574 We add only one results for navigational
525 // suggestions. If we can get more useful information about the score,
526 // consider adding more results.
[email protected]52d08b12009-10-19 18:42:36527 const size_t num_results = is_keyword ?
528 keyword_navigation_results_.size() : default_navigation_results_.size();
529 matches_.push_back(NavigationToMatch(navigation_results.front(),
530 CalculateRelevanceForNavigation(num_results, 0, is_keyword),
531 is_keyword));
[email protected]257ab712009-04-14 17:16:24532 }
533}
534
535void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results,
536 bool is_keyword,
537 int did_not_accept_suggestion,
538 MatchMap* map) {
539 for (HistoryResults::const_iterator i(results.begin()); i != results.end();
540 ++i) {
541 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time, is_keyword),
542 AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion,
543 is_keyword, map);
544 }
545}
546
547void SearchProvider::AddSuggestResultsToMap(
548 const SuggestResults& suggest_results,
549 bool is_keyword,
550 int did_not_accept_suggestion,
551 MatchMap* map) {
552 for (size_t i = 0; i < suggest_results.size(); ++i) {
553 AddMatchToMap(suggest_results[i],
[email protected]52d08b12009-10-19 18:42:36554 CalculateRelevanceForSuggestion(suggest_results.size(), i,
[email protected]257ab712009-04-14 17:16:24555 is_keyword),
556 AutocompleteMatch::SEARCH_SUGGEST,
557 static_cast<int>(i), is_keyword, map);
558 }
initial.commit09911bf2008-07-26 23:55:29559}
560
561int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
[email protected]52d08b12009-10-19 18:42:36562 if (providers_.valid_keyword_provider())
563 return 250;
564
initial.commit09911bf2008-07-26 23:55:29565 switch (input_.type()) {
566 case AutocompleteInput::UNKNOWN:
[email protected]52d08b12009-10-19 18:42:36567 case AutocompleteInput::QUERY:
568 case AutocompleteInput::FORCED_QUERY:
569 return 1300;
initial.commit09911bf2008-07-26 23:55:29570
571 case AutocompleteInput::REQUESTED_URL:
[email protected]52d08b12009-10-19 18:42:36572 return 1150;
initial.commit09911bf2008-07-26 23:55:29573
574 case AutocompleteInput::URL:
[email protected]52d08b12009-10-19 18:42:36575 return 850;
initial.commit09911bf2008-07-26 23:55:29576
577 default:
578 NOTREACHED();
579 return 0;
580 }
581}
582
[email protected]257ab712009-04-14 17:16:24583int SearchProvider::CalculateRelevanceForHistory(const Time& time,
584 bool is_keyword) const {
initial.commit09911bf2008-07-26 23:55:29585 // The relevance of past searches falls off over time. This curve is chosen
586 // so that the relevance of a search 15 minutes ago is discounted about 50
587 // points, while the relevance of a search two weeks ago is discounted about
588 // 450 points.
589 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
590 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
591
[email protected]6c85aa02009-02-27 12:08:09592 // Don't let scores go below 0. Negative relevance scores are meaningful in
593 // a different way.
initial.commit09911bf2008-07-26 23:55:29594 int base_score;
[email protected]52d08b12009-10-19 18:42:36595 if (!providers_.is_primary_provider(is_keyword))
596 base_score = 200;
597 else
598 base_score = (input_.type() == AutocompleteInput::URL) ? 750 : 1050;
initial.commit09911bf2008-07-26 23:55:29599 return std::max(0, base_score - score_discount);
600}
601
[email protected]52d08b12009-10-19 18:42:36602int SearchProvider::CalculateRelevanceForSuggestion(size_t num_results,
603 size_t result_number,
604 bool is_keyword) const {
605 DCHECK(result_number < num_results);
606 int base_score;
607 if (!providers_.is_primary_provider(is_keyword))
608 base_score = 100;
609 else
610 base_score = (input_.type() == AutocompleteInput::URL) ? 300 : 600;
611 return base_score +
612 static_cast<int>(num_results - 1 - result_number);
initial.commit09911bf2008-07-26 23:55:29613}
614
[email protected]52d08b12009-10-19 18:42:36615int SearchProvider::CalculateRelevanceForNavigation(size_t num_results,
616 size_t result_number,
617 bool is_keyword) const {
618 DCHECK(result_number < num_results);
initial.commit09911bf2008-07-26 23:55:29619 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
620 // server if possible.
[email protected]52d08b12009-10-19 18:42:36621 return (providers_.is_primary_provider(is_keyword) ? 800 : 150) +
622 static_cast<int>(num_results - 1 - result_number);
initial.commit09911bf2008-07-26 23:55:29623}
624
625void SearchProvider::AddMatchToMap(const std::wstring& query_string,
626 int relevance,
[email protected]4c1fb7ec2008-11-13 00:19:00627 AutocompleteMatch::Type type,
initial.commit09911bf2008-07-26 23:55:29628 int accepted_suggestion,
[email protected]257ab712009-04-14 17:16:24629 bool is_keyword,
initial.commit09911bf2008-07-26 23:55:29630 MatchMap* map) {
[email protected]257ab712009-04-14 17:16:24631 const std::wstring& input_text =
632 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00633 AutocompleteMatch match(this, relevance, false, type);
initial.commit09911bf2008-07-26 23:55:29634 std::vector<size_t> content_param_offsets;
[email protected]257ab712009-04-14 17:16:24635 const TemplateURL& provider = is_keyword ? providers_.keyword_provider() :
636 providers_.default_provider();
[email protected]fb5153c52009-07-31 19:40:33637 // We do intra-string highlighting for suggestions - the suggested segment
638 // will be highlighted, e.g. for input_text = "you" the suggestion may be
639 // "youtube", so we'll bold the "tube" section: you*tube*.
640 if (input_text != query_string) {
641 match.contents.assign(query_string);
642 size_t input_position = match.contents.find(input_text);
643 if (input_position == std::wstring::npos) {
644 // The input text is not a substring of the query string, e.g. input
645 // text is "slasdot" and the query string is "slashdot", so we bold the
646 // whole thing.
647 match.contents_class.push_back(
648 ACMatchClassification(0, ACMatchClassification::MATCH));
[email protected]ec2379162009-06-09 23:58:17649 } else {
[email protected]fb5153c52009-07-31 19:40:33650 // TODO(beng): ACMatchClassification::MATCH now seems to just mean
651 // "bold" this. Consider modifying the terminology.
652 // We don't iterate over the string here annotating all matches because
653 // it looks odd to have every occurrence of a substring that may be as
654 // short as a single character highlighted in a query suggestion result,
655 // e.g. for input text "s" and query string "southwest airlines", it
656 // looks odd if both the first and last s are highlighted.
657 if (input_position != 0) {
658 match.contents_class.push_back(
659 ACMatchClassification(0, ACMatchClassification::NONE));
660 }
661 match.contents_class.push_back(
662 ACMatchClassification(input_position, ACMatchClassification::DIM));
663 size_t next_fragment_position = input_position + input_text.length();
664 if (next_fragment_position < query_string.length()) {
665 match.contents_class.push_back(
666 ACMatchClassification(next_fragment_position,
667 ACMatchClassification::NONE));
668 }
[email protected]ec2379162009-06-09 23:58:17669 }
initial.commit09911bf2008-07-26 23:55:29670 } else {
[email protected]fb5153c52009-07-31 19:40:33671 // Otherwise, we're dealing with the "default search" result which has no
672 // completion, but has the search provider name as the description.
673 match.contents.assign(query_string);
674 match.contents_class.push_back(
675 ACMatchClassification(0, ACMatchClassification::NONE));
676 match.description.assign(l10n_util::GetStringF(
677 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
[email protected]2c33dd22010-02-11 21:46:35678 provider.AdjustedShortNameForLocaleDirection()));
[email protected]fb5153c52009-07-31 19:40:33679 match.description_class.push_back(
680 ACMatchClassification(0, ACMatchClassification::DIM));
initial.commit09911bf2008-07-26 23:55:29681 }
682
683 // When the user forced a query, we need to make sure all the fill_into_edit
684 // values preserve that property. Otherwise, if the user starts editing a
685 // suggestion, non-Search results will suddenly appear.
686 size_t search_start = 0;
687 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
688 match.fill_into_edit.assign(L"?");
689 ++search_start;
690 }
[email protected]c0048b42009-05-04 21:47:17691 if (is_keyword) {
692 match.fill_into_edit.append(providers_.keyword_provider().keyword() + L" ");
693 match.template_url = &providers_.keyword_provider();
694 }
initial.commit09911bf2008-07-26 23:55:29695 match.fill_into_edit.append(query_string);
[email protected]2c33dd22010-02-11 21:46:35696 // Not all suggestions start with the original input.
initial.commit09911bf2008-07-26 23:55:29697 if (!input_.prevent_inline_autocomplete() &&
[email protected]257ab712009-04-14 17:16:24698 !match.fill_into_edit.compare(search_start, input_text.length(),
699 input_text))
700 match.inline_autocomplete_offset = search_start + input_text.length();
initial.commit09911bf2008-07-26 23:55:29701
[email protected]257ab712009-04-14 17:16:24702 const TemplateURLRef* const search_url = provider.url();
initial.commit09911bf2008-07-26 23:55:29703 DCHECK(search_url->SupportsReplacement());
[email protected]7b9f3672009-06-15 18:31:22704 match.destination_url =
705 GURL(WideToUTF8(search_url->ReplaceSearchTerms(provider, query_string,
706 accepted_suggestion,
707 input_text)));
initial.commit09911bf2008-07-26 23:55:29708
709 // Search results don't look like URLs.
[email protected]0bfc29a2009-04-27 16:15:44710 match.transition =
711 is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED;
initial.commit09911bf2008-07-26 23:55:29712
713 // Try to add |match| to |map|. If a match for |query_string| is already in
714 // |map|, replace it if |match| is more relevant.
715 // NOTE: Keep this ToLower() call in sync with url_database.cc.
716 const std::pair<MatchMap::iterator, bool> i = map->insert(
717 std::pair<std::wstring, AutocompleteMatch>(
718 l10n_util::ToLower(query_string), match));
719 // NOTE: We purposefully do a direct relevance comparison here instead of
720 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
721 // first" rather than "items alphabetically first" when the scores are equal.
722 // The only case this matters is when a user has results with the same score
723 // that differ only by capitalization; because the history system returns
724 // results sorted by recency, this means we'll pick the most recent such
725 // result even if the precision of our relevance score is too low to
726 // distinguish the two.
727 if (!i.second && (match.relevance > i.first->second.relevance))
728 i.first->second = match;
729}
730
731AutocompleteMatch SearchProvider::NavigationToMatch(
732 const NavigationResult& navigation,
[email protected]257ab712009-04-14 17:16:24733 int relevance,
734 bool is_keyword) {
735 const std::wstring& input_text =
736 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00737 AutocompleteMatch match(this, relevance, false,
738 AutocompleteMatch::NAVSUGGEST);
initial.commit09911bf2008-07-26 23:55:29739 match.destination_url = navigation.url;
[email protected]69c579e2010-04-23 20:01:00740 const bool trim_http = !url_util::FindAndCompareScheme(
741 WideToUTF8(input_text), chrome::kHttpScheme, NULL);
742 match.contents = StringForURLDisplay(navigation.url, true, trim_http);
[email protected]257ab712009-04-14 17:16:24743 AutocompleteMatch::ClassifyMatchInString(input_text, match.contents,
initial.commit09911bf2008-07-26 23:55:29744 ACMatchClassification::URL,
745 &match.contents_class);
746
747 match.description = navigation.site_name;
[email protected]257ab712009-04-14 17:16:24748 AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name,
initial.commit09911bf2008-07-26 23:55:29749 ACMatchClassification::NONE,
750 &match.description_class);
751
initial.commit09911bf2008-07-26 23:55:29752 // When the user forced a query, we need to make sure all the fill_into_edit
753 // values preserve that property. Otherwise, if the user starts editing a
754 // suggestion, non-Search results will suddenly appear.
755 if (input_.type() == AutocompleteInput::FORCED_QUERY)
756 match.fill_into_edit.assign(L"?");
[email protected]79845ef2010-06-02 02:37:40757 match.fill_into_edit.append(
758 AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url,
759 match.contents));
initial.commit09911bf2008-07-26 23:55:29760 // TODO(pkasting): http://b/1112879 These should perhaps be
761 // inline-autocompletable?
762
763 return match;
764}