| 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);
|
| +}
|
|
|