• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 "remoting/base/constants.h"
12 #include "remoting/base/logging.h"
13 #include "remoting/base/util.h"
14 
15 namespace remoting {
16 
XServerClipboard()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 
~XServerClipboard()30 XServerClipboard::~XServerClipboard() {
31 }
32 
Init(Display * display,const ClipboardChangedCallback & callback)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     HOST_LOG << "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 
SetClipboard(const std::string & mime_type,const std::string & data)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 
ProcessXEvent(XEvent * event)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 
OnSetSelectionOwnerNotify(Atom selection,Time timestamp)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 
OnPropertyNotify(XEvent * event)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 
OnSelectionNotify(XEvent * event)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 
OnSelectionRequest(XEvent * event)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 
OnSelectionClear(XEvent * event)249 void XServerClipboard::OnSelectionClear(XEvent* event) {
250   selections_owned_.erase(event->xselectionclear.selection);
251 }
252 
SendTargetsResponse(Window requestor,Atom property)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 
SendTimestampResponse(Window requestor,Atom property)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 
SendStringResponse(Window requestor,Atom property,Atom target)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 
HandleSelectionNotify(XSelectionEvent * event,Atom type,int format,int item_count,void * data)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 
HandleSelectionTargetsEvent(XSelectionEvent * event,int format,int item_count,void * data)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 
HandleSelectionStringEvent(XSelectionEvent * event,int format,int item_count,void * data)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 
NotifyClipboardText(const std::string & text)347 void XServerClipboard::NotifyClipboardText(const std::string& text) {
348   data_ = text;
349   callback_.Run(kMimeTypeTextUtf8, data_);
350 }
351 
RequestSelectionTargets(Atom selection)352 void XServerClipboard::RequestSelectionTargets(Atom selection) {
353   XConvertSelection(display_, selection, targets_atom_, targets_atom_,
354                     clipboard_window_, CurrentTime);
355 }
356 
RequestSelectionString(Atom selection,Atom target)357 void XServerClipboard::RequestSelectionString(Atom selection, Atom target) {
358   XConvertSelection(display_, selection, target, selection_string_atom_,
359                     clipboard_window_, CurrentTime);
360 }
361 
AssertSelectionOwnership(Atom selection)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 
IsSelectionOwner(Atom selection)371 bool XServerClipboard::IsSelectionOwner(Atom selection) {
372   return selections_owned_.find(selection) != selections_owned_.end();
373 }
374 
375 }  // namespace remoting
376