blob: 2f123d834798ddb8be5a73d21065f0b3c057ef61 [file] [log] [blame]
[email protected]257ab712009-04-14 17:16:241// Copyright (c) 2009 The Chromium Authors. All rights reserved.
license.botbf09a502008-08-24 00:55:552// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
initial.commit09911bf2008-07-26 23:55:294
5#include "chrome/browser/autocomplete/search_provider.h"
6
7#include "base/message_loop.h"
8#include "base/string_util.h"
[email protected]257ab712009-04-14 17:16:249#include "chrome/browser/autocomplete/keyword_provider.h"
initial.commit09911bf2008-07-26 23:55:2910#include "chrome/browser/browser_process.h"
11#include "chrome/browser/google_util.h"
[email protected]f870a322009-01-16 21:47:2712#include "chrome/browser/net/url_fixer_upper.h"
initial.commit09911bf2008-07-26 23:55:2913#include "chrome/browser/profile.h"
[email protected]d54e03a52009-01-16 00:31:0414#include "chrome/browser/search_engines/template_url_model.h"
initial.commit09911bf2008-07-26 23:55:2915#include "chrome/common/json_value_serializer.h"
16#include "chrome/common/l10n_util.h"
17#include "chrome/common/pref_names.h"
18#include "chrome/common/pref_service.h"
[email protected]dcf7d352009-02-26 01:56:0219#include "chrome/common/url_constants.h"
initial.commit09911bf2008-07-26 23:55:2920#include "googleurl/src/url_util.h"
[email protected]34ac8f32009-02-22 23:03:2721#include "grit/generated_resources.h"
initial.commit09911bf2008-07-26 23:55:2922#include "net/base/escape.h"
[email protected]319d9e6f2009-02-18 19:47:2123#include "net/http/http_response_headers.h"
24#include "net/url_request/url_request_status.h"
initial.commit09911bf2008-07-26 23:55:2925
[email protected]e1acf6f2008-10-27 20:43:3326using base::Time;
27using base::TimeDelta;
28
[email protected]b547666d2009-04-23 16:37:5829// static
30const int SearchProvider::kDefaultProviderURLFetcherID = 1;
31// static
32const int SearchProvider::kKeywordProviderURLFetcherID = 2;
33
34// static
35bool SearchProvider::query_suggest_immediately_ = false;
36
[email protected]257ab712009-04-14 17:16:2437void SearchProvider::Providers::Set(const TemplateURL* default_provider,
38 const TemplateURL* keyword_provider) {
39 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy
40 // this. Nor should we need |default_provider_| and |keyword_provider_|
41 // just to know whether the provider changed.
42 default_provider_ = default_provider;
43 if (default_provider)
44 cached_default_provider_ = *default_provider;
45 keyword_provider_ = keyword_provider;
46 if (keyword_provider)
47 cached_keyword_provider_ = *keyword_provider;
48}
49
initial.commit09911bf2008-07-26 23:55:2950void SearchProvider::Start(const AutocompleteInput& input,
[email protected]8deeb952008-10-09 18:21:2751 bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:2952 matches_.clear();
53
[email protected]6c85aa02009-02-27 12:08:0954 // Can't return search/suggest results for bogus input or without a profile.
initial.commit09911bf2008-07-26 23:55:2955 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
56 Stop();
57 return;
58 }
59
[email protected]257ab712009-04-14 17:16:2460 keyword_input_text_.clear();
61 const TemplateURL* keyword_provider =
62 KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input,
63 &keyword_input_text_);
64 if (!TemplateURL::SupportsReplacement(keyword_provider) ||
65 keyword_input_text_.empty()) {
66 keyword_provider = NULL;
67 }
68
69 const TemplateURL* default_provider =
initial.commit09911bf2008-07-26 23:55:2970 profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
[email protected]257ab712009-04-14 17:16:2471 if (!TemplateURL::SupportsReplacement(default_provider))
72 default_provider = NULL;
73
74 if (keyword_provider == default_provider)
75 keyword_provider = NULL; // No use in querying the same provider twice.
76
77 if (!default_provider && !keyword_provider) {
78 // No valid providers.
initial.commit09911bf2008-07-26 23:55:2979 Stop();
80 return;
81 }
82
83 // If we're still running an old query but have since changed the query text
[email protected]257ab712009-04-14 17:16:2484 // or the providers, abort the query.
initial.commit09911bf2008-07-26 23:55:2985 if (!done_ && (!minimal_changes ||
[email protected]257ab712009-04-14 17:16:2486 !providers_.equals(default_provider, keyword_provider))) {
initial.commit09911bf2008-07-26 23:55:2987 Stop();
[email protected]257ab712009-04-14 17:16:2488 }
initial.commit09911bf2008-07-26 23:55:2989
[email protected]257ab712009-04-14 17:16:2490 providers_.Set(default_provider, keyword_provider);
initial.commit09911bf2008-07-26 23:55:2991
92 if (input.text().empty()) {
93 // User typed "?" alone. Give them a placeholder result indicating what
94 // this syntax does.
[email protected]257ab712009-04-14 17:16:2495 if (default_provider) {
96 AutocompleteMatch match(this, 0, false,
97 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED);
98 static const std::wstring kNoQueryInput(
99 l10n_util::GetString(IDS_AUTOCOMPLETE_NO_QUERY));
100 match.contents.assign(l10n_util::GetStringF(
[email protected]bed9bd6c2009-04-21 17:27:47101 IDS_AUTOCOMPLETE_SEARCH_CONTENTS,
102 default_provider->AdjustedShortNameForLocaleDirection(),
[email protected]257ab712009-04-14 17:16:24103 kNoQueryInput));
104 match.contents_class.push_back(
105 ACMatchClassification(0, ACMatchClassification::DIM));
106 matches_.push_back(match);
107 }
initial.commit09911bf2008-07-26 23:55:29108 Stop();
109 return;
110 }
111
112 input_ = input;
113
[email protected]8deeb952008-10-09 18:21:27114 StartOrStopHistoryQuery(minimal_changes);
115 StartOrStopSuggestQuery(minimal_changes);
initial.commit09911bf2008-07-26 23:55:29116 ConvertResultsToAutocompleteMatches();
117}
118
119void SearchProvider::Run() {
120 // Start a new request with the current input.
121 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24122 suggest_results_pending_ = 0;
123 if (providers_.valid_suggest_for_keyword_provider()) {
124 suggest_results_pending_++;
125 keyword_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58126 CreateSuggestFetcher(kKeywordProviderURLFetcherID,
127 providers_.keyword_provider(),
[email protected]257ab712009-04-14 17:16:24128 keyword_input_text_));
129 }
130 if (providers_.valid_suggest_for_default_provider()) {
131 suggest_results_pending_++;
132 default_fetcher_.reset(
[email protected]b547666d2009-04-23 16:37:58133 CreateSuggestFetcher(kDefaultProviderURLFetcherID,
134 providers_.default_provider(), input_.text()));
[email protected]257ab712009-04-14 17:16:24135 }
136 // We should only get here if we have a suggest url for the keyword or default
137 // providers.
138 DCHECK(suggest_results_pending_ > 0);
initial.commit09911bf2008-07-26 23:55:29139}
140
141void SearchProvider::Stop() {
142 StopHistory();
143 StopSuggest();
144 done_ = true;
145}
146
147void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
148 const GURL& url,
149 const URLRequestStatus& status,
150 int response_code,
151 const ResponseCookies& cookie,
152 const std::string& data) {
153 DCHECK(!done_);
[email protected]257ab712009-04-14 17:16:24154 suggest_results_pending_--;
155 DCHECK(suggest_results_pending_ >= 0); // Should never go negative.
[email protected]ec9207d32008-09-26 00:51:06156 const net::HttpResponseHeaders* const response_headers =
157 source->response_headers();
158 std::string json_data(data);
[email protected]6c85aa02009-02-27 12:08:09159 // JSON is supposed to be UTF-8, but some suggest service providers send JSON
160 // files in non-UTF-8 encodings. The actual encoding is usually specified in
161 // the Content-Type header field.
[email protected]ec9207d32008-09-26 00:51:06162 if (response_headers) {
163 std::string charset;
164 if (response_headers->GetCharset(&charset)) {
165 std::wstring wide_data;
166 // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
167 if (CodepageToWide(data, charset.c_str(),
168 OnStringUtilConversionError::FAIL, &wide_data))
[email protected]f0a51fb52009-03-05 12:46:38169 json_data = WideToUTF8(wide_data);
[email protected]ec9207d32008-09-26 00:51:06170 }
171 }
172
[email protected]257ab712009-04-14 17:16:24173 bool is_keyword_results = (source == keyword_fetcher_.get());
174 SuggestResults* suggest_results = is_keyword_results ?
175 &keyword_suggest_results_ : &default_suggest_results_;
176
[email protected]b4cebf82008-12-29 19:59:08177 if (status.is_success() && response_code == 200) {
178 JSONStringValueSerializer deserializer(json_data);
179 deserializer.set_allow_trailing_comma(true);
180 scoped_ptr<Value> root_val(deserializer.Deserialize(NULL));
[email protected]257ab712009-04-14 17:16:24181 const std::wstring& input_text =
182 is_keyword_results ? keyword_input_text_ : input_.text();
[email protected]b4cebf82008-12-29 19:59:08183 have_suggest_results_ =
[email protected]257ab712009-04-14 17:16:24184 root_val.get() &&
185 ParseSuggestResults(root_val.get(), is_keyword_results, input_text,
186 suggest_results);
[email protected]b4cebf82008-12-29 19:59:08187 }
188
initial.commit09911bf2008-07-26 23:55:29189 ConvertResultsToAutocompleteMatches();
[email protected]257ab712009-04-14 17:16:24190 listener_->OnProviderUpdate(!suggest_results->empty());
initial.commit09911bf2008-07-26 23:55:29191}
192
[email protected]8deeb952008-10-09 18:21:27193void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes) {
initial.commit09911bf2008-07-26 23:55:29194 // For the minimal_changes case, if we finished the previous query and still
195 // have its results, or are allowed to keep running it, just do that, rather
196 // than starting a new query.
197 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27198 (have_history_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29199 return;
200
201 // We can't keep running any previous query, so halt it.
202 StopHistory();
203
204 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27205 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29206 return;
207
[email protected]257ab712009-04-14 17:16:24208 // Request history for both the keyword and default provider.
209 if (providers_.valid_keyword_provider()) {
210 ScheduleHistoryQuery(providers_.keyword_provider().id(),
211 keyword_input_text_);
212 }
213 if (providers_.valid_default_provider()) {
214 ScheduleHistoryQuery(providers_.default_provider().id(),
215 input_.text());
216 }
initial.commit09911bf2008-07-26 23:55:29217}
218
[email protected]8deeb952008-10-09 18:21:27219void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) {
[email protected]6c85aa02009-02-27 12:08:09220 // Don't send any queries to the server until some time has elapsed after
221 // the last keypress, to avoid flooding the server with requests we are
222 // likely to end up throwing away anyway.
223 static const int kQueryDelayMs = 200;
224
[email protected]83c726482008-09-10 06:36:34225 if (!IsQuerySuitableForSuggest()) {
initial.commit09911bf2008-07-26 23:55:29226 StopSuggest();
227 return;
228 }
229
230 // For the minimal_changes case, if we finished the previous query and still
231 // have its results, or are allowed to keep running it, just do that, rather
232 // than starting a new query.
233 if (minimal_changes &&
[email protected]8deeb952008-10-09 18:21:27234 (have_suggest_results_ || (!done_ && !input_.synchronous_only())))
initial.commit09911bf2008-07-26 23:55:29235 return;
236
237 // We can't keep running any previous query, so halt it.
238 StopSuggest();
239
240 // We can't start a new query if we're only allowed synchronous results.
[email protected]8deeb952008-10-09 18:21:27241 if (input_.synchronous_only())
initial.commit09911bf2008-07-26 23:55:29242 return;
243
[email protected]257ab712009-04-14 17:16:24244 // We'll have at least one pending fetch. Set it to 1 now, but the value is
245 // correctly set in Run. As Run isn't invoked immediately we need to set this
246 // now, else we won't think we're waiting on results from the server when we
247 // really are.
248 suggest_results_pending_ = 1;
249
initial.commit09911bf2008-07-26 23:55:29250 // Kick off a timer that will start the URL fetch if it completes before
251 // the user types another character.
[email protected]b547666d2009-04-23 16:37:58252 int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs;
253 timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run);
initial.commit09911bf2008-07-26 23:55:29254}
255
[email protected]83c726482008-09-10 06:36:34256bool SearchProvider::IsQuerySuitableForSuggest() const {
257 // Don't run Suggest when off the record, the engine doesn't support it, or
258 // the user has disabled it.
259 if (profile_->IsOffTheRecord() ||
[email protected]257ab712009-04-14 17:16:24260 (!providers_.valid_suggest_for_keyword_provider() &&
261 !providers_.valid_suggest_for_default_provider()) ||
[email protected]83c726482008-09-10 06:36:34262 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
263 return false;
264
265 // If the input type is URL, we take extra care so that private data in URL
266 // isn't sent to the server.
267 if (input_.type() == AutocompleteInput::URL) {
268 // Don't query the server for URLs that aren't http/https/ftp. Sending
269 // things like file: and data: is both a waste of time and a disclosure of
270 // potentially private, local data.
271 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") &&
272 (input_.scheme() != L"ftp"))
273 return false;
274
275 // Don't leak private data in URL
276 const url_parse::Parsed& parts = input_.parts();
277
278 // Don't send URLs with usernames, queries or refs. Some of these are
279 // private, and the Suggest server is unlikely to have any useful results
280 // for any of them.
281 // Password is optional and may be omitted. Checking username is
282 // sufficient.
283 if (parts.username.is_nonempty() || parts.query.is_nonempty() ||
284 parts.ref.is_nonempty())
285 return false;
286 // Don't send anything for https except hostname and port number.
287 // Hostname and port number are OK because they are visible when TCP
288 // connection is established and the Suggest server may provide some
289 // useful completed URL.
290 if (input_.scheme() == L"https" && parts.path.is_nonempty())
291 return false;
292 }
293
294 return true;
295}
296
initial.commit09911bf2008-07-26 23:55:29297void SearchProvider::StopHistory() {
298 history_request_consumer_.CancelAllRequests();
299 history_request_pending_ = false;
[email protected]257ab712009-04-14 17:16:24300 keyword_history_results_.clear();
301 default_history_results_.clear();
initial.commit09911bf2008-07-26 23:55:29302 have_history_results_ = false;
303}
304
305void SearchProvider::StopSuggest() {
[email protected]257ab712009-04-14 17:16:24306 suggest_results_pending_ = 0;
[email protected]2d316662008-09-03 18:18:14307 timer_.Stop();
[email protected]257ab712009-04-14 17:16:24308 // Stop any in-progress URL fetches.
309 keyword_fetcher_.reset();
310 default_fetcher_.reset();
311 keyword_suggest_results_.clear();
312 default_suggest_results_.clear();
313 keyword_navigation_results_.clear();
314 default_navigation_results_.clear();
initial.commit09911bf2008-07-26 23:55:29315 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29316}
317
[email protected]257ab712009-04-14 17:16:24318void SearchProvider::ScheduleHistoryQuery(TemplateURL::IDType search_id,
319 const std::wstring& text) {
320 DCHECK(!text.empty());
321 HistoryService* const history_service =
322 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
323 HistoryService::Handle request_handle =
324 history_service->GetMostRecentKeywordSearchTerms(
325 search_id, text, static_cast<int>(max_matches()),
326 &history_request_consumer_,
327 NewCallback(this,
328 &SearchProvider::OnGotMostRecentKeywordSearchTerms));
329 history_request_consumer_.SetClientData(history_service, request_handle,
330 search_id);
331 history_request_pending_ = true;
332}
333
initial.commit09911bf2008-07-26 23:55:29334void SearchProvider::OnGotMostRecentKeywordSearchTerms(
335 CancelableRequestProvider::Handle handle,
336 HistoryResults* results) {
[email protected]257ab712009-04-14 17:16:24337 HistoryService* history_service =
338 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
339 DCHECK(history_service);
340 if (providers_.valid_keyword_provider() &&
[email protected]bed9bd6c2009-04-21 17:27:47341 (providers_.keyword_provider().id() ==
[email protected]257ab712009-04-14 17:16:24342 history_request_consumer_.GetClientData(history_service, handle))) {
343 keyword_history_results_ = *results;
344 } else {
345 default_history_results_ = *results;
346 }
[email protected]257ab712009-04-14 17:16:24347
348 if (history_request_consumer_.PendingRequestCount() == 1) {
349 // Requests are removed AFTER the callback is invoked. If the count == 1,
350 // it means no more history requests are pending.
351 history_request_pending_ = false;
352 have_history_results_ = true;
353 }
[email protected]b547666d2009-04-23 16:37:58354
355 ConvertResultsToAutocompleteMatches();
356 listener_->OnProviderUpdate(!results->empty());
initial.commit09911bf2008-07-26 23:55:29357}
358
[email protected]b547666d2009-04-23 16:37:58359URLFetcher* SearchProvider::CreateSuggestFetcher(int id,
360 const TemplateURL& provider,
[email protected]257ab712009-04-14 17:16:24361 const std::wstring& text) {
362 const TemplateURLRef* const suggestions_url = provider.suggestions_url();
363 DCHECK(suggestions_url->SupportsReplacement());
[email protected]b547666d2009-04-23 16:37:58364 URLFetcher* fetcher = URLFetcher::Create(id,
365 suggestions_url->ReplaceSearchTerms(
366 provider, text, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE,
367 std::wstring()),
[email protected]257ab712009-04-14 17:16:24368 URLFetcher::GET, this);
369 fetcher->set_request_context(profile_->GetRequestContext());
370 fetcher->Start();
371 return fetcher;
372}
373
374bool SearchProvider::ParseSuggestResults(Value* root_val,
375 bool is_keyword,
376 const std::wstring& input_text,
377 SuggestResults* suggest_results) {
initial.commit09911bf2008-07-26 23:55:29378 if (!root_val->IsType(Value::TYPE_LIST))
379 return false;
380 ListValue* root_list = static_cast<ListValue*>(root_val);
381
382 Value* query_val;
383 std::wstring query_str;
384 Value* result_val;
385 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
[email protected]257ab712009-04-14 17:16:24386 !query_val->GetAsString(&query_str) || (query_str != input_text) ||
initial.commit09911bf2008-07-26 23:55:29387 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
388 return false;
389
390 ListValue* description_list = NULL;
391 if (root_list->GetSize() > 2) {
392 // 3rd element: Description list.
393 Value* description_val;
394 if (root_list->Get(2, &description_val) &&
395 description_val->IsType(Value::TYPE_LIST))
396 description_list = static_cast<ListValue*>(description_val);
397 }
398
399 // We don't care about the query URL list (the fourth element in the
400 // response) for now.
401
402 // Parse optional data in the results from the Suggest server if any.
403 ListValue* type_list = NULL;
404 // 5th argument: Optional key-value pairs.
405 // TODO: We may iterate the 5th+ arguments of the root_list if any other
406 // optional data are defined.
407 if (root_list->GetSize() > 4) {
408 Value* optional_val;
409 if (root_list->Get(4, &optional_val) &&
410 optional_val->IsType(Value::TYPE_DICTIONARY)) {
411 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
412
413 // Parse Google Suggest specific type extension.
[email protected]8e50b602009-03-03 22:59:43414 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
initial.commit09911bf2008-07-26 23:55:29415 if (dict_val->HasKey(kGoogleSuggestType))
416 dict_val->GetList(kGoogleSuggestType, &type_list);
417 }
418 }
419
420 ListValue* result_list = static_cast<ListValue*>(result_val);
421 for (size_t i = 0; i < result_list->GetSize(); ++i) {
422 Value* suggestion_val;
423 std::wstring suggestion_str;
424 if (!result_list->Get(i, &suggestion_val) ||
425 !suggestion_val->GetAsString(&suggestion_str))
426 return false;
427
428 Value* type_val;
429 std::wstring type_str;
430 if (type_list && type_list->Get(i, &type_val) &&
431 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
432 Value* site_val;
433 std::wstring site_name;
[email protected]257ab712009-04-14 17:16:24434 NavigationResults& navigation_results =
435 is_keyword ? keyword_navigation_results_ :
436 default_navigation_results_;
437 if ((navigation_results.size() < max_matches()) &&
initial.commit09911bf2008-07-26 23:55:29438 description_list && description_list->Get(i, &site_val) &&
439 site_val->IsType(Value::TYPE_STRING) &&
440 site_val->GetAsString(&site_name)) {
[email protected]16afe222009-01-08 18:57:45441 // We can't blindly trust the URL coming from the server to be valid.
442 GURL result_url =
[email protected]1d73faa2009-02-24 19:32:15443 GURL(URLFixerUpper::FixupURL(WideToUTF8(suggestion_str),
444 std::string()));
[email protected]257ab712009-04-14 17:16:24445 if (result_url.is_valid())
446 navigation_results.push_back(NavigationResult(result_url, site_name));
initial.commit09911bf2008-07-26 23:55:29447 }
448 } else {
449 // TODO(kochi): Currently we treat a calculator result as a query, but it
450 // is better to have better presentation for caluculator results.
[email protected]257ab712009-04-14 17:16:24451 if (suggest_results->size() < max_matches())
452 suggest_results->push_back(suggestion_str);
initial.commit09911bf2008-07-26 23:55:29453 }
454 }
455
initial.commit09911bf2008-07-26 23:55:29456 return true;
457}
458
459void SearchProvider::ConvertResultsToAutocompleteMatches() {
460 // Convert all the results to matches and add them to a map, so we can keep
461 // the most relevant match for each result.
462 MatchMap map;
[email protected]257ab712009-04-14 17:16:24463 const Time no_time;
464 int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ?
initial.commit09911bf2008-07-26 23:55:29465 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
466 TemplateURLRef::NO_SUGGESTION_CHOSEN;
[email protected]257ab712009-04-14 17:16:24467 // Keyword what you typed results are handled by the KeywordProvider.
initial.commit09911bf2008-07-26 23:55:29468
[email protected]257ab712009-04-14 17:16:24469 int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
470 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
471 TemplateURLRef::NO_SUGGESTION_CHOSEN;
472 if (providers_.valid_default_provider()) {
473 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
474 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
475 did_not_accept_default_suggestion, false, &map);
initial.commit09911bf2008-07-26 23:55:29476 }
477
[email protected]257ab712009-04-14 17:16:24478 AddHistoryResultsToMap(keyword_history_results_, true,
479 did_not_accept_keyword_suggestion, &map);
480 AddHistoryResultsToMap(default_history_results_, false,
481 did_not_accept_default_suggestion, &map);
482
483 AddSuggestResultsToMap(keyword_suggest_results_, true,
484 did_not_accept_keyword_suggestion, &map);
485 AddSuggestResultsToMap(default_suggest_results_, false,
486 did_not_accept_default_suggestion, &map);
initial.commit09911bf2008-07-26 23:55:29487
488 // Now add the most relevant matches from the map to |matches_|.
489 matches_.clear();
490 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
491 matches_.push_back(i->second);
492
[email protected]257ab712009-04-14 17:16:24493 AddNavigationResultsToMatches(keyword_navigation_results_, true);
494 AddNavigationResultsToMatches(default_navigation_results_, false);
initial.commit09911bf2008-07-26 23:55:29495
496 const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed"
497 std::partial_sort(matches_.begin(),
498 matches_.begin() + std::min(max_total_matches, matches_.size()),
499 matches_.end(), &AutocompleteMatch::MoreRelevant);
500 if (matches_.size() > max_total_matches)
[email protected]a28e95662008-11-12 19:19:02501 matches_.erase(matches_.begin() + max_total_matches, matches_.end());
initial.commit09911bf2008-07-26 23:55:29502
[email protected]cc63dea2008-08-21 20:56:31503 UpdateStarredStateOfMatches();
504
[email protected]6c85aa02009-02-27 12:08:09505 // We're done when both asynchronous subcomponents have finished. We can't
506 // use CancelableRequestConsumer.HasPendingRequests() for history requests
507 // here. A pending request is not cleared until after the completion
508 // callback has returned, but we've reached here from inside that callback.
509 // HasPendingRequests() would therefore return true, and if this is the last
510 // thing left to calculate for this query, we'll never mark the query "done".
[email protected]257ab712009-04-14 17:16:24511 done_ = !history_request_pending_ && !suggest_results_pending_;
512}
513
514void SearchProvider::AddNavigationResultsToMatches(
515 const NavigationResults& navigation_results,
516 bool is_keyword) {
517 if (!navigation_results.empty()) {
518 // TODO(kochi): http://b/1170574 We add only one results for navigational
519 // suggestions. If we can get more useful information about the score,
520 // consider adding more results.
521 matches_.push_back(
522 NavigationToMatch(navigation_results.front(),
523 CalculateRelevanceForNavigation(0, is_keyword),
524 is_keyword));
525 }
526}
527
528void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results,
529 bool is_keyword,
530 int did_not_accept_suggestion,
531 MatchMap* map) {
532 for (HistoryResults::const_iterator i(results.begin()); i != results.end();
533 ++i) {
534 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time, is_keyword),
535 AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion,
536 is_keyword, map);
537 }
538}
539
540void SearchProvider::AddSuggestResultsToMap(
541 const SuggestResults& suggest_results,
542 bool is_keyword,
543 int did_not_accept_suggestion,
544 MatchMap* map) {
545 for (size_t i = 0; i < suggest_results.size(); ++i) {
546 AddMatchToMap(suggest_results[i],
547 CalculateRelevanceForSuggestion(suggest_results, i,
548 is_keyword),
549 AutocompleteMatch::SEARCH_SUGGEST,
550 static_cast<int>(i), is_keyword, map);
551 }
initial.commit09911bf2008-07-26 23:55:29552}
553
554int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
555 switch (input_.type()) {
556 case AutocompleteInput::UNKNOWN:
[email protected]257ab712009-04-14 17:16:24557 return providers_.valid_keyword_provider() ? 250 : 1300;
initial.commit09911bf2008-07-26 23:55:29558
559 case AutocompleteInput::REQUESTED_URL:
[email protected]257ab712009-04-14 17:16:24560 return providers_.valid_keyword_provider() ? 250 : 1200;
initial.commit09911bf2008-07-26 23:55:29561
562 case AutocompleteInput::URL:
[email protected]257ab712009-04-14 17:16:24563 return providers_.valid_keyword_provider() ? 250 : 850;
initial.commit09911bf2008-07-26 23:55:29564
565 case AutocompleteInput::QUERY:
[email protected]257ab712009-04-14 17:16:24566 return providers_.valid_keyword_provider() ? 250 : 1300;
initial.commit09911bf2008-07-26 23:55:29567
568 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24569 return providers_.valid_keyword_provider() ? 250 : 1500;
initial.commit09911bf2008-07-26 23:55:29570
571 default:
572 NOTREACHED();
573 return 0;
574 }
575}
576
[email protected]257ab712009-04-14 17:16:24577int SearchProvider::CalculateRelevanceForHistory(const Time& time,
578 bool is_keyword) const {
initial.commit09911bf2008-07-26 23:55:29579 // The relevance of past searches falls off over time. This curve is chosen
580 // so that the relevance of a search 15 minutes ago is discounted about 50
581 // points, while the relevance of a search two weeks ago is discounted about
582 // 450 points.
583 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
584 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
585
[email protected]6c85aa02009-02-27 12:08:09586 // Don't let scores go below 0. Negative relevance scores are meaningful in
587 // a different way.
initial.commit09911bf2008-07-26 23:55:29588 int base_score;
[email protected]257ab712009-04-14 17:16:24589 bool is_primary = providers_.is_primary_provider(is_keyword);
initial.commit09911bf2008-07-26 23:55:29590 switch (input_.type()) {
591 case AutocompleteInput::UNKNOWN:
592 case AutocompleteInput::REQUESTED_URL:
[email protected]257ab712009-04-14 17:16:24593 base_score = is_primary ? 1050 : 200;
initial.commit09911bf2008-07-26 23:55:29594 break;
595
596 case AutocompleteInput::URL:
[email protected]257ab712009-04-14 17:16:24597 base_score = is_primary ? 750 : 200;
initial.commit09911bf2008-07-26 23:55:29598 break;
599
600 case AutocompleteInput::QUERY:
601 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24602 base_score = is_primary ? 1250 : 200;
initial.commit09911bf2008-07-26 23:55:29603 break;
604
605 default:
606 NOTREACHED();
607 base_score = 0;
608 break;
609 }
610 return std::max(0, base_score - score_discount);
611}
612
613int SearchProvider::CalculateRelevanceForSuggestion(
[email protected]257ab712009-04-14 17:16:24614 const SuggestResults& suggest_results,
615 size_t suggestion_number,
616 bool is_keyword) const {
617 DCHECK(suggestion_number < suggest_results.size());
618 bool is_primary = providers_.is_primary_provider(is_keyword);
initial.commit09911bf2008-07-26 23:55:29619 const int suggestion_value =
[email protected]257ab712009-04-14 17:16:24620 static_cast<int>(suggest_results.size() - 1 - suggestion_number);
initial.commit09911bf2008-07-26 23:55:29621 switch (input_.type()) {
622 case AutocompleteInput::UNKNOWN:
623 case AutocompleteInput::REQUESTED_URL:
[email protected]257ab712009-04-14 17:16:24624 return suggestion_value + (is_primary ? 600 : 100);
initial.commit09911bf2008-07-26 23:55:29625
626 case AutocompleteInput::URL:
[email protected]257ab712009-04-14 17:16:24627 return suggestion_value + (is_primary ? 300 : 100);
initial.commit09911bf2008-07-26 23:55:29628
629 case AutocompleteInput::QUERY:
630 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24631 return suggestion_value + (is_primary ? 800 : 100);
initial.commit09911bf2008-07-26 23:55:29632
633 default:
634 NOTREACHED();
635 return 0;
636 }
637}
638
639int SearchProvider::CalculateRelevanceForNavigation(
[email protected]257ab712009-04-14 17:16:24640 size_t suggestion_number,
641 bool is_keyword) const {
642 DCHECK(
643 (is_keyword && suggestion_number < keyword_navigation_results_.size()) ||
644 (!is_keyword && suggestion_number < default_navigation_results_.size()));
initial.commit09911bf2008-07-26 23:55:29645 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
646 // server if possible.
[email protected]257ab712009-04-14 17:16:24647 bool is_primary = providers_.is_primary_provider(is_keyword);
initial.commit09911bf2008-07-26 23:55:29648 switch (input_.type()) {
649 case AutocompleteInput::QUERY:
650 case AutocompleteInput::FORCED_QUERY:
[email protected]257ab712009-04-14 17:16:24651 return static_cast<int>(suggestion_number) + (is_primary ? 1000 : 150);
initial.commit09911bf2008-07-26 23:55:29652
653 default:
[email protected]257ab712009-04-14 17:16:24654 return static_cast<int>(suggestion_number) + (is_primary ? 800 : 150);
initial.commit09911bf2008-07-26 23:55:29655 }
656}
657
658void SearchProvider::AddMatchToMap(const std::wstring& query_string,
659 int relevance,
[email protected]4c1fb7ec2008-11-13 00:19:00660 AutocompleteMatch::Type type,
initial.commit09911bf2008-07-26 23:55:29661 int accepted_suggestion,
[email protected]257ab712009-04-14 17:16:24662 bool is_keyword,
initial.commit09911bf2008-07-26 23:55:29663 MatchMap* map) {
[email protected]257ab712009-04-14 17:16:24664 const std::wstring& input_text =
665 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00666 AutocompleteMatch match(this, relevance, false, type);
initial.commit09911bf2008-07-26 23:55:29667 std::vector<size_t> content_param_offsets;
[email protected]257ab712009-04-14 17:16:24668 const TemplateURL& provider = is_keyword ? providers_.keyword_provider() :
669 providers_.default_provider();
initial.commit09911bf2008-07-26 23:55:29670 match.contents.assign(l10n_util::GetStringF(IDS_AUTOCOMPLETE_SEARCH_CONTENTS,
[email protected]257ab712009-04-14 17:16:24671 provider.short_name(),
initial.commit09911bf2008-07-26 23:55:29672 query_string,
673 &content_param_offsets));
674 if (content_param_offsets.size() == 2) {
675 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1],
676 query_string.length(),
677 match.contents.length(),
678 ACMatchClassification::NONE,
679 &match.contents_class);
680 } else {
681 // |content_param_offsets| should only not be 2 if:
682 // (a) A translator screws up
683 // (b) The strings have been changed and we haven't been rebuilt properly
684 // (c) Some sort of crazy installer error/DLL version mismatch problem that
685 // gets the wrong data out of the locale DLL?
686 // While none of these are supposed to happen, we've seen this get hit in
687 // the wild, so avoid the vector access in the conditional arm above, which
688 // will crash.
689 NOTREACHED();
690 }
691
692 // When the user forced a query, we need to make sure all the fill_into_edit
693 // values preserve that property. Otherwise, if the user starts editing a
694 // suggestion, non-Search results will suddenly appear.
695 size_t search_start = 0;
696 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
697 match.fill_into_edit.assign(L"?");
698 ++search_start;
699 }
700 match.fill_into_edit.append(query_string);
701 // NOTE: All Google suggestions currently start with the original input, but
702 // not all Yahoo! suggestions do.
703 if (!input_.prevent_inline_autocomplete() &&
[email protected]257ab712009-04-14 17:16:24704 !match.fill_into_edit.compare(search_start, input_text.length(),
705 input_text))
706 match.inline_autocomplete_offset = search_start + input_text.length();
initial.commit09911bf2008-07-26 23:55:29707
[email protected]257ab712009-04-14 17:16:24708 const TemplateURLRef* const search_url = provider.url();
initial.commit09911bf2008-07-26 23:55:29709 DCHECK(search_url->SupportsReplacement());
[email protected]257ab712009-04-14 17:16:24710 match.destination_url = search_url->ReplaceSearchTerms(provider,
initial.commit09911bf2008-07-26 23:55:29711 query_string,
712 accepted_suggestion,
[email protected]257ab712009-04-14 17:16:24713 input_text);
initial.commit09911bf2008-07-26 23:55:29714
715 // Search results don't look like URLs.
[email protected]0bfc29a2009-04-27 16:15:44716 match.transition =
717 is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED;
initial.commit09911bf2008-07-26 23:55:29718
719 // Try to add |match| to |map|. If a match for |query_string| is already in
720 // |map|, replace it if |match| is more relevant.
721 // NOTE: Keep this ToLower() call in sync with url_database.cc.
722 const std::pair<MatchMap::iterator, bool> i = map->insert(
723 std::pair<std::wstring, AutocompleteMatch>(
724 l10n_util::ToLower(query_string), match));
725 // NOTE: We purposefully do a direct relevance comparison here instead of
726 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
727 // first" rather than "items alphabetically first" when the scores are equal.
728 // The only case this matters is when a user has results with the same score
729 // that differ only by capitalization; because the history system returns
730 // results sorted by recency, this means we'll pick the most recent such
731 // result even if the precision of our relevance score is too low to
732 // distinguish the two.
733 if (!i.second && (match.relevance > i.first->second.relevance))
734 i.first->second = match;
735}
736
737AutocompleteMatch SearchProvider::NavigationToMatch(
738 const NavigationResult& navigation,
[email protected]257ab712009-04-14 17:16:24739 int relevance,
740 bool is_keyword) {
741 const std::wstring& input_text =
742 is_keyword ? keyword_input_text_ : input_.text();
[email protected]4c1fb7ec2008-11-13 00:19:00743 AutocompleteMatch match(this, relevance, false,
744 AutocompleteMatch::NAVSUGGEST);
initial.commit09911bf2008-07-26 23:55:29745 match.destination_url = navigation.url;
[email protected]e7a5b7872008-12-10 23:52:43746 match.contents = StringForURLDisplay(navigation.url, true);
initial.commit09911bf2008-07-26 23:55:29747 // TODO(kochi): Consider moving HistoryURLProvider::TrimHttpPrefix() to some
748 // public utility function.
[email protected]257ab712009-04-14 17:16:24749 if (!url_util::FindAndCompareScheme(WideToUTF8(input_text), "http", NULL))
initial.commit09911bf2008-07-26 23:55:29750 TrimHttpPrefix(&match.contents);
[email protected]257ab712009-04-14 17:16:24751 AutocompleteMatch::ClassifyMatchInString(input_text, match.contents,
initial.commit09911bf2008-07-26 23:55:29752 ACMatchClassification::URL,
753 &match.contents_class);
754
755 match.description = navigation.site_name;
[email protected]257ab712009-04-14 17:16:24756 AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name,
initial.commit09911bf2008-07-26 23:55:29757 ACMatchClassification::NONE,
758 &match.description_class);
759
initial.commit09911bf2008-07-26 23:55:29760 // When the user forced a query, we need to make sure all the fill_into_edit
761 // values preserve that property. Otherwise, if the user starts editing a
762 // suggestion, non-Search results will suddenly appear.
763 if (input_.type() == AutocompleteInput::FORCED_QUERY)
764 match.fill_into_edit.assign(L"?");
765 match.fill_into_edit.append(match.contents);
766 // TODO(pkasting): http://b/1112879 These should perhaps be
767 // inline-autocompletable?
768
769 return match;
770}
771
772// TODO(kochi): This is duplicate from HistoryURLProvider.
773// static
774size_t SearchProvider::TrimHttpPrefix(std::wstring* url) {
775 url_parse::Component scheme;
[email protected]dcf7d352009-02-26 01:56:02776 if (!url_util::FindAndCompareScheme(WideToUTF8(*url), chrome::kHttpScheme,
777 &scheme))
initial.commit09911bf2008-07-26 23:55:29778 return 0; // Not "http".
779
780 // Erase scheme plus up to two slashes.
781 size_t prefix_len = scheme.end() + 1; // "http:"
782 const size_t after_slashes = std::min(url->length(),
783 static_cast<size_t>(scheme.end() + 3));
784 while ((prefix_len < after_slashes) && ((*url)[prefix_len] == L'/'))
785 ++prefix_len;
786 if (prefix_len == url->length())
787 url->clear();
788 else
789 url->erase(url->begin(), url->begin() + prefix_len);
790 return prefix_len;
791}