chromium/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"

#include <optional>

#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/io_task.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/volume.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_info.h"
#include "chrome/browser/ash/file_system_provider/service.h"
#include "chrome/browser/chromeos/upload_office_to_cloud/upload_office_to_cloud.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/extensions/api/file_system_provider_capabilities/file_system_provider_capabilities_handler.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "content/public/browser/browser_thread.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/rect.h"

namespace ash::cloud_upload {
namespace {

using file_system_provider::Action;
using file_system_provider::Actions;
using file_system_provider::ProvidedFileSystemInfo;
using file_system_provider::ProvidedFileSystemInterface;
using file_system_provider::ProviderId;
using file_system_provider::Service;

}  // namespace

ODFSEntryMetadata::ODFSEntryMetadata() = default;
ODFSEntryMetadata::ODFSEntryMetadata(const ODFSEntryMetadata&) = default;
ODFSEntryMetadata::~ODFSEntryMetadata() = default;

std::string GetGenericErrorMessage() {
  return l10n_util::GetStringUTF8(IDS_OFFICE_UPLOAD_ERROR_GENERIC);
}

std::string GetReauthenticationRequiredMessage() {
  return l10n_util::GetStringUTF8(
      IDS_OFFICE_UPLOAD_ERROR_REAUTHENTICATION_REQUIRED);
}

std::string GetNotAValidDocumentErrorMessage() {
  return l10n_util::GetStringUTF8(IDS_OFFICE_UPLOAD_ERROR_NOT_A_VALID_DOCUMENT);
}

std::string GetAlreadyBeingOpenedMessage() {
  return l10n_util::GetStringUTF8(IDS_OFFICE_FILE_ALREADY_BEING_OPENED_MESSAGE);
}

std::string GetAlreadyBeingOpenedTitle() {
  return l10n_util::GetStringUTF8(IDS_OFFICE_FILE_ALREADY_BEING_OPENED_TITLE);
}

storage::FileSystemURL FilePathToFileSystemURL(
    Profile* profile,
    scoped_refptr<storage::FileSystemContext> file_system_context,
    base::FilePath file_path) {
  GURL url;
  if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
          profile, file_path, file_manager::util::GetFileManagerURL(), &url)) {
    LOG(ERROR) << "Unable to ConvertAbsoluteFilePathToFileSystemUrl";
    return storage::FileSystemURL();
  }

  return file_system_context->CrackURLInFirstPartyContext(url);
}

OfficeFilesSourceVolume VolumeTypeToSourceVolume(
    file_manager::VolumeType volume_type) {
  switch (volume_type) {
    case file_manager::VOLUME_TYPE_GOOGLE_DRIVE:
      return OfficeFilesSourceVolume::kGoogleDrive;
    case file_manager::VOLUME_TYPE_DOWNLOADS_DIRECTORY:
      return OfficeFilesSourceVolume::kDownloadsDirectory;
    case file_manager::VOLUME_TYPE_REMOVABLE_DISK_PARTITION:
      return OfficeFilesSourceVolume::kRemovableDiskPartition;
    case file_manager::VOLUME_TYPE_MOUNTED_ARCHIVE_FILE:
      return OfficeFilesSourceVolume::kMountedArchiveFile;
    case file_manager::VOLUME_TYPE_PROVIDED:
      return OfficeFilesSourceVolume::kProvided;
    case file_manager::VOLUME_TYPE_MTP:
      return OfficeFilesSourceVolume::kMtp;
    case file_manager::VOLUME_TYPE_MEDIA_VIEW:
      return OfficeFilesSourceVolume::kMediaView;
    case file_manager::VOLUME_TYPE_CROSTINI:
      return OfficeFilesSourceVolume::kCrostini;
    case file_manager::VOLUME_TYPE_ANDROID_FILES:
      return OfficeFilesSourceVolume::kAndriodFiles;
    case file_manager::VOLUME_TYPE_DOCUMENTS_PROVIDER:
      return OfficeFilesSourceVolume::kDocumentsProvider;
    case file_manager::VOLUME_TYPE_SMB:
      return OfficeFilesSourceVolume::kSmb;
    case file_manager::VOLUME_TYPE_SYSTEM_INTERNAL:
      return OfficeFilesSourceVolume::kSystemInternal;
    case file_manager::VOLUME_TYPE_GUEST_OS:
      return OfficeFilesSourceVolume::kGuestOS;
    // TODO(b/304383409): remove default class after making VolumeType an enum
    // class.
    default:
      LOG(ERROR) << "Unknown VolumeType " << volume_type;
      return OfficeFilesSourceVolume::kUnknown;
  }
}

SourceType GetSourceType(Profile* profile,
                         const storage::FileSystemURL& source_url) {
  file_manager::VolumeManager* volume_manager =
      file_manager::VolumeManager::Get(profile);
  base::WeakPtr<file_manager::Volume> source_volume =
      volume_manager->FindVolumeFromPath(source_url.path());
  DCHECK(source_volume)
      << "Unable to find source volume (source path filesystem_id: "
      << source_url.filesystem_id() << ")";
  // Local by default.
  if (!source_volume) {
    return SourceType::LOCAL;
  }
  // First, look at whether the filesystem is read-only.
  if (source_volume->is_read_only()) {
    return SourceType::READ_ONLY;
  }
  // Some volume types are generally associated with cloud filesystems.
  if (source_volume->type() == file_manager::VOLUME_TYPE_GOOGLE_DRIVE ||
      source_volume->type() == file_manager::VOLUME_TYPE_SMB ||
      source_volume->type() == file_manager::VOLUME_TYPE_DOCUMENTS_PROVIDER) {
    return SourceType::CLOUD;
  }
  // For provided file systems, check whether file system's source data is
  // retrieved over the network.
  if (source_volume->type() == file_manager::VOLUME_TYPE_PROVIDED) {
    const base::FilePath source_path = source_url.path();
    file_system_provider::Service* service =
        file_system_provider::Service::Get(profile);
    std::vector<file_system_provider::ProvidedFileSystemInfo> file_systems =
        service->GetProvidedFileSystemInfoList();
    for (const auto& file_system : file_systems) {
      if (file_system.mount_path().IsParent(source_path)) {
        return file_system.source() ==
                       extensions::FileSystemProviderSource::SOURCE_NETWORK
                   ? SourceType::CLOUD
                   : SourceType::LOCAL;
      }
    }
    // Local if unable to find the provided file system.
    return SourceType::LOCAL;
  }
  // Local by default.
  return SourceType::LOCAL;
}

UploadType GetUploadType(Profile* profile,
                         const storage::FileSystemURL& source_url) {
  SourceType source_type = GetSourceType(profile, source_url);
  return source_type == SourceType::LOCAL ? UploadType::kMove
                                          : UploadType::kCopy;
}

void RequestODFSMount(Profile* profile,
                      file_system_provider::RequestMountCallback callback) {
  Service* service = Service::Get(profile);
  ProviderId provider_id =
      ProviderId::CreateFromExtensionId(extension_misc::kODFSExtensionId);
  auto logging_callback = base::BindOnce(
      [](file_system_provider::RequestMountCallback callback,
         base::File::Error error) {
        if (error != base::File::FILE_OK) {
          LOG(ERROR) << "RequestMount: " << base::File::ErrorToString(error);
        }
        std::move(callback).Run(error);
      },
      std::move(callback));
  service->RequestMount(provider_id, std::move(logging_callback));
}

std::optional<ProvidedFileSystemInfo> GetODFSInfo(Profile* profile) {
  Service* service = Service::Get(profile);
  ProviderId provider_id =
      ProviderId::CreateFromExtensionId(extension_misc::kODFSExtensionId);
  auto odfs_infos = service->GetProvidedFileSystemInfoList(provider_id);

  if (odfs_infos.size() == 0) {
    return std::nullopt;
  }
  if (odfs_infos.size() > 1u) {
    LOG(ERROR) << "One and only one filesystem should be mounted for the ODFS "
                  "extension";
    return std::nullopt;
  }

  return odfs_infos[0];
}

ProvidedFileSystemInterface* GetODFS(Profile* profile) {
  Service* service = Service::Get(profile);
  ProviderId provider_id =
      ProviderId::CreateFromExtensionId(extension_misc::kODFSExtensionId);
  auto odfs_info = GetODFSInfo(profile);
  if (!odfs_info) {
    return nullptr;
  }
  return service->GetProvidedFileSystem(provider_id,
                                        odfs_info->file_system_id());
}

base::FilePath GetODFSFuseboxMount(Profile* profile) {
  const auto odfs_info = GetODFSInfo(profile);
  if (!odfs_info) {
    return base::FilePath();
  }

  file_manager::VolumeManager* volume_manager =
      file_manager::VolumeManager::Get(profile);
  if (!volume_manager) {
    return base::FilePath();
  }

  for (const auto& volume : volume_manager->GetVolumeList()) {
    if (volume->volume_label() == odfs_info->display_name() &&
        volume->file_system_type() == file_manager::util::kFuseBox) {
      return volume->mount_path();
    }
  }
  return base::FilePath();
}

bool IsODFSInstalled(Profile* profile) {
  auto* service = ash::file_system_provider::Service::Get(profile);
  return base::ranges::any_of(
      service->GetProviders(), [](const auto& provider) {
        return provider.first ==
               ash::file_system_provider::ProviderId::CreateFromExtensionId(
                   extension_misc::kODFSExtensionId);
      });
}

bool IsODFSMounted(Profile* profile) {
  // Assume any file system mounted by ODFS is the correct one.
  return GetODFSInfo(profile).has_value();
}

bool IsOfficeWebAppInstalled(Profile* profile) {
  if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
    return false;
  }
  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
  bool installed = false;
  proxy->AppRegistryCache().ForOneApp(
      web_app::kMicrosoft365AppId, [&installed](const apps::AppUpdate& update) {
        installed = apps_util::IsInstalled(update.Readiness());
      });
  return installed;
}

bool IsMicrosoftOfficeOneDriveIntegrationAllowedAndOdfsInstalled(
    Profile* profile) {
  return chromeos::cloud_upload::IsMicrosoftOfficeOneDriveIntegrationAllowed(
             profile) &&
         IsODFSInstalled(profile);
}

bool UrlIsOnODFS(const FileSystemURL& url) {
  ash::file_system_provider::util::FileSystemURLParser parser(url);
  if (!parser.Parse()) {
    return false;
  }

  file_system_provider::ProviderId provider_id =
      file_system_provider::ProviderId::CreateFromExtensionId(
          extension_misc::kODFSExtensionId);
  if (parser.file_system()->GetFileSystemInfo().provider_id() != provider_id) {
    return false;
  }
  return true;
}

// Convert |actions| to |ODFSMetadata| and pass the result to |callback|.
// The action id's for the metadata are HIDDEN_ONEDRIVE_USER_EMAIL,
// HIDDEN_ONEDRIVE_REAUTHENTICATION_REQUIRED and HIDDEN_ONEDRIVE_ACCOUNT_STATE.
void OnODFSMetadataActions(GetODFSMetadataCallback callback,
                           const Actions& actions,
                           base::File::Error result) {
  if (result != base::File::Error::FILE_OK) {
    LOG(ERROR) << "Unexpectedly failed to get ODFS metadata actions as these "
                  "should always be returned: "
               << result;
    std::move(callback).Run(base::unexpected(result));
    return;
  }
  ODFSMetadata metadata;
  for (const Action& action : actions) {
    if (action.id == kReauthenticationRequiredId) {
      metadata.reauthentication_required = action.title == "true";
    } else if (action.id == kAccountStateId) {
      if (action.title == "NORMAL") {
        metadata.account_state = OdfsAccountState::kNormal;
      } else if (action.title == "REAUTHENTICATION_REQUIRED") {
        metadata.account_state = OdfsAccountState::kReauthenticationRequired;
      } else if (action.title == "FROZEN_ACCOUNT") {
        metadata.account_state = OdfsAccountState::kFrozenAccount;
      }
    } else if (action.id == kUserEmailActionId) {
      metadata.user_email = action.title;
    }
  }
  std::move(callback).Run(metadata);
}

// Convert ODFS-specific entry metadata returned in `actions` to
// `ODFSEntryMetadata`.
void OnGetODFSEntryActions(GetODFSEntryMetadataCallback callback,
                           const Actions& actions,
                           base::File::Error result) {
  if (result != base::File::Error::FILE_OK) {
    std::move(callback).Run(base::unexpected(result));
    return;
  }
  ODFSEntryMetadata metadata;
  for (const Action& action : actions) {
    if (action.id == kOneDriveUrlActionId) {
      // Custom actions are used to pass a OneDrive document URLs as the "title"
      // attribute.
      metadata.url = action.title;
    }
  }
  std::move(callback).Run(std::move(metadata));
}

void GetODFSMetadata(ProvidedFileSystemInterface* file_system,
                     GetODFSMetadataCallback callback) {
  file_system->GetActions(
      {base::FilePath(cloud_upload::kODFSMetadataQueryPath)},
      base::BindOnce(&OnODFSMetadataActions, std::move(callback)));
}

void GetODFSEntryMetadata(
    file_system_provider::ProvidedFileSystemInterface* file_system,
    const base::FilePath& path,
    GetODFSEntryMetadataCallback callback) {
  file_system->GetActions(
      {path}, base::BindOnce(&OnGetODFSEntryActions, std::move(callback)));
}

bool PathIsOnDriveFS(Profile* profile, const base::FilePath& file_path) {
  drive::DriveIntegrationService* integration_service =
      drive::DriveIntegrationServiceFactory::FindForProfile(profile);
  base::FilePath relative_path;
  return integration_service->GetRelativeDrivePath(file_path, &relative_path);
}

std::optional<base::File::Error> GetFirstTaskError(
    const ::file_manager::io_task::ProgressStatus& status) {
  for (const auto* entries : {&status.sources, &status.outputs}) {
    for (const ::file_manager::io_task::EntryStatus& entry : *entries) {
      if (entry.error && *entry.error != base::File::Error::FILE_OK) {
        return entry.error;
      }
    }
  }
  return std::nullopt;
}

std::optional<gfx::Rect> CalculateAuthWindowBounds(Profile* profile) {
  Browser* browser =
      FindSystemWebAppBrowser(profile, ash::SystemWebAppType::FILE_MANAGER);
  if (!browser) {
    return std::nullopt;
  }

  gfx::Rect files_app_bounds = browser->window()->GetBounds();
  // These are the min sizes needed for the oauth dialog to look subjectively
  // "good".
  const int kMinWidth = 615;
  const int kMinHeight = 660;
  // The dialog won't fit inside Files app's bounds, but we'll try and keep it
  // centered around the same point.
  if (files_app_bounds.width() < kMinWidth ||
      files_app_bounds.height() < kMinHeight) {
    int files_app_center_x =
        files_app_bounds.x() + files_app_bounds.width() / 2;
    int files_app_center_y =
        files_app_bounds.y() + files_app_bounds.height() / 2;
    int target_x = std::max(0, files_app_center_x - kMinWidth / 2);
    int target_y = std::max(0, files_app_center_y - kMinHeight / 2);
    return gfx::Rect(target_x, target_y, kMinWidth, kMinHeight);
  }

  // Files app is bigger in both dimensions - shrink popup to min sizes and keep
  // it centered.
  gfx::Rect popup_bounds(files_app_bounds);
  popup_bounds.ClampToCenteredSize(gfx::Size(kMinWidth, kMinHeight));
  return popup_bounds;
}

}  // namespace ash::cloud_upload