blob: 757979169b22c8cb092f3eadf6d2f3bc82e9f45c [file] [log] [blame]
// Copyright 2021 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/new_tab_page/modules/drive/drive_service.h"
#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include "base/hash/hash.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/search/ntp_features.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/signin/public/identity_manager/scope_set.h"
#include "google_apis/gaia/gaia_constants.h"
#include "mojo/public/cpp/bindings/clone_traits.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/resource_request.h"
namespace {
#if OS_LINUX
constexpr char kPlatform[] = "LINUX";
#elif OS_WIN
constexpr char kPlatform[] = "WINDOWS";
#elif OS_MAC
constexpr char kPlatform[] = "MAC_OS";
#elif OS_CHROMEOS
constexpr char kPlatform[] = "CHROME_OS";
#else
constexpr char kPlatform[] = "UNSPECIFIED_PLATFORM";
#endif
// TODO(crbug/1178869): Add language code to request.
constexpr char kRequestBody[] = R"({
"client_info": {
"platform_type": "%s",
"scenario_type": "CHROME_NTP_FILES",
"language_code": "%s",
"request_type": "LIVE_REQUEST",
"client_tags": {
"name": "%s"
}
},
"max_suggestions": 3,
"type_detail_fields": "drive_item.title,drive_item.mimeType"
})";
// Maximum accepted size of an ItemSuggest response. 1MB.
constexpr int kMaxResponseSize = 1024 * 1024;
const char server_url[] = "https://appsitemsuggest-pa.googleapis.com/v1/items";
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("drive_service", R"(
semantics {
sender: "Drive Service"
description:
"The Drive Service requests suggestions for Drive files from "
"the Drive ItemSuggest API. The response will be displayed in NTP's "
"Drive Module."
trigger:
"Each time a user navigates to the NTP while "
"the Drive module is enabled and the user is "
"signed in."
data:
"OAuth2 access token."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"Users can control this feature by (1) selecting "
"a non-Google default search engine in Chrome "
"settings under 'Search Engine', (2) signing out, "
"or (3) disabling the Drive module."
chrome_policy {
DefaultSearchProviderEnabled {
policy_options {mode: MANDATORY}
DefaultSearchProviderEnabled: false
}
BrowserSignin {
policy_options {mode: MANDATORY}
BrowserSignin: 0
}
NTPCardsVisible {
NTPCardsVisible: false
}
}
})");
constexpr char kFakeData[] = R"({
"item": [
{
"itemId": "foo",
"url": "https://docs.google.com",
"driveItem": {
"title": "foo doc",
"mimeType": "application/vnd.google-apps.document"
},
"justification": {
"displayText": { "textSegment": [{"text": "You opened yesterday"}]}
}
},
{
"itemId": "bar",
"url": "https://sheets.google.com",
"driveItem": {
"title": "bar sheet",
"mimeType": "application/vnd.google-apps.spreadsheet"
},
"justification": {
"displayText": { "textSegment": [{"text": "You opened today"}]}
}
},
{
"itemId": "baz",
"url": "https://slides.google.com",
"driveItem": {
"title": "baz slides",
"mimeType": "application/vnd.google-apps.presentation"
},
"justification": {
"displayText": { "textSegment": [{"text": "You opened on Monday"}]}
}
}
]
}
)";
} // namespace
// static
const char DriveService::kLastDismissedTimePrefName[] =
"NewTabPage.Drive.LastDimissedTime";
// static
const base::TimeDelta DriveService::kDismissDuration =
base::TimeDelta::FromDays(14);
DriveService::~DriveService() = default;
DriveService::DriveService(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
signin::IdentityManager* identity_manager,
const std::string& application_locale,
PrefService* pref_service)
: url_loader_factory_(std::move(url_loader_factory)),
identity_manager_(identity_manager),
application_locale_(application_locale),
pref_service_(pref_service) {}
// static
void DriveService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterTimePref(kLastDismissedTimePrefName, base::Time());
}
void DriveService::GetDriveFiles(GetFilesCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
callbacks_.push_back(std::move(callback));
if (callbacks_.size() > 1) {
return;
}
// Bail if module is still dismissed.
if (!pref_service_->GetTime(kLastDismissedTimePrefName).is_null() &&
base::Time::Now() - pref_service_->GetTime(kLastDismissedTimePrefName) <
kDismissDuration) {
for (auto& callback : callbacks_) {
std::move(callback).Run(std::vector<drive::mojom::FilePtr>());
}
callbacks_.clear();
return;
}
// Skip fetch and jump straight to data parsing when serving fake data.
if (base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpDriveModule,
ntp_features::kNtpDriveModuleDataParam) == "fake") {
data_decoder::DataDecoder::ParseJsonIsolated(
kFakeData, base::BindOnce(&DriveService::OnJsonParsed,
weak_factory_.GetWeakPtr()));
return;
}
token_fetcher_ = std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
"ntp_drive_module", identity_manager_,
signin::ScopeSet({GaiaConstants::kDriveReadOnlyOAuth2Scope}),
base::BindOnce(&DriveService::OnTokenReceived,
weak_factory_.GetWeakPtr()),
signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate,
signin::ConsentLevel::kSync);
}
void DriveService::DismissModule() {
pref_service_->SetTime(kLastDismissedTimePrefName, base::Time::Now());
}
void DriveService::RestoreModule() {
pref_service_->SetTime(kLastDismissedTimePrefName, base::Time());
}
void DriveService::OnTokenReceived(GoogleServiceAuthError error,
signin::AccessTokenInfo token_info) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
token_fetcher_.reset();
if (error.state() != GoogleServiceAuthError::NONE) {
for (auto& callback : callbacks_) {
std::move(callback).Run(std::vector<drive::mojom::FilePtr>());
}
callbacks_.clear();
return;
}
// Skip fetch if data is cached and not expired.
// TODO(crbug.com/1221743): Leverage the standard HTTP cache once ItemSuggest
// supports GET requests.
if (cached_json_ && cached_json_token_ == token_info.token &&
/* We use std::max to guard against negative cache ages. This can happen,
for instance, when modifying the local clock. */
std::max((base::Time::Now() - cached_json_time_).InSeconds(),
INT64_C(0)) <
base::GetFieldTrialParamByFeatureAsInt(
ntp_features::kNtpDriveModule,
ntp_features::kNtpDriveModuleCacheMaxAgeSParam, 0)) {
data_decoder::DataDecoder::ParseJsonIsolated(
*cached_json_, base::BindOnce(&DriveService::OnJsonParsed,
weak_factory_.GetWeakPtr()));
return;
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->method = "POST";
resource_request->url = GURL(server_url);
// Cookies should not be allowed.
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
// Ignore cache for fresh results.
resource_request->load_flags =
net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kContentType,
"application/json");
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
"Bearer " + token_info.token);
DCHECK(!url_loader_);
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
kTrafficAnnotation);
url_loader_->SetRetryOptions(0, network::SimpleURLLoader::RETRY_NEVER);
url_loader_->AttachStringForUpload(
base::StringPrintf(kRequestBody, kPlatform, application_locale_.c_str(),
base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpDriveModule,
ntp_features::kNtpDriveModuleExperimentGroupParam)
.c_str()),
"application/json");
url_loader_->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&DriveService::OnJsonReceived, weak_factory_.GetWeakPtr(),
token_info.token),
kMaxResponseSize);
base::UmaHistogramSparse("NewTabPage.Modules.DataRequest",
base::PersistentHash("drive"));
}
void DriveService::OnJsonReceived(const std::string& token,
std::unique_ptr<std::string> response_body) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const int net_error = url_loader_->NetError();
url_loader_.reset();
if (net_error != net::OK || !response_body) {
for (auto& callback : callbacks_) {
std::move(callback).Run(std::vector<drive::mojom::FilePtr>());
}
callbacks_.clear();
return;
}
cached_json_ = std::move(response_body);
cached_json_time_ = base::Time::Now();
cached_json_token_ = token;
data_decoder::DataDecoder::ParseJsonIsolated(
*cached_json_,
base::BindOnce(&DriveService::OnJsonParsed, weak_factory_.GetWeakPtr()));
}
void DriveService::OnJsonParsed(
data_decoder::DataDecoder::ValueOrError result) {
if (!result.value) {
for (auto& callback : callbacks_) {
std::move(callback).Run(std::vector<drive::mojom::FilePtr>());
}
callbacks_.clear();
return;
}
auto* items = result.value->FindListPath("item");
if (!items) {
for (auto& callback : callbacks_) {
std::move(callback).Run(std::vector<drive::mojom::FilePtr>());
}
callbacks_.clear();
return;
}
std::vector<drive::mojom::FilePtr> document_list;
for (const auto& item : items->GetList()) {
auto* title = item.FindStringPath("driveItem.title");
auto* mime_type = item.FindStringPath("driveItem.mimeType");
auto* justification_text_segments =
item.FindListPath("justification.displayText.textSegment");
if (!justification_text_segments ||
justification_text_segments->GetList().size() == 0) {
continue;
}
std::string justification_text;
for (auto& text_segment : justification_text_segments->GetList()) {
auto* justification_text_path = text_segment.FindStringPath("text");
if (!justification_text_path) {
continue;
}
justification_text += *justification_text_path;
}
auto* id = item.FindStringKey("itemId");
auto* item_url = item.FindStringKey("url");
if (!title || !mime_type || justification_text.empty() || !id ||
!item_url || !GURL(*item_url).is_valid()) {
continue;
}
auto mojo_drive_doc = drive::mojom::File::New();
mojo_drive_doc->title = *title;
mojo_drive_doc->mime_type = *mime_type;
mojo_drive_doc->justification_text = justification_text;
mojo_drive_doc->id = *id;
mojo_drive_doc->item_url = GURL(*item_url);
document_list.push_back(std::move(mojo_drive_doc));
}
for (auto& callback : callbacks_) {
std::move(callback).Run(mojo::Clone(document_list));
}
callbacks_.clear();
}