Index: chrome/browser/android/contextualsearch/contextual_search_delegate.cc |
diff --git a/chrome/browser/android/contextualsearch/contextual_search_delegate.cc b/chrome/browser/android/contextualsearch/contextual_search_delegate.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..332e0b36cc6e7a9d3f30272105e48c19eb0df26f |
--- /dev/null |
+++ b/chrome/browser/android/contextualsearch/contextual_search_delegate.cc |
@@ -0,0 +1,489 @@ |
+// 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/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.h" |
+#include "chrome/browser/sync/profile_sync_service_factory.h" |
+#include "components/search_engines/template_url_service.h" |
+#include "components/variations/net/variations_http_header_provider.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 kContextualSearchResponseResolvedTermParam[] = "resolved_term"; |
+const char kContextualSearchPreventPreload[] = "prevent_preload"; |
+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; |
+// 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 on each side of the selected |
+// expression. |
+const int kSurroundingSizeForUI = 30; |
+ |
+} // 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) { |
+ GatherSurroundingTextWithCallback( |
+ selection, |
+ use_resolved_search_term, |
+ content_view_core, |
+ base::Bind(&ContextualSearchDelegate::StartSearchTermRequestFromSelection, |
+ AsWeakPtr())); |
+} |
+ |
+void ContextualSearchDelegate::GatherAndSaveSurroundingText( |
+ const std::string& selection, |
+ bool use_resolved_search_term, |
+ content::ContentViewCore* content_view_core) { |
+ GatherSurroundingTextWithCallback( |
+ selection, |
+ use_resolved_search_term, |
+ content_view_core, |
+ 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::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders( |
+ 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; |
+ |
+ 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) { |
+ DecodeSearchTermsFromJsonResponse(response, &search_term, &display_text, |
+ &alternate_term, &prevent_preload); |
+ } |
+ } |
+ bool is_invalid = response_code == net::URLFetcher::RESPONSE_CODE_INVALID; |
+ search_term_callback_.Run( |
+ is_invalid, response_code, search_term, display_text, alternate_term, |
+ prevent_preload == kDoPreventPreloadValue); |
+ |
+ // 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, |
+ 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 be sent with the context. |
+ GURL page_url(content_view_core->GetWebContents()->GetURL()); |
+ GURL url_to_send; |
+ if (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. |
+ DCHECK(context_.get()); |
+ SaveSurroundingText(surrounding_text, start_offset, end_offset); |
+ SendSurroundingText(kSurroundingSizeForUI); |
+ ContinueSearchTermResolutionRequest(); |
+} |
+ |
+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; |
+ } |
+ |
+ // Call the Icing callback, unless it has been disabled. |
+ 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 before the selection. |
+ int start_position = std::max( |
+ 0, context_->start_offset - max_surrounding_chars); |
+ int num_before_characters = |
+ std::min(context_->start_offset, max_surrounding_chars); |
+ base::string16 before_text = |
+ surrounding.substr(start_position, num_before_characters); |
+ |
+ // Determine the text after the selection. |
+ int surrounding_size = surrounding.size(); // Cast to int. |
+ int num_after_characters = std::min( |
+ surrounding_size - context_->end_offset, max_surrounding_chars); |
+ base::string16 after_text = surrounding.substr( |
+ context_->end_offset, num_after_characters); |
+ |
+ base::TrimWhitespace(before_text, base::TRIM_ALL, &before_text); |
+ base::TrimWhitespace(after_text, base::TRIM_ALL, &after_text); |
+ surrounding_callback_.Run(UTF16ToUTF8(before_text), 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->IsSyncEnabledAndLoggedIn() || |
+ !sync_prefs.GetPreferredDataTypes(syncer::UserTypes()) |
+ .Has(syncer::PROXY_TABS) || |
+ !service->GetActiveDataTypes().Has(syncer::HISTORY_DELETE_DIRECTIVES)) { |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
+// Decodes the given response from the search term resolution request and sets |
+// the value of the given parameters. |
+void ContextualSearchDelegate::DecodeSearchTermsFromJsonResponse( |
+ const std::string& response, |
+ std::string* search_term, |
+ std::string* display_text, |
+ std::string* alternate_term, |
+ std::string* prevent_preload) { |
+ 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); |
+ // For the display_text, if not present fall back to the "search_term". |
+ if (!dict->GetString(kContextualSearchResponseDisplayTextParam, |
+ display_text)) { |
+ *display_text = *search_term; |
+ } |
+ // 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; |
+} |
+ |
+// 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; |
+} |