| // 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 "base/task/post_task.h" |
| #include "components/google/core/common/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_task_traits.h" |
| #include "ios/web/public/web_thread.h" |
| #include "net/base/escape.h" |
| #include "net/base/url_util.h" |
| #include "net/http/http_status_code.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.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"; |
| |
| // The version of the Contextual Cards API that we want to invoke. |
| const int kContextualCardsNoIntegration = 0; |
| |
| 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->is_dict()) { |
| 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 |
| |
| // 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; |
| base::PostDelayedTaskWithTraits( |
| FROM_HERE, {web::WebThread::UI}, |
| 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()); |
| |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = request_url; |
| |
| SetDiscourseContextAndAddToHeader(*context_, resource_request.get()); |
| |
| // Add Chrome experiment state to the request headers. |
| // Reset will delete any previous loader, and we won't get any callback. |
| url_loader_ = |
| variations::CreateSimpleURLLoaderWithVariationsHeadersUnknownSignedIn( |
| std::move(resource_request), |
| browser_state_->IsOffTheRecord() ? variations::InIncognito::kYes |
| : variations::InIncognito::kNo, |
| NO_TRAFFIC_ANNOTATION_YET); |
| |
| url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| browser_state_->GetSharedURLLoaderFactory().get(), |
| base::BindOnce(&ContextualSearchDelegate::OnURLLoadComplete, |
| base::Unretained(this))); |
| } |
| |
| void ContextualSearchDelegate::CancelSearchTermRequest() { |
| url_loader_.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::OnURLLoadComplete( |
| std::unique_ptr<std::string> response_body) { |
| SearchResolution resolution; |
| std::string prevent_preload; |
| |
| int response_code = -1; |
| if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers) { |
| response_code = url_loader_->ResponseInfo()->headers->response_code(); |
| } |
| resolution.response_code = response_code; |
| if (response_body && response_code == net::HTTP_OK) { |
| resolution.start_offset = -1; |
| resolution.end_offset = -1; |
| DecodeSearchTermsFromJsonResponse( |
| *response_body, &resolution.search_term, &resolution.display_text, |
| &resolution.alternate_term, &prevent_preload, resolution.start_offset, |
| resolution.end_offset); |
| } |
| resolution.is_invalid = resolution.response_code == -1; |
| 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(); |
| |
| std::string request = |
| GetSearchTermResolutionUrlString(selected_text_escaped, base_page_url); |
| |
| return GURL(request); |
| } |
| |
| std::string ContextualSearchDelegate::GetSearchTermResolutionUrlString( |
| const std::string& selected_text, |
| const std::string& base_page_url) { |
| const TemplateURL* template_url = |
| template_url_service_->GetDefaultSearchProvider(); |
| |
| TemplateURLRef::SearchTermsArgs search_terms_args = |
| TemplateURLRef::SearchTermsArgs(base::string16()); |
| |
| TemplateURLRef::SearchTermsArgs::ContextualSearchParams params( |
| kContextualSearchRequestVersion, kContextualCardsNoIntegration, |
| std::string(), 0L, 0); |
| |
| 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, |
| network::ResourceRequest* resource_request) { |
| 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( |
| base::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(), '/', '_'); |
| resource_request->headers.SetHeader(kDiscourseContextHeaderPrefix, |
| encoded_context); |
| } |