chromium/chrome/browser/ash/dbus/vm/vm_applications_service_provider.cc

// Copyright 2018 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/ash/dbus/vm/vm_applications_service_provider.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/exo/chrome_security_delegate.h"
#include "chrome/browser/ash/guest_os/guest_id.h"
#include "chrome/browser/ash/guest_os/guest_os_mime_types_service.h"
#include "chrome/browser/ash/guest_os/guest_os_mime_types_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_file_destination.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/browser/ui/views/select_file_dialog_extension.h"
#include "chromeos/ash/components/dbus/cicerone/cicerone_client.h"
#include "chromeos/ash/components/dbus/cicerone/cicerone_service.pb.h"
#include "chromeos/ash/components/dbus/vm_applications/apps.pb.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/display/types/display_constants.h"
#include "ui/shell_dialogs/selected_file_info.h"

namespace ash {
namespace {

class DialogListener : public ui::SelectFileDialog::Listener {
 public:
  explicit DialogListener(vm_tools::cicerone::FileSelectedSignal signal)
      : dialog_(SelectFileDialogExtension::Create(
            this,
            std::make_unique<ChromeSelectFilePolicy>(nullptr))),
        signal_(signal) {
    CHECK(dialog_);
  }
  DialogListener(const DialogListener&) = delete;
  DialogListener& operator=(const DialogListener&) = delete;
  ~DialogListener() override { dialog_->ListenerDestroyed(); }

  scoped_refptr<SelectFileDialogExtension> dialog() { return dialog_; }

  // ui::SelectFileDialog::Listener:
  void FileSelected(const ui::SelectedFileInfo& file, int index) override {
    MultiFilesSelected({file});
  }
  void MultiFilesSelected(
      const std::vector<ui::SelectedFileInfo>& files) override;
  void FileSelectionCanceled() override { MultiFilesSelected({}); }

 private:
  const scoped_refptr<SelectFileDialogExtension> dialog_;
  const vm_tools::cicerone::FileSelectedSignal signal_;
};

void DialogListener::MultiFilesSelected(
    const std::vector<ui::SelectedFileInfo>& files) {
  ShareWithVMAndTranslateToFileUrls(
      signal_.vm_name(), ui::SelectedFileInfoListToFilePathList(files),
      base::BindOnce(
          [](vm_tools::cicerone::FileSelectedSignal signal,
             std::vector<std::string> file_urls) {
            for (const auto& file_url : file_urls) {
              signal.add_files(file_url);
            }
            CiceroneClient::Get()->FileSelected(signal);
          },
          signal_));
  delete this;
}

}  // namespace

VmApplicationsServiceProvider::VmApplicationsServiceProvider() = default;

VmApplicationsServiceProvider::~VmApplicationsServiceProvider() = default;

void VmApplicationsServiceProvider::Start(
    scoped_refptr<dbus::ExportedObject> exported_object) {
  exported_object->ExportMethod(
      vm_tools::apps::kVmApplicationsServiceInterface,
      vm_tools::apps::kVmApplicationsServiceUpdateApplicationListMethod,
      base::BindRepeating(&VmApplicationsServiceProvider::UpdateApplicationList,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&VmApplicationsServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      vm_tools::apps::kVmApplicationsServiceInterface,
      vm_tools::apps::kVmApplicationsServiceLaunchTerminalMethod,
      base::BindRepeating(&VmApplicationsServiceProvider::LaunchTerminal,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&VmApplicationsServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      vm_tools::apps::kVmApplicationsServiceInterface,
      vm_tools::apps::kVmApplicationsServiceUpdateMimeTypesMethod,
      base::BindRepeating(&VmApplicationsServiceProvider::UpdateMimeTypes,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&VmApplicationsServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      vm_tools::apps::kVmApplicationsServiceInterface,
      vm_tools::apps::kVmApplicationsServiceSelectFileMethod,
      base::BindRepeating(&VmApplicationsServiceProvider::SelectFile,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&VmApplicationsServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
}

void VmApplicationsServiceProvider::OnExported(
    const std::string& interface_name,
    const std::string& method_name,
    bool success) {
  LOG_IF(ERROR, !success) << "Failed to export " << interface_name << "."
                          << method_name;
}

void VmApplicationsServiceProvider::UpdateApplicationList(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  dbus::MessageReader reader(method_call);

  vm_tools::apps::ApplicationList request;

  if (!reader.PopArrayOfBytesAsProto(&request)) {
    constexpr char error_message[] =
        "Unable to parse ApplicationList from message";
    LOG(ERROR) << error_message;
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS, error_message));
    return;
  }

  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  auto* registry_service =
      guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile);
  if (!registry_service) {
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(method_call, DBUS_ERROR_FAILED,
                                                 "Shutting down"));
    return;
  }
  registry_service->UpdateApplicationList(request);

  std::move(response_sender).Run(dbus::Response::FromMethodCall(method_call));
}

void VmApplicationsServiceProvider::LaunchTerminal(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  dbus::MessageReader reader(method_call);

  vm_tools::apps::TerminalParams request;

  if (!reader.PopArrayOfBytesAsProto(&request)) {
    constexpr char error_message[] =
        "Unable to parse TerminalParams from message";
    LOG(ERROR) << error_message;
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS, error_message));
    return;
  }

  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  if (crostini::CrostiniFeatures::Get()->IsEnabled(profile) &&
      request.owner_id() == crostini::CryptohomeIdForProfile(profile)) {
    // kInvalidDisplayId will launch terminal on the current active display.
    guest_os::LaunchTerminal(
        profile, display::kInvalidDisplayId,
        guest_os::GuestId(crostini::kCrostiniDefaultVmType, request.vm_name(),
                          request.container_name()),
        request.cwd(),
        std::vector<std::string>(request.params().begin(),
                                 request.params().end()));
  }

  std::move(response_sender).Run(dbus::Response::FromMethodCall(method_call));
}

void VmApplicationsServiceProvider::UpdateMimeTypes(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  dbus::MessageReader reader(method_call);

  vm_tools::apps::MimeTypes request;

  if (!reader.PopArrayOfBytesAsProto(&request)) {
    constexpr char error_message[] = "Unable to parse MimeTypes from message";
    LOG(ERROR) << error_message;
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS, error_message));
    return;
  }

  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  auto* mime_types_service =
      guest_os::GuestOsMimeTypesServiceFactory::GetForProfile(profile);
  if (!mime_types_service) {
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(method_call, DBUS_ERROR_FAILED,
                                                 "Shutting down"));
    return;
  }
  mime_types_service->UpdateMimeTypes(request);

  std::move(response_sender).Run(dbus::Response::FromMethodCall(method_call));
}

void VmApplicationsServiceProvider::SelectFile(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  dbus::MessageReader reader(method_call);

  vm_tools::apps::SelectFileRequest request;

  if (!reader.PopArrayOfBytesAsProto(&request)) {
    constexpr char error_message[] =
        "Unable to parse SelectFileRequest from message";
    LOG(ERROR) << error_message;
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS, error_message));
    return;
  }
  std::move(response_sender).Run(dbus::Response::FromMethodCall(method_call));

  // Match strings used by FilesApp GetDialogTypeAsString().
  ui::SelectFileDialog::Type type = ui::SelectFileDialog::SELECT_OPEN_FILE;
  if (request.type() == "open-multi-file") {
    type = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
  } else if (request.type() == "saveas-file") {
    type = ui::SelectFileDialog::SELECT_SAVEAS_FILE;
  } else if (request.type() == "folder") {
    type = ui::SelectFileDialog::SELECT_FOLDER;
  } else if (request.type() == "upload-folder") {
    type = ui::SelectFileDialog::SELECT_UPLOAD_FOLDER;
  }
  std::u16string title = base::UTF8ToUTF16(request.title());
  base::FilePath default_path;
  SelectFileDialogExtension::Owner owner;
  if (!request.default_path().empty()) {
    // Parse as file: URL if possible.
    std::vector<ui::FileInfo> file_infos =
        ui::URIListToFileInfos(request.default_path());
    if (file_infos.empty()) {
      file_infos.push_back(ui::FileInfo(base::FilePath(request.default_path()),
                                        base::FilePath()));
    }
    // Translate to path in host and DLP component type if possible.
    std::vector<base::FilePath> paths =
        TranslateVMPathsToHost(request.vm_name(), file_infos);
    default_path =
        !paths.empty() ? std::move(paths[0]) : std::move(file_infos[0].path);
  }
  ui::SelectFileDialog::FileTypeInfo file_types;
  file_types.allowed_paths = ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH;
  int file_type_index = 0;
  ParseSelectFileDialogFileTypes(request.allowed_extensions(), &file_types,
                                 &file_type_index);

  vm_tools::cicerone::FileSelectedSignal signal;
  signal.set_vm_name(request.vm_name());
  signal.set_container_name(request.container_name());
  signal.set_owner_id(request.owner_id());
  signal.set_select_file_token(request.select_file_token());
  auto listener = std::make_unique<DialogListener>(signal);

  // Grab the dialog from `listener` before releasing it.
  scoped_refptr<SelectFileDialogExtension> dialog = listener->dialog();
  // Release ownership of `listener`; it will self-delete when it receives a
  // SelectFile callback.
  listener.release();
  dialog->SelectFileWithFileManagerParams(
      type, title, default_path, &file_types, file_type_index, owner,
      /*search_query=*/"", /*show_android_picker_apps=*/false);
}

// static
void VmApplicationsServiceProvider::ParseSelectFileDialogFileTypes(
    const std::string& allowed_extensions,
    ui::SelectFileDialog::FileTypeInfo* file_types,
    int* file_type_index) {
  file_types->extensions.clear();
  file_types->extension_description_overrides.clear();
  file_types->include_all_files = false;
  *file_type_index = 0;

  // First split on '|'.
  auto items = base::SplitString(allowed_extensions, "|", base::TRIM_WHITESPACE,
                                 base::SPLIT_WANT_NONEMPTY);
  int i = 1;
  for (const auto& item : items) {
    // item '*' means add option on dialog to include all files.
    if (item == "*") {
      file_types->include_all_files = true;
      continue;
    }
    std::string extensions = item;
    // Description after ':'.
    std::string desc;
    size_t pos = item.find(':');
    if (pos != std::string::npos) {
      extensions = item.substr(0, pos);
      desc = item.substr(pos + 1);
    }
    // Comma separated list of extensions.
    auto exts = base::SplitString(extensions, ",", base::TRIM_WHITESPACE,
                                  base::SPLIT_WANT_NONEMPTY);
    if (!exts.empty()) {
      file_types->extensions.push_back(exts);
      file_types->extension_description_overrides.push_back(
          base::UTF8ToUTF16(desc));
      // Leading comma indicates selected item.
      if (item[0] == ',')
        *file_type_index = i;
      ++i;
    }
  }
}

}  // namespace ash