blob: b84b884e0f969b34a749200a1dffde6e50ce6724 [file] [log] [blame]
// 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/exo/chrome_data_exchange_delegate.h"
#include <string>
#include <vector>
#include "ash/public/cpp/app_types.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/borealis/borealis_window_manager.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_files.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/chromeos/extensions/file_manager/event_router.h"
#include "chrome/browser/chromeos/extensions/file_manager/event_router_factory.h"
#include "chrome/browser/chromeos/file_manager/app_id.h"
#include "chrome/browser/chromeos/file_manager/fileapi_util.h"
#include "chrome/browser/chromeos/file_manager/path_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/arc/arc_util.h"
#include "content/public/common/drop_data.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/aura/window.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "url/gurl.h"
namespace chromeos {
namespace {
constexpr char kMimeTypeArcUriList[] = "application/x-arc-uri-list";
constexpr char kMimeTypeTextUriList[] = "text/uri-list";
constexpr char kUriListSeparator[] = "\r\n";
constexpr char kVmFileScheme[] = "vmfile";
// Mime types used in FilesApp to copy/paste files to clipboard.
constexpr char16_t kFilesAppMimeTag[] = u"fs/tag";
constexpr char16_t kFilesAppTagExo[] = u"exo";
constexpr char16_t kFilesAppMimeSources[] = u"fs/sources";
constexpr char kFilesAppSeparator[] = "\n";
constexpr char16_t kFilesAppSeparator16[] = u"\n";
storage::FileSystemContext* GetFileSystemContext() {
Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
if (!primary_profile)
return nullptr;
return file_manager::util::GetFileManagerFileSystemContext(primary_profile);
}
void GetFileSystemUrlsFromPickle(
const base::Pickle& pickle,
std::vector<storage::FileSystemURL>* file_system_urls) {
storage::FileSystemContext* file_system_context = GetFileSystemContext();
if (!file_system_context)
return;
std::vector<content::DropData::FileSystemFileInfo> file_system_files;
if (!content::DropData::FileSystemFileInfo::ReadFileSystemFilesFromPickle(
pickle, &file_system_files))
return;
for (const auto& file_system_file : file_system_files) {
const storage::FileSystemURL file_system_url =
file_system_context->CrackURL(file_system_file.url);
if (file_system_url.is_valid())
file_system_urls->push_back(std::move(file_system_url));
}
}
void SendArcUrls(exo::DataExchangeDelegate::SendDataCallback callback,
const std::vector<GURL>& urls) {
std::vector<std::string> lines;
for (const GURL& url : urls) {
if (!url.is_valid())
continue;
lines.push_back(url.spec());
}
// Arc requires UTF16 for data.
std::u16string data =
base::UTF8ToUTF16(base::JoinString(lines, kUriListSeparator));
std::move(callback).Run(base::RefCountedString16::TakeString(&data));
}
void SendAfterShare(exo::DataExchangeDelegate::SendDataCallback callback,
scoped_refptr<base::RefCountedMemory> data,
bool success,
const std::string& failure_reason) {
if (!success)
LOG(ERROR) << "Error sharing paths for drag and drop: " << failure_reason;
// Still send the data, even if sharing failed.
std::move(callback).Run(data);
}
struct FileInfo {
base::FilePath path;
storage::FileSystemURL url;
};
// Returns true if path is shared with the specified VM, or for crostini if path
// is homedir or dir within.
bool IsPathShared(Profile* profile,
std::string vm_name,
bool is_crostini,
base::FilePath path) {
auto* share_path = guest_os::GuestOsSharePath::GetForProfile(profile);
if (share_path->IsPathShared(vm_name, path)) {
return true;
}
if (is_crostini) {
base::FilePath mount =
file_manager::util::GetCrostiniMountDirectory(profile);
return path == mount || mount.IsParent(path);
}
return false;
}
// Parse text/uri-list data into FilePath and FileSystemURL.
std::vector<FileInfo> GetFileInfo(ui::EndpointType source,
const std::vector<uint8_t>& data) {
std::vector<FileInfo> file_info;
Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
bool is_crostini = source == ui::EndpointType::kCrostini;
bool is_plugin_vm = source == ui::EndpointType::kPluginVm;
base::FilePath vm_mount;
std::string vm_name;
if (is_crostini) {
vm_mount = crostini::ContainerChromeOSBaseDirectory();
vm_name = crostini::kCrostiniDefaultVmName;
} else if (is_plugin_vm) {
vm_mount = plugin_vm::ChromeOSBaseDirectory();
vm_name = plugin_vm::kPluginVmName;
}
std::vector<ui::FileInfo> filenames =
ui::URIListToFileInfos(std::string(data.begin(), data.end()));
for (const ui::FileInfo& file : filenames) {
base::FilePath path = std::move(file.path);
storage::FileSystemURL url;
// Convert the VM path to a path in the host if possible (in homedir or
// /mnt/chromeos for crostini; in //ChromeOS for Plugin VM), otherwise
// prefix with 'vmfile:<vm_name>:' to avoid VMs spoofing host paths.
// E.g. crostini /etc/mime.types => vmfile:termina:/etc/mime.types.
if (is_crostini || is_plugin_vm) {
if (file_manager::util::ConvertPathInsideVMToFileSystemURL(
primary_profile, path, vm_mount,
/*map_crostini_home=*/is_crostini, &url)) {
path = url.path();
// Only allow write to clipboard for paths that are shared.
if (!IsPathShared(primary_profile, vm_name, is_crostini, path)) {
LOG(WARNING) << "Unshared file path: " << path;
continue;
}
} else {
path = base::FilePath(
base::StrCat({kVmFileScheme, ":", vm_name, ":", path.value()}));
}
}
file_info.push_back({std::move(path), std::move(url)});
}
return file_info;
}
void ShareAndSend(ui::EndpointType target,
std::vector<FileInfo> files,
exo::DataExchangeDelegate::SendDataCallback callback) {
Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
bool is_arc = target == ui::EndpointType::kArc;
bool is_crostini = target == ui::EndpointType::kCrostini;
bool is_plugin_vm = target == ui::EndpointType::kPluginVm;
base::FilePath vm_mount;
std::string vm_name;
if (is_arc) {
// For ARC, |share_required| below will always be false and |vm_name| will
// not be used. Setting it to arc::kArcVmName has no effect.
vm_name = arc::kArcVmName;
} else if (is_crostini) {
vm_mount = crostini::ContainerChromeOSBaseDirectory();
vm_name = crostini::kCrostiniDefaultVmName;
} else if (is_plugin_vm) {
vm_mount = plugin_vm::ChromeOSBaseDirectory();
vm_name = plugin_vm::kPluginVmName;
}
const std::string vm_prefix =
base::StrCat({kVmFileScheme, ":", vm_name, ":"});
std::vector<std::string> lines_to_send;
auto* share_path = guest_os::GuestOsSharePath::GetForProfile(primary_profile);
std::vector<base::FilePath> paths_to_share;
for (auto& info : files) {
std::string line_to_send;
bool share_required = false;
if (is_arc) {
GURL arc_url;
if (!file_manager::util::ConvertPathToArcUrl(info.path, &arc_url,
&share_required)) {
LOG(WARNING) << "Could not convert arc path " << info.path;
continue;
}
line_to_send = arc_url.spec();
} else if (is_crostini || is_plugin_vm) {
base::FilePath path;
// Check if it is a path inside the VM: 'vmfile:<vm_name>:'.
if (base::StartsWith(info.path.value(), vm_prefix,
base::CompareCase::SENSITIVE)) {
line_to_send = ui::FilePathToFileURL(
base::FilePath(info.path.value().substr(vm_prefix.size())));
} else if (file_manager::util::ConvertFileSystemURLToPathInsideVM(
primary_profile, info.url, vm_mount,
/*map_crostini_home=*/is_crostini, &path)) {
// Convert to path inside the VM.
line_to_send = ui::FilePathToFileURL(path);
share_required = true;
} else {
LOG(WARNING) << "Could not convert into VM path " << info.path;
continue;
}
} else {
// Use path without conversion as default.
line_to_send = ui::FilePathToFileURL(info.path);
}
lines_to_send.push_back(std::move(line_to_send));
if (share_required && !share_path->IsPathShared(vm_name, info.path))
paths_to_share.push_back(std::move(info.path));
}
// Arc uses utf-16 data.
std::string joined = base::JoinString(lines_to_send, kUriListSeparator);
scoped_refptr<base::RefCountedMemory> data;
if (is_arc) {
std::u16string utf16 = base::UTF8ToUTF16(joined);
data = base::RefCountedString16::TakeString(&utf16);
} else {
data = base::RefCountedString::TakeString(&joined);
}
if (!paths_to_share.empty()) {
if (!is_plugin_vm) {
share_path->SharePaths(
vm_name, std::move(paths_to_share),
/*persist=*/false,
base::BindOnce(&SendAfterShare, std::move(callback),
std::move(data)));
return;
}
// Show FilesApp move-to-windows-files dialog when Plugin VM is not shared.
if (auto* event_router =
file_manager::EventRouterFactory::GetForProfile(primary_profile)) {
event_router->DropFailedPluginVmDirectoryNotShared();
}
std::string empty;
data = base::RefCountedString::TakeString(&empty);
}
std::move(callback).Run(std::move(data));
}
} // namespace
ChromeDataExchangeDelegate::ChromeDataExchangeDelegate() = default;
ChromeDataExchangeDelegate::~ChromeDataExchangeDelegate() = default;
ui::EndpointType ChromeDataExchangeDelegate::GetDataTransferEndpointType(
aura::Window* target) const {
auto* top_level_window = target->GetToplevelWindow();
if (ash::IsArcWindow(top_level_window))
return ui::EndpointType::kArc;
if (borealis::BorealisWindowManager::IsBorealisWindow(top_level_window))
return ui::EndpointType::kBorealis;
if (crostini::IsCrostiniWindow(top_level_window))
return ui::EndpointType::kCrostini;
if (plugin_vm::IsPluginVmAppWindow(top_level_window))
return ui::EndpointType::kPluginVm;
return ui::EndpointType::kUnknownVm;
}
std::vector<ui::FileInfo> ChromeDataExchangeDelegate::GetFilenames(
ui::EndpointType source,
const std::vector<uint8_t>& data) const {
std::vector<ui::FileInfo> result;
for (const auto& info : GetFileInfo(source, data))
result.push_back(ui::FileInfo(std::move(info.path), base::FilePath()));
return result;
}
std::string ChromeDataExchangeDelegate::GetMimeTypeForUriList(
ui::EndpointType target) const {
return target == ui::EndpointType::kArc ? kMimeTypeArcUriList
: kMimeTypeTextUriList;
}
void ChromeDataExchangeDelegate::SendFileInfo(
ui::EndpointType target,
const std::vector<ui::FileInfo>& files,
SendDataCallback callback) const {
storage::ExternalMountPoints* mount_points =
storage::ExternalMountPoints::GetSystemInstance();
base::FilePath virtual_path;
std::vector<FileInfo> list;
for (const auto& info : files) {
// Convert absolute host path to FileSystemURL if possible.
storage::FileSystemURL url;
if (mount_points->GetVirtualPath(info.path, &virtual_path)) {
url = mount_points->CreateCrackedFileSystemURL(
url::Origin(), storage::kFileSystemTypeExternal, virtual_path);
}
list.push_back({info.path, std::move(url)});
}
ShareAndSend(target, std::move(list), std::move(callback));
}
bool ChromeDataExchangeDelegate::HasUrlsInPickle(
const base::Pickle& pickle) const {
std::vector<storage::FileSystemURL> file_system_urls;
GetFileSystemUrlsFromPickle(pickle, &file_system_urls);
return !file_system_urls.empty();
}
void ChromeDataExchangeDelegate::SendPickle(ui::EndpointType target,
const base::Pickle& pickle,
SendDataCallback callback) {
std::vector<storage::FileSystemURL> file_system_urls;
GetFileSystemUrlsFromPickle(pickle, &file_system_urls);
// ARC FileSystemURLs are converted to Content URLs.
if (target == ui::EndpointType::kArc) {
if (file_system_urls.empty()) {
std::move(callback).Run(nullptr);
return;
}
arc::ConvertToContentUrlsAndShare(
ProfileManager::GetPrimaryUserProfile(), file_system_urls,
base::BindOnce(&SendArcUrls, std::move(callback)));
return;
}
std::vector<FileInfo> list;
for (const auto& url : file_system_urls)
list.push_back({url.path(), std::move(url)});
ShareAndSend(target, std::move(list), std::move(callback));
}
base::Pickle ChromeDataExchangeDelegate::CreateClipboardFilenamesPickle(
ui::EndpointType source,
const std::vector<uint8_t>& data) const {
std::vector<std::string> filenames;
std::vector<FileInfo> file_info = GetFileInfo(source, data);
for (const auto& info : file_info) {
if (info.url.is_valid())
filenames.push_back(info.url.ToGURL().spec());
}
base::Pickle pickle;
ui::WriteCustomDataToPickle(
std::unordered_map<std::u16string, std::u16string>(
{{kFilesAppMimeTag, kFilesAppTagExo},
{kFilesAppMimeSources, base::UTF8ToUTF16(base::JoinString(
filenames, kFilesAppSeparator))}}),
&pickle);
return pickle;
}
std::vector<ui::FileInfo> ChromeDataExchangeDelegate::ParseFileSystemSources(
const ui::DataTransferEndpoint* source,
const base::Pickle& pickle) const {
std::vector<ui::FileInfo> file_info;
// We only promote 'fs/sources' custom data pickle to be filenames which can
// be shared and read by clients if it came from the trusted FilesApp.
if (!source || !source->IsSameOriginWith(ui::DataTransferEndpoint(
file_manager::util::GetFilesAppOrigin()))) {
return file_info;
}
std::u16string file_system_url_list;
ui::ReadCustomDataForType(pickle.data(), pickle.size(), kFilesAppMimeSources,
&file_system_url_list);
if (file_system_url_list.empty())
return file_info;
storage::ExternalMountPoints* mount_points =
storage::ExternalMountPoints::GetSystemInstance();
for (const base::StringPiece16& line : base::SplitStringPiece(
file_system_url_list, kFilesAppSeparator16, base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
if (line.empty() || line[0] == '#')
continue;
storage::FileSystemURL url = mount_points->CrackURL(GURL(line));
if (!url.is_valid()) {
LOG(WARNING) << "Invalid clipboard FileSystemURL: " << line;
continue;
}
file_info.push_back(ui::FileInfo(std::move(url.path()), base::FilePath()));
}
return file_info;
}
} // namespace chromeos