#include "remoting/host/linux/x_server_clipboard.h"
#include <limits>
#include "base/functional/callback.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_util.h"
#include "remoting/base/constants.h"
#include "remoting/base/logging.h"
#include "remoting/base/util.h"
#include "ui/gfx/x/extension_manager.h"
#include "ui/gfx/x/future.h"
#include "ui/gfx/x/xproto.h"
#include "ui/gfx/x/xproto_util.h"
namespace remoting {
XServerClipboard::XServerClipboard() = default;
XServerClipboard::~XServerClipboard() = default;
void XServerClipboard::Init(x11::Connection* connection,
const ClipboardChangedCallback& callback) {
connection_ = connection;
callback_ = callback;
if (!connection_->xfixes().present()) {
HOST_LOG << "X server does not support XFixes.";
return;
}
connection_->xfixes().QueryVersion(
{x11::XFixes::major_version, x11::XFixes::minor_version});
clipboard_window_ = connection_->GenerateId<x11::Window>();
connection_->CreateWindow({
.wid = clipboard_window_,
.parent = connection_->default_root(),
.width = 1,
.height = 1,
.override_redirect = x11::Bool32(true),
});
static const char* const kAtomNames[] = {"CLIPBOARD", "INCR",
"SELECTION_STRING", "TARGETS",
"TIMESTAMP", "UTF8_STRING"};
static const int kNumAtomNames = std::size(kAtomNames);
x11::Future<x11::InternAtomReply> futures[kNumAtomNames];
for (size_t i = 0; i < kNumAtomNames; i++) {
futures[i] = connection_->InternAtom({false, kAtomNames[i]});
}
connection_->Flush();
x11::Atom atoms[kNumAtomNames];
memset(atoms, 0, sizeof(atoms));
for (size_t i = 0; i < kNumAtomNames; i++) {
if (auto reply = futures[i].Sync()) {
atoms[i] = reply->atom;
} else {
LOG(ERROR) << "Failed to intern atom(s)";
break;
}
}
clipboard_atom_ = atoms[0];
large_selection_atom_ = atoms[1];
selection_string_atom_ = atoms[2];
targets_atom_ = atoms[3];
timestamp_atom_ = atoms[4];
utf8_string_atom_ = atoms[5];
static_assert(kNumAtomNames >= 6, "kAtomNames is too small");
connection_->xfixes().SelectSelectionInput(
{static_cast<x11::Window>(clipboard_window_),
static_cast<x11::Atom>(clipboard_atom_),
x11::XFixes::SelectionEventMask::SetSelectionOwner});
connection_->Flush();
}
void XServerClipboard::SetClipboard(const std::string& mime_type,
const std::string& data) {
DCHECK(connection_->Ready());
if (clipboard_window_ == x11::Window::None) {
return;
}
if (mime_type != kMimeTypeTextUtf8) {
return;
}
if (!base::IsStringUTF8AllowingNoncharacters(data)) {
LOG(ERROR) << "ClipboardEvent: data is not UTF-8 encoded.";
return;
}
data_ = data;
AssertSelectionOwnership(x11::Atom::PRIMARY);
AssertSelectionOwnership(clipboard_atom_);
}
void XServerClipboard::ProcessXEvent(const x11::Event& event) {
if (clipboard_window_ == x11::Window::None ||
event.window() != clipboard_window_) {
return;
}
if (auto* property_notify = event.As<x11::PropertyNotifyEvent>()) {
OnPropertyNotify(*property_notify);
} else if (auto* selection_notify = event.As<x11::SelectionNotifyEvent>()) {
OnSelectionNotify(*selection_notify);
} else if (auto* selection_request = event.As<x11::SelectionRequestEvent>()) {
OnSelectionRequest(*selection_request);
} else if (auto* selection_clear = event.As<x11::SelectionClearEvent>()) {
OnSelectionClear(*selection_clear);
}
if (auto* xfixes_selection_notify =
event.As<x11::XFixes::SelectionNotifyEvent>()) {
OnSetSelectionOwnerNotify(xfixes_selection_notify->selection,
xfixes_selection_notify->selection_timestamp);
}
}
void XServerClipboard::OnSetSelectionOwnerNotify(x11::Atom selection,
x11::Time timestamp) {
if (!get_selections_time_.is_null() &&
(base::TimeTicks::Now() - get_selections_time_) < base::Seconds(5)) {
return;
}
if (selection != clipboard_atom_) {
return;
}
if (IsSelectionOwner(selection)) {
return;
}
get_selections_time_ = base::TimeTicks::Now();
RequestSelectionTargets(selection);
}
void XServerClipboard::OnPropertyNotify(const x11::PropertyNotifyEvent& event) {
if (large_selection_property_ != x11::Atom::None &&
event.atom == large_selection_property_ &&
event.state == x11::Property::NewValue) {
auto req = connection_->GetProperty({
.c_delete = true,
.window = clipboard_window_,
.property = large_selection_property_,
.type = x11::Atom::Any,
.long_length = std::numeric_limits<uint32_t>::max(),
});
if (auto reply = req.Sync()) {
if (reply->type != x11::Atom::None) {
if (reply->value_len == 0) {
large_selection_property_ = x11::Atom::None;
}
}
}
}
}
void XServerClipboard::OnSelectionNotify(
const x11::SelectionNotifyEvent& event) {
if (event.property != x11::Atom::None) {
auto req = connection_->GetProperty({
.c_delete = true,
.window = clipboard_window_,
.property = event.property,
.type = x11::Atom::Any,
.long_length = std::numeric_limits<uint32_t>::max(),
});
if (auto reply = req.Sync()) {
if (reply->type == large_selection_atom_) {
large_selection_property_ = event.property;
} else {
large_selection_property_ = x11::Atom::None;
if (reply->type != x11::Atom::None) {
HandleSelectionNotify(event, reply->type, reply->format,
reply->value_len, reply->value->data());
return;
}
}
}
}
HandleSelectionNotify(event, x11::Atom::None, 0, 0, nullptr);
}
void XServerClipboard::OnSelectionRequest(
const x11::SelectionRequestEvent& event) {
x11::SelectionNotifyEvent selection_event;
selection_event.requestor = event.requestor;
selection_event.selection = event.selection;
selection_event.time = event.time;
selection_event.target = event.target;
auto property =
event.property == x11::Atom::None ? event.target : event.property;
if (!IsSelectionOwner(selection_event.selection)) {
selection_event.property = x11::Atom::None;
} else {
selection_event.property = property;
if (selection_event.target == static_cast<x11::Atom>(targets_atom_)) {
SendTargetsResponse(selection_event.requestor, selection_event.property);
} else if (selection_event.target ==
static_cast<x11::Atom>(timestamp_atom_)) {
SendTimestampResponse(selection_event.requestor,
selection_event.property);
} else if (selection_event.target ==
static_cast<x11::Atom>(utf8_string_atom_) ||
selection_event.target == x11::Atom::STRING) {
SendStringResponse(selection_event.requestor, selection_event.property,
selection_event.target);
}
}
x11::SendEvent(selection_event, selection_event.requestor,
x11::EventMask::NoEvent, connection_);
}
void XServerClipboard::OnSelectionClear(const x11::SelectionClearEvent& event) {
selections_owned_.erase(event.selection);
}
void XServerClipboard::SendTargetsResponse(x11::Window requestor,
x11::Atom property) {
x11::Atom targets[3] = {
timestamp_atom_,
utf8_string_atom_,
x11::Atom::STRING,
};
connection_->ChangeProperty({
.mode = x11::PropMode::Replace,
.window = requestor,
.property = property,
.type = x11::Atom::ATOM,
.format = CHAR_BIT * sizeof(x11::Atom),
.data_len = std::size(targets),
.data = base::MakeRefCounted<base::RefCountedStaticMemory>(
&targets[0], sizeof(targets)),
});
connection_->Flush();
}
void XServerClipboard::SendTimestampResponse(x11::Window requestor,
x11::Atom property) {
x11::Time time = x11::Time::CurrentTime;
connection_->ChangeProperty({
.mode = x11::PropMode::Replace,
.window = requestor,
.property = property,
.type = x11::Atom::INTEGER,
.format = CHAR_BIT * sizeof(x11::Time),
.data_len = 1,
.data = base::MakeRefCounted<base::RefCountedStaticMemory>(&time,
sizeof(time)),
});
connection_->Flush();
}
void XServerClipboard::SendStringResponse(x11::Window requestor,
x11::Atom property,
x11::Atom target) {
if (!data_.empty()) {
connection_->ChangeProperty({
.mode = x11::PropMode::Replace,
.window = requestor,
.property = property,
.type = target,
.format = 8,
.data_len = static_cast<uint32_t>(data_.size()),
.data = base::MakeRefCounted<base::RefCountedStaticMemory>(
data_.data(), data_.size()),
});
connection_->Flush();
}
}
void XServerClipboard::HandleSelectionNotify(
const x11::SelectionNotifyEvent& event,
x11::Atom type,
int format,
int item_count,
const void* data) {
bool finished = false;
auto target = event.target;
if (target == targets_atom_) {
finished = HandleSelectionTargetsEvent(event, format, item_count, data);
} else if (target == utf8_string_atom_ || target == x11::Atom::STRING) {
finished = HandleSelectionStringEvent(event, format, item_count, data);
}
if (finished) {
get_selections_time_ = base::TimeTicks();
}
}
bool XServerClipboard::HandleSelectionTargetsEvent(
const x11::SelectionNotifyEvent& event,
int format,
int item_count,
const void* data) {
auto selection = event.selection;
if (event.property == targets_atom_) {
if (data && format == 32) {
const uint32_t* targets = static_cast<const uint32_t*>(data);
for (int i = 0; i < item_count; i++) {
if (targets[i] == static_cast<uint32_t>(utf8_string_atom_)) {
RequestSelectionString(selection, utf8_string_atom_);
return false;
}
}
}
}
RequestSelectionString(selection, x11::Atom::STRING);
return false;
}
bool XServerClipboard::HandleSelectionStringEvent(
const x11::SelectionNotifyEvent& event,
int format,
int item_count,
const void* data) {
auto property = event.property;
auto target = event.target;
if (property != selection_string_atom_ || !data || format != 8) {
return true;
}
std::string text(static_cast<const char*>(data), item_count);
if (target == x11::Atom::STRING || target == utf8_string_atom_) {
NotifyClipboardText(text);
}
return true;
}
void XServerClipboard::NotifyClipboardText(const std::string& text) {
data_ = text;
callback_.Run(kMimeTypeTextUtf8, data_);
}
void XServerClipboard::RequestSelectionTargets(x11::Atom selection) {
connection_->ConvertSelection({clipboard_window_, selection, targets_atom_,
targets_atom_, x11::Time::CurrentTime});
}
void XServerClipboard::RequestSelectionString(x11::Atom selection,
x11::Atom target) {
connection_->ConvertSelection({clipboard_window_, selection, target,
selection_string_atom_,
x11::Time::CurrentTime});
}
void XServerClipboard::AssertSelectionOwnership(x11::Atom selection) {
connection_->SetSelectionOwner(
{clipboard_window_, selection, x11::Time::CurrentTime});
auto reply = connection_->GetSelectionOwner({selection}).Sync();
auto owner = reply ? reply->owner : x11::Window::None;
if (owner == clipboard_window_) {
selections_owned_.insert(selection);
} else {
LOG(ERROR) << "XSetSelectionOwner failed for selection "
<< static_cast<uint32_t>(selection);
}
}
bool XServerClipboard::IsSelectionOwner(x11::Atom selection) {
return selections_owned_.find(selection) != selections_owned_.end();
}
}