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