OLD | NEW |
| (Empty) |
1 // Copyright 2014 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. | |
4 | |
5 #include "chrome/browser/autocomplete/base_search_provider.h" | |
6 | |
7 #include "base/i18n/case_conversion.h" | |
8 #include "base/strings/string_util.h" | |
9 #include "base/strings/utf_string_conversions.h" | |
10 #include "components/metrics/proto/omnibox_event.pb.h" | |
11 #include "components/metrics/proto/omnibox_input_type.pb.h" | |
12 #include "components/omnibox/autocomplete_provider_delegate.h" | |
13 #include "components/omnibox/autocomplete_provider_listener.h" | |
14 #include "components/omnibox/omnibox_field_trial.h" | |
15 #include "components/search_engines/template_url.h" | |
16 #include "components/search_engines/template_url_prepopulate_data.h" | |
17 #include "components/search_engines/template_url_service.h" | |
18 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" | |
19 #include "net/url_request/url_fetcher.h" | |
20 #include "net/url_request/url_fetcher_delegate.h" | |
21 #include "url/gurl.h" | |
22 | |
23 using metrics::OmniboxEventProto; | |
24 | |
25 // SuggestionDeletionHandler ------------------------------------------------- | |
26 | |
27 // This class handles making requests to the server in order to delete | |
28 // personalized suggestions. | |
29 class SuggestionDeletionHandler : public net::URLFetcherDelegate { | |
30 public: | |
31 typedef base::Callback<void(bool, SuggestionDeletionHandler*)> | |
32 DeletionCompletedCallback; | |
33 | |
34 SuggestionDeletionHandler( | |
35 const std::string& deletion_url, | |
36 net::URLRequestContextGetter* request_context, | |
37 const DeletionCompletedCallback& callback); | |
38 | |
39 virtual ~SuggestionDeletionHandler(); | |
40 | |
41 private: | |
42 // net::URLFetcherDelegate: | |
43 virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; | |
44 | |
45 scoped_ptr<net::URLFetcher> deletion_fetcher_; | |
46 DeletionCompletedCallback callback_; | |
47 | |
48 DISALLOW_COPY_AND_ASSIGN(SuggestionDeletionHandler); | |
49 }; | |
50 | |
51 SuggestionDeletionHandler::SuggestionDeletionHandler( | |
52 const std::string& deletion_url, | |
53 net::URLRequestContextGetter* request_context, | |
54 const DeletionCompletedCallback& callback) : callback_(callback) { | |
55 GURL url(deletion_url); | |
56 DCHECK(url.is_valid()); | |
57 | |
58 deletion_fetcher_.reset(net::URLFetcher::Create( | |
59 BaseSearchProvider::kDeletionURLFetcherID, | |
60 url, | |
61 net::URLFetcher::GET, | |
62 this)); | |
63 deletion_fetcher_->SetRequestContext(request_context); | |
64 deletion_fetcher_->Start(); | |
65 } | |
66 | |
67 SuggestionDeletionHandler::~SuggestionDeletionHandler() { | |
68 } | |
69 | |
70 void SuggestionDeletionHandler::OnURLFetchComplete( | |
71 const net::URLFetcher* source) { | |
72 DCHECK(source == deletion_fetcher_.get()); | |
73 callback_.Run( | |
74 source->GetStatus().is_success() && (source->GetResponseCode() == 200), | |
75 this); | |
76 } | |
77 | |
78 // BaseSearchProvider --------------------------------------------------------- | |
79 | |
80 // static | |
81 const int BaseSearchProvider::kDefaultProviderURLFetcherID = 1; | |
82 const int BaseSearchProvider::kKeywordProviderURLFetcherID = 2; | |
83 const int BaseSearchProvider::kDeletionURLFetcherID = 3; | |
84 | |
85 BaseSearchProvider::BaseSearchProvider( | |
86 TemplateURLService* template_url_service, | |
87 scoped_ptr<AutocompleteProviderDelegate> delegate, | |
88 AutocompleteProvider::Type type) | |
89 : AutocompleteProvider(type), | |
90 template_url_service_(template_url_service), | |
91 delegate_(delegate.Pass()), | |
92 field_trial_triggered_(false), | |
93 field_trial_triggered_in_session_(false) { | |
94 } | |
95 | |
96 // static | |
97 bool BaseSearchProvider::ShouldPrefetch(const AutocompleteMatch& match) { | |
98 return match.GetAdditionalInfo(kShouldPrefetchKey) == kTrue; | |
99 } | |
100 | |
101 // static | |
102 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion( | |
103 const base::string16& suggestion, | |
104 AutocompleteMatchType::Type type, | |
105 bool from_keyword_provider, | |
106 const TemplateURL* template_url, | |
107 const SearchTermsData& search_terms_data) { | |
108 // This call uses a number of default values. For instance, it assumes that | |
109 // if this match is from a keyword provider than the user is in keyword mode. | |
110 return CreateSearchSuggestion( | |
111 NULL, AutocompleteInput(), from_keyword_provider, | |
112 SearchSuggestionParser::SuggestResult( | |
113 suggestion, type, suggestion, base::string16(), base::string16(), | |
114 base::string16(), base::string16(), std::string(), std::string(), | |
115 from_keyword_provider, 0, false, false, base::string16()), | |
116 template_url, search_terms_data, 0, false); | |
117 } | |
118 | |
119 void BaseSearchProvider::DeleteMatch(const AutocompleteMatch& match) { | |
120 DCHECK(match.deletable); | |
121 if (!match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey).empty()) { | |
122 deletion_handlers_.push_back(new SuggestionDeletionHandler( | |
123 match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey), | |
124 delegate_->RequestContext(), | |
125 base::Bind(&BaseSearchProvider::OnDeletionComplete, | |
126 base::Unretained(this)))); | |
127 } | |
128 | |
129 TemplateURL* template_url = | |
130 match.GetTemplateURL(template_url_service_, false); | |
131 // This may be NULL if the template corresponding to the keyword has been | |
132 // deleted or there is no keyword set. | |
133 if (template_url != NULL) { | |
134 delegate_->DeleteMatchingURLsForKeywordFromHistory(template_url->id(), | |
135 match.contents); | |
136 } | |
137 | |
138 // Immediately update the list of matches to show the match was deleted, | |
139 // regardless of whether the server request actually succeeds. | |
140 DeleteMatchFromMatches(match); | |
141 } | |
142 | |
143 void BaseSearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const { | |
144 provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo()); | |
145 metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back(); | |
146 new_entry.set_provider(AsOmniboxEventProviderType()); | |
147 new_entry.set_provider_done(done_); | |
148 std::vector<uint32> field_trial_hashes; | |
149 OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes); | |
150 for (size_t i = 0; i < field_trial_hashes.size(); ++i) { | |
151 if (field_trial_triggered_) | |
152 new_entry.mutable_field_trial_triggered()->Add(field_trial_hashes[i]); | |
153 if (field_trial_triggered_in_session_) { | |
154 new_entry.mutable_field_trial_triggered_in_session()->Add( | |
155 field_trial_hashes[i]); | |
156 } | |
157 } | |
158 } | |
159 | |
160 // static | |
161 const char BaseSearchProvider::kRelevanceFromServerKey[] = | |
162 "relevance_from_server"; | |
163 const char BaseSearchProvider::kShouldPrefetchKey[] = "should_prefetch"; | |
164 const char BaseSearchProvider::kSuggestMetadataKey[] = "suggest_metadata"; | |
165 const char BaseSearchProvider::kDeletionUrlKey[] = "deletion_url"; | |
166 const char BaseSearchProvider::kTrue[] = "true"; | |
167 const char BaseSearchProvider::kFalse[] = "false"; | |
168 | |
169 BaseSearchProvider::~BaseSearchProvider() {} | |
170 | |
171 void BaseSearchProvider::SetDeletionURL(const std::string& deletion_url, | |
172 AutocompleteMatch* match) { | |
173 if (deletion_url.empty()) | |
174 return; | |
175 if (!template_url_service_) | |
176 return; | |
177 GURL url = | |
178 template_url_service_->GetDefaultSearchProvider()->GenerateSearchURL( | |
179 template_url_service_->search_terms_data()); | |
180 url = url.GetOrigin().Resolve(deletion_url); | |
181 if (url.is_valid()) { | |
182 match->RecordAdditionalInfo(BaseSearchProvider::kDeletionUrlKey, | |
183 url.spec()); | |
184 match->deletable = true; | |
185 } | |
186 } | |
187 | |
188 // static | |
189 AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion( | |
190 AutocompleteProvider* autocomplete_provider, | |
191 const AutocompleteInput& input, | |
192 const bool in_keyword_mode, | |
193 const SearchSuggestionParser::SuggestResult& suggestion, | |
194 const TemplateURL* template_url, | |
195 const SearchTermsData& search_terms_data, | |
196 int accepted_suggestion, | |
197 bool append_extra_query_params) { | |
198 AutocompleteMatch match(autocomplete_provider, suggestion.relevance(), false, | |
199 suggestion.type()); | |
200 | |
201 if (!template_url) | |
202 return match; | |
203 match.keyword = template_url->keyword(); | |
204 match.contents = suggestion.match_contents(); | |
205 match.contents_class = suggestion.match_contents_class(); | |
206 match.answer_contents = suggestion.answer_contents(); | |
207 match.answer_type = suggestion.answer_type(); | |
208 if (suggestion.type() == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) { | |
209 match.RecordAdditionalInfo( | |
210 kACMatchPropertyInputText, base::UTF16ToUTF8(input.text())); | |
211 match.RecordAdditionalInfo( | |
212 kACMatchPropertyContentsPrefix, | |
213 base::UTF16ToUTF8(suggestion.match_contents_prefix())); | |
214 match.RecordAdditionalInfo( | |
215 kACMatchPropertyContentsStartIndex, | |
216 static_cast<int>( | |
217 suggestion.suggestion().length() - match.contents.length())); | |
218 } | |
219 | |
220 if (!suggestion.annotation().empty()) | |
221 match.description = suggestion.annotation(); | |
222 | |
223 // suggestion.match_contents() should have already been collapsed. | |
224 match.allowed_to_be_default_match = | |
225 (!in_keyword_mode || suggestion.from_keyword_provider()) && | |
226 (base::CollapseWhitespace(input.text(), false) == | |
227 suggestion.match_contents()); | |
228 | |
229 // When the user forced a query, we need to make sure all the fill_into_edit | |
230 // values preserve that property. Otherwise, if the user starts editing a | |
231 // suggestion, non-Search results will suddenly appear. | |
232 if (input.type() == metrics::OmniboxInputType::FORCED_QUERY) | |
233 match.fill_into_edit.assign(base::ASCIIToUTF16("?")); | |
234 if (suggestion.from_keyword_provider()) | |
235 match.fill_into_edit.append(match.keyword + base::char16(' ')); | |
236 if (!input.prevent_inline_autocomplete() && | |
237 (!in_keyword_mode || suggestion.from_keyword_provider()) && | |
238 StartsWith(suggestion.suggestion(), input.text(), false)) { | |
239 match.inline_autocompletion = | |
240 suggestion.suggestion().substr(input.text().length()); | |
241 match.allowed_to_be_default_match = true; | |
242 } | |
243 match.fill_into_edit.append(suggestion.suggestion()); | |
244 | |
245 const TemplateURLRef& search_url = template_url->url_ref(); | |
246 DCHECK(search_url.SupportsReplacement(search_terms_data)); | |
247 match.search_terms_args.reset( | |
248 new TemplateURLRef::SearchTermsArgs(suggestion.suggestion())); | |
249 match.search_terms_args->original_query = input.text(); | |
250 match.search_terms_args->accepted_suggestion = accepted_suggestion; | |
251 match.search_terms_args->enable_omnibox_start_margin = true; | |
252 match.search_terms_args->suggest_query_params = | |
253 suggestion.suggest_query_params(); | |
254 match.search_terms_args->append_extra_query_params = | |
255 append_extra_query_params; | |
256 // This is the destination URL sans assisted query stats. This must be set | |
257 // so the AutocompleteController can properly de-dupe; the controller will | |
258 // eventually overwrite it before it reaches the user. | |
259 match.destination_url = | |
260 GURL(search_url.ReplaceSearchTerms(*match.search_terms_args.get(), | |
261 search_terms_data)); | |
262 | |
263 // Search results don't look like URLs. | |
264 match.transition = suggestion.from_keyword_provider() ? | |
265 content::PAGE_TRANSITION_KEYWORD : content::PAGE_TRANSITION_GENERATED; | |
266 | |
267 return match; | |
268 } | |
269 | |
270 // static | |
271 bool BaseSearchProvider::ZeroSuggestEnabled( | |
272 const GURL& suggest_url, | |
273 const TemplateURL* template_url, | |
274 OmniboxEventProto::PageClassification page_classification, | |
275 const SearchTermsData& search_terms_data, | |
276 AutocompleteProviderDelegate* delegate) { | |
277 if (!OmniboxFieldTrial::InZeroSuggestFieldTrial()) | |
278 return false; | |
279 | |
280 // Make sure we are sending the suggest request through HTTPS to prevent | |
281 // exposing the current page URL or personalized results without encryption. | |
282 if (!suggest_url.SchemeIs(url::kHttpsScheme)) | |
283 return false; | |
284 | |
285 // Don't show zero suggest on the NTP. | |
286 // TODO(hfung): Experiment with showing MostVisited zero suggest on NTP | |
287 // under the conditions described in crbug.com/305366. | |
288 if ((page_classification == | |
289 OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS) || | |
290 (page_classification == | |
291 OmniboxEventProto::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS)) | |
292 return false; | |
293 | |
294 // Don't run if in incognito mode. | |
295 if (delegate->IsOffTheRecord()) | |
296 return false; | |
297 | |
298 // Don't run if we can't get preferences or search suggest is not enabled. | |
299 if (!delegate->SearchSuggestEnabled()) | |
300 return false; | |
301 | |
302 // Only make the request if we know that the provider supports zero suggest | |
303 // (currently only the prepopulated Google provider). | |
304 if (template_url == NULL || | |
305 !template_url->SupportsReplacement(search_terms_data) || | |
306 TemplateURLPrepopulateData::GetEngineType( | |
307 *template_url, search_terms_data) != SEARCH_ENGINE_GOOGLE) | |
308 return false; | |
309 | |
310 return true; | |
311 } | |
312 | |
313 // static | |
314 bool BaseSearchProvider::CanSendURL( | |
315 const GURL& current_page_url, | |
316 const GURL& suggest_url, | |
317 const TemplateURL* template_url, | |
318 OmniboxEventProto::PageClassification page_classification, | |
319 const SearchTermsData& search_terms_data, | |
320 AutocompleteProviderDelegate* delegate) { | |
321 if (!ZeroSuggestEnabled(suggest_url, template_url, page_classification, | |
322 search_terms_data, delegate)) | |
323 return false; | |
324 | |
325 if (!current_page_url.is_valid()) | |
326 return false; | |
327 | |
328 // Only allow HTTP URLs or HTTPS URLs for the same domain as the search | |
329 // provider. | |
330 if ((current_page_url.scheme() != url::kHttpScheme) && | |
331 ((current_page_url.scheme() != url::kHttpsScheme) || | |
332 !net::registry_controlled_domains::SameDomainOrHost( | |
333 current_page_url, suggest_url, | |
334 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES))) | |
335 return false; | |
336 | |
337 if (!delegate->TabSyncEnabledAndUnencrypted()) | |
338 return false; | |
339 | |
340 return true; | |
341 } | |
342 | |
343 void BaseSearchProvider::AddMatchToMap( | |
344 const SearchSuggestionParser::SuggestResult& result, | |
345 const std::string& metadata, | |
346 int accepted_suggestion, | |
347 bool mark_as_deletable, | |
348 bool in_keyword_mode, | |
349 MatchMap* map) { | |
350 AutocompleteMatch match = CreateSearchSuggestion( | |
351 this, GetInput(result.from_keyword_provider()), in_keyword_mode, result, | |
352 GetTemplateURL(result.from_keyword_provider()), | |
353 template_url_service_->search_terms_data(), accepted_suggestion, | |
354 ShouldAppendExtraParams(result)); | |
355 if (!match.destination_url.is_valid()) | |
356 return; | |
357 match.search_terms_args->bookmark_bar_pinned = delegate_->ShowBookmarkBar(); | |
358 match.RecordAdditionalInfo(kRelevanceFromServerKey, | |
359 result.relevance_from_server() ? kTrue : kFalse); | |
360 match.RecordAdditionalInfo(kShouldPrefetchKey, | |
361 result.should_prefetch() ? kTrue : kFalse); | |
362 SetDeletionURL(result.deletion_url(), &match); | |
363 if (mark_as_deletable) | |
364 match.deletable = true; | |
365 // Metadata is needed only for prefetching queries. | |
366 if (result.should_prefetch()) | |
367 match.RecordAdditionalInfo(kSuggestMetadataKey, metadata); | |
368 | |
369 // Try to add |match| to |map|. If a match for this suggestion is | |
370 // already in |map|, replace it if |match| is more relevant. | |
371 // NOTE: Keep this ToLower() call in sync with url_database.cc. | |
372 MatchKey match_key( | |
373 std::make_pair(base::i18n::ToLower(result.suggestion()), | |
374 match.search_terms_args->suggest_query_params)); | |
375 const std::pair<MatchMap::iterator, bool> i( | |
376 map->insert(std::make_pair(match_key, match))); | |
377 | |
378 bool should_prefetch = result.should_prefetch(); | |
379 if (!i.second) { | |
380 // NOTE: We purposefully do a direct relevance comparison here instead of | |
381 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items | |
382 // added first" rather than "items alphabetically first" when the scores | |
383 // are equal. The only case this matters is when a user has results with | |
384 // the same score that differ only by capitalization; because the history | |
385 // system returns results sorted by recency, this means we'll pick the most | |
386 // recent such result even if the precision of our relevance score is too | |
387 // low to distinguish the two. | |
388 if (match.relevance > i.first->second.relevance) { | |
389 match.duplicate_matches.insert(match.duplicate_matches.end(), | |
390 i.first->second.duplicate_matches.begin(), | |
391 i.first->second.duplicate_matches.end()); | |
392 i.first->second.duplicate_matches.clear(); | |
393 match.duplicate_matches.push_back(i.first->second); | |
394 i.first->second = match; | |
395 } else { | |
396 i.first->second.duplicate_matches.push_back(match); | |
397 if (match.keyword == i.first->second.keyword) { | |
398 // Old and new matches are from the same search provider. It is okay to | |
399 // record one match's prefetch data onto a different match (for the same | |
400 // query string) for the following reasons: | |
401 // 1. Because the suggest server only sends down a query string from | |
402 // which we construct a URL, rather than sending a full URL, and because | |
403 // we construct URLs from query strings in the same way every time, the | |
404 // URLs for the two matches will be the same. Therefore, we won't end up | |
405 // prefetching something the server didn't intend. | |
406 // 2. Presumably the server sets the prefetch bit on a match it things | |
407 // is sufficiently relevant that the user is likely to choose it. | |
408 // Surely setting the prefetch bit on a match of even higher relevance | |
409 // won't violate this assumption. | |
410 should_prefetch |= ShouldPrefetch(i.first->second); | |
411 i.first->second.RecordAdditionalInfo(kShouldPrefetchKey, | |
412 should_prefetch ? kTrue : kFalse); | |
413 if (should_prefetch) | |
414 i.first->second.RecordAdditionalInfo(kSuggestMetadataKey, metadata); | |
415 } | |
416 } | |
417 } | |
418 } | |
419 | |
420 bool BaseSearchProvider::ParseSuggestResults( | |
421 const base::Value& root_val, | |
422 int default_result_relevance, | |
423 bool is_keyword_result, | |
424 SearchSuggestionParser::Results* results) { | |
425 if (!SearchSuggestionParser::ParseSuggestResults( | |
426 root_val, GetInput(is_keyword_result), | |
427 delegate_->SchemeClassifier(), default_result_relevance, | |
428 delegate_->AcceptLanguages(), is_keyword_result, results)) | |
429 return false; | |
430 | |
431 for (std::vector<GURL>::const_iterator it = | |
432 results->answers_image_urls.begin(); | |
433 it != results->answers_image_urls.end(); ++it) | |
434 delegate_->PrefetchImage(*it); | |
435 | |
436 field_trial_triggered_ |= results->field_trial_triggered; | |
437 field_trial_triggered_in_session_ |= results->field_trial_triggered; | |
438 return true; | |
439 } | |
440 | |
441 void BaseSearchProvider::DeleteMatchFromMatches( | |
442 const AutocompleteMatch& match) { | |
443 for (ACMatches::iterator i(matches_.begin()); i != matches_.end(); ++i) { | |
444 // Find the desired match to delete by checking the type and contents. | |
445 // We can't check the destination URL, because the autocomplete controller | |
446 // may have reformulated that. Not that while checking for matching | |
447 // contents works for personalized suggestions, if more match types gain | |
448 // deletion support, this algorithm may need to be re-examined. | |
449 if (i->contents == match.contents && i->type == match.type) { | |
450 matches_.erase(i); | |
451 break; | |
452 } | |
453 } | |
454 } | |
455 | |
456 void BaseSearchProvider::OnDeletionComplete( | |
457 bool success, SuggestionDeletionHandler* handler) { | |
458 RecordDeletionResult(success); | |
459 SuggestionDeletionHandlers::iterator it = std::find( | |
460 deletion_handlers_.begin(), deletion_handlers_.end(), handler); | |
461 DCHECK(it != deletion_handlers_.end()); | |
462 deletion_handlers_.erase(it); | |
463 } | |
OLD | NEW |