1 // Copyright 2015 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.
5 #include "chrome/browser/android/contextualsearch/contextual_search_delegate.h"
9 #include "base/base64.h"
10 #include "base/command_line.h"
11 #include "base/json/json_string_value_serializer.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/browser/android/proto/client_discourse_context.pb.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/profiles/profile_manager.h"
18 #include "chrome/browser/sync/profile_sync_service.h"
19 #include "chrome/browser/sync/profile_sync_service_factory.h"
20 #include "components/search_engines/template_url_service.h"
21 #include "components/variations/net/variations_http_header_provider.h"
22 #include "components/variations/variations_associated_data.h"
23 #include "content/public/browser/android/content_view_core.h"
24 #include "content/public/browser/web_contents.h"
25 #include "net/base/escape.h"
26 #include "net/url_request/url_fetcher.h"
29 using content::ContentViewCore
;
33 const char kContextualSearchFieldTrialName
[] = "ContextualSearch";
34 const char kContextualSearchSurroundingSizeParamName
[] = "surrounding_size";
35 const char kContextualSearchIcingSurroundingSizeParamName
[] =
36 "icing_surrounding_size";
37 const char kContextualSearchResolverURLParamName
[] = "resolver_url";
38 const char kContextualSearchDoNotSendURLParamName
[] = "do_not_send_url";
39 const char kContextualSearchResponseDisplayTextParam
[] = "display_text";
40 const char kContextualSearchResponseSelectedTextParam
[] = "selected_text";
41 const char kContextualSearchResponseSearchTermParam
[] = "search_term";
42 const char kContextualSearchResponseResolvedTermParam
[] = "resolved_term";
43 const char kContextualSearchPreventPreload
[] = "prevent_preload";
44 const char kContextualSearchMentions
[] = "mentions";
45 const char kContextualSearchServerEndpoint
[] = "_/contextualsearch?";
46 const int kContextualSearchRequestVersion
= 2;
47 const char kContextualSearchResolverUrl
[] =
48 "contextual-search-resolver-url";
49 // The default size of the content surrounding the selection to gather, allowing
50 // room for other parameters.
51 const int kContextualSearchDefaultContentSize
= 1536;
52 const int kContextualSearchDefaultIcingSurroundingSize
= 400;
53 const int kContextualSearchMaxSelection
= 100;
54 // The maximum length of a URL to build.
55 const int kMaxURLSize
= 2048;
56 const char kXssiEscape
[] = ")]}'\n";
57 const char kDiscourseContextHeaderPrefix
[] = "X-Additional-Discourse-Context: ";
58 const char kDoPreventPreloadValue
[] = "1";
60 // The number of characters that should be shown on each side of the selected
62 const int kSurroundingSizeForUI
= 30;
66 // URLFetcher ID, only used for tests: we only have one kind of fetcher.
67 const int ContextualSearchDelegate::kContextualSearchURLFetcherID
= 1;
69 // Handles tasks for the ContextualSearchManager in a separable, testable way.
70 ContextualSearchDelegate::ContextualSearchDelegate(
71 net::URLRequestContextGetter
* url_request_context
,
72 TemplateURLService
* template_url_service
,
73 const ContextualSearchDelegate::SearchTermResolutionCallback
&
75 const ContextualSearchDelegate::SurroundingTextCallback
&
77 const ContextualSearchDelegate::IcingCallback
& icing_callback
)
78 : url_request_context_(url_request_context
),
79 template_url_service_(template_url_service
),
80 search_term_callback_(search_term_callback
),
81 surrounding_callback_(surrounding_callback
),
82 icing_callback_(icing_callback
) {
85 ContextualSearchDelegate::~ContextualSearchDelegate() {
88 void ContextualSearchDelegate::StartSearchTermResolutionRequest(
89 const std::string
& selection
,
90 bool use_resolved_search_term
,
91 content::ContentViewCore
* content_view_core
,
92 bool may_send_base_page_url
) {
93 GatherSurroundingTextWithCallback(
94 selection
, use_resolved_search_term
, content_view_core
,
95 may_send_base_page_url
,
96 base::Bind(&ContextualSearchDelegate::StartSearchTermRequestFromSelection
,
100 void ContextualSearchDelegate::GatherAndSaveSurroundingText(
101 const std::string
& selection
,
102 bool use_resolved_search_term
,
103 content::ContentViewCore
* content_view_core
,
104 bool may_send_base_page_url
) {
105 GatherSurroundingTextWithCallback(
106 selection
, use_resolved_search_term
, content_view_core
,
107 may_send_base_page_url
,
108 base::Bind(&ContextualSearchDelegate::SaveSurroundingText
, AsWeakPtr()));
109 // TODO(donnd): clear the context here, since we're done with it (but risky).
112 void ContextualSearchDelegate::ContinueSearchTermResolutionRequest() {
113 DCHECK(context_
.get());
116 GURL
request_url(BuildRequestUrl());
117 DCHECK(request_url
.is_valid());
119 // Reset will delete any previous fetcher, and we won't get any callback.
120 search_term_fetcher_
.reset(
121 net::URLFetcher::Create(kContextualSearchURLFetcherID
, request_url
,
122 net::URLFetcher::GET
, this).release());
123 search_term_fetcher_
->SetRequestContext(url_request_context_
);
125 // Add Chrome experiment state to the request headers.
126 net::HttpRequestHeaders headers
;
127 variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders(
128 search_term_fetcher_
->GetOriginalURL(),
129 false, // Impossible to be incognito at this point.
132 search_term_fetcher_
->SetExtraRequestHeaders(headers
.ToString());
134 SetDiscourseContextAndAddToHeader(*context_
);
136 search_term_fetcher_
->Start();
139 void ContextualSearchDelegate::OnURLFetchComplete(
140 const net::URLFetcher
* source
) {
141 DCHECK(source
== search_term_fetcher_
.get());
142 int response_code
= source
->GetResponseCode();
143 std::string search_term
;
144 std::string display_text
;
145 std::string alternate_term
;
146 std::string prevent_preload
;
147 int mention_start
= 0;
149 int start_adjust
= 0;
152 if (source
->GetStatus().is_success() && response_code
== 200) {
153 std::string response
;
154 bool has_string_response
= source
->GetResponseAsString(&response
);
155 DCHECK(has_string_response
);
156 if (has_string_response
) {
157 DecodeSearchTermsFromJsonResponse(response
, &search_term
, &display_text
,
158 &alternate_term
, &prevent_preload
,
159 &mention_start
, &mention_end
);
160 if (mention_start
!= 0 || mention_end
!= 0) {
161 // Sanity check that our selection is non-zero and it is less than
162 // 100 characters as that would make contextual search bar hide.
163 // We also check that there is at least one character overlap between
164 // the new and old selection.
165 if (mention_start
>= mention_end
166 || (mention_end
- mention_start
) > kContextualSearchMaxSelection
167 || mention_end
<= context_
->start_offset
168 || mention_start
>= context_
->end_offset
) {
172 start_adjust
= mention_start
- context_
->start_offset
;
173 end_adjust
= mention_end
- context_
->end_offset
;
178 bool is_invalid
= response_code
== net::URLFetcher::RESPONSE_CODE_INVALID
;
179 search_term_callback_
.Run(
180 is_invalid
, response_code
, search_term
, display_text
, alternate_term
,
181 prevent_preload
== kDoPreventPreloadValue
, start_adjust
, end_adjust
);
183 // The ContextualSearchContext is consumed once the request has completed.
187 // TODO(jeremycho): Remove selected_text and base_page_url CGI parameters.
188 GURL
ContextualSearchDelegate::BuildRequestUrl() {
189 // TODO(jeremycho): Confirm this is the right way to handle TemplateURL fails.
190 if (!template_url_service_
||
191 !template_url_service_
->GetDefaultSearchProvider()) {
195 std::string
selected_text_escaped(
196 net::EscapeQueryParamValue(context_
->selected_text
, true));
197 std::string
base_page_url_escaped(
198 net::EscapeQueryParamValue(context_
->page_url
.spec(), true));
199 bool use_resolved_search_term
= context_
->use_resolved_search_term
;
201 // If the request is too long, don't include the base-page URL.
202 std::string request
= GetSearchTermResolutionUrlString(
203 selected_text_escaped
, base_page_url_escaped
, use_resolved_search_term
);
204 if (request
.length() >= kMaxURLSize
) {
205 request
= GetSearchTermResolutionUrlString(
206 selected_text_escaped
, "", use_resolved_search_term
);
208 return GURL(request
);
211 std::string
ContextualSearchDelegate::GetSearchTermResolutionUrlString(
212 const std::string
& selected_text
,
213 const std::string
& base_page_url
,
214 const bool use_resolved_search_term
) {
215 TemplateURL
* template_url
= template_url_service_
->GetDefaultSearchProvider();
217 TemplateURLRef::SearchTermsArgs search_terms_args
=
218 TemplateURLRef::SearchTermsArgs(base::string16());
220 TemplateURLRef::SearchTermsArgs::ContextualSearchParams
params(
221 kContextualSearchRequestVersion
,
224 use_resolved_search_term
);
226 search_terms_args
.contextual_search_params
= params
;
229 template_url
->contextual_search_url_ref().ReplaceSearchTerms(
231 template_url_service_
->search_terms_data(),
234 // The switch/param should be the URL up to and including the endpoint.
235 std::string replacement_url
;
236 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
237 kContextualSearchResolverUrl
)) {
239 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
240 kContextualSearchResolverUrl
);
242 std::string param_value
= variations::GetVariationParamValue(
243 kContextualSearchFieldTrialName
, kContextualSearchResolverURLParamName
);
244 if (!param_value
.empty()) replacement_url
= param_value
;
247 // If a replacement URL was specified above, do the substitution.
248 if (!replacement_url
.empty()) {
249 size_t pos
= request
.find(kContextualSearchServerEndpoint
);
250 if (pos
!= std::string::npos
) {
251 request
.replace(0, pos
+ strlen(kContextualSearchServerEndpoint
),
258 void ContextualSearchDelegate::GatherSurroundingTextWithCallback(
259 const std::string
& selection
,
260 bool use_resolved_search_term
,
261 content::ContentViewCore
* content_view_core
,
262 bool may_send_base_page_url
,
263 HandleSurroundingsCallback callback
) {
264 // Immediately cancel any request that's in flight, since we're building a new
265 // context (and the response disposes of any existing context).
266 search_term_fetcher_
.reset();
267 // Decide if the URL should be sent with the context.
268 GURL
page_url(content_view_core
->GetWebContents()->GetURL());
270 if (may_send_base_page_url
&&
271 CanSendPageURL(page_url
, ProfileManager::GetActiveUserProfile(),
272 template_url_service_
)) {
273 url_to_send
= page_url
;
275 std::string
encoding(content_view_core
->GetWebContents()->GetEncoding());
276 context_
.reset(new ContextualSearchContext(
277 selection
, use_resolved_search_term
, url_to_send
, encoding
));
278 content_view_core
->RequestTextSurroundingSelection(
279 GetSearchTermSurroundingSize(), callback
);
282 void ContextualSearchDelegate::StartSearchTermRequestFromSelection(
283 const base::string16
& surrounding_text
,
286 // TODO(donnd): figure out how to gather text surrounding the selection
287 // for other purposes too: e.g. to determine if we should select the
288 // word where the user tapped.
289 DCHECK(context_
.get());
290 SaveSurroundingText(surrounding_text
, start_offset
, end_offset
);
291 SendSurroundingText(kSurroundingSizeForUI
);
292 ContinueSearchTermResolutionRequest();
295 void ContextualSearchDelegate::SaveSurroundingText(
296 const base::string16
& surrounding_text
,
299 DCHECK(context_
.get());
300 // Sometimes the surroundings are 0, 0, '', so fall back on the selection.
301 // See crbug.com/393100.
302 if (start_offset
== 0 && end_offset
== 0 && surrounding_text
.length() == 0) {
303 context_
->surrounding_text
= base::UTF8ToUTF16(context_
->selected_text
);
304 context_
->start_offset
= 0;
305 context_
->end_offset
= context_
->selected_text
.length();
307 context_
->surrounding_text
= surrounding_text
;
308 context_
->start_offset
= start_offset
;
309 context_
->end_offset
= end_offset
;
312 // Call the Icing callback, unless it has been disabled.
313 int icing_surrounding_size
= GetIcingSurroundingSize();
314 size_t selection_start
= context_
->start_offset
;
315 size_t selection_end
= context_
->end_offset
;
316 if (icing_surrounding_size
>= 0 && selection_start
< selection_end
) {
317 int icing_padding_each_side
= icing_surrounding_size
/ 2;
318 base::string16 icing_surrounding_text
= SurroundingTextForIcing(
319 context_
->surrounding_text
, icing_padding_each_side
, &selection_start
,
321 if (selection_start
< selection_end
)
322 icing_callback_
.Run(context_
->encoding
, icing_surrounding_text
,
323 selection_start
, selection_end
);
327 void ContextualSearchDelegate::SendSurroundingText(int max_surrounding_chars
) {
328 const base::string16 surrounding
= context_
->surrounding_text
;
330 // Determine the text before the selection.
331 int start_position
= std::max(
332 0, context_
->start_offset
- max_surrounding_chars
);
333 int num_before_characters
=
334 std::min(context_
->start_offset
, max_surrounding_chars
);
335 base::string16 before_text
=
336 surrounding
.substr(start_position
, num_before_characters
);
338 // Determine the text after the selection.
339 int surrounding_size
= surrounding
.size(); // Cast to int.
340 int num_after_characters
= std::min(
341 surrounding_size
- context_
->end_offset
, max_surrounding_chars
);
342 base::string16 after_text
= surrounding
.substr(
343 context_
->end_offset
, num_after_characters
);
345 base::TrimWhitespace(before_text
, base::TRIM_ALL
, &before_text
);
346 base::TrimWhitespace(after_text
, base::TRIM_ALL
, &after_text
);
347 surrounding_callback_
.Run(UTF16ToUTF8(before_text
), UTF16ToUTF8(after_text
));
350 void ContextualSearchDelegate::SetDiscourseContextAndAddToHeader(
351 const ContextualSearchContext
& context
) {
352 discourse_context::ClientDiscourseContext proto
;
353 discourse_context::Display
* display
= proto
.add_display();
354 display
->set_uri(context
.page_url
.spec());
356 discourse_context::Media
* media
= display
->mutable_media();
357 media
->set_mime_type(context
.encoding
);
359 discourse_context::Selection
* selection
= display
->mutable_selection();
360 selection
->set_content(UTF16ToUTF8(context
.surrounding_text
));
361 selection
->set_start(context
.start_offset
);
362 selection
->set_end(context
.end_offset
);
363 selection
->set_is_uri_encoded(false);
365 std::string serialized
;
366 proto
.SerializeToString(&serialized
);
368 std::string encoded_context
;
369 base::Base64Encode(serialized
, &encoded_context
);
370 // The server memoizer expects a web-safe encoding.
371 std::replace(encoded_context
.begin(), encoded_context
.end(), '+', '-');
372 std::replace(encoded_context
.begin(), encoded_context
.end(), '/', '_');
373 search_term_fetcher_
->AddExtraRequestHeader(
374 kDiscourseContextHeaderPrefix
+ encoded_context
);
377 bool ContextualSearchDelegate::CanSendPageURL(
378 const GURL
& current_page_url
,
380 TemplateURLService
* template_url_service
) {
381 // Check whether there is a Finch parameter preventing us from sending the
383 std::string param_value
= variations::GetVariationParamValue(
384 kContextualSearchFieldTrialName
, kContextualSearchDoNotSendURLParamName
);
385 if (!param_value
.empty())
388 // Ensure that the default search provider is Google.
389 TemplateURL
* default_search_provider
=
390 template_url_service
->GetDefaultSearchProvider();
391 bool is_default_search_provider_google
=
392 default_search_provider
&&
393 default_search_provider
->url_ref().HasGoogleBaseURLs(
394 template_url_service
->search_terms_data());
395 if (!is_default_search_provider_google
)
398 // Only allow HTTP URLs or HTTPS URLs.
399 if (current_page_url
.scheme() != url::kHttpScheme
&&
400 (current_page_url
.scheme() != url::kHttpsScheme
))
403 // Check that the user has sync enabled, is logged in, and syncs their Chrome
405 ProfileSyncService
* service
=
406 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile
);
407 sync_driver::SyncPrefs
sync_prefs(profile
->GetPrefs());
408 if (service
== NULL
|| !service
->CanSyncStart() ||
409 !sync_prefs
.GetPreferredDataTypes(syncer::UserTypes())
410 .Has(syncer::PROXY_TABS
) ||
411 !service
->GetActiveDataTypes().Has(syncer::HISTORY_DELETE_DIRECTIVES
)) {
418 // Decodes the given response from the search term resolution request and sets
419 // the value of the given parameters.
420 void ContextualSearchDelegate::DecodeSearchTermsFromJsonResponse(
421 const std::string
& response
,
422 std::string
* search_term
,
423 std::string
* display_text
,
424 std::string
* alternate_term
,
425 std::string
* prevent_preload
,
428 bool contains_xssi_escape
= response
.find(kXssiEscape
) == 0;
429 const std::string
& proper_json
=
430 contains_xssi_escape
? response
.substr(strlen(kXssiEscape
)) : response
;
431 JSONStringValueDeserializer
deserializer(proper_json
);
432 scoped_ptr
<base::Value
> root(deserializer
.Deserialize(NULL
, NULL
));
434 if (root
.get() != NULL
&& root
->IsType(base::Value::TYPE_DICTIONARY
)) {
435 base::DictionaryValue
* dict
=
436 static_cast<base::DictionaryValue
*>(root
.get());
437 dict
->GetString(kContextualSearchPreventPreload
, prevent_preload
);
438 dict
->GetString(kContextualSearchResponseSearchTermParam
, search_term
);
439 // For the display_text, if not present fall back to the "search_term".
440 if (!dict
->GetString(kContextualSearchResponseDisplayTextParam
,
442 *display_text
= *search_term
;
444 // Extract mentions for selection expansion.
445 base::ListValue
* mentions_list
;
446 dict
->GetList(kContextualSearchMentions
, &mentions_list
);
447 if (mentions_list
!= NULL
&& mentions_list
->GetSize() >= 2)
448 ExtractMentionsStartEnd(*mentions_list
, mention_start
, mention_end
);
449 // If either the selected text or the resolved term is not the search term,
450 // use it as the alternate term.
451 std::string selected_text
;
452 dict
->GetString(kContextualSearchResponseSelectedTextParam
, &selected_text
);
453 if (selected_text
!= *search_term
) {
454 *alternate_term
= selected_text
;
456 std::string resolved_term
;
457 dict
->GetString(kContextualSearchResponseResolvedTermParam
,
459 if (resolved_term
!= *search_term
) {
460 *alternate_term
= resolved_term
;
466 // Returns the size of the surroundings to be sent to the server for search term
468 int ContextualSearchDelegate::GetSearchTermSurroundingSize() {
469 const std::string param_value
= variations::GetVariationParamValue(
470 kContextualSearchFieldTrialName
,
471 kContextualSearchSurroundingSizeParamName
);
473 if (!param_value
.empty() && base::StringToInt(param_value
, ¶m_length
))
475 return kContextualSearchDefaultContentSize
;
478 // Extract the Start/End of the mentions in the surrounding text
479 // for selection-expansion.
480 void ContextualSearchDelegate::ExtractMentionsStartEnd(
481 const base::ListValue
& mentions_list
,
485 if (mentions_list
.GetInteger(0, &int_value
))
486 *startResult
= std::max(0, int_value
);
487 if (mentions_list
.GetInteger(1, &int_value
))
488 *endResult
= std::max(0, int_value
);
491 // Returns the size of the surroundings to be sent to Icing.
492 int ContextualSearchDelegate::GetIcingSurroundingSize() {
493 std::string param_string
= variations::GetVariationParamValue(
494 kContextualSearchFieldTrialName
,
495 kContextualSearchIcingSurroundingSizeParamName
);
496 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
497 kContextualSearchIcingSurroundingSizeParamName
)) {
498 param_string
= base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
499 kContextualSearchIcingSurroundingSizeParamName
);
502 if (!param_string
.empty() && base::StringToInt(param_string
, ¶m_value
))
504 return kContextualSearchDefaultIcingSurroundingSize
;
507 base::string16
ContextualSearchDelegate::SurroundingTextForIcing(
508 const base::string16
& surrounding_text
,
509 int padding_each_side
,
512 base::string16 result_text
= surrounding_text
;
513 size_t start_offset
= *start
;
514 size_t end_offset
= *end
;
515 size_t padding_each_side_pinned
=
516 padding_each_side
>= 0 ? padding_each_side
: 0;
517 // Now trim the context so the portions before or after the selection
518 // are within the given limit.
519 if (start_offset
> padding_each_side_pinned
) {
521 int trim
= start_offset
- padding_each_side_pinned
;
522 result_text
= result_text
.substr(trim
);
523 start_offset
-= trim
;
526 if (result_text
.length() > end_offset
+ padding_each_side_pinned
) {
528 result_text
= result_text
.substr(0, end_offset
+ padding_each_side_pinned
);
530 *start
= start_offset
;