Index: ios/chrome/browser/ui/contextual_search/contextual_search_delegate.cc |
diff --git a/ios/chrome/browser/ui/contextual_search/contextual_search_delegate.cc b/ios/chrome/browser/ui/contextual_search/contextual_search_delegate.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3acaf6243c6bcf438fdbfdfa8efe1a4fa0fb8e00 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/contextual_search/contextual_search_delegate.cc |
@@ -0,0 +1,333 @@ |
+// Copyright 2014 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 "ios/chrome/browser/ui/contextual_search/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 "components/google/core/browser/google_util.h" |
+#include "components/search_engines/template_url_service.h" |
+#include "components/search_engines/util.h" |
+#include "components/variations/net/variations_http_headers.h" |
+#include "components/variations/variations_associated_data.h" |
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
+#include "ios/chrome/browser/search_engines/template_url_service_factory.h" |
+#include "ios/chrome/browser/ui/contextual_search/protos/client_discourse_context.pb.h" |
+#include "ios/web/public/web_thread.h" |
+#include "net/base/escape.h" |
+#include "net/base/url_util.h" |
+#include "net/url_request/url_fetcher.h" |
+#include "url/gurl.h" |
+ |
+namespace { |
+ |
+const char kContextualSearchFieldTrialName[] = "ContextualSearch"; |
+const char kContextualSearchPreventPreload[] = "prevent_preload"; |
+const char kContextualSearchResolverUrl[] = "contextual-search-resolver-url"; |
+const char kContextualSearchResolverURLParamName[] = "resolver_url"; |
+const char kContextualSearchResponseDisplayTextParam[] = "display_text"; |
+const char kContextualSearchResponseMentionsParam[] = "mentions"; |
+const char kContextualSearchResponseResolvedTermParam[] = "resolved_term"; |
+const char kContextualSearchResponseSelectedTextParam[] = "selected_text"; |
+const char kContextualSearchResponseSearchTermParam[] = "search_term"; |
+const int kContextualSearchRequestVersion = 2; |
+const char kContextualSearchServerEndpoint[] = "_/contextualsearch?"; |
+const char kDiscourseContextHeaderPrefix[] = "X-Additional-Discourse-Context: "; |
+const char kDoPreventPreloadValue[] = "1"; |
+const char kXssiEscape[] = ")]}'\n"; |
+ |
+const double kMinimumDelayBetweenRequestSeconds = 1; |
+ |
+// Decodes the given response from the search term resolution request and sets |
+// the value of the given search-term and display_text parameters. |
+void DecodeSearchTermsFromJsonResponse(const std::string& response, |
+ std::string* search_term, |
+ std::string* display_text, |
+ std::string* alternate_term, |
+ std::string* prevent_preload, |
+ int& start_offset, |
+ int& end_offset) { |
+ 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); |
+ std::unique_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); |
+ |
+ const base::ListValue* mentionsList; |
+ if (dict->GetList(kContextualSearchResponseMentionsParam, &mentionsList)) { |
+ DCHECK(mentionsList->GetSize() == 2); |
+ mentionsList->GetInteger(0, &start_offset); |
+ mentionsList->GetInteger(1, &end_offset); |
+ } |
+ |
+ 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; |
+ } |
+ } |
+ } |
+} |
+ |
+} // 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( |
+ ios::ChromeBrowserState* browser_state, |
+ const ContextualSearchDelegate::SearchTermResolutionCallback& |
+ search_term_callback) |
+ : template_url_service_( |
+ ios::TemplateURLServiceFactory::GetForBrowserState(browser_state)), |
+ browser_state_(browser_state), |
+ search_term_callback_(search_term_callback), |
+ weak_ptr_factory_(this) {} |
+ |
+ContextualSearchDelegate::~ContextualSearchDelegate() {} |
+ |
+void ContextualSearchDelegate::PostSearchTermRequest( |
+ std::shared_ptr<ContextualSearchContext> context) { |
+ context_ = context; |
+ if (request_pending_) { |
+ return; |
+ } |
+ request_pending_ = true; |
+ |
+ base::TimeDelta interval = |
+ base::TimeDelta::FromSecondsD(kMinimumDelayBetweenRequestSeconds); |
+ base::Time now = base::Time::Now(); |
+ if (now > last_request_startup_time_ + interval) { |
+ StartPendingSearchTermRequest(); |
+ } else { |
+ base::TimeDelta delay = last_request_startup_time_ + interval - now; |
+ web::WebThread::PostDelayedTask( |
+ web::WebThread::UI, FROM_HERE, |
+ base::Bind(&ContextualSearchDelegate::StartPendingSearchTermRequest, |
+ weak_ptr_factory_.GetWeakPtr()), |
+ delay); |
+ } |
+} |
+ |
+void ContextualSearchDelegate::StartPendingSearchTermRequest() { |
+ if (!request_pending_ || !context_) |
+ return; |
+ request_pending_ = false; |
+ last_request_startup_time_ = base::Time::Now(); |
+ if (context_->HasSurroundingText()) { |
+ RequestServerSearchTerm(); |
+ } else { |
+ RequestLocalSearchTerm(); |
+ } |
+} |
+ |
+void ContextualSearchDelegate::RequestLocalSearchTerm() { |
+ SearchResolution resolution; |
+ resolution.is_invalid = false; |
+ resolution.response_code = 200; // HTTP success. |
+ resolution.search_term = context_->selected_text; |
+ resolution.display_text = context_->selected_text; |
+ resolution.alternate_term = context_->selected_text; |
+ resolution.prevent_preload = false; |
+ resolution.start_offset = -1; |
+ resolution.end_offset = -1; |
+ search_term_callback_.Run(resolution); |
+} |
+ |
+void ContextualSearchDelegate::RequestServerSearchTerm() { |
+ 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_ = net::URLFetcher::Create( |
+ kContextualSearchURLFetcherID, request_url, net::URLFetcher::GET, this); |
+ search_term_fetcher_->SetRequestContext(browser_state_->GetRequestContext()); |
+ |
+ // Add Chrome experiment state to the request headers. |
+ net::HttpRequestHeaders headers; |
+ // Note: It's fine to pass in |is_signed_in| false, which does not affect |
+ // transmission of experiment ids coming from the variations server. |
+ bool is_signed_in = false; |
+ variations::AppendVariationHeaders(search_term_fetcher_->GetOriginalURL(), |
+ browser_state_->IsOffTheRecord(), false, |
+ is_signed_in, &headers); |
+ search_term_fetcher_->SetExtraRequestHeaders(headers.ToString()); |
+ |
+ SetDiscourseContextAndAddToHeader(*context_); |
+ |
+ search_term_fetcher_->Start(); |
+} |
+ |
+void ContextualSearchDelegate::CancelSearchTermRequest() { |
+ search_term_fetcher_.reset(); |
+ context_.reset(); |
+} |
+ |
+// Adapted from /chrome/browser/search_engines/template_url_service_android.cc |
+GURL ContextualSearchDelegate::GetURLForResolvedSearch( |
+ SearchResolution resolution, |
+ bool should_prefetch) { |
+ GURL url; |
+ if (!resolution.search_term.empty()) { |
+ url = GetDefaultSearchURLForSearchTerms( |
+ template_url_service_, base::UTF8ToUTF16(resolution.search_term)); |
+ if (google_util::IsGoogleSearchUrl(url)) { |
+ url = net::AppendQueryParameter(url, "ctxs", "2"); |
+ if (should_prefetch) { |
+ // Indicate that the search page is being prefetched. |
+ url = net::AppendQueryParameter(url, "pf", "c"); |
+ } |
+ |
+ if (!resolution.alternate_term.empty()) { |
+ url = net::AppendQueryParameter(url, "ctxsl_alternate_term", |
+ resolution.alternate_term); |
+ } |
+ } |
+ } |
+ return url; |
+} |
+ |
+void ContextualSearchDelegate::OnURLFetchComplete( |
+ const net::URLFetcher* source) { |
+ DCHECK(source == search_term_fetcher_.get()); |
+ SearchResolution resolution; |
+ std::string prevent_preload; |
+ resolution.response_code = source->GetResponseCode(); |
+ if (source->GetStatus().is_success() && resolution.response_code == 200) { |
+ std::string response; |
+ bool has_string_response = source->GetResponseAsString(&response); |
+ DCHECK(has_string_response); |
+ if (has_string_response) { |
+ resolution.start_offset = -1; |
+ resolution.end_offset = -1; |
+ DecodeSearchTermsFromJsonResponse( |
+ response, &resolution.search_term, &resolution.display_text, |
+ &resolution.alternate_term, &prevent_preload, resolution.start_offset, |
+ resolution.end_offset); |
+ } |
+ } |
+ resolution.is_invalid = |
+ resolution.response_code == net::URLFetcher::RESPONSE_CODE_INVALID; |
+ resolution.prevent_preload = prevent_preload == kDoPreventPreloadValue; |
+ |
+ search_term_callback_.Run(resolution); |
+} |
+ |
+// TODO(donnd): use HTTP headers for the context instead of CGI params in GET. |
+// See https://code.google.com/p/chromium/issues/detail?id=341762 |
+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 = context_->page_url.spec(); |
+ bool use_resolved_search_term = context_->use_resolved_search_term; |
+ |
+ std::string request = GetSearchTermResolutionUrlString( |
+ selected_text_escaped, base_page_url, 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::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( |
+ net::EscapeQueryParamValue(UTF16ToUTF8(context.surrounding_text), true)); |
+ selection->set_start(context.start_offset); |
+ selection->set_end(context.end_offset); |
+ |
+ 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); |
+} |