// Copyright (c) 2012 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 "remoting/host/linux/x_server_clipboard.h"
#include <X11/extensions/Xfixes.h>
#include "base/basictypes.h"
#include "base/callback.h"
#include "remoting/base/constants.h"
#include "remoting/base/logging.h"
#include "remoting/base/util.h"
namespace remoting {
XServerClipboard::XServerClipboard()
: display_(NULL),
clipboard_window_(BadValue),
xfixes_event_base_(-1),
clipboard_atom_(None),
large_selection_atom_(None),
selection_string_atom_(None),
targets_atom_(None),
timestamp_atom_(None),
utf8_string_atom_(None),
large_selection_property_(None) {
}
XServerClipboard::~XServerClipboard() {
}
void XServerClipboard::Init(Display* display,
const ClipboardChangedCallback& callback) {
display_ = display;
callback_ = callback;
// If any of these X API calls fail, an X Error will be raised, crashing the
// process. This is unlikely to occur in practice, and even if it does, it
// would mean the X server is in a bad state, so it's not worth trying to
// trap such errors here.
// TODO(lambroslambrou): Consider using ScopedXErrorHandler here, or consider
// placing responsibility for handling X Errors outside this class, since
// X Error handlers are global to all X connections.
int xfixes_error_base;
if (!XFixesQueryExtension(display_, &xfixes_event_base_,
&xfixes_error_base)) {
HOST_LOG << "X server does not support XFixes.";
return;
}
clipboard_window_ = XCreateSimpleWindow(display_,
DefaultRootWindow(display_),
0, 0, 1, 1, // x, y, width, height
0, 0, 0);
// TODO(lambroslambrou): Use ui::X11AtomCache for this, either by adding a
// dependency on ui/ or by moving X11AtomCache to base/.
static const char* const kAtomNames[] = {
"CLIPBOARD",
"INCR",
"SELECTION_STRING",
"TARGETS",
"TIMESTAMP",
"UTF8_STRING"
};
static const int kNumAtomNames = arraysize(kAtomNames);
Atom atoms[kNumAtomNames];
if (XInternAtoms(display_, const_cast<char**>(kAtomNames), kNumAtomNames,
False, atoms)) {
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];
COMPILE_ASSERT(kNumAtomNames >= 6, kAtomNames_too_small);
} else {
LOG(ERROR) << "XInternAtoms failed";
}
XFixesSelectSelectionInput(display_, clipboard_window_, clipboard_atom_,
XFixesSetSelectionOwnerNotifyMask);
}
void XServerClipboard::SetClipboard(const std::string& mime_type,
const std::string& data) {
DCHECK(display_);
if (clipboard_window_ == BadValue)
return;
// Currently only UTF-8 is supported.
if (mime_type != kMimeTypeTextUtf8)
return;
if (!StringIsUtf8(data.c_str(), data.length())) {
LOG(ERROR) << "ClipboardEvent: data is not UTF-8 encoded.";
return;
}
data_ = data;
AssertSelectionOwnership(XA_PRIMARY);
AssertSelectionOwnership(clipboard_atom_);
}
void XServerClipboard::ProcessXEvent(XEvent* event) {
if (clipboard_window_ == BadValue ||
event->xany.window != clipboard_window_) {
return;
}
switch (event->type) {
case PropertyNotify:
OnPropertyNotify(event);
break;
case SelectionNotify:
OnSelectionNotify(event);
break;
case SelectionRequest:
OnSelectionRequest(event);
break;
case SelectionClear:
OnSelectionClear(event);
break;
default:
break;
}
if (event->type == xfixes_event_base_ + XFixesSetSelectionOwnerNotify) {
XFixesSelectionNotifyEvent* notify_event =
reinterpret_cast<XFixesSelectionNotifyEvent*>(event);
OnSetSelectionOwnerNotify(notify_event->selection,
notify_event->selection_timestamp);
}
}
void XServerClipboard::OnSetSelectionOwnerNotify(Atom selection,
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::TimeDelta::FromSeconds(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(XEvent* event) {
if (large_selection_property_ != None &&
event->xproperty.atom == large_selection_property_ &&
event->xproperty.state == PropertyNewValue) {
Atom type;
int format;
unsigned long item_count, after;
unsigned char *data;
XGetWindowProperty(display_, clipboard_window_, large_selection_property_,
0, ~0L, True, AnyPropertyType, &type, &format,
&item_count, &after, &data);
if (type != None) {
// TODO(lambroslambrou): Properly support large transfers -
// http://crbug.com/151447.
XFree(data);
// If the property is zero-length then the large transfer is complete.
if (item_count == 0)
large_selection_property_ = None;
}
}
}
void XServerClipboard::OnSelectionNotify(XEvent* event) {
if (event->xselection.property != None) {
Atom type;
int format;
unsigned long item_count, after;
unsigned char *data;
XGetWindowProperty(display_, clipboard_window_,
event->xselection.property, 0, ~0L, True,
AnyPropertyType, &type, &format,
&item_count, &after, &data);
if (type == large_selection_atom_) {
// Large selection - just read and ignore these for now.
large_selection_property_ = event->xselection.property;
} else {
// Standard selection - call the selection notifier.
large_selection_property_ = None;
if (type != None) {
HandleSelectionNotify(&event->xselection, type, format, item_count,
data);
XFree(data);
return;
}
}
}
HandleSelectionNotify(&event->xselection, 0, 0, 0, 0);
}
void XServerClipboard::OnSelectionRequest(XEvent* event) {
XSelectionEvent selection_event;
selection_event.type = SelectionNotify;
selection_event.display = event->xselectionrequest.display;
selection_event.requestor = event->xselectionrequest.requestor;
selection_event.selection = event->xselectionrequest.selection;
selection_event.time = event->xselectionrequest.time;
selection_event.target = event->xselectionrequest.target;
if (event->xselectionrequest.property == None)
event->xselectionrequest.property = event->xselectionrequest.target;
if (!IsSelectionOwner(selection_event.selection)) {
selection_event.property = None;
} else {
selection_event.property = event->xselectionrequest.property;
if (selection_event.target == targets_atom_) {
SendTargetsResponse(selection_event.requestor, selection_event.property);
} else if (selection_event.target == timestamp_atom_) {
SendTimestampResponse(selection_event.requestor,
selection_event.property);
} else if (selection_event.target == utf8_string_atom_ ||
selection_event.target == XA_STRING) {
SendStringResponse(selection_event.requestor, selection_event.property,
selection_event.target);
}
}
XSendEvent(display_, selection_event.requestor, False, 0,
reinterpret_cast<XEvent*>(&selection_event));
}
void XServerClipboard::OnSelectionClear(XEvent* event) {
selections_owned_.erase(event->xselectionclear.selection);
}
void XServerClipboard::SendTargetsResponse(Window requestor, Atom property) {
// Respond advertising XA_STRING, UTF8_STRING and TIMESTAMP data for the
// selection.
Atom targets[3];
targets[0] = timestamp_atom_;
targets[1] = utf8_string_atom_;
targets[2] = XA_STRING;
XChangeProperty(display_, requestor, property, XA_ATOM, 32, PropModeReplace,
reinterpret_cast<unsigned char*>(targets), 3);
}
void XServerClipboard::SendTimestampResponse(Window requestor, 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.
Time time = CurrentTime;
XChangeProperty(display_, requestor, property, XA_INTEGER, 32,
PropModeReplace, reinterpret_cast<unsigned char*>(&time), 1);
}
void XServerClipboard::SendStringResponse(Window requestor, Atom property,
Atom target) {
if (!data_.empty()) {
// Return the actual string data; we always return UTF8, regardless of
// the configured locale.
XChangeProperty(display_, requestor, property, target, 8, PropModeReplace,
reinterpret_cast<unsigned char*>(
const_cast<char*>(data_.data())),
data_.size());
}
}
void XServerClipboard::HandleSelectionNotify(XSelectionEvent* event,
Atom type,
int format,
int item_count,
void* data) {
bool finished = false;
if (event->target == targets_atom_) {
finished = HandleSelectionTargetsEvent(event, format, item_count, data);
} else if (event->target == utf8_string_atom_ ||
event->target == XA_STRING) {
finished = HandleSelectionStringEvent(event, format, item_count, data);
}
if (finished)
get_selections_time_ = base::TimeTicks();
}
bool XServerClipboard::HandleSelectionTargetsEvent(XSelectionEvent* event,
int format,
int item_count,
void* data) {
if (event->property == targets_atom_) {
if (data && format == 32) {
// The XGetWindowProperty man-page specifies that the returned
// property data will be an array of |long|s in the case where
// |format| == 32. Although the items are 32-bit values (as stored and
// sent over the X protocol), Xlib presents the data to the client as an
// array of |long|s, with zero-padding on a 64-bit system where |long|
// is bigger than 32 bits.
const long* targets = static_cast<const long*>(data);
for (int i = 0; i < item_count; i++) {
if (targets[i] == static_cast<long>(utf8_string_atom_)) {
RequestSelectionString(event->selection, utf8_string_atom_);
return false;
}
}
}
}
RequestSelectionString(event->selection, XA_STRING);
return false;
}
bool XServerClipboard::HandleSelectionStringEvent(XSelectionEvent* event,
int format,
int item_count,
void* data) {
if (event->property != selection_string_atom_ || !data || format != 8)
return true;
std::string text(static_cast<char*>(data), item_count);
if (event->target == XA_STRING || event->target == utf8_string_atom_)
NotifyClipboardText(text);
return true;
}
void XServerClipboard::NotifyClipboardText(const std::string& text) {
data_ = text;
callback_.Run(kMimeTypeTextUtf8, data_);
}
void XServerClipboard::RequestSelectionTargets(Atom selection) {
XConvertSelection(display_, selection, targets_atom_, targets_atom_,
clipboard_window_, CurrentTime);
}
void XServerClipboard::RequestSelectionString(Atom selection, Atom target) {
XConvertSelection(display_, selection, target, selection_string_atom_,
clipboard_window_, CurrentTime);
}
void XServerClipboard::AssertSelectionOwnership(Atom selection) {
XSetSelectionOwner(display_, selection, clipboard_window_, CurrentTime);
if (XGetSelectionOwner(display_, selection) == clipboard_window_) {
selections_owned_.insert(selection);
} else {
LOG(ERROR) << "XSetSelectionOwner failed for selection " << selection;
}
}
bool XServerClipboard::IsSelectionOwner(Atom selection) {
return selections_owned_.find(selection) != selections_owned_.end();
}
} // namespace remoting