| // Copyright 2020 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 "chrome/browser/chromeos/login/marketing_backend_connector.h" |
| #include <cstddef> |
| |
| #include "base/bind_helpers.h" |
| #include "base/command_line.h" |
| #include "base/json/json_writer.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/chromeos/profiles/profile_helper.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chromeos/constants/chromeos_switches.h" |
| #include "components/signin/public/identity_manager/access_token_info.h" |
| #include "components/signin/public/identity_manager/account_info.h" |
| #include "components/signin/public/identity_manager/consent_level.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/signin/public/identity_manager/scope_set.h" |
| #include "components/user_manager/user.h" |
| #include "components/user_manager/user_manager.h" |
| #include "net/base/load_flags.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_status_code.h" |
| |
| namespace chromeos { |
| |
| namespace { |
| // The scope that will be used to access the ChromebookEmailService API. |
| const char kChromebookOAuth2Scope[] = |
| "https://www.googleapis.com/auth/chromebook.email"; |
| |
| // API Endpoint |
| const char kAccessPointsApiEndpoint[] = "https://accesspoints.googleapis.com/"; |
| const char kChromebookEmailServicePath[] = "v2/chromebookEmailPreferences"; |
| constexpr size_t kResponseMaxBodySize = 4 * 1024 * 1024; // 4MiB |
| |
| const GURL GetChromebookServiceEndpoint() { |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| |
| // Allows the URL to be completely overridden from the command line. |
| return (command_line->HasSwitch(switches::kMarketingOptInUrl)) |
| ? GURL(command_line->GetSwitchValueASCII( |
| switches::kMarketingOptInUrl)) |
| : GURL(kAccessPointsApiEndpoint + |
| std::string(kChromebookEmailServicePath)); |
| } |
| |
| // UMA Metrics |
| void RecordUMAHistogram(MarketingBackendConnector::BackendConnectorEvent event, |
| const std::string& country) { |
| base::UmaHistogramEnumeration( |
| "OOBE.MarketingOptInScreen.BackendConnector." + country, event); |
| |
| // Generic event aggregating data from all countries. |
| base::UmaHistogramEnumeration("OOBE.MarketingOptInScreen.BackendConnector", |
| event); |
| } |
| |
| std::unique_ptr<network::ResourceRequest> GetResourceRequest() { |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = GetChromebookServiceEndpoint(); |
| resource_request->load_flags = net::LOAD_DISABLE_CACHE; |
| resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| resource_request->method = "POST"; |
| return resource_request; |
| } |
| } // namespace |
| |
| // static |
| base::RepeatingCallback<void(std::string)>* |
| MarketingBackendConnector::request_finished_for_tests_ = nullptr; |
| |
| // static |
| void MarketingBackendConnector::UpdateEmailPreferences( |
| Profile* profile, |
| const std::string& country_code) { |
| DCHECK(profile); |
| VLOG(1) << "Subscribing the user to all chromebook email campaigns."; |
| |
| // Early exit for testing |
| if (MarketingBackendConnector::request_finished_for_tests_ != nullptr) { |
| std::move(*MarketingBackendConnector::request_finished_for_tests_) |
| .Run(country_code); |
| return; |
| } |
| |
| // No requests without a Gaia account |
| if (profile->IsOffTheRecord()) |
| return; |
| |
| scoped_refptr<MarketingBackendConnector> ref = |
| new MarketingBackendConnector(profile); |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&MarketingBackendConnector::PerformRequest, ref, |
| country_code)); |
| } |
| |
| MarketingBackendConnector::MarketingBackendConnector(Profile* profile) |
| : profile_(profile) {} |
| |
| void MarketingBackendConnector::PerformRequest( |
| const std::string& country_code) { |
| country_code_ = country_code; |
| StartTokenFetch(); |
| } |
| |
| void MarketingBackendConnector::StartTokenFetch() { |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(profile_); |
| if (!identity_manager) { |
| RecordUMAHistogram(BackendConnectorEvent::kErrorOther, country_code_); |
| return; |
| } |
| |
| signin::ScopeSet chromebook_scope; |
| chromebook_scope.insert(kChromebookOAuth2Scope); |
| token_fetcher_ = std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| "MarketingBackendConnector", identity_manager, chromebook_scope, |
| base::BindOnce(&MarketingBackendConnector::OnAccessTokenRequestCompleted, |
| this), |
| signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate); |
| } |
| |
| void MarketingBackendConnector::OnAccessTokenRequestCompleted( |
| GoogleServiceAuthError error, |
| signin::AccessTokenInfo access_token_info) { |
| token_fetcher_.reset(); |
| |
| if (error.state() == GoogleServiceAuthError::NONE) { |
| access_token_ = access_token_info.token; |
| VLOG(2) << "Token fetch succeeded."; |
| SetTokenAndStartRequest(); |
| } else { |
| VLOG(1) << "Auth Error: " << error.ToString(); |
| RecordUMAHistogram(BackendConnectorEvent::kErrorAuth, country_code_); |
| } |
| } |
| |
| void MarketingBackendConnector::SetTokenAndStartRequest() { |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("chromebook_mail_api", R"( |
| semantics { |
| sender: "Chrome OS Marketing Opt-In Screen" |
| description: |
| "Communication with the Chromebook Email API to change the user's" |
| "preference regarding marketing emails. It is only used on the" |
| "last screen of the Chrome OS OOBE - Marketing Opt-In Screen. |
| trigger: |
| "The request is triggered when the user opts-in for marketing" |
| "emails by enabling the toggle on the marketing opt-in screen." |
| data: |
| "The only transmitted information is the country and the language" |
| "of the user's account. This information is used for delivering" |
| "emails to the user in the requested language." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| setting: |
| "Not opting-in to the emails will not generate a request." |
| cookies_allowed: NO |
| policy_exception_justification: |
| "Managed users are not presented with the option to opt-in." |
| } |
| })"); |
| |
| auto resource_request = GetResourceRequest(); |
| resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization, |
| std::string("Bearer ") + access_token_); |
| |
| simple_url_loader_ = network::SimpleURLLoader::Create( |
| std::move(resource_request), traffic_annotation); |
| |
| simple_url_loader_->SetAllowHttpErrorResults(true); |
| simple_url_loader_->AttachStringForUpload(GetRequestContent(), |
| "application/json"); |
| |
| url_loader_factory_ = profile_->GetURLLoaderFactory(); |
| simple_url_loader_->DownloadToString( |
| url_loader_factory_.get(), |
| base::BindOnce(&MarketingBackendConnector::OnSimpleLoaderComplete, this), |
| kResponseMaxBodySize); |
| } |
| |
| void MarketingBackendConnector::OnSimpleLoaderComplete( |
| std::unique_ptr<std::string> response_body) { |
| int response_code = -1; |
| std::string raw_header; |
| if (simple_url_loader_->ResponseInfo() && |
| simple_url_loader_->ResponseInfo()->headers) { |
| response_code = |
| simple_url_loader_->ResponseInfo()->headers->response_code(); |
| } |
| |
| std::string data; |
| if (response_body) |
| data = std::move(*response_body); |
| |
| OnSimpleLoaderCompleteInternal(response_code, data); |
| } |
| |
| void MarketingBackendConnector::OnSimpleLoaderCompleteInternal( |
| int response_code, |
| const std::string& data) { |
| VLOG(2) << "Response Code = " << response_code << " Data = " << data; |
| |
| switch (response_code) { |
| case net::HTTP_OK: { |
| VLOG(1) << "Successfully set the user preferences on the server."; |
| RecordUMAHistogram(BackendConnectorEvent::kSuccess, country_code_); |
| return; |
| } |
| case net::HTTP_INTERNAL_SERVER_ERROR: { |
| VLOG(1) << "Internal server error occurred."; |
| RecordUMAHistogram(BackendConnectorEvent::kErrorServerInternal, |
| country_code_); |
| return; |
| } |
| case net::HTTP_REQUEST_TIMEOUT: { |
| RecordUMAHistogram(BackendConnectorEvent::kErrorRequestTimeout, |
| country_code_); |
| return; |
| } |
| case net::HTTP_UNAUTHORIZED: { |
| RecordUMAHistogram(BackendConnectorEvent::kErrorAuth, country_code_); |
| return; |
| } |
| } |
| // Failure. There is nothing we can do at this point. |
| RecordUMAHistogram(BackendConnectorEvent::kErrorOther, country_code_); |
| } |
| |
| std::string MarketingBackendConnector::GetRequestContent() { |
| base::Value request_dict(base::Value::Type::DICTIONARY); |
| request_dict.SetKey("country_code", base::Value(country_code_)); |
| request_dict.SetKey("language", base::Value("en")); |
| |
| std::string request_content; |
| base::JSONWriter::Write(request_dict, &request_content); |
| return request_content; |
| } |
| |
| MarketingBackendConnector::~MarketingBackendConnector() {} |
| |
| ScopedRequestCallbackSetter::ScopedRequestCallbackSetter( |
| std::unique_ptr<base::RepeatingCallback<void(std::string)>> callback) |
| : callback_(std::move(callback)) { |
| MarketingBackendConnector::request_finished_for_tests_ = callback_.get(); |
| } |
| |
| ScopedRequestCallbackSetter::~ScopedRequestCallbackSetter() { |
| MarketingBackendConnector::request_finished_for_tests_ = nullptr; |
| } |
| |
| } // namespace chromeos |