// 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