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 <limits>
8 
9 #include "base/callback.h"
10 #include "base/memory/ref_counted_memory.h"
11 #include "base/memory/scoped_refptr.h"
12 #include "base/stl_util.h"
13 #include "remoting/base/constants.h"
14 #include "remoting/base/logging.h"
15 #include "remoting/base/util.h"
16 #include "ui/gfx/x/extension_manager.h"
17 #include "ui/gfx/x/xproto.h"
18 #include "ui/gfx/x/xproto_util.h"
19 
20 namespace remoting {
21 
22 XServerClipboard::XServerClipboard() = default;
23 
24 XServerClipboard::~XServerClipboard() = default;
25 
Init(x11::Connection * connection,const ClipboardChangedCallback & callback)26 void XServerClipboard::Init(x11::Connection* connection,
27                             const ClipboardChangedCallback& callback) {
28   connection_ = connection;
29   callback_ = callback;
30 
31   if (!connection_->xfixes().present()) {
32     HOST_LOG << "X server does not support XFixes.";
33     return;
34   }
35 
36   // Let the server know the client version.
37   connection_->xfixes().QueryVersion(
38       {x11::XFixes::major_version, x11::XFixes::minor_version});
39 
40   clipboard_window_ = connection_->GenerateId<x11::Window>();
41   connection_->CreateWindow({
42       .wid = clipboard_window_,
43       .parent = connection_->default_root(),
44       .width = 1,
45       .height = 1,
46       .override_redirect = x11::Bool32(true),
47   });
48 
49   // TODO(lambroslambrou): Use ui::X11AtomCache for this, either by adding a
50   // dependency on ui/ or by moving X11AtomCache to base/.
51   static const char* const kAtomNames[] = {"CLIPBOARD",        "INCR",
52                                            "SELECTION_STRING", "TARGETS",
53                                            "TIMESTAMP",        "UTF8_STRING"};
54   static const int kNumAtomNames = base::size(kAtomNames);
55 
56   x11::Future<x11::InternAtomReply> futures[kNumAtomNames];
57   for (size_t i = 0; i < kNumAtomNames; i++)
58     futures[i] = connection_->InternAtom({false, kAtomNames[i]});
59   connection_->Flush();
60   x11::Atom atoms[kNumAtomNames];
61   memset(atoms, 0, sizeof(atoms));
62   for (size_t i = 0; i < kNumAtomNames; i++) {
63     if (auto reply = futures[i].Sync()) {
64       atoms[i] = reply->atom;
65     } else {
66       LOG(ERROR) << "Failed to intern atom(s)";
67       break;
68     }
69   }
70   clipboard_atom_ = atoms[0];
71   large_selection_atom_ = atoms[1];
72   selection_string_atom_ = atoms[2];
73   targets_atom_ = atoms[3];
74   timestamp_atom_ = atoms[4];
75   utf8_string_atom_ = atoms[5];
76   static_assert(kNumAtomNames >= 6, "kAtomNames is too small");
77 
78   connection_->xfixes().SelectSelectionInput(
79       {static_cast<x11::Window>(clipboard_window_),
80        static_cast<x11::Atom>(clipboard_atom_),
81        x11::XFixes::SelectionEventMask::SetSelectionOwner});
82   connection_->Flush();
83 }
84 
SetClipboard(const std::string & mime_type,const std::string & data)85 void XServerClipboard::SetClipboard(const std::string& mime_type,
86                                     const std::string& data) {
87   DCHECK(connection_->Ready());
88 
89   if (clipboard_window_ == x11::Window::None)
90     return;
91 
92   // Currently only UTF-8 is supported.
93   if (mime_type != kMimeTypeTextUtf8)
94     return;
95   if (!StringIsUtf8(data.c_str(), data.length())) {
96     LOG(ERROR) << "ClipboardEvent: data is not UTF-8 encoded.";
97     return;
98   }
99 
100   data_ = data;
101 
102   AssertSelectionOwnership(x11::Atom::PRIMARY);
103   AssertSelectionOwnership(clipboard_atom_);
104 }
105 
ProcessXEvent(const x11::Event & event)106 void XServerClipboard::ProcessXEvent(const x11::Event& event) {
107   if (clipboard_window_ == x11::Window::None ||
108       event.window() != clipboard_window_) {
109     return;
110   }
111 
112   if (auto* property_notify = event.As<x11::PropertyNotifyEvent>())
113     OnPropertyNotify(*property_notify);
114   else if (auto* selection_notify = event.As<x11::SelectionNotifyEvent>())
115     OnSelectionNotify(*selection_notify);
116   else if (auto* selection_request = event.As<x11::SelectionRequestEvent>())
117     OnSelectionRequest(*selection_request);
118   else if (auto* selection_clear = event.As<x11::SelectionClearEvent>())
119     OnSelectionClear(*selection_clear);
120 
121   if (auto* xfixes_selection_notify =
122           event.As<x11::XFixes::SelectionNotifyEvent>()) {
123     OnSetSelectionOwnerNotify(xfixes_selection_notify->selection,
124                               xfixes_selection_notify->selection_timestamp);
125   }
126 }
127 
OnSetSelectionOwnerNotify(x11::Atom selection,x11::Time timestamp)128 void XServerClipboard::OnSetSelectionOwnerNotify(x11::Atom selection,
129                                                  x11::Time timestamp) {
130   // Protect against receiving new XFixes selection notifications whilst we're
131   // in the middle of waiting for information from the current selection owner.
132   // A reasonable timeout allows for misbehaving apps that don't respond
133   // quickly to our requests.
134   if (!get_selections_time_.is_null() &&
135       (base::TimeTicks::Now() - get_selections_time_) <
136           base::TimeDelta::FromSeconds(5)) {
137     // TODO(lambroslambrou): Instead of ignoring this notification, cancel any
138     // pending request operations and ignore the resulting events, before
139     // dispatching new requests here.
140     return;
141   }
142 
143   // Only process CLIPBOARD selections.
144   if (selection != clipboard_atom_)
145     return;
146 
147   // If we own the selection, don't request details for it.
148   if (IsSelectionOwner(selection))
149     return;
150 
151   get_selections_time_ = base::TimeTicks::Now();
152 
153   // Before getting the value of the chosen selection, request the list of
154   // target formats it supports.
155   RequestSelectionTargets(selection);
156 }
157 
OnPropertyNotify(const x11::PropertyNotifyEvent & event)158 void XServerClipboard::OnPropertyNotify(const x11::PropertyNotifyEvent& event) {
159   if (large_selection_property_ != x11::Atom::None &&
160       event.atom == large_selection_property_ &&
161       event.state == x11::Property::NewValue) {
162     auto req = connection_->GetProperty({
163         .c_delete = true,
164         .window = clipboard_window_,
165         .property = large_selection_property_,
166         .type = x11::Atom::Any,
167         .long_length = std::numeric_limits<uint32_t>::max(),
168     });
169     if (auto reply = req.Sync()) {
170       if (reply->type != x11::Atom::None) {
171         // TODO(lambroslambrou): Properly support large transfers -
172         // http://crbug.com/151447.
173 
174         // If the property is zero-length then the large transfer is complete.
175         if (reply->value_len == 0)
176           large_selection_property_ = x11::Atom::None;
177       }
178     }
179   }
180 }
181 
OnSelectionNotify(const x11::SelectionNotifyEvent & event)182 void XServerClipboard::OnSelectionNotify(
183     const x11::SelectionNotifyEvent& event) {
184   if (event.property != x11::Atom::None) {
185     auto req = connection_->GetProperty({
186         .c_delete = true,
187         .window = clipboard_window_,
188         .property = event.property,
189         .type = x11::Atom::Any,
190         .long_length = std::numeric_limits<uint32_t>::max(),
191     });
192     if (auto reply = req.Sync()) {
193       if (reply->type == large_selection_atom_) {
194         // Large selection - just read and ignore these for now.
195         large_selection_property_ = event.property;
196       } else {
197         // Standard selection - call the selection notifier.
198         large_selection_property_ = x11::Atom::None;
199         if (reply->type != x11::Atom::None) {
200           HandleSelectionNotify(event, reply->type, reply->format,
201                                 reply->value_len, reply->value->data());
202           return;
203         }
204       }
205     }
206   }
207   HandleSelectionNotify(event, x11::Atom::None, 0, 0, nullptr);
208 }
209 
OnSelectionRequest(const x11::SelectionRequestEvent & event)210 void XServerClipboard::OnSelectionRequest(
211     const x11::SelectionRequestEvent& event) {
212   x11::SelectionNotifyEvent selection_event;
213   selection_event.requestor = event.requestor;
214   selection_event.selection = event.selection;
215   selection_event.time = event.time;
216   selection_event.target = event.target;
217   auto property =
218       event.property == x11::Atom::None ? event.target : event.property;
219   if (!IsSelectionOwner(selection_event.selection)) {
220     selection_event.property = x11::Atom::None;
221   } else {
222     selection_event.property = property;
223     if (selection_event.target == static_cast<x11::Atom>(targets_atom_)) {
224       SendTargetsResponse(selection_event.requestor, selection_event.property);
225     } else if (selection_event.target ==
226                static_cast<x11::Atom>(timestamp_atom_)) {
227       SendTimestampResponse(selection_event.requestor,
228                             selection_event.property);
229     } else if (selection_event.target ==
230                    static_cast<x11::Atom>(utf8_string_atom_) ||
231                selection_event.target == x11::Atom::STRING) {
232       SendStringResponse(selection_event.requestor, selection_event.property,
233                          selection_event.target);
234     }
235   }
236   x11::SendEvent(selection_event, selection_event.requestor,
237                  x11::EventMask::NoEvent, connection_);
238 }
239 
OnSelectionClear(const x11::SelectionClearEvent & event)240 void XServerClipboard::OnSelectionClear(const x11::SelectionClearEvent& event) {
241   selections_owned_.erase(event.selection);
242 }
243 
SendTargetsResponse(x11::Window requestor,x11::Atom property)244 void XServerClipboard::SendTargetsResponse(x11::Window requestor,
245                                            x11::Atom property) {
246   // Respond advertising x11::Atom::STRING, UTF8_STRING and TIMESTAMP data for
247   // the selection.
248   x11::Atom targets[3] = {
249       timestamp_atom_,
250       utf8_string_atom_,
251       x11::Atom::STRING,
252   };
253   connection_->ChangeProperty({
254       .mode = x11::PropMode::Replace,
255       .window = requestor,
256       .property = property,
257       .type = x11::Atom::ATOM,
258       .format = CHAR_BIT * sizeof(x11::Atom),
259       .data_len = base::size(targets),
260       .data = base::MakeRefCounted<base::RefCountedStaticMemory>(
261           &targets[0], sizeof(targets)),
262   });
263   connection_->Flush();
264 }
265 
SendTimestampResponse(x11::Window requestor,x11::Atom property)266 void XServerClipboard::SendTimestampResponse(x11::Window requestor,
267                                              x11::Atom property) {
268   // Respond with the timestamp of our selection; we always return
269   // CurrentTime since our selections are set by remote clients, so there
270   // is no associated local X event.
271 
272   // TODO(lambroslambrou): Should use a proper timestamp here instead of
273   // CurrentTime.  ICCCM recommends doing a zero-length property append,
274   // and getting a timestamp from the subsequent PropertyNotify event.
275   x11::Time time = x11::Time::CurrentTime;
276   connection_->ChangeProperty({
277       .mode = x11::PropMode::Replace,
278       .window = requestor,
279       .property = property,
280       .type = x11::Atom::INTEGER,
281       .format = CHAR_BIT * sizeof(x11::Time),
282       .data_len = 1,
283       .data = base::MakeRefCounted<base::RefCountedStaticMemory>(&time,
284                                                                  sizeof(time)),
285   });
286   connection_->Flush();
287 }
288 
SendStringResponse(x11::Window requestor,x11::Atom property,x11::Atom target)289 void XServerClipboard::SendStringResponse(x11::Window requestor,
290                                           x11::Atom property,
291                                           x11::Atom target) {
292   if (!data_.empty()) {
293     // Return the actual string data; we always return UTF8, regardless of
294     // the configured locale.
295     connection_->ChangeProperty({
296         .mode = x11::PropMode::Replace,
297         .window = requestor,
298         .property = property,
299         .type = target,
300         .format = 8,
301         .data_len = data_.size(),
302         .data = base::MakeRefCounted<base::RefCountedStaticMemory>(
303             data_.data(), data_.size()),
304     });
305     connection_->Flush();
306   }
307 }
308 
HandleSelectionNotify(const x11::SelectionNotifyEvent & event,x11::Atom type,int format,int item_count,const void * data)309 void XServerClipboard::HandleSelectionNotify(
310     const x11::SelectionNotifyEvent& event,
311     x11::Atom type,
312     int format,
313     int item_count,
314     const void* data) {
315   bool finished = false;
316 
317   auto target = event.target;
318   if (target == targets_atom_)
319     finished = HandleSelectionTargetsEvent(event, format, item_count, data);
320   else if (target == utf8_string_atom_ || target == x11::Atom::STRING)
321     finished = HandleSelectionStringEvent(event, format, item_count, data);
322 
323   if (finished)
324     get_selections_time_ = base::TimeTicks();
325 }
326 
HandleSelectionTargetsEvent(const x11::SelectionNotifyEvent & event,int format,int item_count,const void * data)327 bool XServerClipboard::HandleSelectionTargetsEvent(
328     const x11::SelectionNotifyEvent& event,
329     int format,
330     int item_count,
331     const void* data) {
332   auto selection = event.selection;
333   if (event.property == targets_atom_) {
334     if (data && format == 32) {
335       const uint32_t* targets = static_cast<const uint32_t*>(data);
336       for (int i = 0; i < item_count; i++) {
337         if (targets[i] == static_cast<uint32_t>(utf8_string_atom_)) {
338           RequestSelectionString(selection, utf8_string_atom_);
339           return false;
340         }
341       }
342     }
343   }
344   RequestSelectionString(selection, x11::Atom::STRING);
345   return false;
346 }
347 
HandleSelectionStringEvent(const x11::SelectionNotifyEvent & event,int format,int item_count,const void * data)348 bool XServerClipboard::HandleSelectionStringEvent(
349     const x11::SelectionNotifyEvent& event,
350     int format,
351     int item_count,
352     const void* data) {
353   auto property = event.property;
354   auto target = event.target;
355 
356   if (property != selection_string_atom_ || !data || format != 8)
357     return true;
358 
359   std::string text(static_cast<const char*>(data), item_count);
360 
361   if (target == x11::Atom::STRING || target == utf8_string_atom_)
362     NotifyClipboardText(text);
363 
364   return true;
365 }
366 
NotifyClipboardText(const std::string & text)367 void XServerClipboard::NotifyClipboardText(const std::string& text) {
368   data_ = text;
369   callback_.Run(kMimeTypeTextUtf8, data_);
370 }
371 
RequestSelectionTargets(x11::Atom selection)372 void XServerClipboard::RequestSelectionTargets(x11::Atom selection) {
373   connection_->ConvertSelection({clipboard_window_, selection, targets_atom_,
374                                  targets_atom_, x11::Time::CurrentTime});
375 }
376 
RequestSelectionString(x11::Atom selection,x11::Atom target)377 void XServerClipboard::RequestSelectionString(x11::Atom selection,
378                                               x11::Atom target) {
379   connection_->ConvertSelection({clipboard_window_, selection, target,
380                                  selection_string_atom_,
381                                  x11::Time::CurrentTime});
382 }
383 
AssertSelectionOwnership(x11::Atom selection)384 void XServerClipboard::AssertSelectionOwnership(x11::Atom selection) {
385   connection_->SetSelectionOwner(
386       {clipboard_window_, selection, x11::Time::CurrentTime});
387   auto reply = connection_->GetSelectionOwner({selection}).Sync();
388   auto owner = reply ? reply->owner : x11::Window::None;
389   if (owner == clipboard_window_) {
390     selections_owned_.insert(selection);
391   } else {
392     LOG(ERROR) << "XSetSelectionOwner failed for selection "
393                << static_cast<uint32_t>(selection);
394   }
395 }
396 
IsSelectionOwner(x11::Atom selection)397 bool XServerClipboard::IsSelectionOwner(x11::Atom selection) {
398   return selections_owned_.find(selection) != selections_owned_.end();
399 }
400 
401 }  // namespace remoting
402