| // 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/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/strings/grit/components_strings.h" |
| #include "net/url_request/url_fetcher.h" |
| #include "net/url_request/url_request_status.h" |
| #include "services/identity/public/cpp/primary_account_access_token_fetcher.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, |
| ContextualSuggestion::PtrVector* suggestions) { |
| for (const auto& value : list) { |
| const base::DictionaryValue* dict = nullptr; |
| if (!value.GetAsDictionary(&dict)) { |
| return false; |
| } |
| |
| if (DLOG_IS_ON(INFO)) { |
| std::string s; |
| dict->GetAsString(&s); |
| DVLOG(1) << "AddSuggestionsFromListValue " << s; |
| } |
| std::unique_ptr<ContextualSuggestion> suggestion = |
| ContextualSuggestion::CreateFromDictionary(*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_ = std::make_unique<identity::PrimaryAccountAccessTokenFetcher>( |
| "ntp_snippets", signin_manager_, token_service_, scopes, |
| base::BindOnce( |
| &ContextualSuggestionsFetcherImpl::AccessTokenFetchFinished, |
| base::Unretained(this)), |
| identity::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable); |
| } |
| |
| 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<identity::PrimaryAccountAccessTokenFetcher> |
| 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 = ContextualSuggestion::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, |
| ContextualSuggestion::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 |