blob: e2be1a5e50a6c03da10461d33ee141ed468aebe4 [file] [log] [blame]
license.botbf09a502008-08-24 00:55:551// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
2// 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]68de8b72008-09-09 23:08:139#include "chrome/browser/bookmarks/bookmark_bar_model.h"
initial.commit09911bf2008-07-26 23:55:2910#include "chrome/browser/browser_process.h"
11#include "chrome/browser/google_util.h"
12#include "chrome/browser/profile.h"
13#include "chrome/browser/template_url_model.h"
14#include "chrome/common/json_value_serializer.h"
15#include "chrome/common/l10n_util.h"
16#include "chrome/common/pref_names.h"
17#include "chrome/common/pref_service.h"
18#include "googleurl/src/url_util.h"
19#include "net/base/escape.h"
20
21#include "generated_resources.h"
22
23const int SearchProvider::kQueryDelayMs = 200;
24
25void SearchProvider::Start(const AutocompleteInput& input,
26 bool minimal_changes,
27 bool synchronous_only) {
28 matches_.clear();
29
30 // Can't return search/suggest results for bogus input or if there is no
31 // profile.
32 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) {
33 Stop();
34 return;
35 }
36
37 // Can't search with no default provider.
38 const TemplateURL* const current_default_provider =
39 profile_->GetTemplateURLModel()->GetDefaultSearchProvider();
40 // TODO(pkasting): http://b/1155786 Eventually we should not need all these
41 // checks.
42 if (!current_default_provider || !current_default_provider->url() ||
43 !current_default_provider->url()->SupportsReplacement()) {
44 Stop();
45 return;
46 }
47
48 // If we're still running an old query but have since changed the query text
49 // or the default provider, abort the query.
50 if (!done_ && (!minimal_changes ||
51 (last_default_provider_ != current_default_provider)))
52 Stop();
53
54 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy this.
55 // Nor should we need |last_default_provider_| just to know whether the
56 // provider changed.
57 default_provider_ = *current_default_provider;
58 last_default_provider_ = current_default_provider;
59
60 if (input.text().empty()) {
61 // User typed "?" alone. Give them a placeholder result indicating what
62 // this syntax does.
63 AutocompleteMatch match;
64 static const std::wstring kNoQueryInput(
65 l10n_util::GetString(IDS_AUTOCOMPLETE_NO_QUERY));
66 match.contents.assign(l10n_util::GetStringF(
67 IDS_AUTOCOMPLETE_SEARCH_CONTENTS, default_provider_.short_name(),
68 kNoQueryInput));
69 match.contents_class.push_back(
70 ACMatchClassification(0, ACMatchClassification::DIM));
71 match.type = AutocompleteMatch::SEARCH;
72 matches_.push_back(match);
73 Stop();
74 return;
75 }
76
77 input_ = input;
78
79 StartOrStopHistoryQuery(minimal_changes, synchronous_only);
80 StartOrStopSuggestQuery(minimal_changes, synchronous_only);
81 ConvertResultsToAutocompleteMatches();
82}
83
84void SearchProvider::Run() {
85 // Start a new request with the current input.
86 DCHECK(!done_);
87 const TemplateURLRef* const suggestions_url =
88 default_provider_.suggestions_url();
89 DCHECK(suggestions_url->SupportsReplacement());
90 fetcher_.reset(new URLFetcher(GURL(suggestions_url->ReplaceSearchTerms(
91 default_provider_, input_.text(),
92 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, std::wstring())),
93 URLFetcher::GET, this));
94 fetcher_->set_request_context(profile_->GetRequestContext());
95 fetcher_->Start();
96}
97
98void SearchProvider::Stop() {
99 StopHistory();
100 StopSuggest();
101 done_ = true;
102}
103
104void SearchProvider::OnURLFetchComplete(const URLFetcher* source,
105 const GURL& url,
106 const URLRequestStatus& status,
107 int response_code,
108 const ResponseCookies& cookie,
109 const std::string& data) {
110 DCHECK(!done_);
111 suggest_results_pending_ = false;
112 suggest_results_.clear();
113 navigation_results_.clear();
114 JSONStringValueSerializer deserializer(data);
[email protected]53eaee82008-08-01 01:48:35115 deserializer.set_allow_trailing_comma(true);
initial.commit09911bf2008-07-26 23:55:29116 Value* root_val = NULL;
117 have_suggest_results_ = status.is_success() && (response_code == 200) &&
118 deserializer.Deserialize(&root_val) && ParseSuggestResults(root_val);
119 delete root_val;
120 ConvertResultsToAutocompleteMatches();
121 listener_->OnProviderUpdate(!suggest_results_.empty());
122}
123
124void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes,
125 bool synchronous_only) {
126 // For the minimal_changes case, if we finished the previous query and still
127 // have its results, or are allowed to keep running it, just do that, rather
128 // than starting a new query.
129 if (minimal_changes &&
130 (have_history_results_ || (!done_ && !synchronous_only)))
131 return;
132
133 // We can't keep running any previous query, so halt it.
134 StopHistory();
135
136 // We can't start a new query if we're only allowed synchronous results.
137 if (synchronous_only)
138 return;
139
140 // Start the history query.
141 HistoryService* const history_service =
142 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
143 history_service->GetMostRecentKeywordSearchTerms(default_provider_.id(),
144 input_.text(), static_cast<int>(max_matches()),
145 &history_request_consumer_,
146 NewCallback(this, &SearchProvider::OnGotMostRecentKeywordSearchTerms));
147 history_request_pending_ = true;
148}
149
150void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes,
151 bool synchronous_only) {
[email protected]83c726482008-09-10 06:36:34152 if (!IsQuerySuitableForSuggest()) {
initial.commit09911bf2008-07-26 23:55:29153 StopSuggest();
154 return;
155 }
156
157 // For the minimal_changes case, if we finished the previous query and still
158 // have its results, or are allowed to keep running it, just do that, rather
159 // than starting a new query.
160 if (minimal_changes &&
161 (have_suggest_results_ || (!done_ && !synchronous_only)))
162 return;
163
164 // We can't keep running any previous query, so halt it.
165 StopSuggest();
166
167 // We can't start a new query if we're only allowed synchronous results.
168 if (synchronous_only)
169 return;
170
171 // Kick off a timer that will start the URL fetch if it completes before
172 // the user types another character.
173 suggest_results_pending_ = true;
[email protected]2d316662008-09-03 18:18:14174
175 timer_.Stop();
176 timer_.Start(TimeDelta::FromMilliseconds(kQueryDelayMs), this,
177 &SearchProvider::Run);
initial.commit09911bf2008-07-26 23:55:29178}
179
[email protected]83c726482008-09-10 06:36:34180bool SearchProvider::IsQuerySuitableForSuggest() const {
181 // Don't run Suggest when off the record, the engine doesn't support it, or
182 // the user has disabled it.
183 if (profile_->IsOffTheRecord() ||
184 !default_provider_.suggestions_url() ||
185 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled))
186 return false;
187
188 // If the input type is URL, we take extra care so that private data in URL
189 // isn't sent to the server.
190 if (input_.type() == AutocompleteInput::URL) {
191 // Don't query the server for URLs that aren't http/https/ftp. Sending
192 // things like file: and data: is both a waste of time and a disclosure of
193 // potentially private, local data.
194 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") &&
195 (input_.scheme() != L"ftp"))
196 return false;
197
198 // Don't leak private data in URL
199 const url_parse::Parsed& parts = input_.parts();
200
201 // Don't send URLs with usernames, queries or refs. Some of these are
202 // private, and the Suggest server is unlikely to have any useful results
203 // for any of them.
204 // Password is optional and may be omitted. Checking username is
205 // sufficient.
206 if (parts.username.is_nonempty() || parts.query.is_nonempty() ||
207 parts.ref.is_nonempty())
208 return false;
209 // Don't send anything for https except hostname and port number.
210 // Hostname and port number are OK because they are visible when TCP
211 // connection is established and the Suggest server may provide some
212 // useful completed URL.
213 if (input_.scheme() == L"https" && parts.path.is_nonempty())
214 return false;
215 }
216
217 return true;
218}
219
initial.commit09911bf2008-07-26 23:55:29220void SearchProvider::StopHistory() {
221 history_request_consumer_.CancelAllRequests();
222 history_request_pending_ = false;
223 history_results_.clear();
224 have_history_results_ = false;
225}
226
227void SearchProvider::StopSuggest() {
228 suggest_results_pending_ = false;
[email protected]2d316662008-09-03 18:18:14229 timer_.Stop();
initial.commit09911bf2008-07-26 23:55:29230 fetcher_.reset(); // Stop any in-progress URL fetch.
231 suggest_results_.clear();
232 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29233}
234
235void SearchProvider::OnGotMostRecentKeywordSearchTerms(
236 CancelableRequestProvider::Handle handle,
237 HistoryResults* results) {
238 history_request_pending_ = false;
239 have_history_results_ = true;
240 history_results_ = *results;
241 ConvertResultsToAutocompleteMatches();
242 listener_->OnProviderUpdate(!history_results_.empty());
243}
244
initial.commit09911bf2008-07-26 23:55:29245bool SearchProvider::ParseSuggestResults(Value* root_val) {
246 if (!root_val->IsType(Value::TYPE_LIST))
247 return false;
248 ListValue* root_list = static_cast<ListValue*>(root_val);
249
250 Value* query_val;
251 std::wstring query_str;
252 Value* result_val;
253 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
254 !query_val->GetAsString(&query_str) || (query_str != input_.text()) ||
255 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
256 return false;
257
258 ListValue* description_list = NULL;
259 if (root_list->GetSize() > 2) {
260 // 3rd element: Description list.
261 Value* description_val;
262 if (root_list->Get(2, &description_val) &&
263 description_val->IsType(Value::TYPE_LIST))
264 description_list = static_cast<ListValue*>(description_val);
265 }
266
267 // We don't care about the query URL list (the fourth element in the
268 // response) for now.
269
270 // Parse optional data in the results from the Suggest server if any.
271 ListValue* type_list = NULL;
272 // 5th argument: Optional key-value pairs.
273 // TODO: We may iterate the 5th+ arguments of the root_list if any other
274 // optional data are defined.
275 if (root_list->GetSize() > 4) {
276 Value* optional_val;
277 if (root_list->Get(4, &optional_val) &&
278 optional_val->IsType(Value::TYPE_DICTIONARY)) {
279 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
280
281 // Parse Google Suggest specific type extension.
282 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
283 if (dict_val->HasKey(kGoogleSuggestType))
284 dict_val->GetList(kGoogleSuggestType, &type_list);
285 }
286 }
287
288 ListValue* result_list = static_cast<ListValue*>(result_val);
289 for (size_t i = 0; i < result_list->GetSize(); ++i) {
290 Value* suggestion_val;
291 std::wstring suggestion_str;
292 if (!result_list->Get(i, &suggestion_val) ||
293 !suggestion_val->GetAsString(&suggestion_str))
294 return false;
295
296 Value* type_val;
297 std::wstring type_str;
298 if (type_list && type_list->Get(i, &type_val) &&
299 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
300 Value* site_val;
301 std::wstring site_name;
302 if (navigation_results_.size() < max_matches() &&
303 description_list && description_list->Get(i, &site_val) &&
304 site_val->IsType(Value::TYPE_STRING) &&
305 site_val->GetAsString(&site_name)) {
306 navigation_results_.push_back(NavigationResult(suggestion_str,
307 site_name));
308 }
309 } else {
310 // TODO(kochi): Currently we treat a calculator result as a query, but it
311 // is better to have better presentation for caluculator results.
312 if (suggest_results_.size() < max_matches())
313 suggest_results_.push_back(suggestion_str);
314 }
315 }
316
initial.commit09911bf2008-07-26 23:55:29317 return true;
318}
319
320void SearchProvider::ConvertResultsToAutocompleteMatches() {
321 // Convert all the results to matches and add them to a map, so we can keep
322 // the most relevant match for each result.
323 MatchMap map;
324 const int did_not_accept_suggestion = suggest_results_.empty() ?
325 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
326 TemplateURLRef::NO_SUGGESTION_CHOSEN;
327 const Time no_time;
328 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
329 did_not_accept_suggestion, &map);
330
331 for (HistoryResults::const_iterator i(history_results_.begin());
332 i != history_results_.end(); ++i) {
333 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time),
334 did_not_accept_suggestion, &map);
335 }
336
337 for (size_t i = 0; i < suggest_results_.size(); ++i) {
338 AddMatchToMap(suggest_results_[i], CalculateRelevanceForSuggestion(i),
339 static_cast<int>(i), &map);
340 }
341
342 // Now add the most relevant matches from the map to |matches_|.
343 matches_.clear();
344 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
345 matches_.push_back(i->second);
346
347 if (navigation_results_.size()) {
348 // TODO(kochi): http://b/1170574 We add only one results for navigational
349 // suggestions. If we can get more useful information about the score,
350 // consider adding more results.
351 matches_.push_back(NavigationToMatch(navigation_results_[0],
[email protected]cc63dea2008-08-21 20:56:31352 CalculateRelevanceForNavigation(0)));
initial.commit09911bf2008-07-26 23:55:29353 }
354
355 const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed"
356 std::partial_sort(matches_.begin(),
357 matches_.begin() + std::min(max_total_matches, matches_.size()),
358 matches_.end(), &AutocompleteMatch::MoreRelevant);
359 if (matches_.size() > max_total_matches)
360 matches_.resize(max_total_matches);
361
[email protected]cc63dea2008-08-21 20:56:31362 UpdateStarredStateOfMatches();
363
initial.commit09911bf2008-07-26 23:55:29364 // We're done when both asynchronous subcomponents have finished.
365 // We can't use CancelableRequestConsumer.HasPendingRequests() for
[email protected]cc63dea2008-08-21 20:56:31366 // history requests here. A pending request is not cleared until after the
367 // completion callback has returned, but we've reached here from inside that
368 // callback. HasPendingRequests() would therefore return true, and if this is
369 // the last thing left to calculate for this query, we'll never mark the query
370 // "done".
initial.commit09911bf2008-07-26 23:55:29371 done_ = !history_request_pending_ &&
[email protected]cc63dea2008-08-21 20:56:31372 !suggest_results_pending_;
initial.commit09911bf2008-07-26 23:55:29373}
374
375int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
376 switch (input_.type()) {
377 case AutocompleteInput::UNKNOWN:
378 return 1300;
379
380 case AutocompleteInput::REQUESTED_URL:
381 return 1200;
382
383 case AutocompleteInput::URL:
384 return 850;
385
386 case AutocompleteInput::QUERY:
387 return 1300;
388
389 case AutocompleteInput::FORCED_QUERY:
390 return 1500;
391
392 default:
393 NOTREACHED();
394 return 0;
395 }
396}
397
398int SearchProvider::CalculateRelevanceForHistory(const Time& time) const {
399 // The relevance of past searches falls off over time. This curve is chosen
400 // so that the relevance of a search 15 minutes ago is discounted about 50
401 // points, while the relevance of a search two weeks ago is discounted about
402 // 450 points.
403 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
404 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
405
406 // Don't let scores go below 0. Negative relevance scores are meaningful in a
407 // different way.
408 int base_score;
409 switch (input_.type()) {
410 case AutocompleteInput::UNKNOWN:
411 case AutocompleteInput::REQUESTED_URL:
412 base_score = 1050;
413 break;
414
415 case AutocompleteInput::URL:
416 base_score = 750;
417 break;
418
419 case AutocompleteInput::QUERY:
420 case AutocompleteInput::FORCED_QUERY:
421 base_score = 1250;
422 break;
423
424 default:
425 NOTREACHED();
426 base_score = 0;
427 break;
428 }
429 return std::max(0, base_score - score_discount);
430}
431
432int SearchProvider::CalculateRelevanceForSuggestion(
433 size_t suggestion_number) const {
434 DCHECK(suggestion_number < suggest_results_.size());
435 const int suggestion_value =
436 static_cast<int>(suggest_results_.size() - 1 - suggestion_number);
437 switch (input_.type()) {
438 case AutocompleteInput::UNKNOWN:
439 case AutocompleteInput::REQUESTED_URL:
440 return 600 + suggestion_value;
441
442 case AutocompleteInput::URL:
443 return 300 + suggestion_value;
444
445 case AutocompleteInput::QUERY:
446 case AutocompleteInput::FORCED_QUERY:
447 return 800 + suggestion_value;
448
449 default:
450 NOTREACHED();
451 return 0;
452 }
453}
454
455int SearchProvider::CalculateRelevanceForNavigation(
456 size_t suggestion_number) const {
457 DCHECK(suggestion_number < navigation_results_.size());
458 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
459 // server if possible.
460 switch (input_.type()) {
461 case AutocompleteInput::QUERY:
462 case AutocompleteInput::FORCED_QUERY:
463 return 1000 + static_cast<int>(suggestion_number);
464
465 default:
466 return 800 + static_cast<int>(suggestion_number);
467 }
468}
469
470void SearchProvider::AddMatchToMap(const std::wstring& query_string,
471 int relevance,
472 int accepted_suggestion,
473 MatchMap* map) {
474 AutocompleteMatch match(this, relevance, false);
475 match.type = AutocompleteMatch::SEARCH;
476 std::vector<size_t> content_param_offsets;
477 match.contents.assign(l10n_util::GetStringF(IDS_AUTOCOMPLETE_SEARCH_CONTENTS,
478 default_provider_.short_name(),
479 query_string,
480 &content_param_offsets));
481 if (content_param_offsets.size() == 2) {
482 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1],
483 query_string.length(),
484 match.contents.length(),
485 ACMatchClassification::NONE,
486 &match.contents_class);
487 } else {
488 // |content_param_offsets| should only not be 2 if:
489 // (a) A translator screws up
490 // (b) The strings have been changed and we haven't been rebuilt properly
491 // (c) Some sort of crazy installer error/DLL version mismatch problem that
492 // gets the wrong data out of the locale DLL?
493 // While none of these are supposed to happen, we've seen this get hit in
494 // the wild, so avoid the vector access in the conditional arm above, which
495 // will crash.
496 NOTREACHED();
497 }
498
499 // When the user forced a query, we need to make sure all the fill_into_edit
500 // values preserve that property. Otherwise, if the user starts editing a
501 // suggestion, non-Search results will suddenly appear.
502 size_t search_start = 0;
503 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
504 match.fill_into_edit.assign(L"?");
505 ++search_start;
506 }
507 match.fill_into_edit.append(query_string);
508 // NOTE: All Google suggestions currently start with the original input, but
509 // not all Yahoo! suggestions do.
510 if (!input_.prevent_inline_autocomplete() &&
511 !match.fill_into_edit.compare(search_start, input_.text().length(),
512 input_.text()))
513 match.inline_autocomplete_offset = search_start + input_.text().length();
514
515 const TemplateURLRef* const search_url = default_provider_.url();
516 DCHECK(search_url->SupportsReplacement());
517 match.destination_url = search_url->ReplaceSearchTerms(default_provider_,
518 query_string,
519 accepted_suggestion,
520 input_.text());
521
522 // Search results don't look like URLs.
523 match.transition = PageTransition::GENERATED;
524
525 // Try to add |match| to |map|. If a match for |query_string| is already in
526 // |map|, replace it if |match| is more relevant.
527 // NOTE: Keep this ToLower() call in sync with url_database.cc.
528 const std::pair<MatchMap::iterator, bool> i = map->insert(
529 std::pair<std::wstring, AutocompleteMatch>(
530 l10n_util::ToLower(query_string), match));
531 // NOTE: We purposefully do a direct relevance comparison here instead of
532 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
533 // first" rather than "items alphabetically first" when the scores are equal.
534 // The only case this matters is when a user has results with the same score
535 // that differ only by capitalization; because the history system returns
536 // results sorted by recency, this means we'll pick the most recent such
537 // result even if the precision of our relevance score is too low to
538 // distinguish the two.
539 if (!i.second && (match.relevance > i.first->second.relevance))
540 i.first->second = match;
541}
542
543AutocompleteMatch SearchProvider::NavigationToMatch(
544 const NavigationResult& navigation,
[email protected]cc63dea2008-08-21 20:56:31545 int relevance) {
initial.commit09911bf2008-07-26 23:55:29546 AutocompleteMatch match(this, relevance, false);
547 match.destination_url = navigation.url;
548 match.contents = StringForURLDisplay(GURL(navigation.url), true);
549 // TODO(kochi): Consider moving HistoryURLProvider::TrimHttpPrefix() to some
550 // public utility function.
551 if (!url_util::FindAndCompareScheme(input_.text(), "http", NULL))
552 TrimHttpPrefix(&match.contents);
553 AutocompleteMatch::ClassifyMatchInString(input_.text(), match.contents,
554 ACMatchClassification::URL,
555 &match.contents_class);
556
557 match.description = navigation.site_name;
558 AutocompleteMatch::ClassifyMatchInString(input_.text(), navigation.site_name,
559 ACMatchClassification::NONE,
560 &match.description_class);
561
initial.commit09911bf2008-07-26 23:55:29562 // When the user forced a query, we need to make sure all the fill_into_edit
563 // values preserve that property. Otherwise, if the user starts editing a
564 // suggestion, non-Search results will suddenly appear.
565 if (input_.type() == AutocompleteInput::FORCED_QUERY)
566 match.fill_into_edit.assign(L"?");
567 match.fill_into_edit.append(match.contents);
568 // TODO(pkasting): http://b/1112879 These should perhaps be
569 // inline-autocompletable?
570
571 return match;
572}
573
574// TODO(kochi): This is duplicate from HistoryURLProvider.
575// static
576size_t SearchProvider::TrimHttpPrefix(std::wstring* url) {
577 url_parse::Component scheme;
578 if (!url_util::FindAndCompareScheme(*url, "http", &scheme))
579 return 0; // Not "http".
580
581 // Erase scheme plus up to two slashes.
582 size_t prefix_len = scheme.end() + 1; // "http:"
583 const size_t after_slashes = std::min(url->length(),
584 static_cast<size_t>(scheme.end() + 3));
585 while ((prefix_len < after_slashes) && ((*url)[prefix_len] == L'/'))
586 ++prefix_len;
587 if (prefix_len == url->length())
588 url->clear();
589 else
590 url->erase(url->begin(), url->begin() + prefix_len);
591 return prefix_len;
592}