summaryrefslogtreecommitdiff
path: root/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc')
-rw-r--r--chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc305
1 files changed, 305 insertions, 0 deletions
diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc
new file mode 100644
index 00000000000..dcf9d84eca7
--- /dev/null
+++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc
@@ -0,0 +1,305 @@
+// Copyright 2017 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 "components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.h"
+
+#include "base/memory/ptr_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/time/default_clock.h"
+#include "base/time/time.h"
+#include "base/values.h"
+#include "components/ntp_snippets/category.h"
+#include "components/signin/core/browser/access_token_fetcher.h"
+#include "components/strings/grit/components_strings.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_request_status.h"
+#include "ui/base/l10n/l10n_util.h"
+
+using net::HttpRequestHeaders;
+using net::URLFetcher;
+using net::URLRequestContextGetter;
+
+namespace ntp_snippets {
+
+using internal::ContextualJsonRequest;
+using internal::FetchResult;
+
+namespace {
+
+const char kContentSuggestionsApiScope[] =
+ "https://www.googleapis.com/auth/chrome-content-suggestions";
+const char kAuthorizationRequestHeaderFormat[] = "Bearer %s";
+
+std::string FetchResultToString(FetchResult result) {
+ switch (result) {
+ case FetchResult::SUCCESS:
+ return "OK";
+ case FetchResult::URL_REQUEST_STATUS_ERROR:
+ return "URLRequestStatus error";
+ case FetchResult::HTTP_ERROR:
+ return "HTTP error";
+ case FetchResult::JSON_PARSE_ERROR:
+ return "Received invalid JSON";
+ case FetchResult::INVALID_SNIPPET_CONTENT_ERROR:
+ return "Invalid / empty list.";
+ case FetchResult::OAUTH_TOKEN_ERROR:
+ return "Error in obtaining an OAuth2 access token.";
+ case FetchResult::MISSING_API_KEY:
+ return "No API key available.";
+ case FetchResult::RESULT_MAX:
+ break;
+ }
+ NOTREACHED();
+ return "Unknown error";
+}
+
+Status FetchResultToStatus(FetchResult result) {
+ switch (result) {
+ case FetchResult::SUCCESS:
+ return Status::Success();
+ // Permanent errors occur if it is more likely that the error originated
+ // from the client.
+ case FetchResult::OAUTH_TOKEN_ERROR:
+ case FetchResult::MISSING_API_KEY:
+ return Status(StatusCode::PERMANENT_ERROR, FetchResultToString(result));
+ // Temporary errors occur if it's more likely that the client behaved
+ // correctly but the server failed to respond as expected.
+ // TODO(fhorschig): Revisit HTTP_ERROR once the rescheduling was reworked.
+ case FetchResult::HTTP_ERROR:
+ case FetchResult::URL_REQUEST_STATUS_ERROR:
+ case FetchResult::INVALID_SNIPPET_CONTENT_ERROR:
+ case FetchResult::JSON_PARSE_ERROR:
+ return Status(StatusCode::TEMPORARY_ERROR, FetchResultToString(result));
+ case FetchResult::RESULT_MAX:
+ break;
+ }
+ NOTREACHED();
+ return Status(StatusCode::PERMANENT_ERROR, std::string());
+}
+
+std::string GetFetchEndpoint() {
+ return "https://alpha-chromecontentsuggestions-pa.sandbox.googleapis.com/v1"
+ "/publicdebate"
+ "/getsuggestions";
+}
+
+// Creates suggestions from dictionary values in |list| and adds them to
+// |suggestions|. Returns true on success, false if anything went wrong.
+bool AddSuggestionsFromListValue(bool content_suggestions_api,
+ const base::ListValue& list,
+ RemoteSuggestion::PtrVector* suggestions) {
+ for (const auto& value : list) {
+ const base::DictionaryValue* dict = nullptr;
+ if (!value.GetAsDictionary(&dict)) {
+ return false;
+ }
+
+ std::string s;
+ dict->GetAsString(&s);
+ DVLOG(1) << "AddSuggestionsFromListValue " << s;
+ std::unique_ptr<RemoteSuggestion> suggestion =
+ RemoteSuggestion::CreateFromContextualSuggestionsDictionary(*dict);
+ suggestions->push_back(std::move(suggestion));
+ }
+ return true;
+}
+
+} // namespace
+
+ContextualSuggestionsFetcherImpl::ContextualSuggestionsFetcherImpl(
+ SigninManagerBase* signin_manager,
+ OAuth2TokenService* token_service,
+ scoped_refptr<URLRequestContextGetter> url_request_context_getter,
+ PrefService* pref_service,
+ const ParseJSONCallback& parse_json_callback)
+ : signin_manager_(signin_manager),
+ token_service_(token_service),
+ url_request_context_getter_(std::move(url_request_context_getter)),
+ parse_json_callback_(parse_json_callback),
+ fetch_url_(GetFetchEndpoint()) {}
+
+ContextualSuggestionsFetcherImpl::~ContextualSuggestionsFetcherImpl() = default;
+
+const std::string& ContextualSuggestionsFetcherImpl::GetLastStatusForTesting()
+ const {
+ return last_status_;
+}
+const std::string& ContextualSuggestionsFetcherImpl::GetLastJsonForTesting()
+ const {
+ return last_fetch_json_;
+}
+const GURL& ContextualSuggestionsFetcherImpl::GetFetchUrlForTesting() const {
+ return fetch_url_;
+}
+
+void ContextualSuggestionsFetcherImpl::FetchContextualSuggestions(
+ const GURL& url,
+ SuggestionsAvailableCallback callback) {
+ ContextualJsonRequest::Builder builder;
+ builder.SetParseJsonCallback(parse_json_callback_)
+ .SetUrlRequestContextGetter(url_request_context_getter_)
+ .SetContentUrl(url);
+
+ pending_requests_.emplace(std::move(builder), std::move(callback));
+ StartTokenRequest();
+}
+
+void ContextualSuggestionsFetcherImpl::StartRequest(
+ ContextualJsonRequest::Builder builder,
+ SuggestionsAvailableCallback callback,
+ const std::string& oauth_access_token) {
+ builder.SetUrl(fetch_url_)
+ .SetAuthentication(signin_manager_->GetAuthenticatedAccountId(),
+ base::StringPrintf(kAuthorizationRequestHeaderFormat,
+ oauth_access_token.c_str()));
+ DVLOG(1) << "ContextualSuggestionsFetcherImpl::StartRequest";
+ std::unique_ptr<ContextualJsonRequest> request = builder.Build();
+ ContextualJsonRequest* raw_request = request.get();
+ raw_request->Start(base::BindOnce(
+ &ContextualSuggestionsFetcherImpl::JsonRequestDone,
+ base::Unretained(this), std::move(request), std::move(callback)));
+}
+
+void ContextualSuggestionsFetcherImpl::StartTokenRequest() {
+ // If there is already an ongoing token request, just wait for that.
+ if (token_fetcher_) {
+ return;
+ }
+
+ OAuth2TokenService::ScopeSet scopes{kContentSuggestionsApiScope};
+ token_fetcher_ = base::MakeUnique<AccessTokenFetcher>(
+ "ntp_snippets", signin_manager_, token_service_, scopes,
+ base::BindOnce(
+ &ContextualSuggestionsFetcherImpl::AccessTokenFetchFinished,
+ base::Unretained(this)));
+}
+
+void ContextualSuggestionsFetcherImpl::AccessTokenFetchFinished(
+ const GoogleServiceAuthError& error,
+ const std::string& access_token) {
+ // Delete the fetcher only after we leave this method (which is called from
+ // the fetcher itself).
+ DCHECK(token_fetcher_);
+ std::unique_ptr<AccessTokenFetcher> token_fetcher_deleter(
+ std::move(token_fetcher_));
+
+ if (error.state() != GoogleServiceAuthError::NONE) {
+ AccessTokenError(error);
+ return;
+ }
+
+ DCHECK(!access_token.empty());
+
+ while (!pending_requests_.empty()) {
+ std::pair<ContextualJsonRequest::Builder, SuggestionsAvailableCallback>
+ builder_and_callback = std::move(pending_requests_.front());
+ pending_requests_.pop();
+ StartRequest(std::move(builder_and_callback.first),
+ std::move(builder_and_callback.second), access_token);
+ }
+}
+
+void ContextualSuggestionsFetcherImpl::AccessTokenError(
+ const GoogleServiceAuthError& error) {
+ DCHECK_NE(error.state(), GoogleServiceAuthError::NONE);
+
+ LOG(WARNING) << "ContextualSuggestionsFetcherImpl::AccessTokenError "
+ "Unable to get token: "
+ << error.ToString();
+
+ while (!pending_requests_.empty()) {
+ std::pair<ContextualJsonRequest::Builder, SuggestionsAvailableCallback>
+ builder_and_callback = std::move(pending_requests_.front());
+
+ FetchFinished(OptionalSuggestions(), std::move(builder_and_callback.second),
+ FetchResult::OAUTH_TOKEN_ERROR,
+ /*error_details=*/
+ base::StringPrintf(" (%s)", error.ToString().c_str()));
+ pending_requests_.pop();
+ }
+}
+
+void ContextualSuggestionsFetcherImpl::JsonRequestDone(
+ std::unique_ptr<ContextualJsonRequest> request,
+ SuggestionsAvailableCallback callback,
+ std::unique_ptr<base::Value> result,
+ FetchResult status_code,
+ const std::string& error_details) {
+ DCHECK(request);
+
+ DVLOG(1) << "ContextualSuggestionsFetcherImpl::JsonRequestDone status_code="
+ << static_cast<int>(status_code)
+ << " error_details=" << error_details;
+ last_fetch_json_ = request->GetResponseString();
+
+ // TODO(gaschler): Add UMA metrics for fetch duration of the request
+
+ if (!result) {
+ FetchFinished(OptionalSuggestions(), std::move(callback), status_code,
+ error_details);
+ return;
+ }
+
+ OptionalSuggestions optional_suggestions = RemoteSuggestion::PtrVector();
+ if (!JsonToSuggestions(*result, &optional_suggestions.value())) {
+ DLOG(WARNING) << "Received invalid suggestions: " << last_fetch_json_;
+ FetchFinished(OptionalSuggestions(), std::move(callback),
+ FetchResult::INVALID_SNIPPET_CONTENT_ERROR, std::string());
+ return;
+ }
+
+ FetchFinished(std::move(optional_suggestions), std::move(callback),
+ FetchResult::SUCCESS, std::string());
+}
+
+void ContextualSuggestionsFetcherImpl::FetchFinished(
+ OptionalSuggestions optional_suggestions,
+ SuggestionsAvailableCallback callback,
+ FetchResult fetch_result,
+ const std::string& error_details) {
+ DCHECK(fetch_result == FetchResult::SUCCESS ||
+ !optional_suggestions.has_value());
+
+ last_status_ = FetchResultToString(fetch_result) + error_details;
+
+ DVLOG(1) << "Fetch finished: " << last_status_;
+
+ std::move(callback).Run(FetchResultToStatus(fetch_result),
+ std::move(optional_suggestions));
+}
+
+bool ContextualSuggestionsFetcherImpl::JsonToSuggestions(
+ const base::Value& parsed,
+ RemoteSuggestion::PtrVector* suggestions) {
+ const base::DictionaryValue* top_dict = nullptr;
+ if (!parsed.GetAsDictionary(&top_dict)) {
+ return false;
+ }
+
+ const base::ListValue* categories_value = nullptr;
+ if (!top_dict->GetList("categories", &categories_value)) {
+ return false;
+ }
+
+ for (const auto& v : *categories_value) {
+ const base::DictionaryValue* category_value = nullptr;
+ if (!(v.GetAsDictionary(&category_value))) {
+ return false;
+ }
+
+ const base::ListValue* suggestions_list = nullptr;
+ // Absence of a list of suggestions is treated as an empty list, which
+ // is permissible.
+ if (category_value->GetList("suggestions", &suggestions_list)) {
+ if (!AddSuggestionsFromListValue(true, *suggestions_list, suggestions)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+} // namespace ntp_snippets