Home | History | Annotate | Download | only in linux
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "remoting/host/linux/x_server_clipboard.h"
      6 
      7 #include <X11/extensions/Xfixes.h>
      8 
      9 #include "base/basictypes.h"
     10 #include "base/callback.h"
     11 #include "base/logging.h"
     12 #include "remoting/base/constants.h"
     13 #include "remoting/base/util.h"
     14 
     15 namespace remoting {
     16 
     17 XServerClipboard::XServerClipboard()
     18     : display_(NULL),
     19       clipboard_window_(BadValue),
     20       xfixes_event_base_(-1),
     21       clipboard_atom_(None),
     22       large_selection_atom_(None),
     23       selection_string_atom_(None),
     24       targets_atom_(None),
     25       timestamp_atom_(None),
     26       utf8_string_atom_(None),
     27       large_selection_property_(None) {
     28 }
     29 
     30 XServerClipboard::~XServerClipboard() {
     31 }
     32 
     33 void XServerClipboard::Init(Display* display,
     34                             const ClipboardChangedCallback& callback) {
     35   display_ = display;
     36   callback_ = callback;
     37 
     38   // If any of these X API calls fail, an X Error will be raised, crashing the
     39   // process.  This is unlikely to occur in practice, and even if it does, it
     40   // would mean the X server is in a bad state, so it's not worth trying to
     41   // trap such errors here.
     42 
     43   // TODO(lambroslambrou): Consider using ScopedXErrorHandler here, or consider
     44   // placing responsibility for handling X Errors outside this class, since
     45   // X Error handlers are global to all X connections.
     46   int xfixes_error_base;
     47   if (!XFixesQueryExtension(display_, &xfixes_event_base_,
     48                             &xfixes_error_base)) {
     49     LOG(INFO) << "X server does not support XFixes.";
     50     return;
     51   }
     52 
     53   clipboard_window_ = XCreateSimpleWindow(display_,
     54                                           DefaultRootWindow(display_),
     55                                           0, 0, 1, 1,  // x, y, width, height
     56                                           0, 0, 0);
     57 
     58   // TODO(lambroslambrou): Use ui::X11AtomCache for this, either by adding a
     59   // dependency on ui/ or by moving X11AtomCache to base/.
     60   static const char* const kAtomNames[] = {
     61     "CLIPBOARD",
     62     "INCR",
     63     "SELECTION_STRING",
     64     "TARGETS",
     65     "TIMESTAMP",
     66     "UTF8_STRING"
     67   };
     68   static const int kNumAtomNames = arraysize(kAtomNames);
     69 
     70   Atom atoms[kNumAtomNames];
     71   if (XInternAtoms(display_, const_cast<char**>(kAtomNames), kNumAtomNames,
     72                    False, atoms)) {
     73     clipboard_atom_ = atoms[0];
     74     large_selection_atom_ = atoms[1];
     75     selection_string_atom_ = atoms[2];
     76     targets_atom_ = atoms[3];
     77     timestamp_atom_ = atoms[4];
     78     utf8_string_atom_ = atoms[5];
     79     COMPILE_ASSERT(kNumAtomNames >= 6, kAtomNames_too_small);
     80   } else {
     81     LOG(ERROR) << "XInternAtoms failed";
     82   }
     83 
     84   XFixesSelectSelectionInput(display_, clipboard_window_, clipboard_atom_,
     85                              XFixesSetSelectionOwnerNotifyMask);
     86 }
     87 
     88 void XServerClipboard::SetClipboard(const std::string& mime_type,
     89                                     const std::string& data) {
     90   DCHECK(display_);
     91 
     92   if (clipboard_window_ == BadValue)
     93     return;
     94 
     95   // Currently only UTF-8 is supported.
     96   if (mime_type != kMimeTypeTextUtf8)
     97     return;
     98   if (!StringIsUtf8(data.c_str(), data.length())) {
     99     LOG(ERROR) << "ClipboardEvent: data is not UTF-8 encoded.";
    100     return;
    101   }
    102 
    103   data_ = data;
    104 
    105   AssertSelectionOwnership(XA_PRIMARY);
    106   AssertSelectionOwnership(clipboard_atom_);
    107 }
    108 
    109 void XServerClipboard::ProcessXEvent(XEvent* event) {
    110   if (clipboard_window_ == BadValue ||
    111       event->xany.window != clipboard_window_) {
    112     return;
    113   }
    114 
    115   switch (event->type) {
    116     case PropertyNotify:
    117       OnPropertyNotify(event);
    118       break;
    119     case SelectionNotify:
    120       OnSelectionNotify(event);
    121       break;
    122     case SelectionRequest:
    123       OnSelectionRequest(event);
    124       break;
    125     case SelectionClear:
    126       OnSelectionClear(event);
    127       break;
    128     default:
    129       break;
    130   }
    131 
    132   if (event->type == xfixes_event_base_ + XFixesSetSelectionOwnerNotify) {
    133     XFixesSelectionNotifyEvent* notify_event =
    134         reinterpret_cast<XFixesSelectionNotifyEvent*>(event);
    135     OnSetSelectionOwnerNotify(notify_event->selection,
    136                               notify_event->selection_timestamp);
    137   }
    138 }
    139 
    140 void XServerClipboard::OnSetSelectionOwnerNotify(Atom selection,
    141                                                  Time timestamp) {
    142   // Protect against receiving new XFixes selection notifications whilst we're
    143   // in the middle of waiting for information from the current selection owner.
    144   // A reasonable timeout allows for misbehaving apps that don't respond
    145   // quickly to our requests.
    146   if (!get_selections_time_.is_null() &&
    147       (base::TimeTicks::Now() - get_selections_time_) <
    148           base::TimeDelta::FromSeconds(5)) {
    149     // TODO(lambroslambrou): Instead of ignoring this notification, cancel any
    150     // pending request operations and ignore the resulting events, before
    151     // dispatching new requests here.
    152     return;
    153   }
    154 
    155   // Only process CLIPBOARD selections.
    156   if (selection != clipboard_atom_)
    157     return;
    158 
    159   // If we own the selection, don't request details for it.
    160   if (IsSelectionOwner(selection))
    161     return;
    162 
    163   get_selections_time_ = base::TimeTicks::Now();
    164 
    165   // Before getting the value of the chosen selection, request the list of
    166   // target formats it supports.
    167   RequestSelectionTargets(selection);
    168 }
    169 
    170 void XServerClipboard::OnPropertyNotify(XEvent* event) {
    171   if (large_selection_property_ != None &&
    172       event->xproperty.atom == large_selection_property_ &&
    173       event->xproperty.state == PropertyNewValue) {
    174     Atom type;
    175     int format;
    176     unsigned long item_count, after;
    177     unsigned char *data;
    178     XGetWindowProperty(display_, clipboard_window_, large_selection_property_,
    179                        0, ~0L, True, AnyPropertyType, &type, &format,
    180                        &item_count, &after, &data);
    181     if (type != None) {
    182       // TODO(lambroslambrou): Properly support large transfers -
    183       // http://crbug.com/151447.
    184       XFree(data);
    185 
    186       // If the property is zero-length then the large transfer is complete.
    187       if (item_count == 0)
    188         large_selection_property_ = None;
    189     }
    190   }
    191 }
    192 
    193 void XServerClipboard::OnSelectionNotify(XEvent* event) {
    194   if (event->xselection.property != None) {
    195     Atom type;
    196     int format;
    197     unsigned long item_count, after;
    198     unsigned char *data;
    199     XGetWindowProperty(display_, clipboard_window_,
    200                        event->xselection.property, 0, ~0L, True,
    201                        AnyPropertyType, &type, &format,
    202                        &item_count, &after, &data);
    203     if (type == large_selection_atom_) {
    204       // Large selection - just read and ignore these for now.
    205       large_selection_property_ = event->xselection.property;
    206     } else {
    207       // Standard selection - call the selection notifier.
    208       large_selection_property_ = None;
    209       if (type != None) {
    210         HandleSelectionNotify(&event->xselection, type, format, item_count,
    211                               data);
    212         XFree(data);
    213         return;
    214       }
    215     }
    216   }
    217   HandleSelectionNotify(&event->xselection, 0, 0, 0, 0);
    218 }
    219 
    220 void XServerClipboard::OnSelectionRequest(XEvent* event) {
    221   XSelectionEvent selection_event;
    222   selection_event.type = SelectionNotify;
    223   selection_event.display = event->xselectionrequest.display;
    224   selection_event.requestor = event->xselectionrequest.requestor;
    225   selection_event.selection = event->xselectionrequest.selection;
    226   selection_event.time = event->xselectionrequest.time;
    227   selection_event.target = event->xselectionrequest.target;
    228   if (event->xselectionrequest.property == None)
    229     event->xselectionrequest.property = event->xselectionrequest.target;
    230   if (!IsSelectionOwner(selection_event.selection)) {
    231     selection_event.property = None;
    232   } else {
    233     selection_event.property = event->xselectionrequest.property;
    234     if (selection_event.target == targets_atom_) {
    235       SendTargetsResponse(selection_event.requestor, selection_event.property);
    236     } else if (selection_event.target == timestamp_atom_) {
    237       SendTimestampResponse(selection_event.requestor,
    238                             selection_event.property);
    239     } else if (selection_event.target == utf8_string_atom_ ||
    240                selection_event.target == XA_STRING) {
    241       SendStringResponse(selection_event.requestor, selection_event.property,
    242                          selection_event.target);
    243     }
    244   }
    245   XSendEvent(display_, selection_event.requestor, False, 0,
    246              reinterpret_cast<XEvent*>(&selection_event));
    247 }
    248 
    249 void XServerClipboard::OnSelectionClear(XEvent* event) {
    250   selections_owned_.erase(event->xselectionclear.selection);
    251 }
    252 
    253 void XServerClipboard::SendTargetsResponse(Window requestor, Atom property) {
    254   // Respond advertising XA_STRING, UTF8_STRING and TIMESTAMP data for the
    255   // selection.
    256   Atom targets[3];
    257   targets[0] = timestamp_atom_;
    258   targets[1] = utf8_string_atom_;
    259   targets[2] = XA_STRING;
    260   XChangeProperty(display_, requestor, property, XA_ATOM, 32, PropModeReplace,
    261                   reinterpret_cast<unsigned char*>(targets), 3);
    262 }
    263 
    264 void XServerClipboard::SendTimestampResponse(Window requestor, Atom property) {
    265   // Respond with the timestamp of our selection; we always return
    266   // CurrentTime since our selections are set by remote clients, so there
    267   // is no associated local X event.
    268 
    269   // TODO(lambroslambrou): Should use a proper timestamp here instead of
    270   // CurrentTime.  ICCCM recommends doing a zero-length property append,
    271   // and getting a timestamp from the subsequent PropertyNotify event.
    272   Time time = CurrentTime;
    273   XChangeProperty(display_, requestor, property, XA_INTEGER, 32,
    274                   PropModeReplace, reinterpret_cast<unsigned char*>(&time), 1);
    275 }
    276 
    277 void XServerClipboard::SendStringResponse(Window requestor, Atom property,
    278                                           Atom target) {
    279   if (!data_.empty()) {
    280     // Return the actual string data; we always return UTF8, regardless of
    281     // the configured locale.
    282     XChangeProperty(display_, requestor, property, target, 8, PropModeReplace,
    283                     reinterpret_cast<unsigned char*>(
    284                         const_cast<char*>(data_.data())),
    285                     data_.size());
    286   }
    287 }
    288 
    289 void XServerClipboard::HandleSelectionNotify(XSelectionEvent* event,
    290                                              Atom type,
    291                                              int format,
    292                                              int item_count,
    293                                              void* data) {
    294   bool finished = false;
    295 
    296   if (event->target == targets_atom_) {
    297     finished = HandleSelectionTargetsEvent(event, format, item_count, data);
    298   } else if (event->target == utf8_string_atom_ ||
    299              event->target == XA_STRING) {
    300     finished = HandleSelectionStringEvent(event, format, item_count, data);
    301   }
    302 
    303   if (finished)
    304     get_selections_time_ = base::TimeTicks();
    305 }
    306 
    307 bool XServerClipboard::HandleSelectionTargetsEvent(XSelectionEvent* event,
    308                                                    int format,
    309                                                    int item_count,
    310                                                    void* data) {
    311   if (event->property == targets_atom_) {
    312     if (data && format == 32) {
    313       // The XGetWindowProperty man-page specifies that the returned
    314       // property data will be an array of |long|s in the case where
    315       // |format| == 32.  Although the items are 32-bit values (as stored and
    316       // sent over the X protocol), Xlib presents the data to the client as an
    317       // array of |long|s, with zero-padding on a 64-bit system where |long|
    318       // is bigger than 32 bits.
    319       const long* targets = static_cast<const long*>(data);
    320       for (int i = 0; i < item_count; i++) {
    321         if (targets[i] == static_cast<long>(utf8_string_atom_)) {
    322           RequestSelectionString(event->selection, utf8_string_atom_);
    323           return false;
    324         }
    325       }
    326     }
    327   }
    328   RequestSelectionString(event->selection, XA_STRING);
    329   return false;
    330 }
    331 
    332 bool XServerClipboard::HandleSelectionStringEvent(XSelectionEvent* event,
    333                                                   int format,
    334                                                   int item_count,
    335                                                   void* data) {
    336   if (event->property != selection_string_atom_ || !data || format != 8)
    337     return true;
    338 
    339   std::string text(static_cast<char*>(data), item_count);
    340 
    341   if (event->target == XA_STRING || event->target == utf8_string_atom_)
    342     NotifyClipboardText(text);
    343 
    344   return true;
    345 }
    346 
    347 void XServerClipboard::NotifyClipboardText(const std::string& text) {
    348   data_ = text;
    349   callback_.Run(kMimeTypeTextUtf8, data_);
    350 }
    351 
    352 void XServerClipboard::RequestSelectionTargets(Atom selection) {
    353   XConvertSelection(display_, selection, targets_atom_, targets_atom_,
    354                     clipboard_window_, CurrentTime);
    355 }
    356 
    357 void XServerClipboard::RequestSelectionString(Atom selection, Atom target) {
    358   XConvertSelection(display_, selection, target, selection_string_atom_,
    359                     clipboard_window_, CurrentTime);
    360 }
    361 
    362 void XServerClipboard::AssertSelectionOwnership(Atom selection) {
    363   XSetSelectionOwner(display_, selection, clipboard_window_, CurrentTime);
    364   if (XGetSelectionOwner(display_, selection) == clipboard_window_) {
    365     selections_owned_.insert(selection);
    366   } else {
    367     LOG(ERROR) << "XSetSelectionOwner failed for selection " << selection;
    368   }
    369 }
    370 
    371 bool XServerClipboard::IsSelectionOwner(Atom selection) {
    372   return selections_owned_.find(selection) != selections_owned_.end();
    373 }
    374 
    375 }  // namespace remoting
    376