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