blob: e1d85673d75e7a03bdeb373b686a6d0c4cbe15db [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]d6e58c6e2009-10-10 20:40:508#include "base/i18n/icu_string_conversions.h"
initial.commit09911bf2008-07-26 23:55:299#include "base/message_loop.h"
10#include "base/string_util.h"
[email protected]257ab712009-04-14 17:16:2411#include "chrome/browser/autocomplete/keyword_provider.h"
initial.commit09911bf2008-07-26 23:55:2912#include "chrome/browser/browser_process.h"
13#include "chrome/browser/google_util.h"
[email protected]ce560f82009-06-03 09:39:4414#include "chrome/browser/history/history.h"
[email protected]f870a322009-01-16 21:47:2715#include "chrome/browser/net/url_fixer_upper.h"
initial.commit09911bf2008-07-26 23:55:2916#include "chrome/browser/profile.h"
[email protected]d54e03a52009-01-16 00:31:0417#include "chrome/browser/search_engines/template_url_model.h"
initial.commit09911bf2008-07-26 23:55:2918#include "chrome/common/json_value_serializer.h"
initial.commit09911bf2008-07-26 23:55:2919#include "chrome/common/pref_names.h"
20#include "chrome/common/pref_service.h"
[email protected]dcf7d352009-02-26 01:56:0221#include "chrome/common/url_constants.h"
initial.commit09911bf2008-07-26 23:55:2922#include "googleurl/src/url_util.h"
[email protected]34ac8f32009-02-22 23:03:2723#include "grit/generated_resources.h"
initial.commit09911bf2008-07-26 23:55:2924#include "net/base/escape.h"
[email protected]319d9e6f2009-02-18 19:47:2125#include "net/http/http_response_headers.h"
26#include "net/url_request/url_request_status.h"
initial.commit09911bf2008-07-26 23:55:2927
[email protected]e1acf6f2008-10-27 20:43:3328using base::Time;
29using base::TimeDelta;
30
[email protected]b547666d2009-04-23 16:37:5831// static
32const int SearchProvider::kDefaultProviderURLFetcherID = 1;
33// static
34const int SearchProvider::kKeywordProviderURLFetcherID = 2;
35
36// static
37bool SearchProvider::query_suggest_immediately_ = false;
38
[email protected]257ab712009-04-14 17:16:2439void SearchProvider::Providers::Set(const TemplateURL* default_provider,
40 const TemplateURL* keyword_provider) {
41 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy
42 // this. Nor should we need |default_provider_| and |keyword_provider_|
43 // just to know whether the provider changed.
44 default_provider_ = default_provider;
45 if (default_provider)
46 cached_default_provider_ = *default_provider;
47 keyword_provider_ = keyword_provider;
48 if (keyword_provider)
49 cached_keyword_provider_ = *keyword_provider;
50}
51
initial.commit09911bf2008-07-26 23:55:2952void SearchProvider::Start(const AutocompleteInput& input,
[email protected]8deeb952008-10-09 18:21:2753 bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:2954 matches_.clear();
55
[email protected]6c85aa02009-02-27 12:08:0956 // Can't return search/suggest results for bogus input or without a profile.
initial.commit09911bf2008-07-26 23:55:2957 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
58 Stop();
59 return;
60 }
61
[email protected]257ab712009-04-14 17:16:2462 keyword_input_text_.clear();
63 const TemplateURL* keyword_provider =
64 KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input,
65 &keyword_input_text_);
66 if (!TemplateURL::SupportsReplacement(keyword_provider) ||
67 keyword_input_text_.empty()) {
68 keyword_provider = NULL;
69 }
70
71 const TemplateURL* default_provider =
initial.commit09911bf2008-07-26 23:55:2972 profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
[email protected]257ab712009-04-14 17:16:2473 if (!TemplateURL::SupportsReplacement(default_provider))
74 default_provider = NULL;
75
76 if (keyword_provider == default_provider)
77 keyword_provider = NULL; // No use in querying the same provider twice.
78
79 if (!default_provider && !keyword_provider) {
80 // No valid providers.
initial.commit09911bf2008-07-26 23:55:2981 Stop();
82 return;
83 }
84
85 // If we're still running an old query but have since changed the query text
[email protected]257ab712009-04-14 17:16:2486 // or the providers, abort the query.
initial.commit09911bf2008-07-26 23:55:2987 if (!done_ && (!minimal_changes ||
[email protected]257ab712009-04-14 17:16:2488 !providers_.equals(default_provider, keyword_provider))) {
initial.commit09911bf2008-07-26 23:55:2989 Stop();
[email protected]257ab712009-04-14 17:16:2490 }
initial.commit09911bf2008-07-26 23:55:2991
[email protected]257ab712009-04-14 17:16:2492 providers_.Set(default_provider, keyword_provider);
initial.commit09911bf2008-07-26 23:55:2993
94 if (input.text().empty()) {
95 // User typed "?" alone. Give them a placeholder result indicating what
96 // this syntax does.
[email protected]257ab712009-04-14 17:16:2497 if (default_provider) {
98 AutocompleteMatch match(this, 0, false,
99 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED);
[email protected]2c33dd22010-02-11 21:46:35100 match.contents.assign(l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE));
[email protected]257ab712009-04-14 17:16:24101 match.contents_class.push_back(
[email protected]2c33dd22010-02-11 21:46:35102 ACMatchClassification(0, ACMatchClassification::NONE));
103 match.description.assign(l10n_util::GetStringF(
104 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
105 default_provider->AdjustedShortNameForLocaleDirection()));
106 match.description_class.push_back(
[email protected]257ab712009-04-14 17:16:24107 ACMatchClassification(0, ACMatchClassification::DIM));
108 matches_.push_back(match);
109 }
initial.commit09911bf2008-07-26 23:55:29110 Stop();
111 return;
112 }
113
114 input_ = input;
115
[email protected]8deeb952008-10-09 18:21:27116 StartOrStopHistoryQuery(minimal_changes);
117 StartOrStopSuggestQuery(minimal_changes);
initial.commit09911bf2008-07-26 23:55:29118 ConvertResultsToAutocompleteMatches();
119}
120
121void SearchProvider::Run() {
122 // Start a new request with the current input.
123 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24124 suggest_results_pending_ = 0;
125 if (providers_.valid_suggest_for_keyword_provider()) {
126 suggest_results_pending_++;
127 keyword_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58128 CreateSuggestFetcher(kKeywordProviderURLFetcherID,
129 providers_.keyword_provider(),
[email protected]257ab712009-04-14 17:16:24130 keyword_input_text_));
131 }
132 if (providers_.valid_suggest_for_default_provider()) {
133 suggest_results_pending_++;
134 default_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58135 CreateSuggestFetcher(kDefaultProviderURLFetcherID,
136 providers_.default_provider(), input_.text()));
[email protected]257ab712009-04-14 17:16:24137 }
138 // We should only get here if we have a suggest url for the keyword or default
139 // providers.
140 DCHECK(suggest_results_pending_ > 0);
initial.commit09911bf2008-07-26 23:55:29141}
142
143void SearchProvider::Stop() {
144 StopHistory();
145 StopSuggest();
146 done_ = true;
147}
148
149void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
150 const GURL& url,
151 const URLRequestStatus& status,
152 int response_code,
153 const ResponseCookies& cookie,
154 const std::string& data) {
155 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24156 suggest_results_pending_--;
157 DCHECK(suggest_results_pending_ >= 0); // Should never go negative.
[email protected]ec9207d32008-09-26 00:51:06158 const net::HttpResponseHeaders* const response_headers =
159 source->response_headers();
160 std::string json_data(data);
[email protected]6c85aa02009-02-27 12:08:09161 // JSON is supposed to be UTF-8, but some suggest service providers send JSON
162 // files in non-UTF-8 encodings. The actual encoding is usually specified in
163 // the Content-Type header field.
[email protected]ec9207d32008-09-26 00:51:06164 if (response_headers) {
165 std::string charset;
166 if (response_headers->GetCharset(&charset)) {
167 std::wstring wide_data;
168 // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
[email protected]d6e58c6e2009-10-10 20:40:50169 if (base::CodepageToWide(data, charset.c_str(),
170 base::OnStringConversionError::FAIL,
171 &wide_data))
[email protected]f0a51fb52009-03-05 12:46:38172 json_data = WideToUTF8(wide_data);
[email protected]ec9207d32008-09-26 00:51:06173 }
174 }
175
[email protected]257ab712009-04-14 17:16:24176 bool is_keyword_results = (source == keyword_fetcher_.get());
177 SuggestResults* suggest_results = is_keyword_results ?
178 &keyword_suggest_results_ : &default_suggest_results_;
179
[email protected]b4cebf82008-12-29 19:59:08180 if (status.is_success() && response_code == 200) {
181 JSONStringValueSerializer deserializer(json_data);
182 deserializer.set_allow_trailing_comma(true);
183 scoped_ptr<Value> root_val(deserializer.Deserialize(NULL));
[email protected]257ab712009-04-14 17:16:24184 const std::wstring& input_text =
185 is_keyword_results ? keyword_input_text_ : input_.text();
[email protected]b4cebf82008-12-29 19:59:08186 have_suggest_results_ =
[email protected]257ab712009-04-14 17:16:24187 root_val.get() &&
188 ParseSuggestResults(root_val.get(), is_keyword_results, input_text,
189 suggest_results);
[email protected]b4cebf82008-12-29 19:59:08190 }
191
initial.commit09911bf2008-07-26 23:55:29192 ConvertResultsToAutocompleteMatches();
[email protected]257ab712009-04-14 17:16:24193 listener_->OnProviderUpdate(!suggest_results->empty());
initial.commit09911bf2008-07-26 23:55:29194}
195
[email protected]8deeb952008-10-09 18:21:27196void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:29197 // For the minimal_changes case, if we finished the previous query and still
198 // have its results, or are allowed to keep running it, just do that, rather
199 // than starting a new query.
200 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27201 (have_history_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29202 return;
203
204 // We can't keep running any previous query, so halt it.
205 StopHistory();
206
207 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27208 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29209 return;
210
[email protected]257ab712009-04-14 17:16:24211 // Request history for both the keyword and default provider.
212 if (providers_.valid_keyword_provider()) {
213 ScheduleHistoryQuery(providers_.keyword_provider().id(),
214 keyword_input_text_);
215 }
216 if (providers_.valid_default_provider()) {
217 ScheduleHistoryQuery(providers_.default_provider().id(),
218 input_.text());
219 }
initial.commit09911bf2008-07-26 23:55:29220}
221
[email protected]8deeb952008-10-09 18:21:27222void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) {
[email protected]6c85aa02009-02-27 12:08:09223 // Don't send any queries to the server until some time has elapsed after
224 // the last keypress, to avoid flooding the server with requests we are
225 // likely to end up throwing away anyway.
226 static const int kQueryDelayMs = 200;
227
[email protected]83c726482008-09-10 06:36:34228 if (!IsQuerySuitableForSuggest()) {
initial.commit09911bf2008-07-26 23:55:29229 StopSuggest();
230 return;
231 }
232
233 // For the minimal_changes case, if we finished the previous query and still
234 // have its results, or are allowed to keep running it, just do that, rather
235 // than starting a new query.
236 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27237 (have_suggest_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29238 return;
239
240 // We can't keep running any previous query, so halt it.
241 StopSuggest();
242
243 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27244 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29245 return;
246
[email protected]257ab712009-04-14 17:16:24247 // We'll have at least one pending fetch. Set it to 1 now, but the value is
248 // correctly set in Run. As Run isn't invoked immediately we need to set this
249 // now, else we won't think we're waiting on results from the server when we
250 // really are.
251 suggest_results_pending_ = 1;
252
initial.commit09911bf2008-07-26 23:55:29253 // Kick off a timer that will start the URL fetch if it completes before
254 // the user types another character.
[email protected]b547666d2009-04-23 16:37:58255 int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs;
256 timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run);
initial.commit09911bf2008-07-26 23:55:29257}
258
[email protected]83c726482008-09-10 06:36:34259bool SearchProvider::IsQuerySuitableForSuggest() const {
260 // Don't run Suggest when off the record, the engine doesn't support it, or
261 // the user has disabled it.
262 if (profile_->IsOffTheRecord() ||
[email protected]257ab712009-04-14 17:16:24263 (!providers_.valid_suggest_for_keyword_provider() &&
264 !providers_.valid_suggest_for_default_provider()) ||
[email protected]83c726482008-09-10 06:36:34265 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
266 return false;
267
268 // If the input type is URL, we take extra care so that private data in URL
269 // isn't sent to the server.
270 if (input_.type() == AutocompleteInput::URL) {
271 // Don't query the server for URLs that aren't http/https/ftp. Sending
272 // things like file: and data: is both a waste of time and a disclosure of
273 // potentially private, local data.
274 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") &&
275 (input_.scheme() != L"ftp"))
276 return false;
277
278 // Don't leak private data in URL
279 const url_parse::Parsed& parts = input_.parts();
280
281 // Don't send URLs with usernames, queries or refs. Some of these are
282 // private, and the Suggest server is unlikely to have any useful results
283 // for any of them.
284 // Password is optional and may be omitted. Checking username is
285 // sufficient.
286 if (parts.username.is_nonempty() || parts.query.is_nonempty() ||
287 parts.ref.is_nonempty())
288 return false;
289 // Don't send anything for https except hostname and port number.
290 // Hostname and port number are OK because they are visible when TCP
291 // connection is established and the Suggest server may provide some
292 // useful completed URL.
293 if (input_.scheme() == L"https" && parts.path.is_nonempty())
294 return false;
295 }
296
297 return true;
298}
299
initial.commit09911bf2008-07-26 23:55:29300void SearchProvider::StopHistory() {
301 history_request_consumer_.CancelAllRequests();
302 history_request_pending_ = false;
[email protected]257ab712009-04-14 17:16:24303 keyword_history_results_.clear();
304 default_history_results_.clear();
initial.commit09911bf2008-07-26 23:55:29305 have_history_results_ = false;
306}
307
308void SearchProvider::StopSuggest() {
[email protected]257ab712009-04-14 17:16:24309 suggest_results_pending_ = 0;
[email protected]2d316662008-09-03 18:18:14310 timer_.Stop();
[email protected]257ab712009-04-14 17:16:24311 // Stop any in-progress URL fetches.
312 keyword_fetcher_.reset();
313 default_fetcher_.reset();
314 keyword_suggest_results_.clear();
315 default_suggest_results_.clear();
316 keyword_navigation_results_.clear();
317 default_navigation_results_.clear();
initial.commit09911bf2008-07-26 23:55:29318 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29319}
320
[email protected]257ab712009-04-14 17:16:24321void SearchProvider::ScheduleHistoryQuery(TemplateURL::IDType search_id,
322 const std::wstring& text) {
323 DCHECK(!text.empty());
324 HistoryService* const history_service =
325 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
326 HistoryService::Handle request_handle =
327 history_service->GetMostRecentKeywordSearchTerms(
328 search_id, text, static_cast<int>(max_matches()),
329 &history_request_consumer_,
330 NewCallback(this,
331 &SearchProvider::OnGotMostRecentKeywordSearchTerms));
332 history_request_consumer_.SetClientData(history_service, request_handle,
333 search_id);
334 history_request_pending_ = true;
335}
336
initial.commit09911bf2008-07-26 23:55:29337void SearchProvider::OnGotMostRecentKeywordSearchTerms(
338 CancelableRequestProvider::Handle handle,
339 HistoryResults* results) {
[email protected]257ab712009-04-14 17:16:24340 HistoryService* history_service =
341 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
342 DCHECK(history_service);
343 if (providers_.valid_keyword_provider() &&
[email protected]bed9bd6c2009-04-21 17:27:47344 (providers_.keyword_provider().id() ==
[email protected]257ab712009-04-14 17:16:24345 history_request_consumer_.GetClientData(history_service, handle))) {
346 keyword_history_results_ = *results;
347 } else {
348 default_history_results_ = *results;
349 }
[email protected]257ab712009-04-14 17:16:24350
351 if (history_request_consumer_.PendingRequestCount() == 1) {
352 // Requests are removed AFTER the callback is invoked. If the count == 1,
353 // it means no more history requests are pending.
354 history_request_pending_ = false;
355 have_history_results_ = true;
356 }
[email protected]b547666d2009-04-23 16:37:58357
358 ConvertResultsToAutocompleteMatches();
359 listener_->OnProviderUpdate(!results->empty());
initial.commit09911bf2008-07-26 23:55:29360}
361
[email protected]b547666d2009-04-23 16:37:58362URLFetcher* SearchProvider::CreateSuggestFetcher(int id,
363 const TemplateURL& provider,
[email protected]257ab712009-04-14 17:16:24364 const std::wstring& text) {
365 const TemplateURLRef* const suggestions_url = provider.suggestions_url();
366 DCHECK(suggestions_url->SupportsReplacement());
[email protected]b547666d2009-04-23 16:37:58367 URLFetcher* fetcher = URLFetcher::Create(id,
[email protected]7b9f3672009-06-15 18:31:22368 GURL(WideToUTF8(suggestions_url->ReplaceSearchTerms(
[email protected]b547666d2009-04-23 16:37:58369 provider, text, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE,
[email protected]7b9f3672009-06-15 18:31:22370 std::wstring()))),
[email protected]257ab712009-04-14 17:16:24371 URLFetcher::GET, this);
372 fetcher->set_request_context(profile_->GetRequestContext());
373 fetcher->Start();
374 return fetcher;
375}
376
377bool SearchProvider::ParseSuggestResults(Value* root_val,
378 bool is_keyword,
379 const std::wstring& input_text,
380 SuggestResults* suggest_results) {
initial.commit09911bf2008-07-26 23:55:29381 if (!root_val->IsType(Value::TYPE_LIST))
382 return false;
383 ListValue* root_list = static_cast<ListValue*>(root_val);
384
385 Value* query_val;
386 std::wstring query_str;
387 Value* result_val;
388 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
[email protected]257ab712009-04-14 17:16:24389 !query_val->GetAsString(&query_str) || (query_str != input_text) ||
initial.commit09911bf2008-07-26 23:55:29390 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
391 return false;
392
393 ListValue* description_list = NULL;
394 if (root_list->GetSize() > 2) {
395 // 3rd element: Description list.
396 Value* description_val;
397 if (root_list->Get(2, &description_val) &&
398 description_val->IsType(Value::TYPE_LIST))
399 description_list = static_cast<ListValue*>(description_val);
400 }
401
402 // We don't care about the query URL list (the fourth element in the
403 // response) for now.
404
405 // Parse optional data in the results from the Suggest server if any.
406 ListValue* type_list = NULL;
407 // 5th argument: Optional key-value pairs.
408 // TODO: We may iterate the 5th+ arguments of the root_list if any other
409 // optional data are defined.
410 if (root_list->GetSize() > 4) {
411 Value* optional_val;
412 if (root_list->Get(4, &optional_val) &&
413 optional_val->IsType(Value::TYPE_DICTIONARY)) {
414 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
415
416 // Parse Google Suggest specific type extension.
[email protected]8e50b602009-03-03 22:59:43417 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
initial.commit09911bf2008-07-26 23:55:29418 if (dict_val->HasKey(kGoogleSuggestType))
419 dict_val->GetList(kGoogleSuggestType, &type_list);
420 }
421 }
422
423 ListValue* result_list = static_cast<ListValue*>(result_val);
424 for (size_t i = 0; i < result_list->GetSize(); ++i) {
425 Value* suggestion_val;
426 std::wstring suggestion_str;
427 if (!result_list->Get(i, &suggestion_val) ||
428 !suggestion_val->GetAsString(&suggestion_str))
429 return false;
430
431 Value* type_val;
432 std::wstring type_str;
433 if (type_list && type_list->Get(i, &type_val) &&
434 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
435 Value* site_val;
436 std::wstring site_name;
[email protected]257ab712009-04-14 17:16:24437 NavigationResults& navigation_results =
438 is_keyword ? keyword_navigation_results_ :
439 default_navigation_results_;
440 if ((navigation_results.size() < max_matches()) &&
initial.commit09911bf2008-07-26 23:55:29441 description_list && description_list->Get(i, &site_val) &&
442 site_val->IsType(Value::TYPE_STRING) &&
443 site_val->GetAsString(&site_name)) {
[email protected]16afe222009-01-08 18:57:45444 // We can't blindly trust the URL coming from the server to be valid.
445 GURL result_url =
[email protected]1d73faa2009-02-24 19:32:15446 GURL(URLFixerUpper::FixupURL(WideToUTF8(suggestion_str),
447 std::string()));
[email protected]257ab712009-04-14 17:16:24448 if (result_url.is_valid())
449 navigation_results.push_back(NavigationResult(result_url, site_name));
initial.commit09911bf2008-07-26 23:55:29450 }
451 } else {
452 // TODO(kochi): Currently we treat a calculator result as a query, but it
453 // is better to have better presentation for caluculator results.
[email protected]257ab712009-04-14 17:16:24454 if (suggest_results->size() < max_matches())
455 suggest_results->push_back(suggestion_str);
initial.commit09911bf2008-07-26 23:55:29456 }
457 }
458
initial.commit09911bf2008-07-26 23:55:29459 return true;
460}
461
462void SearchProvider::ConvertResultsToAutocompleteMatches() {
463 // Convert all the results to matches and add them to a map, so we can keep
464 // the most relevant match for each result.
465 MatchMap map;
[email protected]257ab712009-04-14 17:16:24466 const Time no_time;
467 int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ?
initial.commit09911bf2008-07-26 23:55:29468 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
469 TemplateURLRef::NO_SUGGESTION_CHOSEN;
[email protected]257ab712009-04-14 17:16:24470 // Keyword what you typed results are handled by the KeywordProvider.
initial.commit09911bf2008-07-26 23:55:29471
[email protected]257ab712009-04-14 17:16:24472 int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
473 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
474 TemplateURLRef::NO_SUGGESTION_CHOSEN;
475 if (providers_.valid_default_provider()) {
476 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
477 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
478 did_not_accept_default_suggestion, false, &map);
initial.commit09911bf2008-07-26 23:55:29479 }
480
[email protected]257ab712009-04-14 17:16:24481 AddHistoryResultsToMap(keyword_history_results_, true,
482 did_not_accept_keyword_suggestion, &map);
483 AddHistoryResultsToMap(default_history_results_, false,
484 did_not_accept_default_suggestion, &map);
485
486 AddSuggestResultsToMap(keyword_suggest_results_, true,
487 did_not_accept_keyword_suggestion, &map);
488 AddSuggestResultsToMap(default_suggest_results_, false,
489 did_not_accept_default_suggestion, &map);
initial.commit09911bf2008-07-26 23:55:29490
491 // Now add the most relevant matches from the map to |matches_|.
492 matches_.clear();
493 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
494 matches_.push_back(i->second);
495
[email protected]257ab712009-04-14 17:16:24496 AddNavigationResultsToMatches(keyword_navigation_results_, true);
497 AddNavigationResultsToMatches(default_navigation_results_, false);
initial.commit09911bf2008-07-26 23:55:29498
499 const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed"
500 std::partial_sort(matches_.begin(),
501 matches_.begin() + std::min(max_total_matches, matches_.size()),
502 matches_.end(), &AutocompleteMatch::MoreRelevant);
503 if (matches_.size() > max_total_matches)
[email protected]a28e95662008-11-12 19:19:02504 matches_.erase(matches_.begin() + max_total_matches, matches_.end());
initial.commit09911bf2008-07-26 23:55:29505
[email protected]cc63dea2008-08-21 20:56:31506 UpdateStarredStateOfMatches();
507
[email protected]6c85aa02009-02-27 12:08:09508 // We're done when both asynchronous subcomponents have finished. We can't
509 // use CancelableRequestConsumer.HasPendingRequests() for history requests
510 // here. A pending request is not cleared until after the completion
511 // callback has returned, but we've reached here from inside that callback.
512 // HasPendingRequests() would therefore return true, and if this is the last
513 // thing left to calculate for this query, we'll never mark the query "done".
[email protected]257ab712009-04-14 17:16:24514 done_ = !history_request_pending_ && !suggest_results_pending_;
515}
516
517void SearchProvider::AddNavigationResultsToMatches(
518 const NavigationResults& navigation_results,
519 bool is_keyword) {
520 if (!navigation_results.empty()) {
521 // TODO(kochi): http://b/1170574 We add only one results for navigational
522 // suggestions. If we can get more useful information about the score,
523 // consider adding more results.
[email protected]52d08b12009-10-19 18:42:36524 const size_t num_results = is_keyword ?
525 keyword_navigation_results_.size() : default_navigation_results_.size();
526 matches_.push_back(NavigationToMatch(navigation_results.front(),
527 CalculateRelevanceForNavigation(num_results, 0, is_keyword),
528 is_keyword));
[email protected]257ab712009-04-14 17:16:24529 }
530}
531
532void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results,
533 bool is_keyword,
534 int did_not_accept_suggestion,
535 MatchMap* map) {
536 for (HistoryResults::const_iterator i(results.begin()); i != results.end();
537 ++i) {
538 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time, is_keyword),
539 AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion,
540 is_keyword, map);
541 }
542}
543
544void SearchProvider::AddSuggestResultsToMap(
545 const SuggestResults& suggest_results,
546 bool is_keyword,
547 int did_not_accept_suggestion,
548 MatchMap* map) {
549 for (size_t i = 0; i < suggest_results.size(); ++i) {
550 AddMatchToMap(suggest_results[i],
[email protected]52d08b12009-10-19 18:42:36551 CalculateRelevanceForSuggestion(suggest_results.size(), i,
[email protected]257ab712009-04-14 17:16:24552 is_keyword),
553 AutocompleteMatch::SEARCH_SUGGEST,
554 static_cast<int>(i), is_keyword, map);
555 }
initial.commit09911bf2008-07-26 23:55:29556}
557
558int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
[email protected]52d08b12009-10-19 18:42:36559 if (providers_.valid_keyword_provider())
560 return 250;
561
initial.commit09911bf2008-07-26 23:55:29562 switch (input_.type()) {
563 case AutocompleteInput::UNKNOWN:
[email protected]52d08b12009-10-19 18:42:36564 case AutocompleteInput::QUERY:
565 case AutocompleteInput::FORCED_QUERY:
566 return 1300;
initial.commit09911bf2008-07-26 23:55:29567
568 case AutocompleteInput::REQUESTED_URL:
[email protected]52d08b12009-10-19 18:42:36569 return 1150;
initial.commit09911bf2008-07-26 23:55:29570
571 case AutocompleteInput::URL:
[email protected]52d08b12009-10-19 18:42:36572 return 850;
initial.commit09911bf2008-07-26 23:55:29573
574 default:
575 NOTREACHED();
576 return 0;
577 }
578}
579
[email protected]257ab712009-04-14 17:16:24580int SearchProvider::CalculateRelevanceForHistory(const Time& time,
581 bool is_keyword) const {
initial.commit09911bf2008-07-26 23:55:29582 // The relevance of past searches falls off over time. This curve is chosen
583 // so that the relevance of a search 15 minutes ago is discounted about 50
584 // points, while the relevance of a search two weeks ago is discounted about
585 // 450 points.
586 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
587 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
588
[email protected]6c85aa02009-02-27 12:08:09589 // Don't let scores go below 0. Negative relevance scores are meaningful in
590 // a different way.
initial.commit09911bf2008-07-26 23:55:29591 int base_score;
[email protected]52d08b12009-10-19 18:42:36592 if (!providers_.is_primary_provider(is_keyword))
593 base_score = 200;
594 else
595 base_score = (input_.type() == AutocompleteInput::URL) ? 750 : 1050;
initial.commit09911bf2008-07-26 23:55:29596 return std::max(0, base_score - score_discount);
597}
598
[email protected]52d08b12009-10-19 18:42:36599int SearchProvider::CalculateRelevanceForSuggestion(size_t num_results,
600 size_t result_number,
601 bool is_keyword) const {
602 DCHECK(result_number < num_results);
603 int base_score;
604 if (!providers_.is_primary_provider(is_keyword))
605 base_score = 100;
606 else
607 base_score = (input_.type() == AutocompleteInput::URL) ? 300 : 600;
608 return base_score +
609 static_cast<int>(num_results - 1 - result_number);
initial.commit09911bf2008-07-26 23:55:29610}
611
[email protected]52d08b12009-10-19 18:42:36612int SearchProvider::CalculateRelevanceForNavigation(size_t num_results,
613 size_t result_number,
614 bool is_keyword) const {
615 DCHECK(result_number < num_results);
initial.commit09911bf2008-07-26 23:55:29616 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
617 // server if possible.
[email protected]52d08b12009-10-19 18:42:36618 return (providers_.is_primary_provider(is_keyword) ? 800 : 150) +
619 static_cast<int>(num_results - 1 - result_number);
initial.commit09911bf2008-07-26 23:55:29620}
621
622void SearchProvider::AddMatchToMap(const std::wstring& query_string,
623 int relevance,
[email protected]4c1fb7ec2008-11-13 00:19:00624 AutocompleteMatch::Type type,
initial.commit09911bf2008-07-26 23:55:29625 int accepted_suggestion,
[email protected]257ab712009-04-14 17:16:24626 bool is_keyword,
initial.commit09911bf2008-07-26 23:55:29627 MatchMap* map) {
[email protected]257ab712009-04-14 17:16:24628 const std::wstring& input_text =
629 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00630 AutocompleteMatch match(this, relevance, false, type);
initial.commit09911bf2008-07-26 23:55:29631 std::vector<size_t> content_param_offsets;
[email protected]257ab712009-04-14 17:16:24632 const TemplateURL& provider = is_keyword ? providers_.keyword_provider() :
633 providers_.default_provider();
[email protected]fb5153c52009-07-31 19:40:33634 // We do intra-string highlighting for suggestions - the suggested segment
635 // will be highlighted, e.g. for input_text = "you" the suggestion may be
636 // "youtube", so we'll bold the "tube" section: you*tube*.
637 if (input_text != query_string) {
638 match.contents.assign(query_string);
639 size_t input_position = match.contents.find(input_text);
640 if (input_position == std::wstring::npos) {
641 // The input text is not a substring of the query string, e.g. input
642 // text is "slasdot" and the query string is "slashdot", so we bold the
643 // whole thing.
644 match.contents_class.push_back(
645 ACMatchClassification(0, ACMatchClassification::MATCH));
[email protected]ec2379162009-06-09 23:58:17646 } else {
[email protected]fb5153c52009-07-31 19:40:33647 // TODO(beng): ACMatchClassification::MATCH now seems to just mean
648 // "bold" this. Consider modifying the terminology.
649 // We don't iterate over the string here annotating all matches because
650 // it looks odd to have every occurrence of a substring that may be as
651 // short as a single character highlighted in a query suggestion result,
652 // e.g. for input text "s" and query string "southwest airlines", it
653 // looks odd if both the first and last s are highlighted.
654 if (input_position != 0) {
655 match.contents_class.push_back(
656 ACMatchClassification(0, ACMatchClassification::NONE));
657 }
658 match.contents_class.push_back(
659 ACMatchClassification(input_position, ACMatchClassification::DIM));
660 size_t next_fragment_position = input_position + input_text.length();
661 if (next_fragment_position < query_string.length()) {
662 match.contents_class.push_back(
663 ACMatchClassification(next_fragment_position,
664 ACMatchClassification::NONE));
665 }
[email protected]ec2379162009-06-09 23:58:17666 }
initial.commit09911bf2008-07-26 23:55:29667 } else {
[email protected]fb5153c52009-07-31 19:40:33668 // Otherwise, we're dealing with the "default search" result which has no
669 // completion, but has the search provider name as the description.
670 match.contents.assign(query_string);
671 match.contents_class.push_back(
672 ACMatchClassification(0, ACMatchClassification::NONE));
673 match.description.assign(l10n_util::GetStringF(
674 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
[email protected]2c33dd22010-02-11 21:46:35675 provider.AdjustedShortNameForLocaleDirection()));
[email protected]fb5153c52009-07-31 19:40:33676 match.description_class.push_back(
677 ACMatchClassification(0, ACMatchClassification::DIM));
initial.commit09911bf2008-07-26 23:55:29678 }
679
680 // When the user forced a query, we need to make sure all the fill_into_edit
681 // values preserve that property. Otherwise, if the user starts editing a
682 // suggestion, non-Search results will suddenly appear.
683 size_t search_start = 0;
684 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
685 match.fill_into_edit.assign(L"?");
686 ++search_start;
687 }
[email protected]c0048b42009-05-04 21:47:17688 if (is_keyword) {
689 match.fill_into_edit.append(providers_.keyword_provider().keyword() + L" ");
690 match.template_url = &providers_.keyword_provider();
691 }
initial.commit09911bf2008-07-26 23:55:29692 match.fill_into_edit.append(query_string);
[email protected]2c33dd22010-02-11 21:46:35693 // Not all suggestions start with the original input.
initial.commit09911bf2008-07-26 23:55:29694 if (!input_.prevent_inline_autocomplete() &&
[email protected]257ab712009-04-14 17:16:24695 !match.fill_into_edit.compare(search_start, input_text.length(),
696 input_text))
697 match.inline_autocomplete_offset = search_start + input_text.length();
initial.commit09911bf2008-07-26 23:55:29698
[email protected]257ab712009-04-14 17:16:24699 const TemplateURLRef* const search_url = provider.url();
initial.commit09911bf2008-07-26 23:55:29700 DCHECK(search_url->SupportsReplacement());
[email protected]7b9f3672009-06-15 18:31:22701 match.destination_url =
702 GURL(WideToUTF8(search_url->ReplaceSearchTerms(provider, query_string,
703 accepted_suggestion,
704 input_text)));
initial.commit09911bf2008-07-26 23:55:29705
706 // Search results don't look like URLs.
[email protected]0bfc29a2009-04-27 16:15:44707 match.transition =
708 is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED;
initial.commit09911bf2008-07-26 23:55:29709
710 // Try to add |match| to |map|. If a match for |query_string| is already in
711 // |map|, replace it if |match| is more relevant.
712 // NOTE: Keep this ToLower() call in sync with url_database.cc.
713 const std::pair<MatchMap::iterator, bool> i = map->insert(
714 std::pair<std::wstring, AutocompleteMatch>(
715 l10n_util::ToLower(query_string), match));
716 // NOTE: We purposefully do a direct relevance comparison here instead of
717 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
718 // first" rather than "items alphabetically first" when the scores are equal.
719 // The only case this matters is when a user has results with the same score
720 // that differ only by capitalization; because the history system returns
721 // results sorted by recency, this means we'll pick the most recent such
722 // result even if the precision of our relevance score is too low to
723 // distinguish the two.
724 if (!i.second && (match.relevance > i.first->second.relevance))
725 i.first->second = match;
726}
727
728AutocompleteMatch SearchProvider::NavigationToMatch(
729 const NavigationResult& navigation,
[email protected]257ab712009-04-14 17:16:24730 int relevance,
731 bool is_keyword) {
732 const std::wstring& input_text =
733 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00734 AutocompleteMatch match(this, relevance, false,
735 AutocompleteMatch::NAVSUGGEST);
initial.commit09911bf2008-07-26 23:55:29736 match.destination_url = navigation.url;
[email protected]e7a5b7872008-12-10 23:52:43737 match.contents = StringForURLDisplay(navigation.url, true);
[email protected]45b2e16d2009-05-29 00:10:17738 if (!url_util::FindAndCompareScheme(WideToUTF8(input_text),
739 chrome::kHttpScheme, NULL))
initial.commit09911bf2008-07-26 23:55:29740 TrimHttpPrefix(&match.contents);
[email protected]257ab712009-04-14 17:16:24741 AutocompleteMatch::ClassifyMatchInString(input_text, match.contents,
initial.commit09911bf2008-07-26 23:55:29742 ACMatchClassification::URL,
743 &match.contents_class);
744
745 match.description = navigation.site_name;
[email protected]257ab712009-04-14 17:16:24746 AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name,
initial.commit09911bf2008-07-26 23:55:29747 ACMatchClassification::NONE,
748 &match.description_class);
749
initial.commit09911bf2008-07-26 23:55:29750 // When the user forced a query, we need to make sure all the fill_into_edit
751 // values preserve that property. Otherwise, if the user starts editing a
752 // suggestion, non-Search results will suddenly appear.
753 if (input_.type() == AutocompleteInput::FORCED_QUERY)
754 match.fill_into_edit.assign(L"?");
755 match.fill_into_edit.append(match.contents);
756 // TODO(pkasting): http://b/1112879 These should perhaps be
757 // inline-autocompletable?
758
759 return match;
760}