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
15namespace remoting {
16
17XServerClipboard::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
30XServerClipboard::~XServerClipboard() {
31}
32
33void 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
88void 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
109void 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
140void 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
170void 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
193void 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
220void 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
249void XServerClipboard::OnSelectionClear(XEvent* event) {
250  selections_owned_.erase(event->xselectionclear.selection);
251}
252
253void 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
264void 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
277void 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
289void 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
307bool 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
332bool 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
347void XServerClipboard::NotifyClipboardText(const std::string& text) {
348  data_ = text;
349  callback_.Run(kMimeTypeTextUtf8, data_);
350}
351
352void XServerClipboard::RequestSelectionTargets(Atom selection) {
353  XConvertSelection(display_, selection, targets_atom_, targets_atom_,
354                    clipboard_window_, CurrentTime);
355}
356
357void XServerClipboard::RequestSelectionString(Atom selection, Atom target) {
358  XConvertSelection(display_, selection, target, selection_string_atom_,
359                    clipboard_window_, CurrentTime);
360}
361
362void 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
371bool XServerClipboard::IsSelectionOwner(Atom selection) {
372  return selections_owned_.find(selection) != selections_owned_.end();
373}
374
375}  // namespace remoting
376