1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
|
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/android/contextualsearch/contextual_search_delegate.h"
#include <algorithm>
#include "base/base64.h"
#include "base/command_line.h"
#include "base/json/json_string_value_serializer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/android/contextualsearch/resolved_search_term.h"
#include "chrome/browser/android/proto/client_discourse_context.pb.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "chrome/browser/translate/translate_service.h"
#include "chrome/common/pref_names.h"
#include "components/browser_sync/browser/profile_sync_service.h"
#include "components/prefs/pref_service.h"
#include "components/search_engines/template_url_service.h"
#include "components/variations/net/variations_http_headers.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/android/content_view_core.h"
#include "content/public/browser/web_contents.h"
#include "net/base/escape.h"
#include "net/url_request/url_fetcher.h"
#include "url/gurl.h"
using content::ContentViewCore;
namespace {
const char kContextualSearchFieldTrialName[] = "ContextualSearch";
const char kContextualSearchSurroundingSizeParamName[] = "surrounding_size";
const char kContextualSearchIcingSurroundingSizeParamName[] =
"icing_surrounding_size";
const char kContextualSearchResolverURLParamName[] = "resolver_url";
const char kContextualSearchDoNotSendURLParamName[] = "do_not_send_url";
const char kContextualSearchResponseDisplayTextParam[] = "display_text";
const char kContextualSearchResponseSelectedTextParam[] = "selected_text";
const char kContextualSearchResponseSearchTermParam[] = "search_term";
const char kContextualSearchResponseLanguageParam[] = "lang";
const char kContextualSearchResponseResolvedTermParam[] = "resolved_term";
const char kContextualSearchPreventPreload[] = "prevent_preload";
const char kContextualSearchMentions[] = "mentions";
const char kContextualSearchServerEndpoint[] = "_/contextualsearch?";
const int kContextualSearchRequestVersion = 2;
const char kContextualSearchResolverUrl[] =
"contextual-search-resolver-url";
// The default size of the content surrounding the selection to gather, allowing
// room for other parameters.
const int kContextualSearchDefaultContentSize = 1536;
const int kContextualSearchDefaultIcingSurroundingSize = 400;
const int kContextualSearchMaxSelection = 100;
// The maximum length of a URL to build.
const int kMaxURLSize = 2048;
const char kXssiEscape[] = ")]}'\n";
const char kDiscourseContextHeaderPrefix[] = "X-Additional-Discourse-Context: ";
const char kDoPreventPreloadValue[] = "1";
// The number of characters that should be shown after the selected expression.
const int kSurroundingSizeForUI = 60;
} // namespace
// URLFetcher ID, only used for tests: we only have one kind of fetcher.
const int ContextualSearchDelegate::kContextualSearchURLFetcherID = 1;
// Handles tasks for the ContextualSearchManager in a separable, testable way.
ContextualSearchDelegate::ContextualSearchDelegate(
net::URLRequestContextGetter* url_request_context,
TemplateURLService* template_url_service,
const ContextualSearchDelegate::SearchTermResolutionCallback&
search_term_callback,
const ContextualSearchDelegate::SurroundingTextCallback&
surrounding_callback,
const ContextualSearchDelegate::IcingCallback& icing_callback)
: url_request_context_(url_request_context),
template_url_service_(template_url_service),
search_term_callback_(search_term_callback),
surrounding_callback_(surrounding_callback),
icing_callback_(icing_callback) {
}
ContextualSearchDelegate::~ContextualSearchDelegate() {
}
void ContextualSearchDelegate::StartSearchTermResolutionRequest(
const std::string& selection,
bool use_resolved_search_term,
content::ContentViewCore* content_view_core,
bool may_send_base_page_url) {
GatherSurroundingTextWithCallback(
selection, use_resolved_search_term, content_view_core,
may_send_base_page_url,
base::Bind(&ContextualSearchDelegate::StartSearchTermRequestFromSelection,
AsWeakPtr()));
}
void ContextualSearchDelegate::GatherAndSaveSurroundingText(
const std::string& selection,
bool use_resolved_search_term,
content::ContentViewCore* content_view_core,
bool may_send_base_page_url) {
GatherSurroundingTextWithCallback(
selection, use_resolved_search_term, content_view_core,
may_send_base_page_url,
base::Bind(&ContextualSearchDelegate::SaveSurroundingText, AsWeakPtr()));
// TODO(donnd): clear the context here, since we're done with it (but risky).
}
void ContextualSearchDelegate::ContinueSearchTermResolutionRequest() {
DCHECK(context_.get());
if (!context_.get())
return;
GURL request_url(BuildRequestUrl());
DCHECK(request_url.is_valid());
// Reset will delete any previous fetcher, and we won't get any callback.
search_term_fetcher_.reset(
net::URLFetcher::Create(kContextualSearchURLFetcherID, request_url,
net::URLFetcher::GET, this).release());
search_term_fetcher_->SetRequestContext(url_request_context_);
// Add Chrome experiment state to the request headers.
net::HttpRequestHeaders headers;
variations::AppendVariationHeaders(
search_term_fetcher_->GetOriginalURL(),
false, // Impossible to be incognito at this point.
false, &headers);
search_term_fetcher_->SetExtraRequestHeaders(headers.ToString());
SetDiscourseContextAndAddToHeader(*context_);
search_term_fetcher_->Start();
}
void ContextualSearchDelegate::OnURLFetchComplete(
const net::URLFetcher* source) {
DCHECK(source == search_term_fetcher_.get());
int response_code = source->GetResponseCode();
std::string search_term;
std::string display_text;
std::string alternate_term;
std::string prevent_preload;
int mention_start = 0;
int mention_end = 0;
int start_adjust = 0;
int end_adjust = 0;
std::string context_language;
std::string target_language;
if (source->GetStatus().is_success() && response_code == 200) {
std::string response;
bool has_string_response = source->GetResponseAsString(&response);
DCHECK(has_string_response);
if (has_string_response) {
DecodeSearchTermFromJsonResponse(
response, &search_term, &display_text, &alternate_term,
&prevent_preload, &mention_start, &mention_end, &context_language);
if (mention_start != 0 || mention_end != 0) {
// Sanity check that our selection is non-zero and it is less than
// 100 characters as that would make contextual search bar hide.
// We also check that there is at least one character overlap between
// the new and old selection.
if (mention_start >= mention_end
|| (mention_end - mention_start) > kContextualSearchMaxSelection
|| mention_end <= context_->start_offset
|| mention_start >= context_->end_offset) {
start_adjust = 0;
end_adjust = 0;
} else {
start_adjust = mention_start - context_->start_offset;
end_adjust = mention_end - context_->end_offset;
}
}
}
}
bool is_invalid = response_code == net::URLFetcher::RESPONSE_CODE_INVALID;
ResolvedSearchTerm resolved_search_term(
is_invalid, response_code, search_term, display_text, alternate_term,
prevent_preload == kDoPreventPreloadValue, start_adjust, end_adjust,
context_language);
search_term_callback_.Run(resolved_search_term);
// The ContextualSearchContext is consumed once the request has completed.
context_.reset();
}
// TODO(jeremycho): Remove selected_text and base_page_url CGI parameters.
GURL ContextualSearchDelegate::BuildRequestUrl() {
// TODO(jeremycho): Confirm this is the right way to handle TemplateURL fails.
if (!template_url_service_ ||
!template_url_service_->GetDefaultSearchProvider()) {
return GURL();
}
std::string selected_text_escaped(
net::EscapeQueryParamValue(context_->selected_text, true));
std::string base_page_url_escaped(
net::EscapeQueryParamValue(context_->page_url.spec(), true));
bool use_resolved_search_term = context_->use_resolved_search_term;
// If the request is too long, don't include the base-page URL.
std::string request = GetSearchTermResolutionUrlString(
selected_text_escaped, base_page_url_escaped, use_resolved_search_term);
if (request.length() >= kMaxURLSize) {
request = GetSearchTermResolutionUrlString(
selected_text_escaped, "", use_resolved_search_term);
}
return GURL(request);
}
std::string ContextualSearchDelegate::GetSearchTermResolutionUrlString(
const std::string& selected_text,
const std::string& base_page_url,
const bool use_resolved_search_term) {
TemplateURL* template_url = template_url_service_->GetDefaultSearchProvider();
TemplateURLRef::SearchTermsArgs search_terms_args =
TemplateURLRef::SearchTermsArgs(base::string16());
TemplateURLRef::SearchTermsArgs::ContextualSearchParams params(
kContextualSearchRequestVersion,
selected_text,
base_page_url,
use_resolved_search_term);
search_terms_args.contextual_search_params = params;
std::string request(
template_url->contextual_search_url_ref().ReplaceSearchTerms(
search_terms_args,
template_url_service_->search_terms_data(),
NULL));
// The switch/param should be the URL up to and including the endpoint.
std::string replacement_url;
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
kContextualSearchResolverUrl)) {
replacement_url =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kContextualSearchResolverUrl);
} else {
std::string param_value = variations::GetVariationParamValue(
kContextualSearchFieldTrialName, kContextualSearchResolverURLParamName);
if (!param_value.empty()) replacement_url = param_value;
}
// If a replacement URL was specified above, do the substitution.
if (!replacement_url.empty()) {
size_t pos = request.find(kContextualSearchServerEndpoint);
if (pos != std::string::npos) {
request.replace(0, pos + strlen(kContextualSearchServerEndpoint),
replacement_url);
}
}
return request;
}
void ContextualSearchDelegate::GatherSurroundingTextWithCallback(
const std::string& selection,
bool use_resolved_search_term,
content::ContentViewCore* content_view_core,
bool may_send_base_page_url,
HandleSurroundingsCallback callback) {
// Immediately cancel any request that's in flight, since we're building a new
// context (and the response disposes of any existing context).
search_term_fetcher_.reset();
// Decide if the URL should be sent with the context.
GURL page_url(content_view_core->GetWebContents()->GetURL());
GURL url_to_send;
if (may_send_base_page_url &&
CanSendPageURL(page_url, ProfileManager::GetActiveUserProfile(),
template_url_service_)) {
url_to_send = page_url;
}
std::string encoding(content_view_core->GetWebContents()->GetEncoding());
context_.reset(new ContextualSearchContext(
selection, use_resolved_search_term, url_to_send, encoding));
content_view_core->RequestTextSurroundingSelection(
GetSearchTermSurroundingSize(), callback);
}
void ContextualSearchDelegate::StartSearchTermRequestFromSelection(
const base::string16& surrounding_text,
int start_offset,
int end_offset) {
// TODO(donnd): figure out how to gather text surrounding the selection
// for other purposes too: e.g. to determine if we should select the
// word where the user tapped.
if (context_.get()) {
SaveSurroundingText(surrounding_text, start_offset, end_offset);
SendSurroundingText(kSurroundingSizeForUI);
ContinueSearchTermResolutionRequest();
} else {
DVLOG(1) << "ctxs: Null context, ignored!";
}
}
void ContextualSearchDelegate::SaveSurroundingText(
const base::string16& surrounding_text,
int start_offset,
int end_offset) {
DCHECK(context_.get());
// Sometimes the surroundings are 0, 0, '', so fall back on the selection.
// See crbug.com/393100.
if (start_offset == 0 && end_offset == 0 && surrounding_text.length() == 0) {
context_->surrounding_text = base::UTF8ToUTF16(context_->selected_text);
context_->start_offset = 0;
context_->end_offset = context_->selected_text.length();
} else {
context_->surrounding_text = surrounding_text;
context_->start_offset = start_offset;
context_->end_offset = end_offset;
}
// Pin the start and end offsets to ensure they point within the string.
int surrounding_length = context_->surrounding_text.length();
context_->start_offset =
std::min(surrounding_length, std::max(0, context_->start_offset));
context_->end_offset =
std::min(surrounding_length, std::max(0, context_->end_offset));
// Call the Icing callback with a shortened copy of the surroundings.
int icing_surrounding_size = GetIcingSurroundingSize();
size_t selection_start = context_->start_offset;
size_t selection_end = context_->end_offset;
if (icing_surrounding_size >= 0 && selection_start < selection_end) {
int icing_padding_each_side = icing_surrounding_size / 2;
base::string16 icing_surrounding_text = SurroundingTextForIcing(
context_->surrounding_text, icing_padding_each_side, &selection_start,
&selection_end);
if (selection_start < selection_end)
icing_callback_.Run(context_->encoding, icing_surrounding_text,
selection_start, selection_end);
}
}
void ContextualSearchDelegate::SendSurroundingText(int max_surrounding_chars) {
const base::string16& surrounding = context_->surrounding_text;
// Determine the text after the selection.
int surrounding_length = surrounding.length(); // Cast to int.
int num_after_characters = std::min(
surrounding_length - context_->end_offset, max_surrounding_chars);
base::string16 after_text = surrounding.substr(
context_->end_offset, num_after_characters);
base::TrimWhitespace(after_text, base::TRIM_ALL, &after_text);
surrounding_callback_.Run(UTF16ToUTF8(after_text));
}
void ContextualSearchDelegate::SetDiscourseContextAndAddToHeader(
const ContextualSearchContext& context) {
discourse_context::ClientDiscourseContext proto;
discourse_context::Display* display = proto.add_display();
display->set_uri(context.page_url.spec());
discourse_context::Media* media = display->mutable_media();
media->set_mime_type(context.encoding);
discourse_context::Selection* selection = display->mutable_selection();
selection->set_content(UTF16ToUTF8(context.surrounding_text));
selection->set_start(context.start_offset);
selection->set_end(context.end_offset);
selection->set_is_uri_encoded(false);
std::string serialized;
proto.SerializeToString(&serialized);
std::string encoded_context;
base::Base64Encode(serialized, &encoded_context);
// The server memoizer expects a web-safe encoding.
std::replace(encoded_context.begin(), encoded_context.end(), '+', '-');
std::replace(encoded_context.begin(), encoded_context.end(), '/', '_');
search_term_fetcher_->AddExtraRequestHeader(
kDiscourseContextHeaderPrefix + encoded_context);
}
bool ContextualSearchDelegate::CanSendPageURL(
const GURL& current_page_url,
Profile* profile,
TemplateURLService* template_url_service) {
// Check whether there is a Finch parameter preventing us from sending the
// page URL.
std::string param_value = variations::GetVariationParamValue(
kContextualSearchFieldTrialName, kContextualSearchDoNotSendURLParamName);
if (!param_value.empty())
return false;
// Ensure that the default search provider is Google.
TemplateURL* default_search_provider =
template_url_service->GetDefaultSearchProvider();
bool is_default_search_provider_google =
default_search_provider &&
default_search_provider->url_ref().HasGoogleBaseURLs(
template_url_service->search_terms_data());
if (!is_default_search_provider_google)
return false;
// Only allow HTTP URLs or HTTPS URLs.
if (current_page_url.scheme() != url::kHttpScheme &&
(current_page_url.scheme() != url::kHttpsScheme))
return false;
// Check that the user has sync enabled, is logged in, and syncs their Chrome
// History.
ProfileSyncService* service =
ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
sync_driver::SyncPrefs sync_prefs(profile->GetPrefs());
if (service == NULL || !service->CanSyncStart() ||
!sync_prefs.GetPreferredDataTypes(syncer::UserTypes())
.Has(syncer::PROXY_TABS) ||
!service->GetActiveDataTypes().Has(syncer::HISTORY_DELETE_DIRECTIVES)) {
return false;
}
return true;
}
// Gets the target language from the translate service using the user's profile.
std::string ContextualSearchDelegate::GetTargetLanguage() {
Profile* profile = ProfileManager::GetActiveUserProfile();
PrefService* pref_service = profile->GetPrefs();
std::string result = TranslateService::GetTargetLanguage(pref_service);
DCHECK(!result.empty());
return result;
}
// Returns the accept languages preference string.
std::string ContextualSearchDelegate::GetAcceptLanguages() {
Profile* profile = ProfileManager::GetActiveUserProfile();
PrefService* pref_service = profile->GetPrefs();
return pref_service->GetString(prefs::kAcceptLanguages);
}
// Decodes the given response from the search term resolution request and sets
// the value of the given parameters.
void ContextualSearchDelegate::DecodeSearchTermFromJsonResponse(
const std::string& response,
std::string* search_term,
std::string* display_text,
std::string* alternate_term,
std::string* prevent_preload,
int* mention_start,
int* mention_end,
std::string* lang) {
bool contains_xssi_escape = response.find(kXssiEscape) == 0;
const std::string& proper_json =
contains_xssi_escape ? response.substr(strlen(kXssiEscape)) : response;
JSONStringValueDeserializer deserializer(proper_json);
scoped_ptr<base::Value> root = deserializer.Deserialize(NULL, NULL);
if (root.get() != NULL && root->IsType(base::Value::TYPE_DICTIONARY)) {
base::DictionaryValue* dict =
static_cast<base::DictionaryValue*>(root.get());
dict->GetString(kContextualSearchPreventPreload, prevent_preload);
dict->GetString(kContextualSearchResponseSearchTermParam, search_term);
dict->GetString(kContextualSearchResponseLanguageParam, lang);
// For the display_text, if not present fall back to the "search_term".
if (!dict->GetString(kContextualSearchResponseDisplayTextParam,
display_text)) {
*display_text = *search_term;
}
// Extract mentions for selection expansion.
base::ListValue* mentions_list = NULL;
dict->GetList(kContextualSearchMentions, &mentions_list);
if (mentions_list != NULL && mentions_list->GetSize() >= 2)
ExtractMentionsStartEnd(*mentions_list, mention_start, mention_end);
// If either the selected text or the resolved term is not the search term,
// use it as the alternate term.
std::string selected_text;
dict->GetString(kContextualSearchResponseSelectedTextParam, &selected_text);
if (selected_text != *search_term) {
*alternate_term = selected_text;
} else {
std::string resolved_term;
dict->GetString(kContextualSearchResponseResolvedTermParam,
&resolved_term);
if (resolved_term != *search_term) {
*alternate_term = resolved_term;
}
}
}
}
// Returns the size of the surroundings to be sent to the server for search term
// resolution.
int ContextualSearchDelegate::GetSearchTermSurroundingSize() {
const std::string param_value = variations::GetVariationParamValue(
kContextualSearchFieldTrialName,
kContextualSearchSurroundingSizeParamName);
int param_length;
if (!param_value.empty() && base::StringToInt(param_value, ¶m_length))
return param_length;
return kContextualSearchDefaultContentSize;
}
// Extract the Start/End of the mentions in the surrounding text
// for selection-expansion.
void ContextualSearchDelegate::ExtractMentionsStartEnd(
const base::ListValue& mentions_list,
int* startResult,
int* endResult) {
int int_value;
if (mentions_list.GetInteger(0, &int_value))
*startResult = std::max(0, int_value);
if (mentions_list.GetInteger(1, &int_value))
*endResult = std::max(0, int_value);
}
// Returns the size of the surroundings to be sent to Icing.
int ContextualSearchDelegate::GetIcingSurroundingSize() {
std::string param_string = variations::GetVariationParamValue(
kContextualSearchFieldTrialName,
kContextualSearchIcingSurroundingSizeParamName);
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
kContextualSearchIcingSurroundingSizeParamName)) {
param_string = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kContextualSearchIcingSurroundingSizeParamName);
}
int param_value;
if (!param_string.empty() && base::StringToInt(param_string, ¶m_value))
return param_value;
return kContextualSearchDefaultIcingSurroundingSize;
}
base::string16 ContextualSearchDelegate::SurroundingTextForIcing(
const base::string16& surrounding_text,
int padding_each_side,
size_t* start,
size_t* end) {
base::string16 result_text = surrounding_text;
size_t start_offset = *start;
size_t end_offset = *end;
size_t padding_each_side_pinned =
padding_each_side >= 0 ? padding_each_side : 0;
// Now trim the context so the portions before or after the selection
// are within the given limit.
if (start_offset > padding_each_side_pinned) {
// Trim the start.
int trim = start_offset - padding_each_side_pinned;
result_text = result_text.substr(trim);
start_offset -= trim;
end_offset -= trim;
}
if (result_text.length() > end_offset + padding_each_side_pinned) {
// Trim the end.
result_text = result_text.substr(0, end_offset + padding_each_side_pinned);
}
*start = start_offset;
*end = end_offset;
return result_text;
}
|