// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/data_offer.h"
#include <iterator>
#include <memory>
#include <utility>
#include <vector>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/i18n/icu_string_conversions.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/not_fatal_until.h"
#include "base/pickle.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "components/exo/data_device.h"
#include "components/exo/data_exchange_delegate.h"
#include "components/exo/data_offer_delegate.h"
#include "components/exo/data_offer_observer.h"
#include "components/exo/security_delegate.h"
#include "net/base/filename_util.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint_serializer.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "url/gurl.h"
namespace exo {
namespace {
constexpr char kTextMimeTypeUtf16[] = "text/plain;charset=utf-16";
constexpr char kTextHtmlMimeTypeUtf16[] = "text/html;charset=utf-16";
constexpr char kUTF8[] = "utf8";
constexpr char kUTF16[] = "utf16";
void WriteFileDescriptorOnWorkerThread(
base::ScopedFD fd,
scoped_refptr<base::RefCountedMemory> memory) {
if (!base::WriteFileDescriptor(fd.get(), *memory))
DLOG(ERROR) << "Failed to write drop data";
}
void WriteFileDescriptor(base::ScopedFD fd,
scoped_refptr<base::RefCountedMemory> memory) {
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::BindOnce(&WriteFileDescriptorOnWorkerThread, std::move(fd),
std::move(memory)));
}
ui::ClipboardFormatType GetClipboardFormatType() {
static const char kFormatString[] = "chromium/x-file-system-files";
static base::NoDestructor<ui::ClipboardFormatType> format_type(
ui::ClipboardFormatType::GetType(kFormatString));
return *format_type;
}
scoped_refptr<base::RefCountedString> EncodeAsRefCountedString(
const std::u16string& text,
const std::string& charset) {
std::string encoded_text;
base::UTF16ToCodepage(text, charset.c_str(),
base::OnStringConversionError::SUBSTITUTE,
&encoded_text);
return base::MakeRefCounted<base::RefCountedString>(std::move(encoded_text));
}
DataOffer::AsyncSendDataCallback AsyncEncodeAsRefCountedString(
const std::u16string& text,
const std::string& charset) {
return base::BindOnce(
[](const std::u16string& text, const std::string& charset,
DataOffer::SendDataCallback callback) {
std::move(callback).Run(EncodeAsRefCountedString(text, charset));
},
text, charset);
}
void ReadDataTransferEndpointFromClipboard(
const std::string& charset,
const ui::DataTransferEndpoint data_dst,
DataOffer::SendDataCallback callback) {
std::optional<ui::DataTransferEndpoint> data_src =
ui::Clipboard::GetForCurrentThread()->GetSource(
ui::ClipboardBuffer::kCopyPaste);
std::u16string encoded_endpoint;
if (data_src) {
encoded_endpoint =
base::UTF8ToUTF16(ui::ConvertDataTransferEndpointToJson(*data_src));
} else {
DCHECK(data_src) << "Clipboard source DataTransferEndpoint has changed "
"after initial MIME advertising. If you see this "
"please file a bug and contact the chromeos-dlp team.";
}
std::move(callback).Run(EncodeAsRefCountedString(encoded_endpoint, charset));
}
void ReadTextFromClipboard(const std::string& charset,
const ui::DataTransferEndpoint data_dst,
DataOffer::SendDataCallback callback) {
std::u16string text;
ui::Clipboard::GetForCurrentThread()->ReadText(
ui::ClipboardBuffer::kCopyPaste, &data_dst, &text);
std::move(callback).Run(EncodeAsRefCountedString(text, charset));
}
void ReadHTMLFromClipboard(const std::string& charset,
const ui::DataTransferEndpoint data_dst,
DataOffer::SendDataCallback callback) {
std::u16string text;
std::string url;
uint32_t start, end;
ui::Clipboard::GetForCurrentThread()->ReadHTML(
ui::ClipboardBuffer::kCopyPaste, &data_dst, &text, &url, &start, &end);
std::move(callback).Run(EncodeAsRefCountedString(text, charset));
}
void ReadRTFFromClipboard(const ui::DataTransferEndpoint data_dst,
DataOffer::SendDataCallback callback) {
std::string text;
ui::Clipboard::GetForCurrentThread()->ReadRTF(ui::ClipboardBuffer::kCopyPaste,
&data_dst, &text);
std::move(callback).Run(
base::MakeRefCounted<base::RefCountedString>(std::move(text)));
}
void OnReceivePNGFromClipboard(DataOffer::SendDataCallback callback,
const std::vector<uint8_t>& png) {
scoped_refptr<base::RefCountedMemory> rc_mem =
base::MakeRefCounted<base::RefCountedBytes>(png);
std::move(callback).Run(std::move(rc_mem));
}
void ReadPNGFromClipboard(const ui::DataTransferEndpoint data_dst,
DataOffer::SendDataCallback callback) {
ui::Clipboard::GetForCurrentThread()->ReadPng(
ui::ClipboardBuffer::kCopyPaste, &data_dst,
base::BindOnce(&OnReceivePNGFromClipboard, std::move(callback)));
}
} // namespace
ScopedDataOffer::ScopedDataOffer(DataOffer* data_offer,
DataOfferObserver* observer)
: data_offer_(data_offer), observer_(observer) {
data_offer_->AddObserver(observer_);
}
ScopedDataOffer::~ScopedDataOffer() {
data_offer_->RemoveObserver(observer_);
}
DataOffer::DataOffer(DataOfferDelegate* delegate)
: delegate_(delegate), dnd_action_(DndAction::kNone), finished_(false) {}
DataOffer::~DataOffer() {
delegate_->OnDataOfferDestroying(this);
for (DataOfferObserver& observer : observers_) {
observer.OnDataOfferDestroying(this);
}
}
void DataOffer::AddObserver(DataOfferObserver* observer) {
observers_.AddObserver(observer);
}
void DataOffer::RemoveObserver(DataOfferObserver* observer) {
observers_.RemoveObserver(observer);
}
void DataOffer::Accept(const std::string* mime_type) {}
void DataOffer::Receive(const std::string& mime_type, base::ScopedFD fd) {
const auto callbacks_it = data_callbacks_.find(mime_type);
if (callbacks_it != data_callbacks_.end()) {
// Set cache with empty data to indicate in process.
DCHECK(data_cache_.count(mime_type) == 0);
data_cache_.emplace(mime_type, nullptr);
std::move(callbacks_it->second)
.Run(base::BindOnce(&DataOffer::OnDataReady,
weak_ptr_factory_.GetWeakPtr(), mime_type,
std::move(fd)));
data_callbacks_.erase(callbacks_it);
return;
}
const auto cache_it = data_cache_.find(mime_type);
if (cache_it == data_cache_.end()) {
DLOG(ERROR) << "Unexpected mime type is requested " << mime_type;
return;
}
if (cache_it->second) {
WriteFileDescriptor(std::move(fd), cache_it->second);
} else {
// Data bytes for this mime type are being processed currently.
pending_receive_requests_.push_back(
std::make_pair(mime_type, std::move(fd)));
}
}
void DataOffer::Finish() {
finished_ = true;
}
void DataOffer::SetActions(const base::flat_set<DndAction>& dnd_actions,
DndAction preferred_action) {
dnd_action_ = preferred_action;
delegate_->OnAction(preferred_action);
}
void DataOffer::SetSourceActions(
const base::flat_set<DndAction>& source_actions) {
source_actions_ = source_actions;
delegate_->OnSourceActions(source_actions);
}
void DataOffer::SetDropData(DataExchangeDelegate* data_exchange_delegate,
aura::Window* target,
const ui::OSExchangeData& data) {
DCHECK_EQ(0u, data_callbacks_.size());
ui::EndpointType endpoint_type =
data_exchange_delegate->GetDataTransferEndpointType(target);
// Drag & Drop source metadata (if any) is synced between Ash and Lacros by
// encoding the metadata into a custom MIME type.
if (endpoint_type == ui::EndpointType::kLacros && data.GetSource()) {
std::u16string encoded_endpoint = base::UTF8ToUTF16(
ui::ConvertDataTransferEndpointToJson(*data.GetSource()));
data_callbacks_.emplace(
ui::kMimeTypeDataTransferEndpoint,
AsyncEncodeAsRefCountedString(encoded_endpoint, kUTF8));
delegate_->OnOffer(ui::kMimeTypeDataTransferEndpoint);
}
const std::string uri_list_mime_type =
data_exchange_delegate->GetMimeTypeForUriList(endpoint_type);
// We accept the filenames pickle from FilesApp, or
// OSExchangeData::GetFilenames().
std::vector<ui::FileInfo> filenames;
if (std::optional<base::Pickle> pickle = data.GetPickledData(
ui::ClipboardFormatType::DataTransferCustomType());
pickle.has_value()) {
filenames = data_exchange_delegate->ParseFileSystemSources(data.GetSource(),
pickle.value());
}
if (filenames.empty() && data.HasFile()) {
if (std::optional<std::vector<ui::FileInfo>> file_info =
data.GetFilenames();
file_info.has_value()) {
std::ranges::move(file_info.value(), std::back_inserter(filenames));
}
}
if (!filenames.empty()) {
data_callbacks_.emplace(
uri_list_mime_type,
base::BindOnce(&SecurityDelegate::SendFileInfo,
base::Unretained(delegate_->GetSecurityDelegate()),
endpoint_type, std::move(filenames)));
delegate_->OnOffer(uri_list_mime_type);
return;
}
if (std::optional<base::Pickle> pickle =
data.GetPickledData(GetClipboardFormatType());
pickle.has_value() &&
data_exchange_delegate->HasUrlsInPickle(pickle.value())) {
data_callbacks_.emplace(
uri_list_mime_type,
base::BindOnce(&SecurityDelegate::SendPickle,
base::Unretained(delegate_->GetSecurityDelegate()),
endpoint_type, pickle.value()));
delegate_->OnOffer(uri_list_mime_type);
return;
}
if (std::optional<ui::OSExchangeDataProvider::FileContentsInfo>
file_contents = data.provider().GetFileContents();
file_contents.has_value()) {
std::string filename = file_contents->filename.value();
base::ReplaceChars(filename, "\\", "\\\\", &filename);
base::ReplaceChars(filename, "\"", "\\\"", &filename);
const std::string mime_type =
base::StrCat({"application/octet-stream;name=\"", filename, "\""});
auto callback = base::BindOnce(
[](scoped_refptr<base::RefCountedString> contents,
DataOffer::SendDataCallback callback) {
std::move(callback).Run(std::move(contents));
},
base::MakeRefCounted<base::RefCountedString>(
std::move(file_contents->file_contents)));
data_callbacks_.emplace(mime_type, std::move(callback));
delegate_->OnOffer(mime_type);
}
if (std::optional<std::u16string> string_content = data.GetString();
string_content.has_value()) {
const std::string utf8_mime_type = std::string(ui::kMimeTypeTextUtf8);
data_callbacks_.emplace(
utf8_mime_type, AsyncEncodeAsRefCountedString(*string_content, kUTF8));
delegate_->OnOffer(utf8_mime_type);
const std::string utf16_mime_type = std::string(kTextMimeTypeUtf16);
data_callbacks_.emplace(utf16_mime_type, AsyncEncodeAsRefCountedString(
*string_content, kUTF16));
delegate_->OnOffer(utf16_mime_type);
const std::string text_plain_mime_type = std::string(ui::kMimeTypeText);
// The MIME type standard says that new text/ subtypes should default to a
// UTF-8 encoding, but that old ones, including text/plain, keep ASCII as
// the default. Nonetheless, we use UTF8 here because it is a superset of
// ASCII and the defacto standard text encoding.
data_callbacks_.emplace(text_plain_mime_type, AsyncEncodeAsRefCountedString(
*string_content, kUTF8));
delegate_->OnOffer(text_plain_mime_type);
}
if (std::optional<ui::OSExchangeData::HtmlInfo> html_content = data.GetHtml();
html_content.has_value()) {
const std::string utf8_html_mime_type = std::string(ui::kMimeTypeHTMLUtf8);
data_callbacks_.emplace(
utf8_html_mime_type,
AsyncEncodeAsRefCountedString(html_content->html, kUTF8));
delegate_->OnOffer(utf8_html_mime_type);
const std::string utf16_html_mime_type =
std::string(kTextHtmlMimeTypeUtf16);
data_callbacks_.emplace(
utf16_html_mime_type,
AsyncEncodeAsRefCountedString(html_content->html, kUTF16));
delegate_->OnOffer(utf16_html_mime_type);
}
}
void DataOffer::SetClipboardData(DataExchangeDelegate* data_exchange_delegate,
const ui::Clipboard& data,
ui::EndpointType endpoint_type) {
DCHECK_EQ(0u, data_callbacks_.size());
const ui::DataTransferEndpoint data_dst(endpoint_type);
// Clipboard source metadata (if any) is synced between Ash and Lacros by
// encoding the metadata into a custom MIME type.
if (endpoint_type == ui::EndpointType::kLacros &&
data.GetSource(ui::ClipboardBuffer::kCopyPaste)) {
delegate_->OnOffer(std::string(ui::kMimeTypeDataTransferEndpoint));
data_callbacks_.emplace(
std::string(ui::kMimeTypeDataTransferEndpoint),
base::BindOnce(&ReadDataTransferEndpointFromClipboard,
std::string(kUTF8), data_dst));
}
if (data.IsFormatAvailable(ui::ClipboardFormatType::PlainTextType(),
ui::ClipboardBuffer::kCopyPaste, &data_dst)) {
auto utf8_callback = base::BindRepeating(&ReadTextFromClipboard,
std::string(kUTF8), data_dst);
delegate_->OnOffer(std::string(ui::kMimeTypeTextUtf8));
data_callbacks_.emplace(std::string(ui::kMimeTypeTextUtf8), utf8_callback);
delegate_->OnOffer(std::string(ui::kMimeTypeLinuxUtf8String));
data_callbacks_.emplace(std::string(ui::kMimeTypeLinuxUtf8String),
utf8_callback);
delegate_->OnOffer(std::string(kTextMimeTypeUtf16));
data_callbacks_.emplace(
std::string(kTextMimeTypeUtf16),
base::BindOnce(&ReadTextFromClipboard, std::string(kUTF16), data_dst));
}
if (data.IsFormatAvailable(ui::ClipboardFormatType::HtmlType(),
ui::ClipboardBuffer::kCopyPaste, &data_dst)) {
delegate_->OnOffer(std::string(ui::kMimeTypeHTMLUtf8));
data_callbacks_.emplace(
std::string(ui::kMimeTypeHTMLUtf8),
base::BindOnce(&ReadHTMLFromClipboard, std::string(kUTF8), data_dst));
delegate_->OnOffer(std::string(kTextHtmlMimeTypeUtf16));
data_callbacks_.emplace(
std::string(kTextHtmlMimeTypeUtf16),
base::BindOnce(&ReadHTMLFromClipboard, std::string(kUTF16), data_dst));
delegate_->OnOffer(std::string(ui::kMimeTypeHTML));
data_callbacks_.emplace(
std::string(ui::kMimeTypeHTML),
base::BindOnce(&ReadHTMLFromClipboard, std::string(kUTF8), data_dst));
}
if (data.IsFormatAvailable(ui::ClipboardFormatType::RtfType(),
ui::ClipboardBuffer::kCopyPaste, &data_dst)) {
delegate_->OnOffer(std::string(ui::kMimeTypeRTF));
data_callbacks_.emplace(std::string(ui::kMimeTypeRTF),
base::BindOnce(&ReadRTFFromClipboard, data_dst));
}
if (data.IsFormatAvailable(ui::ClipboardFormatType::BitmapType(),
ui::ClipboardBuffer::kCopyPaste, &data_dst)) {
delegate_->OnOffer(std::string(ui::kMimeTypePNG));
data_callbacks_.emplace(std::string(ui::kMimeTypePNG),
base::BindOnce(&ReadPNGFromClipboard, data_dst));
}
// For clipboard, FilesApp filenames pickle is already converted to files
// in VolumeManager::OnClipboardDataChanged().
std::vector<ui::FileInfo> filenames;
if (data.IsFormatAvailable(ui::ClipboardFormatType::FilenamesType(),
ui::ClipboardBuffer::kCopyPaste, &data_dst)) {
data.ReadFilenames(ui::ClipboardBuffer::kCopyPaste, &data_dst, &filenames);
}
if (!filenames.empty()) {
delegate_->OnOffer(std::string(ui::kMimeTypeURIList));
data_callbacks_.emplace(
std::string(ui::kMimeTypeURIList),
base::BindOnce(&SecurityDelegate::SendFileInfo,
base::Unretained(delegate_->GetSecurityDelegate()),
endpoint_type, std::move(filenames)));
}
}
void DataOffer::OnDataReady(const std::string& mime_type,
base::ScopedFD fd,
scoped_refptr<base::RefCountedMemory> data) {
// Update cache from nullptr to data.
const auto cache_it = data_cache_.find(mime_type);
CHECK(cache_it != data_cache_.end(), base::NotFatalUntil::M130);
DCHECK(!cache_it->second);
data_cache_.erase(cache_it);
data_cache_.emplace(mime_type, data);
WriteFileDescriptor(std::move(fd), data);
// Process pending receive requests for this mime type, if there are any.
auto it = pending_receive_requests_.begin();
while (it != pending_receive_requests_.end()) {
if (it->first == mime_type) {
WriteFileDescriptor(std::move(it->second), data);
it = pending_receive_requests_.erase(it);
} else {
++it;
}
}
}
} // namespace exo