blob: 6b9f001037cc0f1013689d890052032cf18a9ff9 [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/enterprise/connectors/file_system/rename_handler.h"
#include <utility>
#include "base/files/file_util.h"
#include "base/observer_list.h"
#include "chrome/browser/enterprise/connectors/file_system/access_token_fetcher.h"
#include "chrome/browser/enterprise/connectors/file_system/account_info_utils.h"
#include "chrome/browser/enterprise/connectors/file_system/box_uploader.h"
#include "chrome/browser/enterprise/connectors/file_system/signin_dialog_delegate.h"
#include "chrome/browser/enterprise/connectors/file_system/signin_experience.h"
#include "chrome/browser/enterprise/connectors/file_system/uma_metrics_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
#include "components/download/public/common/download_interrupt_reasons_utils.h"
#include "components/download/public/common/download_item.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/storage_partition.h"
namespace enterprise_connectors {
namespace {
// The OAuth token consumer name.
const char kOAuthConsumerName[] = "file_system_rename_handler";
PrefService* PrefsFromBrowserContext(content::BrowserContext* context) {
return Profile::FromBrowserContext(context)->GetPrefs();
}
PrefService* PrefsFromDownloadItem(download::DownloadItem* item) {
content::BrowserContext* context =
content::DownloadItemUtils::GetBrowserContext(item);
return context ? PrefsFromBrowserContext(context) : nullptr;
}
using download::ConvertNetErrorToInterruptReason;
} // namespace
// static
std::unique_ptr<download::DownloadItemRenameHandler>
FileSystemRenameHandler::CreateIfNeeded(download::DownloadItem* download_item) {
if (download_item->GetState() == download::DownloadItem::COMPLETE) {
auto rename_handler =
download_item->GetRerouteInfo().IsInitialized()
? std::make_unique<FileSystemRenameHandler>(download_item)
: nullptr;
return rename_handler;
}
// TODO(https://crbug.com/1213761) Resume upload if state is IN_PROGRESS, and
// perhaps also INTERRUPTED and CANCELLED.
absl::optional<FileSystemSettings> settings =
GetFileSystemSettings(download_item);
auto rename_handler = settings.has_value()
? std::make_unique<FileSystemRenameHandler>(
download_item, std::move(settings.value()))
: nullptr;
if (!rename_handler) {
UmaLogDownloadsRoutingDestination(
EnterpriseFileSystemDownloadsRoutingDestination::NOT_ROUTED);
} else if (rename_handler->settings_.service_provider ==
kFileSystemServiceProviderPrefNameBox) {
UmaLogDownloadsRoutingDestination(
EnterpriseFileSystemDownloadsRoutingDestination::ROUTED_TO_BOX);
} else {
NOTREACHED() << "Unsupported service provider: "
<< rename_handler->settings_.service_provider;
}
return rename_handler;
}
// The only permitted use of |download_item| in this class other than the ctor
// is passing it to content::DownloadItemUtils methods. Methods in this class
// run on the UI thread where methods of |download_item| should not be called.
FileSystemRenameHandler::FileSystemRenameHandler(
download::DownloadItem* download_item,
FileSystemSettings settings)
: download::DownloadItemRenameHandler(download_item),
settings_(std::move(settings)),
uploader_(BoxUploader::Create(download_item)) {
DCHECK_EQ(settings_.service_provider, kFileSystemServiceProviderPrefNameBox);
}
FileSystemRenameHandler::FileSystemRenameHandler(
download::DownloadItem* download_item)
: download::DownloadItemRenameHandler(download_item),
uploader_(BoxUploader::Create(download_item)) {}
FileSystemRenameHandler::~FileSystemRenameHandler() {
for (auto& observer : observers_)
observer.OnDestruction();
}
void FileSystemRenameHandler::Start(ProgressUpdateCallback progress_update_cb,
DownloadCallback upload_complete_cb) {
uploader_->Init(
base::BindRepeating(&FileSystemRenameHandler::OnApiAuthenticationError,
weak_factory_.GetWeakPtr()),
std::move(progress_update_cb), std::move(upload_complete_cb), GetPrefs());
for (auto& observer : observers_)
observer.OnStart();
StartInternal();
}
void FileSystemRenameHandler::TryUploaderTask(content::BrowserContext* context,
const std::string& access_token) {
uploader_->TryTask(GetURLLoaderFactory(context), access_token);
}
void FileSystemRenameHandler::PromptUserSignInForAuthorization(
content::WebContents* contents) {
StartFileSystemConnectorSigninExperienceForDownloadItem(
contents, settings_, GetPrefs(),
base::BindOnce(&FileSystemRenameHandler::OnAuthorization,
weak_factory_.GetWeakPtr()),
signin_observer_);
}
void FileSystemRenameHandler::FetchAccessToken(
content::BrowserContext* context,
const std::string& refresh_token) {
token_fetcher_ = std::make_unique<AccessTokenFetcher>(
GetURLLoaderFactory(context), settings_.service_provider,
settings_.token_endpoint, refresh_token, /*auth_code=*/std::string(),
kOAuthConsumerName,
base::BindOnce(&FileSystemRenameHandler::OnAccessTokenFetched,
weak_factory_.GetWeakPtr()));
for (auto& observer : observers_)
observer.OnFetchAccessTokenStart();
token_fetcher_->Start(settings_.client_id, settings_.client_secret,
settings_.scopes);
}
void FileSystemRenameHandler::SetUploaderForTesting(
std::unique_ptr<BoxUploader> fake_uploader) {
CHECK(fake_uploader);
uploader_ = std::move(fake_uploader);
}
void FileSystemRenameHandler::OpenDownload() {
AddTabToShowDownload(uploader_->GetUploadedFileUrl());
}
void FileSystemRenameHandler::ShowDownloadInContext() {
AddTabToShowDownload(uploader_->GetDestinationFolderUrl());
}
void FileSystemRenameHandler::AddTabToShowDownload(const GURL& url) {
if (url.is_valid()) {
content::BrowserContext* context =
content::DownloadItemUtils::GetBrowserContext(download_item());
Profile* profile = Profile::FromBrowserContext(context);
chrome::ScopedTabbedBrowserDisplayer displayer(profile);
Browser* browser = displayer.browser();
chrome::AddTabAt(browser, url, /* index = */ -1, /* foreground = */ true);
}
// The uploaded file or folder url's may not be valid before file upload completes, and we should
// avoid just opening a new empty tab. Therefore, a new tab is only opened when the url is valid.
}
void FileSystemRenameHandler::StartInternal(std::string access_token) {
PrefService* prefs;
content::BrowserContext* context =
content::DownloadItemUtils::GetBrowserContext(download_item());
content::WebContents* contents =
content::DownloadItemUtils::GetWebContents(download_item());
// Check these pointers because they are pulled from a map keyed by
// |download_item| from the UI thread. Since |download_item| lives in the
// download thread and can remove the mapping at any time, the pointers are
// not to be assumed valid on the UI thread.
if (!context || !contents || !(prefs = PrefsFromBrowserContext(context))) {
DLOG(ERROR) << "Empty pointers???";
uploader_->TerminateTask(kBrowserFailure);
return;
}
std::string refresh_token;
bool ok = access_token.size() ||
GetFileSystemOAuth2Tokens(prefs, settings_.service_provider,
&access_token, &refresh_token);
if (ok && access_token.size()) { // Case 2.
TryUploaderTask(context, access_token);
} else if (ok && refresh_token.size()) { // Case 3.
// Start AccessTokenFetcher to obtain access token with refresh token.
FetchAccessToken(context, refresh_token);
} else { // Case 1.
// Neither token is available, so prompt user to sign in & save new tokens.
PromptUserSignInForAuthorization(contents);
}
}
////////////////////////////////////////////////////////////////////////////////
// The OAuth2 "Dance":
// AToken || RToken || Action
// (1) N || N || PromptUserSignInForAuthorization()
// (2) Y || || TryUploaderTask()
// (3) N || Y || FetchAccessToken()
//
// (1) PromptUserSignInForAuthorization()
// (a) Success: SaveTokens -> (2).
// (b) Failure with GoogleServiceAuthError::State::REQUEST_CANCELED: [Abort].
// (c) Other failures (no network error): ClearRToken->(1).
// (d) Network Error: [Abort].
// (2) TryUploaderTask()
// (a) No authentication error: result sent back to download thread [Done].
// (b) Authentication error: ClearAToken -> (3).
// (3) FetchAccessToken()
// (a) Success: SaveTokens->(2).
// (b) None-Network Failures: ClearRToken->(1).
// (c) Network Error: [Abort].
////////////////////////////////////////////////////////////////////////////////
scoped_refptr<network::SharedURLLoaderFactory>
FileSystemRenameHandler::GetURLLoaderFactory(content::BrowserContext* context) {
content::StoragePartition* partition = context->GetDefaultStoragePartition();
return partition->GetURLLoaderFactoryForBrowserProcess();
}
// Case 1:
void FileSystemRenameHandler::OnAuthorization(
const GoogleServiceAuthError& status,
const std::string& access_token,
const std::string& refresh_token) {
if (status.state() == GoogleServiceAuthError::State::REQUEST_CANCELED) {
// 1b:
OnSignInCancellation();
} else {
// 1a or 1c:
OnAccessTokenFetched(status, access_token, refresh_token);
}
}
// Case 3 but overlaps with Case 1a and 1c
void FileSystemRenameHandler::OnAccessTokenFetched(
const GoogleServiceAuthError& status,
const std::string& access_token,
const std::string& refresh_token) {
for (auto& observer : observers_)
observer.OnAccessTokenFetched(status);
// Case 1d or 3c:
const net::Error net_error = static_cast<net::Error>(status.network_error());
if (net_error) {
// Don't clear the OAuth2 tokens if it's only a network error.
DCHECK_EQ(status.state(), GoogleServiceAuthError::State::CONNECTION_FAILED);
return uploader_->TerminateTask(ConvertNetErrorToInterruptReason(
net_error, download::DOWNLOAD_INTERRUPT_FROM_NETWORK));
}
// Case 1a and 3a:
if (status.state() == GoogleServiceAuthError::State::NONE) {
const bool save_success = SetFileSystemOAuth2Tokens(
GetPrefs(), settings_.service_provider, access_token, refresh_token);
LOG_IF(ERROR, !save_success) << "Failed to save OAuth2 tokens.";
// Can proceed with current task using this token even if failed to save.
return StartInternal(access_token);
}
// Case 1c and 3b:
if (ClearFileSystemOAuth2Tokens(GetPrefs(), settings_.service_provider)) {
return OnAuthenticationError(status);
}
// Handle token storage operations failure.
return uploader_->TerminateTask(kCredentialUpdateFailure);
}
void FileSystemRenameHandler::OnAuthenticationError(
const GoogleServiceAuthError& error) {
if (ClearFileSystemAccessToken(GetPrefs(), settings_.service_provider)) {
// Case 2b, but also Case 1c and 3b so that now both tokens are cleared.
VLOG(20) << "Re-authenticating...";
StartInternal();
} else {
LOG(ERROR) << "Failed to clear access token. Will notify failure back.";
uploader_->TerminateTask(kCredentialUpdateFailure);
}
}
void FileSystemRenameHandler::OnSignInCancellation() {
DLOG(ERROR) << "Sign in canceled!";
const bool clear_success =
ClearFileSystemOAuth2Tokens(GetPrefs(), settings_.service_provider);
LOG_IF(ERROR, !clear_success) << "Failed to clear OAuth2 tokens.";
uploader_->TerminateTask(kSignInCancellation);
}
void FileSystemRenameHandler::OnApiAuthenticationError() {
VLOG(20) << "Authentication failed in service provider API calls.";
return OnAuthenticationError(
GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
GoogleServiceAuthError::InvalidGaiaCredentialsReason::
CREDENTIALS_REJECTED_BY_SERVER));
}
PrefService* FileSystemRenameHandler::GetPrefs() {
return PrefsFromDownloadItem(download_item());
}
base::WeakPtr<FileSystemRenameHandler>
FileSystemRenameHandler::RegisterSigninObserverForTesting(
SigninExperienceTestObserver* observer) {
// The FileSystemRenameHandler should open trigger the Box signin workflow
// once. The observer should be set only once, otherwise one observer will
// forcibly detach another observer.
DCHECK(signin_observer_ == nullptr);
signin_observer_ = observer;
return weak_factory_.GetWeakPtr();
}
void FileSystemRenameHandler::UnregisterSigninObserverForTesting(
SigninExperienceTestObserver* observer) {
DCHECK(observer == signin_observer_);
signin_observer_ = nullptr;
}
// FileSystemRenameHandler::TestObserver
FileSystemRenameHandler::TestObserver::TestObserver(
FileSystemRenameHandler* rename_handler)
: rename_handler_(rename_handler->weak_factory_.GetWeakPtr()) {
rename_handler->observers_.AddObserver(this);
}
FileSystemRenameHandler::TestObserver::~TestObserver() {
if (rename_handler_)
rename_handler_->observers_.RemoveObserver(this);
}
void FileSystemRenameHandler::TestObserver::OnDestruction() {
rename_handler_.reset();
}
// static
BoxUploader* FileSystemRenameHandler::TestObserver::GetBoxUploader(
FileSystemRenameHandler* rename_handler) {
return rename_handler->uploader_.get();
}
// RenameStartObserver
RenameStartObserver::RenameStartObserver(
FileSystemRenameHandler* rename_handler)
: FileSystemRenameHandler::TestObserver(rename_handler) {}
void RenameStartObserver::OnStart() {
started_ = true;
if (run_loop_.running())
run_loop_.Quit();
}
void RenameStartObserver::WaitForStart() {
if (!started_)
run_loop_.Run();
}
// BoxFetchAccessTokenTestObserver
BoxFetchAccessTokenTestObserver::BoxFetchAccessTokenTestObserver(
FileSystemRenameHandler* rename_handler)
: FileSystemRenameHandler::TestObserver(rename_handler) {}
void BoxFetchAccessTokenTestObserver::OnFetchAccessTokenStart() {
status_ = Status::kInProgress;
}
void BoxFetchAccessTokenTestObserver::OnAccessTokenFetched(
const GoogleServiceAuthError& status) {
fetch_token_err_ = status;
status_ = Status::kSucceeded;
if (run_loop_.running())
run_loop_.Quit();
}
bool BoxFetchAccessTokenTestObserver::WaitForFetch() {
if (status_ != Status::kSucceeded)
run_loop_.Run();
return fetch_token_err_.state() == GoogleServiceAuthError::State::NONE;
}
} // namespace enterprise_connectors