blob: f170cd054592562ec444ceae0a4a2a609bcf4aa7 [file] [log] [blame]
// 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 <utility>
#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/chrome_feature_list.h"
#include "chrome/browser/android/contextualsearch/contextual_search_field_trial.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/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/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/escape.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "url/gurl.h"
using content::ContentViewCore;
using content::RenderFrameHost;
namespace {
const char kContextualSearchResponseDisplayTextParam[] = "display_text";
const char kContextualSearchResponseSelectedTextParam[] = "selected_text";
const char kContextualSearchResponseSearchTermParam[] = "search_term";
const char kContextualSearchResponseLanguageParam[] = "lang";
const char kContextualSearchResponseMidParam[] = "mid";
const char kContextualSearchResponseResolvedTermParam[] = "resolved_term";
const char kContextualSearchPreventPreload[] = "prevent_preload";
const char kContextualSearchMentions[] = "mentions";
const char kContextualSearchCaption[] = "caption";
const char kContextualSearchThumbnail[] = "thumbnail";
const char kContextualSearchAction[] = "action";
const char kContextualSearchCategory[] = "category";
const char kActionCategoryAddress[] = "ADDRESS";
const char kActionCategoryEmail[] = "EMAIL";
const char kActionCategoryEvent[] = "EVENT";
const char kActionCategoryPhone[] = "PHONE";
const char kContextualSearchServerEndpoint[] = "_/contextualsearch?";
const int kContextualSearchRequestVersion = 2;
const int kContextualSearchMaxSelection = 100;
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;
// The version of the Contextual Cards API that we want to invoke.
const int kContextualCardsNoIntegration = 0;
const int kContextualCardsBarIntegration = 1;
const int kContextualCardsSingleAction = 2;
const char kContextualCardsVersionOverride[] = "contextual_cards_version";
} // 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) {
field_trial_.reset(new ContextualSearchFieldTrial());
}
ContextualSearchDelegate::~ContextualSearchDelegate() {
}
void ContextualSearchDelegate::StartSearchTermResolutionRequest(
const std::string& selection,
bool use_resolved_search_term,
content::WebContents* web_contents,
bool may_send_base_page_url) {
GatherSurroundingTextWithCallback(
selection, use_resolved_search_term, web_contents, may_send_base_page_url,
base::Bind(&ContextualSearchDelegate::StartSearchTermRequestFromSelection,
AsWeakPtr()));
}
void ContextualSearchDelegate::GatherAndSaveSurroundingText(
const std::string& selection,
bool use_resolved_search_term,
content::WebContents* web_contents,
bool may_send_base_page_url) {
GatherSurroundingTextWithCallback(
selection, use_resolved_search_term, web_contents, 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(context_->selected_text));
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::unique_ptr<ResolvedSearchTerm> resolved_search_term(
new ResolvedSearchTerm(response_code));
if (source->GetStatus().is_success() && response_code == net::HTTP_OK) {
std::string response;
bool has_string_response = source->GetResponseAsString(&response);
DCHECK(has_string_response);
if (has_string_response) {
resolved_search_term =
GetResolvedSearchTermFromJson(response_code, response);
}
}
search_term_callback_.Run(*resolved_search_term);
// The ContextualSearchContext is consumed once the request has completed.
context_.reset();
}
std::unique_ptr<ResolvedSearchTerm>
ContextualSearchDelegate::GetResolvedSearchTermFromJson(
int response_code,
const std::string& json_string) {
std::string search_term;
std::string display_text;
std::string alternate_term;
std::string mid;
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 thumbnail_url = "";
std::string caption = "";
std::string quick_action_uri = "";
QuickActionCategory quick_action_category = QUICK_ACTION_CATEGORY_NONE;
DecodeSearchTermFromJsonResponse(
json_string, &search_term, &display_text, &alternate_term, &mid,
&prevent_preload, &mention_start, &mention_end, &context_language,
&thumbnail_url, &caption, &quick_action_uri, &quick_action_category);
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;
return std::unique_ptr<ResolvedSearchTerm>(new ResolvedSearchTerm(
is_invalid, response_code, search_term, display_text, alternate_term, mid,
prevent_preload == kDoPreventPreloadValue, start_adjust, end_adjust,
context_language, thumbnail_url, caption, quick_action_uri,
quick_action_category));
}
std::string ContextualSearchDelegate::BuildRequestUrl(std::string selection) {
// TODO(donnd): Confirm this is the right way to handle TemplateURL fails.
if (!template_url_service_ ||
!template_url_service_->GetDefaultSearchProvider()) {
return std::string();
}
std::string selected_text(net::EscapeQueryParamValue(selection, true));
TemplateURL* template_url = template_url_service_->GetDefaultSearchProvider();
TemplateURLRef::SearchTermsArgs search_terms_args =
TemplateURLRef::SearchTermsArgs(base::string16());
int contextual_cards_version = kContextualCardsNoIntegration;
if (field_trial_->IsContextualCardsBarIntegrationEnabled())
contextual_cards_version = kContextualCardsBarIntegration;
if (base::FeatureList::IsEnabled(
chrome::android::kContextualSearchSingleActions)) {
contextual_cards_version = kContextualCardsSingleAction;
}
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
kContextualCardsVersionOverride)){
contextual_cards_version =
std::stoi(base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kContextualCardsVersionOverride), nullptr);
}
TemplateURLRef::SearchTermsArgs::ContextualSearchParams params(
kContextualSearchRequestVersion, selected_text, std::string(),
contextual_cards_version);
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 = field_trial_->GetResolverURLPrefix();
// 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::WebContents* web_contents,
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();
BuildContext(selection, use_resolved_search_term, web_contents,
may_send_base_page_url);
DCHECK(web_contents);
RenderFrameHost* focused_frame = web_contents->GetFocusedFrame();
if (focused_frame) {
focused_frame->RequestTextSurroundingSelection(
callback, field_trial_->GetSurroundingSize());
} else {
callback.Run(base::string16(), 0, 0);
}
}
void ContextualSearchDelegate::BuildContext(
const std::string& selection,
bool use_resolved_search_term,
content::WebContents* web_contents,
bool may_send_base_page_url) {
// Decide if the URL should be sent with the context.
GURL page_url(web_contents->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(web_contents->GetEncoding());
context_.reset(new ContextualSearchContext(
selection, use_resolved_search_term, url_to_send, encoding));
}
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 = field_trial_->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) {
search_term_fetcher_->AddExtraRequestHeader(GetDiscourseContext(context));
}
std::string ContextualSearchDelegate::GetDiscourseContext(
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(), '/', '_');
return 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.
if (field_trial_->IsSendBasePageURLDisabled())
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.
browser_sync::ProfileSyncService* service =
ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
syncer::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* mid,
std::string* prevent_preload,
int* mention_start,
int* mention_end,
std::string* lang,
std::string* thumbnail_url,
std::string* caption,
std::string* quick_action_uri,
QuickActionCategory* quick_action_category) {
bool contains_xssi_escape =
base::StartsWith(response, kXssiEscape, base::CompareCase::SENSITIVE);
const std::string& proper_json =
contains_xssi_escape ? response.substr(sizeof(kXssiEscape) - 1)
: response;
JSONStringValueDeserializer deserializer(proper_json);
std::unique_ptr<base::Value> root =
deserializer.Deserialize(nullptr, nullptr);
const std::unique_ptr<base::DictionaryValue> dict =
base::DictionaryValue::From(std::move(root));
if (!dict)
return;
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;
}
dict->GetString(kContextualSearchResponseMidParam, mid);
// Extract mentions for selection expansion.
if (!field_trial_->IsDecodeMentionsDisabled()) {
base::ListValue* mentions_list = nullptr;
dict->GetList(kContextualSearchMentions, &mentions_list);
if (mentions_list && 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;
}
}
if (field_trial_->IsContextualCardsBarIntegrationEnabled()) {
// Contextual Cards V1 Integration.
// Get the basic Bar data for Contextual Cards integration directly
// from the root.
dict->GetString(kContextualSearchCaption, caption);
dict->GetString(kContextualSearchThumbnail, thumbnail_url);
}
if (base::FeatureList::IsEnabled(
chrome::android::kContextualSearchSingleActions)) {
// Contextual Cards V2 Integration.
// Get the Single Action data.
dict->GetString(kContextualSearchAction, quick_action_uri);
std::string quick_action_category_string;
dict->GetString(kContextualSearchCategory, &quick_action_category_string);
if (!quick_action_category_string.empty()) {
if (quick_action_category_string == kActionCategoryAddress) {
*quick_action_category = QUICK_ACTION_CATEGORY_ADDRESS;
} else if (quick_action_category_string == kActionCategoryEmail) {
*quick_action_category = QUICK_ACTION_CATEGORY_EMAIL;
} else if (quick_action_category_string == kActionCategoryEvent) {
*quick_action_category = QUICK_ACTION_CATEGORY_EVENT;
} else if (quick_action_category_string == kActionCategoryPhone) {
*quick_action_category = QUICK_ACTION_CATEGORY_PHONE;
}
}
}
if (field_trial_->IsContextualCardsBarIntegrationEnabled() ||
base::FeatureList::IsEnabled(
chrome::android::kContextualSearchSingleActions)) {
// Any Contextual Cards integration.
// For testing purposes check if there was a Contextual Cards backend
// failure and flag that in the log.
// TODO(donnd): remove after full Contextual Cards integration.
bool contextual_cards_backend_responded = true;
dict->GetBoolean("coca_responded", &contextual_cards_backend_responded);
if (!contextual_cards_backend_responded) {
DVLOG(0) << "";
DVLOG(0) << "!!! CONTEXTUAL SEARCH WARNING !!!";
DVLOG(0)
<< "The Contextual Cards backend did not respond to this "
"request!!! The backend server may not be configured or is down.";
DVLOG(0) << "";
}
}
}
// 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);
}
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;
}