blob: 6c82b932f7cb713a36d4518031c348964cd5e178 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/linux/x_server_clipboard.h"
#include <array>
#include <limits>
#include "base/compiler_specific.h"
#include "base/containers/contains.h"
#include "base/containers/span.h"
#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"
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;
}
clipboard_window_ = connection_->GenerateId<x11::Window>();
connection_->CreateWindow({
.wid = clipboard_window_,
.parent = connection_->default_root(),
.width = 1,
.height = 1,
.override_redirect = x11::Bool32(true),
});
// TODO(lambroslambrou): Use ui::X11AtomCache for this, either by adding a
// dependency on ui/ or by moving X11AtomCache to base/.
static const auto kAtomNames = std::to_array<const char*>({
"CLIPBOARD",
"INCR",
"SELECTION_STRING",
"TARGETS",
"TIMESTAMP",
"UTF8_STRING",
});
static const int kNumAtomNames = std::size(kAtomNames);
std::array<x11::Future<x11::InternAtomReply>, kNumAtomNames> futures;
for (size_t i = 0; i < kNumAtomNames; i++) {
futures[i] = connection_->InternAtom({false, kAtomNames[i]});
}
connection_->Flush();
std::array<x11::Atom, kNumAtomNames> 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;
}
// Currently only UTF-8 is supported.
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) {
return;
}
if (auto* property_notify = event.As<x11::PropertyNotifyEvent>()) {
if (property_notify->window == clipboard_window_) {
OnPropertyNotify(*property_notify);
}
} else if (auto* selection_notify = event.As<x11::SelectionNotifyEvent>()) {
if (selection_notify->requestor == clipboard_window_) {
OnSelectionNotify(*selection_notify);
}
} else if (auto* selection_request = event.As<x11::SelectionRequestEvent>()) {
if (selection_request->owner == clipboard_window_) {
OnSelectionRequest(*selection_request);
}
} else if (auto* selection_clear = event.As<x11::SelectionClearEvent>()) {
if (selection_clear->owner == clipboard_window_) {
OnSelectionClear(*selection_clear);
}
} else if (auto* xfixes_selection_notify =
event.As<x11::XFixes::SelectionNotifyEvent>()) {
if (xfixes_selection_notify->window == clipboard_window_) {
OnSetSelectionOwnerNotify(xfixes_selection_notify->selection,
xfixes_selection_notify->selection_timestamp);
}
}
}
void XServerClipboard::OnSetSelectionOwnerNotify(x11::Atom selection,
x11::Time timestamp) {
// Protect against receiving new XFixes selection notifications whilst we're
// in the middle of waiting for information from the current selection owner.
// A reasonable timeout allows for misbehaving apps that don't respond
// quickly to our requests.
if (!get_selections_time_.is_null() &&
(base::TimeTicks::Now() - get_selections_time_) < base::Seconds(5)) {
// TODO(lambroslambrou): Instead of ignoring this notification, cancel any
// pending request operations and ignore the resulting events, before
// dispatching new requests here.
return;
}
// Only process CLIPBOARD selections.
if (selection != clipboard_atom_) {
return;
}
// If we own the selection, don't request details for it.
if (IsSelectionOwner(selection)) {
return;
}
get_selections_time_ = base::TimeTicks::Now();
// Before getting the value of the chosen selection, request the list of
// target formats it supports.
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) {
// TODO(lambroslambrou): Properly support large transfers -
// http://crbug.com/151447.
// If the property is zero-length then the large transfer is complete.
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 - just read and ignore these for now.
large_selection_property_ = event.property;
} else {
// Standard selection - call the selection notifier.
large_selection_property_ = x11::Atom::None;
if (reply->type != x11::Atom::None) {
HandleSelectionNotify(event, reply->type, reply->format,
reply->value_len, reply->value->bytes());
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);
}
}
connection_->SendEvent(selection_event, selection_event.requestor,
x11::EventMask::NoEvent);
}
void XServerClipboard::OnSelectionClear(const x11::SelectionClearEvent& event) {
selections_owned_.erase(event.selection);
}
void XServerClipboard::SendTargetsResponse(x11::Window requestor,
x11::Atom property) {
// Respond advertising x11::Atom::STRING, UTF8_STRING and TIMESTAMP data for
// the selection.
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>(
base::as_byte_span(targets)),
});
connection_->Flush();
}
void XServerClipboard::SendTimestampResponse(x11::Window requestor,
x11::Atom property) {
// Respond with the timestamp of our selection; we always return
// CurrentTime since our selections are set by remote clients, so there
// is no associated local X event.
// TODO(lambroslambrou): Should use a proper timestamp here instead of
// CurrentTime. ICCCM recommends doing a zero-length property append,
// and getting a timestamp from the subsequent PropertyNotify event.
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>(
base::byte_span_from_ref(time)),
});
connection_->Flush();
}
void XServerClipboard::SendStringResponse(x11::Window requestor,
x11::Atom property,
x11::Atom target) {
if (!data_.empty()) {
// Return the actual string data; we always return UTF8, regardless of
// the configured locale.
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>(
base::as_byte_span(data_)),
});
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 (UNSAFE_TODO(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 base::Contains(selections_owned_, selection);
}
} // namespace remoting