blob: 23b6063a577e0081407e6443ae9fb9c25ea1c917 [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]f25387b2008-08-21 15:20:339#include "chrome/browser/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) {
152 // Don't run Suggest when off the record, the engine doesn't support it, or
153 // the user has disabled it. Also don't query the server for URLs that aren't
154 // http/https/ftp. Sending things like file: and data: is both a waste of
155 // time and a disclosure of potentially private, local data.
156 if (profile_->IsOffTheRecord() ||
157 !default_provider_.suggestions_url() ||
158 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled) ||
159 ((input_.type() == AutocompleteInput::URL) &&
160 (input_.scheme() != L"http") && (input_.scheme() != L"https") &&
161 (input_.scheme() != L"ftp"))) {
162 StopSuggest();
163 return;
164 }
165
166 // For the minimal_changes case, if we finished the previous query and still
167 // have its results, or are allowed to keep running it, just do that, rather
168 // than starting a new query.
169 if (minimal_changes &&
170 (have_suggest_results_ || (!done_ && !synchronous_only)))
171 return;
172
173 // We can't keep running any previous query, so halt it.
174 StopSuggest();
175
176 // We can't start a new query if we're only allowed synchronous results.
177 if (synchronous_only)
178 return;
179
180 // Kick off a timer that will start the URL fetch if it completes before
181 // the user types another character.
182 suggest_results_pending_ = true;
183 MessageLoop::current()->timer_manager()->ResetTimer(timer_.get());
184}
185
186void SearchProvider::StopHistory() {
187 history_request_consumer_.CancelAllRequests();
188 history_request_pending_ = false;
189 history_results_.clear();
190 have_history_results_ = false;
191}
192
193void SearchProvider::StopSuggest() {
194 suggest_results_pending_ = false;
195 MessageLoop::current()->timer_manager()->StopTimer(timer_.get());
196 fetcher_.reset(); // Stop any in-progress URL fetch.
197 suggest_results_.clear();
198 have_suggest_results_ = false;
initial.commit09911bf2008-07-26 23:55:29199}
200
201void SearchProvider::OnGotMostRecentKeywordSearchTerms(
202 CancelableRequestProvider::Handle handle,
203 HistoryResults* results) {
204 history_request_pending_ = false;
205 have_history_results_ = true;
206 history_results_ = *results;
207 ConvertResultsToAutocompleteMatches();
208 listener_->OnProviderUpdate(!history_results_.empty());
209}
210
initial.commit09911bf2008-07-26 23:55:29211bool SearchProvider::ParseSuggestResults(Value* root_val) {
212 if (!root_val->IsType(Value::TYPE_LIST))
213 return false;
214 ListValue* root_list = static_cast<ListValue*>(root_val);
215
216 Value* query_val;
217 std::wstring query_str;
218 Value* result_val;
219 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) ||
220 !query_val->GetAsString(&query_str) || (query_str != input_.text()) ||
221 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST))
222 return false;
223
224 ListValue* description_list = NULL;
225 if (root_list->GetSize() > 2) {
226 // 3rd element: Description list.
227 Value* description_val;
228 if (root_list->Get(2, &description_val) &&
229 description_val->IsType(Value::TYPE_LIST))
230 description_list = static_cast<ListValue*>(description_val);
231 }
232
233 // We don't care about the query URL list (the fourth element in the
234 // response) for now.
235
236 // Parse optional data in the results from the Suggest server if any.
237 ListValue* type_list = NULL;
238 // 5th argument: Optional key-value pairs.
239 // TODO: We may iterate the 5th+ arguments of the root_list if any other
240 // optional data are defined.
241 if (root_list->GetSize() > 4) {
242 Value* optional_val;
243 if (root_list->Get(4, &optional_val) &&
244 optional_val->IsType(Value::TYPE_DICTIONARY)) {
245 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val);
246
247 // Parse Google Suggest specific type extension.
248 static const std::wstring kGoogleSuggestType(L"google:suggesttype");
249 if (dict_val->HasKey(kGoogleSuggestType))
250 dict_val->GetList(kGoogleSuggestType, &type_list);
251 }
252 }
253
254 ListValue* result_list = static_cast<ListValue*>(result_val);
255 for (size_t i = 0; i < result_list->GetSize(); ++i) {
256 Value* suggestion_val;
257 std::wstring suggestion_str;
258 if (!result_list->Get(i, &suggestion_val) ||
259 !suggestion_val->GetAsString(&suggestion_str))
260 return false;
261
262 Value* type_val;
263 std::wstring type_str;
264 if (type_list && type_list->Get(i, &type_val) &&
265 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) {
266 Value* site_val;
267 std::wstring site_name;
268 if (navigation_results_.size() < max_matches() &&
269 description_list && description_list->Get(i, &site_val) &&
270 site_val->IsType(Value::TYPE_STRING) &&
271 site_val->GetAsString(&site_name)) {
272 navigation_results_.push_back(NavigationResult(suggestion_str,
273 site_name));
274 }
275 } else {
276 // TODO(kochi): Currently we treat a calculator result as a query, but it
277 // is better to have better presentation for caluculator results.
278 if (suggest_results_.size() < max_matches())
279 suggest_results_.push_back(suggestion_str);
280 }
281 }
282
initial.commit09911bf2008-07-26 23:55:29283 return true;
284}
285
286void SearchProvider::ConvertResultsToAutocompleteMatches() {
287 // Convert all the results to matches and add them to a map, so we can keep
288 // the most relevant match for each result.
289 MatchMap map;
290 const int did_not_accept_suggestion = suggest_results_.empty() ?
291 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
292 TemplateURLRef::NO_SUGGESTION_CHOSEN;
293 const Time no_time;
294 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(),
295 did_not_accept_suggestion, &map);
296
297 for (HistoryResults::const_iterator i(history_results_.begin());
298 i != history_results_.end(); ++i) {
299 AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time),
300 did_not_accept_suggestion, &map);
301 }
302
303 for (size_t i = 0; i < suggest_results_.size(); ++i) {
304 AddMatchToMap(suggest_results_[i], CalculateRelevanceForSuggestion(i),
305 static_cast<int>(i), &map);
306 }
307
308 // Now add the most relevant matches from the map to |matches_|.
309 matches_.clear();
310 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i)
311 matches_.push_back(i->second);
312
313 if (navigation_results_.size()) {
314 // TODO(kochi): http://b/1170574 We add only one results for navigational
315 // suggestions. If we can get more useful information about the score,
316 // consider adding more results.
317 matches_.push_back(NavigationToMatch(navigation_results_[0],
[email protected]cc63dea2008-08-21 20:56:31318 CalculateRelevanceForNavigation(0)));
initial.commit09911bf2008-07-26 23:55:29319 }
320
321 const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed"
322 std::partial_sort(matches_.begin(),
323 matches_.begin() + std::min(max_total_matches, matches_.size()),
324 matches_.end(), &AutocompleteMatch::MoreRelevant);
325 if (matches_.size() > max_total_matches)
326 matches_.resize(max_total_matches);
327
[email protected]cc63dea2008-08-21 20:56:31328 UpdateStarredStateOfMatches();
329
initial.commit09911bf2008-07-26 23:55:29330 // We're done when both asynchronous subcomponents have finished.
331 // We can't use CancelableRequestConsumer.HasPendingRequests() for
[email protected]cc63dea2008-08-21 20:56:31332 // history requests here. A pending request is not cleared until after the
333 // completion callback has returned, but we've reached here from inside that
334 // callback. HasPendingRequests() would therefore return true, and if this is
335 // the last thing left to calculate for this query, we'll never mark the query
336 // "done".
initial.commit09911bf2008-07-26 23:55:29337 done_ = !history_request_pending_ &&
[email protected]cc63dea2008-08-21 20:56:31338 !suggest_results_pending_;
initial.commit09911bf2008-07-26 23:55:29339}
340
341int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
342 switch (input_.type()) {
343 case AutocompleteInput::UNKNOWN:
344 return 1300;
345
346 case AutocompleteInput::REQUESTED_URL:
347 return 1200;
348
349 case AutocompleteInput::URL:
350 return 850;
351
352 case AutocompleteInput::QUERY:
353 return 1300;
354
355 case AutocompleteInput::FORCED_QUERY:
356 return 1500;
357
358 default:
359 NOTREACHED();
360 return 0;
361 }
362}
363
364int SearchProvider::CalculateRelevanceForHistory(const Time& time) const {
365 // The relevance of past searches falls off over time. This curve is chosen
366 // so that the relevance of a search 15 minutes ago is discounted about 50
367 // points, while the relevance of a search two weeks ago is discounted about
368 // 450 points.
369 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.);
370 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3));
371
372 // Don't let scores go below 0. Negative relevance scores are meaningful in a
373 // different way.
374 int base_score;
375 switch (input_.type()) {
376 case AutocompleteInput::UNKNOWN:
377 case AutocompleteInput::REQUESTED_URL:
378 base_score = 1050;
379 break;
380
381 case AutocompleteInput::URL:
382 base_score = 750;
383 break;
384
385 case AutocompleteInput::QUERY:
386 case AutocompleteInput::FORCED_QUERY:
387 base_score = 1250;
388 break;
389
390 default:
391 NOTREACHED();
392 base_score = 0;
393 break;
394 }
395 return std::max(0, base_score - score_discount);
396}
397
398int SearchProvider::CalculateRelevanceForSuggestion(
399 size_t suggestion_number) const {
400 DCHECK(suggestion_number < suggest_results_.size());
401 const int suggestion_value =
402 static_cast<int>(suggest_results_.size() - 1 - suggestion_number);
403 switch (input_.type()) {
404 case AutocompleteInput::UNKNOWN:
405 case AutocompleteInput::REQUESTED_URL:
406 return 600 + suggestion_value;
407
408 case AutocompleteInput::URL:
409 return 300 + suggestion_value;
410
411 case AutocompleteInput::QUERY:
412 case AutocompleteInput::FORCED_QUERY:
413 return 800 + suggestion_value;
414
415 default:
416 NOTREACHED();
417 return 0;
418 }
419}
420
421int SearchProvider::CalculateRelevanceForNavigation(
422 size_t suggestion_number) const {
423 DCHECK(suggestion_number < navigation_results_.size());
424 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest
425 // server if possible.
426 switch (input_.type()) {
427 case AutocompleteInput::QUERY:
428 case AutocompleteInput::FORCED_QUERY:
429 return 1000 + static_cast<int>(suggestion_number);
430
431 default:
432 return 800 + static_cast<int>(suggestion_number);
433 }
434}
435
436void SearchProvider::AddMatchToMap(const std::wstring& query_string,
437 int relevance,
438 int accepted_suggestion,
439 MatchMap* map) {
440 AutocompleteMatch match(this, relevance, false);
441 match.type = AutocompleteMatch::SEARCH;
442 std::vector<size_t> content_param_offsets;
443 match.contents.assign(l10n_util::GetStringF(IDS_AUTOCOMPLETE_SEARCH_CONTENTS,
444 default_provider_.short_name(),
445 query_string,
446 &content_param_offsets));
447 if (content_param_offsets.size() == 2) {
448 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1],
449 query_string.length(),
450 match.contents.length(),
451 ACMatchClassification::NONE,
452 &match.contents_class);
453 } else {
454 // |content_param_offsets| should only not be 2 if:
455 // (a) A translator screws up
456 // (b) The strings have been changed and we haven't been rebuilt properly
457 // (c) Some sort of crazy installer error/DLL version mismatch problem that
458 // gets the wrong data out of the locale DLL?
459 // While none of these are supposed to happen, we've seen this get hit in
460 // the wild, so avoid the vector access in the conditional arm above, which
461 // will crash.
462 NOTREACHED();
463 }
464
465 // When the user forced a query, we need to make sure all the fill_into_edit
466 // values preserve that property. Otherwise, if the user starts editing a
467 // suggestion, non-Search results will suddenly appear.
468 size_t search_start = 0;
469 if (input_.type() == AutocompleteInput::FORCED_QUERY) {
470 match.fill_into_edit.assign(L"?");
471 ++search_start;
472 }
473 match.fill_into_edit.append(query_string);
474 // NOTE: All Google suggestions currently start with the original input, but
475 // not all Yahoo! suggestions do.
476 if (!input_.prevent_inline_autocomplete() &&
477 !match.fill_into_edit.compare(search_start, input_.text().length(),
478 input_.text()))
479 match.inline_autocomplete_offset = search_start + input_.text().length();
480
481 const TemplateURLRef* const search_url = default_provider_.url();
482 DCHECK(search_url->SupportsReplacement());
483 match.destination_url = search_url->ReplaceSearchTerms(default_provider_,
484 query_string,
485 accepted_suggestion,
486 input_.text());
487
488 // Search results don't look like URLs.
489 match.transition = PageTransition::GENERATED;
490
491 // Try to add |match| to |map|. If a match for |query_string| is already in
492 // |map|, replace it if |match| is more relevant.
493 // NOTE: Keep this ToLower() call in sync with url_database.cc.
494 const std::pair<MatchMap::iterator, bool> i = map->insert(
495 std::pair<std::wstring, AutocompleteMatch>(
496 l10n_util::ToLower(query_string), match));
497 // NOTE: We purposefully do a direct relevance comparison here instead of
498 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added
499 // first" rather than "items alphabetically first" when the scores are equal.
500 // The only case this matters is when a user has results with the same score
501 // that differ only by capitalization; because the history system returns
502 // results sorted by recency, this means we'll pick the most recent such
503 // result even if the precision of our relevance score is too low to
504 // distinguish the two.
505 if (!i.second && (match.relevance > i.first->second.relevance))
506 i.first->second = match;
507}
508
509AutocompleteMatch SearchProvider::NavigationToMatch(
510 const NavigationResult& navigation,
[email protected]cc63dea2008-08-21 20:56:31511 int relevance) {
initial.commit09911bf2008-07-26 23:55:29512 AutocompleteMatch match(this, relevance, false);
513 match.destination_url = navigation.url;
514 match.contents = StringForURLDisplay(GURL(navigation.url), true);
515 // TODO(kochi): Consider moving HistoryURLProvider::TrimHttpPrefix() to some
516 // public utility function.
517 if (!url_util::FindAndCompareScheme(input_.text(), "http", NULL))
518 TrimHttpPrefix(&match.contents);
519 AutocompleteMatch::ClassifyMatchInString(input_.text(), match.contents,
520 ACMatchClassification::URL,
521 &match.contents_class);
522
523 match.description = navigation.site_name;
524 AutocompleteMatch::ClassifyMatchInString(input_.text(), navigation.site_name,
525 ACMatchClassification::NONE,
526 &match.description_class);
527
initial.commit09911bf2008-07-26 23:55:29528 // When the user forced a query, we need to make sure all the fill_into_edit
529 // values preserve that property. Otherwise, if the user starts editing a
530 // suggestion, non-Search results will suddenly appear.
531 if (input_.type() == AutocompleteInput::FORCED_QUERY)
532 match.fill_into_edit.assign(L"?");
533 match.fill_into_edit.append(match.contents);
534 // TODO(pkasting): http://b/1112879 These should perhaps be
535 // inline-autocompletable?
536
537 return match;
538}
539
540// TODO(kochi): This is duplicate from HistoryURLProvider.
541// static
542size_t SearchProvider::TrimHttpPrefix(std::wstring* url) {
543 url_parse::Component scheme;
544 if (!url_util::FindAndCompareScheme(*url, "http", &scheme))
545 return 0; // Not "http".
546
547 // Erase scheme plus up to two slashes.
548 size_t prefix_len = scheme.end() + 1; // "http:"
549 const size_t after_slashes = std::min(url->length(),
550 static_cast<size_t>(scheme.end() + 3));
551 while ((prefix_len < after_slashes) && ((*url)[prefix_len] == L'/'))
552 ++prefix_len;
553 if (prefix_len == url->length())
554 url->clear();
555 else
556 url->erase(url->begin(), url->begin() + prefix_len);
557 return prefix_len;
558}
license.botbf09a502008-08-24 00:55:55559