Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of Xpra. 

2# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com> 

3# Copyright (C) 2012-2020 Antoine Martin <antoine@xpra.org> 

4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

5# later version. See the file COPYING for details. 

6 

7import os 

8import struct 

9import re 

10from io import BytesIO 

11from gi.repository import GLib 

12 

13from xpra.net.compression import Compressible 

14from xpra.os_util import POSIX, monotonic_time, strtobytes, bytestostr, hexstr, get_hex_uuid 

15from xpra.util import csv, envint, envbool, repr_ellipsized, ellipsizer, typedict 

16from xpra.platform.features import CLIPBOARDS as PLATFORM_CLIPBOARDS 

17from xpra.log import Logger, is_debug_enabled 

18 

19log = Logger("clipboard") 

20 

21MIN_CLIPBOARD_COMPRESS_SIZE = envint("XPRA_MIN_CLIPBOARD_COMPRESS_SIZE", 512) 

22MAX_CLIPBOARD_PACKET_SIZE = 16*1024*1024 

23MAX_CLIPBOARD_RECEIVE_SIZE = envint("XPRA_MAX_CLIPBOARD_RECEIVE_SIZE", -1) 

24MAX_CLIPBOARD_SEND_SIZE = envint("XPRA_MAX_CLIPBOARD_SEND_SIZE", -1) 

25 

26ALL_CLIPBOARDS = [strtobytes(x) for x in PLATFORM_CLIPBOARDS] 

27CLIPBOARDS = PLATFORM_CLIPBOARDS 

28CLIPBOARDS_ENV = os.environ.get("XPRA_CLIPBOARDS") 

29if CLIPBOARDS_ENV is not None: 

30 CLIPBOARDS = CLIPBOARDS_ENV.split(",") 

31 CLIPBOARDS = [strtobytes(x).upper().strip() for x in CLIPBOARDS] 

32del CLIPBOARDS_ENV 

33 

34TEST_DROP_CLIPBOARD_REQUESTS = envint("XPRA_TEST_DROP_CLIPBOARD") 

35DELAY_SEND_TOKEN = envint("XPRA_DELAY_SEND_TOKEN", 100) 

36 

37LOOP_DISABLE = envbool("XPRA_CLIPBOARD_LOOP_DISABLE", True) 

38LOOP_PREFIX = os.environ.get("XPRA_CLIPBOARD_LOOP_PREFIX", "Xpra-Clipboard-Loop-Detection:") 

39 

40def get_discard_targets(envname="DISCARD", default_value=()): 

41 _discard_target_strs_ = os.environ.get("XPRA_%s_TARGETS" % envname) 

42 if _discard_target_strs_ is None: 

43 return default_value 

44 return _discard_target_strs_.split(",") 

45#targets we never wish to handle: 

46DISCARD_TARGETS = tuple(re.compile(dt) for dt in get_discard_targets("DISCARD", ( 

47 r"^NeXT", 

48 r"^com\.apple\.", 

49 r"^CorePasteboardFlavorType", 

50 r"^dyn\.", 

51 r"^resource-transfer-format", #eclipse 

52 r"^x-special/", #ie: gnome file copy 

53 ))) 

54#targets some applications are known to request, 

55#even when the peer did not expose them as valid targets, 

56#rather than forwarding the request and then timing out, 

57#we will just drop them 

58DISCARD_EXTRA_TARGETS = tuple(re.compile(dt) for dt in get_discard_targets("DISCARD_EXTRA", ( 

59 r"^SAVE_TARGETS$", 

60 r"^COMPOUND_TEXT", 

61 r"GTK_TEXT_BUFFER_CONTENTS", 

62 ))) 

63log("DISCARD_TARGETS=%s", csv(DISCARD_TARGETS)) 

64log("DISCARD_EXTRA_TARGETS=%s", csv(DISCARD_EXTRA_TARGETS)) 

65 

66 

67TEXT_TARGETS = ("UTF8_STRING", "TEXT", "STRING", "text/plain") 

68 

69TRANSLATED_TARGETS = { 

70 "application/x-moz-nativehtml" : "UTF8_STRING" 

71 } 

72 

73sizeof_long = struct.calcsize(b'@L') 

74assert sizeof_long in (4, 8), "struct.calcsize('@L')=%s" % sizeof_long 

75sizeof_short = struct.calcsize(b'=H') 

76assert sizeof_short == 2, "struct.calcsize('=H')=%s" % sizeof_short 

77 

78 

79def must_discard(target): 

80 return any(x for x in DISCARD_TARGETS if x.match(target)) 

81 

82def must_discard_extra(target): 

83 return any(x for x in DISCARD_EXTRA_TARGETS if x.match(target)) 

84 

85 

86def _filter_targets(targets): 

87 targets_strs = tuple(bytestostr(x) for x in targets) 

88 f = tuple(target for target in targets_strs if not must_discard(target)) 

89 log("_filter_targets(%s)=%s", csv(targets_strs), f) 

90 return f 

91 

92#CARD32 can actually be 64-bits... 

93CARD32_SIZE = sizeof_long*8 

94def get_format_size(dformat): 

95 return max(8, {32 : CARD32_SIZE}.get(dformat, dformat)) 

96 

97 

98class ClipboardProtocolHelperCore: 

99 def __init__(self, send_packet_cb, progress_cb=None, **kwargs): 

100 d = typedict(kwargs) 

101 self.send = send_packet_cb 

102 self.progress_cb = progress_cb 

103 self.can_send = d.boolget("can-send", True) 

104 self.can_receive = d.boolget("can-receive", True) 

105 self.max_clipboard_packet_size = d.intget("max-packet-size", MAX_CLIPBOARD_PACKET_SIZE) 

106 self.max_clipboard_receive_size = d.intget("max-receive-size", MAX_CLIPBOARD_RECEIVE_SIZE) 

107 self.max_clipboard_send_size = d.intget("max-send-size", MAX_CLIPBOARD_SEND_SIZE) 

108 self.clipboard_contents_slice_fix = False 

109 self.disabled_by_loop = [] 

110 self.filter_res = [] 

111 filter_res = d.strtupleget("filters") 

112 if filter_res: 

113 for x in filter_res: 

114 try: 

115 self.filter_res.append(re.compile(x)) 

116 except Exception as e: 

117 log.error("Error: invalid clipboard filter regular expression") 

118 log.error(" '%s': %s", x, e) 

119 self._clipboard_request_counter = 0 

120 self._clipboard_outstanding_requests = {} 

121 self._local_to_remote = {} 

122 self._remote_to_local = {} 

123 self.init_translation(kwargs) 

124 self._want_targets = False 

125 self.init_packet_handlers() 

126 self.init_proxies(d.strtupleget("clipboards.local", CLIPBOARDS)) 

127 remote_loop_uuids = d.dictget("remote-loop-uuids", {}) 

128 self.verify_remote_loop_uuids(remote_loop_uuids) 

129 self.remote_clipboards = d.strtupleget("clipboards.remote", CLIPBOARDS) 

130 

131 def init_translation(self, kwargs): 

132 def getselection(name): 

133 v = kwargs.get("clipboard.%s" % name) #ie: clipboard.remote 

134 env_value = os.environ.get("XPRA_TRANSLATEDCLIPBOARD_%s_SELECTION" % name.upper()) 

135 selections = kwargs.get("clipboards.%s" % name) #ie: clipboards.remote 

136 if not selections: 

137 return None 

138 for x in (env_value, v): 

139 if x and x in selections: 

140 return x 

141 return selections[0] 

142 local = getselection("local") 

143 remote = getselection("remote") 

144 if local and remote: 

145 self._local_to_remote[local] = remote 

146 self._remote_to_local[remote] = local 

147 

148 def local_to_remote(self, selection): 

149 return self._local_to_remote.get(selection, selection) 

150 

151 def remote_to_local(self, selection): 

152 return self._remote_to_local.get(selection, selection) 

153 

154 def __repr__(self): 

155 return "ClipboardProtocolHelperCore" 

156 

157 def get_info(self) -> dict: 

158 info = { 

159 "type" : str(self).replace("ClipboardProtocolHelper", ""), 

160 "max_size" : self.max_clipboard_packet_size, 

161 "max_recv_size": self.max_clipboard_receive_size, 

162 "max_send_size": self.max_clipboard_send_size, 

163 "filters" : [x.pattern for x in self.filter_res], 

164 "requests" : self._clipboard_request_counter, 

165 "pending" : tuple(self._clipboard_outstanding_requests.keys()), 

166 "can-send" : self.can_send, 

167 "can-receive" : self.can_receive, 

168 "want_targets" : self._want_targets, 

169 } 

170 for clipboard, proxy in self._clipboard_proxies.items(): 

171 info[clipboard] = proxy.get_info() 

172 return info 

173 

174 def cleanup(self): 

175 def nosend(*_args): 

176 pass 

177 self.send = nosend 

178 for x in self._clipboard_proxies.values(): 

179 x.cleanup() 

180 self._clipboard_proxies = {} 

181 

182 def client_reset(self): 

183 #if the client disconnects, 

184 #we can re-enable the clipboards it had problems with: 

185 l = self.disabled_by_loop 

186 self.disabled_by_loop = [] 

187 for x in l: 

188 proxy = self._clipboard_proxies.get(x) 

189 proxy.set_enabled(True) 

190 

191 

192 def get_loop_uuids(self): 

193 uuids = {} 

194 for proxy in self._clipboard_proxies.values(): 

195 uuids[proxy._selection] = proxy._loop_uuid 

196 log("get_loop_uuids()=%s", uuids) 

197 return uuids 

198 

199 def verify_remote_loop_uuids(self, uuids): 

200 log("verify_remote_loop_uuids(%s)", uuids) 

201 

202 def _verify_remote_loop_uuids(self, clipboard, value, user_data): 

203 pass 

204 

205 def set_direction(self, can_send, can_receive, max_send_size=None, max_receive_size=None): 

206 self.can_send = can_send 

207 self.can_receive = can_receive 

208 self.set_limits(max_send_size, max_receive_size) 

209 for proxy in self._clipboard_proxies.values(): 

210 proxy.set_direction(can_send, can_receive) 

211 

212 def set_limits(self, max_send_size, max_receive_size): 

213 if max_send_size is not None: 

214 self.max_clipboard_send_size = max_send_size 

215 if max_receive_size is not None: 

216 self.max_clipboard_receive_size = max_receive_size 

217 

218 def set_clipboard_contents_slice_fix(self, v): 

219 self.clipboard_contents_slice_fix = v 

220 

221 def enable_selections(self, selections): 

222 #when clients first connect or later through the "clipboard-enable-selections" packet, 

223 #they can tell us which clipboard selections they want enabled 

224 #(ie: OSX and win32 only use "CLIPBOARD" by default, and not "PRIMARY" or "SECONDARY") 

225 log("enabling selections: %s", csv(selections)) 

226 for selection, proxy in self._clipboard_proxies.items(): 

227 proxy.set_enabled(bytestostr(selection) in selections) 

228 

229 def set_greedy_client(self, greedy): 

230 for proxy in self._clipboard_proxies.values(): 

231 proxy.set_greedy_client(greedy) 

232 

233 def set_want_targets_client(self, want_targets): 

234 log("set_want_targets_client(%s)", want_targets) 

235 self._want_targets = want_targets 

236 

237 def set_preferred_targets(self, preferred_targets): 

238 for proxy in self._clipboard_proxies.values(): 

239 proxy.set_preferred_targets(preferred_targets) 

240 

241 

242 def init_packet_handlers(self): 

243 self._packet_handlers = { 

244 "clipboard-token" : self._process_clipboard_token, 

245 "clipboard-request" : self._process_clipboard_request, 

246 "clipboard-contents" : self._process_clipboard_contents, 

247 "clipboard-contents-none" : self._process_clipboard_contents_none, 

248 "clipboard-pending-requests" : self._process_clipboard_pending_requests, 

249 "clipboard-enable-selections" : self._process_clipboard_enable_selections, 

250 "clipboard-loop-uuids" : self._process_clipboard_loop_uuids, 

251 } 

252 

253 def make_proxy(self, selection): 

254 raise NotImplementedError() 

255 

256 def init_proxies(self, selections): 

257 self._clipboard_proxies = {} 

258 for selection in selections: 

259 proxy = self.make_proxy(selection) 

260 self._clipboard_proxies[selection] = proxy 

261 log("%s.init_proxies : %s", self, self._clipboard_proxies) 

262 

263 def init_proxies_uuid(self): 

264 for proxy in self._clipboard_proxies.values(): 

265 proxy.init_uuid() 

266 

267 

268 # Used by the client during startup: 

269 def send_tokens(self, selections=()): 

270 for selection in selections: 

271 proxy = self._clipboard_proxies.get(selection) 

272 if proxy: 

273 proxy._have_token = False 

274 proxy.do_emit_token() 

275 

276 def send_all_tokens(self): 

277 self.send_tokens(CLIPBOARDS) 

278 

279 

280 def _process_clipboard_token(self, packet): 

281 selection = bytestostr(packet[1]) 

282 name = self.remote_to_local(selection) 

283 proxy = self._clipboard_proxies.get(name) 

284 if proxy is None: 

285 #this can happen if the server has fewer clipboards than the client, 

286 #ie: with win32 shadow servers 

287 l = log 

288 if name in ALL_CLIPBOARDS: 

289 l = log.warn 

290 l("ignoring token for clipboard '%s' (no proxy)", name) 

291 return 

292 if not proxy.is_enabled(): 

293 l = log 

294 if name not in self.disabled_by_loop: 

295 l = log.warn 

296 l("ignoring token for disabled clipboard '%s'", name) 

297 return 

298 log("process clipboard token selection=%s, local clipboard name=%s, proxy=%s", selection, name, proxy) 

299 targets = None 

300 target_data = None 

301 if proxy._can_receive: 

302 if len(packet)>=3: 

303 targets = packet[2] 

304 if len(packet)>=8: 

305 target, dtype, dformat, wire_encoding, wire_data = packet[3:8] 

306 if target: 

307 assert dformat in (8, 16, 32), "invalid format '%s' for datatype=%s and wire encoding=%s" % ( 

308 dformat, dtype, wire_encoding) 

309 target = bytestostr(target) 

310 wire_encoding = bytestostr(wire_encoding) 

311 dtype = bytestostr(dtype) 

312 raw_data = self._munge_wire_selection_to_raw(wire_encoding, dtype, dformat, wire_data) 

313 target_data = {target : (dtype, dformat, raw_data)} 

314 #older versions always claimed the selection when the token is received: 

315 claim = True 

316 if len(packet)>=10: 

317 claim = bool(packet[8]) 

318 #clients can now also change the greedy flag on the fly, 

319 #this is needed for clipboard direction restrictions: 

320 #the client may want to be notified of clipboard changes, just like a greedy client 

321 proxy._greedy_client = bool(packet[9]) 

322 synchronous_client = len(packet)>=11 and bool(packet[10]) 

323 proxy.got_token(targets, target_data, claim, synchronous_client) 

324 

325 def _munge_raw_selection_to_wire(self, target, dtype, dformat, data): 

326 log("_munge_raw_selection_to_wire%s", (target, dtype, dformat, repr_ellipsized(bytestostr(data)))) 

327 # Some types just cannot be marshalled: 

328 if dtype in ("WINDOW", "PIXMAP", "BITMAP", "DRAWABLE", 

329 "PIXEL", "COLORMAP"): 

330 log("skipping clipboard data of type: %s, format=%s, len(data)=%s", dtype, dformat, len(data or b"")) 

331 return None, None 

332 if target=="TARGETS" and dtype=="ATOM" and isinstance(data, (tuple, list)): 

333 #targets is special cased here 

334 #because we can get the values in wire format already (not atoms) 

335 #thanks to the request_targets() function (required on win32) 

336 return "atoms", _filter_targets(data) 

337 try: 

338 return self._do_munge_raw_selection_to_wire(target, dtype, dformat, data) 

339 except Exception: 

340 log.error("Error: failed to convert selection data to wire format") 

341 log.error(" target was %s", target) 

342 log.error(" dtype=%s, dformat=%s, data=%s (%s)", dtype, dformat, repr_ellipsized(str(data)), type(data)) 

343 raise 

344 

345 def _do_munge_raw_selection_to_wire(self, target, dtype, dformat, data): 

346 """ this method is overriden in xclipboard to parse X11 atoms """ 

347 # Other types need special handling, and all types need to be 

348 # converting into an endian-neutral format: 

349 log("_do_munge_raw_selection_to_wire(%s, %s, %s, %s:%s)", target, dtype, dformat, type(data), len(data or "")) 

350 if dformat == 32: 

351 #you should be using gdk_clipboard for atom support! 

352 if dtype in ("ATOM", "ATOM_PAIR") and POSIX: 

353 #we cannot handle gdk atoms here (but gdk_clipboard does) 

354 return None, None 

355 #important note: on 64 bits, format=32 means 8 bytes, not 4 

356 #that's just the way it is... 

357 binfmt = b"@" + b"L" * (len(data) // sizeof_long) 

358 ints = struct.unpack(binfmt, data) 

359 return b"integers", ints 

360 if dformat == 16: 

361 binfmt = b"=" + b"H" * (len(data) // sizeof_short) 

362 ints = struct.unpack(binfmt, data) 

363 return b"integers", ints 

364 if dformat == 8: 

365 for x in self.filter_res: 

366 if x.match(data): 

367 log.warn("clipboard buffer contains blacklisted pattern '%s' and has been dropped!", x.pattern) 

368 return None, None 

369 return b"bytes", data 

370 log.error("unhandled format %s for clipboard data type %s" % (dformat, dtype)) 

371 return None, None 

372 

373 def _munge_wire_selection_to_raw(self, encoding, dtype, dformat, data): 

374 log("wire selection to raw, encoding=%s, type=%s, format=%s, len(data)=%s", 

375 encoding, dtype, dformat, len(data or b"")) 

376 if self.max_clipboard_receive_size > 0: 

377 max_recv_datalen = self.max_clipboard_receive_size * 8 // get_format_size(dformat) 

378 if len(data) > max_recv_datalen: 

379 olen = len(data) 

380 data = data[:max_recv_datalen] 

381 log.info("Data copied out truncated because of clipboard policy %d to %d", olen, max_recv_datalen) 

382 if encoding == "bytes": 

383 return data 

384 if encoding == "integers": 

385 if not data: 

386 return "" 

387 if dformat == 32: 

388 format_char = b"L" 

389 elif dformat == 16: 

390 format_char = b"H" 

391 elif dformat == 8: 

392 format_char = b"B" 

393 else: 

394 raise Exception("unknown encoding format: %s" % dformat) 

395 fstr = b"@" + format_char * len(data) 

396 log("struct.pack(%s, %s)", fstr, data) 

397 return struct.pack(fstr, *data) 

398 raise Exception("unhanled encoding: %s" % ((encoding, dtype, dformat),)) 

399 

400 def _process_clipboard_request(self, packet): 

401 request_id, selection, target = packet[1:4] 

402 selection = bytestostr(selection) 

403 target = bytestostr(target) 

404 def no_contents(): 

405 self.send("clipboard-contents-none", request_id, selection) 

406 if must_discard(target): 

407 log("invalid target '%s'", target) 

408 no_contents() 

409 return 

410 name = self.remote_to_local(selection) 

411 log("process clipboard request, request_id=%s, selection=%s, local name=%s, target=%s", 

412 request_id, selection, name, target) 

413 proxy = self._clipboard_proxies.get(name) 

414 if proxy is None: 

415 #err, we were asked about a clipboard we don't handle.. 

416 log.error("Error: clipboard request for '%s' (no proxy, ignored)", name) 

417 no_contents() 

418 return 

419 if not proxy.is_enabled(): 

420 l = log 

421 if selection not in self.disabled_by_loop: 

422 l = log.warn 

423 l("Warning: ignoring clipboard request for '%s' (disabled)", name) 

424 no_contents() 

425 return 

426 if not proxy._can_send: 

427 log("request for %s but sending is disabled, sending 'none' back", name) 

428 no_contents() 

429 return 

430 if TEST_DROP_CLIPBOARD_REQUESTS>0 and (request_id % TEST_DROP_CLIPBOARD_REQUESTS)==0: 

431 log.warn("clipboard request %s dropped for testing!", request_id) 

432 return 

433 def got_contents(dtype, dformat, data): 

434 self.proxy_got_contents(request_id, selection, target, dtype, dformat, data) 

435 proxy.get_contents(target, got_contents) 

436 

437 def proxy_got_contents(self, request_id, selection, target, dtype, dformat, data): 

438 def no_contents(): 

439 self.send("clipboard-contents-none", request_id, selection) 

440 dtype = bytestostr(dtype) 

441 if is_debug_enabled("clipboard"): 

442 log("proxy_got_contents(%s, %s, %s, %s, %s, %s:%s) data=0x%s..", 

443 request_id, selection, target, 

444 dtype, dformat, type(data), len(data or ""), hexstr((data or "")[:200])) 

445 if dtype is None or data is None or (dformat==0 and not data): 

446 no_contents() 

447 return 

448 truncated = 0 

449 if self.max_clipboard_send_size > 0: 

450 log("perform clipboard limit checking - datasize - %d, %d", len(data), self.max_clipboard_send_size) 

451 max_send_datalen = self.max_clipboard_send_size * 8 // get_format_size(dformat) 

452 if len(data) > max_send_datalen: 

453 truncated = len(data) - max_send_datalen 

454 data = data[:max_send_datalen] 

455 munged = self._munge_raw_selection_to_wire(target, dtype, dformat, data) 

456 if is_debug_enabled("clipboard"): 

457 log("clipboard raw -> wire: %r -> %r", 

458 (dtype, dformat, ellipsizer(data)), ellipsizer(munged)) 

459 wire_encoding, wire_data = munged 

460 if wire_encoding is None: 

461 no_contents() 

462 return 

463 wire_data = self._may_compress(dtype, dformat, wire_data) 

464 if wire_data is not None: 

465 packet = ["clipboard-contents", request_id, selection, 

466 dtype, dformat, wire_encoding, wire_data] 

467 if self.clipboard_contents_slice_fix: 

468 #sending the extra argument requires the fix 

469 packet.append(truncated) 

470 self.send(*packet) 

471 

472 def _may_compress(self, dtype, dformat, wire_data): 

473 if len(wire_data)>self.max_clipboard_packet_size: 

474 log.warn("Warning: clipboard contents are too big and have not been sent") 

475 log.warn(" %s compressed bytes dropped (maximum is %s)", len(wire_data), self.max_clipboard_packet_size) 

476 return None 

477 if isinstance(wire_data, (str, bytes)) and len(wire_data)>=MIN_CLIPBOARD_COMPRESS_SIZE: 

478 return Compressible("clipboard: %s / %s" % (dtype, dformat), wire_data) 

479 return wire_data 

480 

481 def _process_clipboard_contents(self, packet): 

482 request_id, selection, dtype, dformat, wire_encoding, wire_data = packet[1:7] 

483 selection = bytestostr(selection) 

484 wire_encoding = bytestostr(wire_encoding) 

485 dtype = bytestostr(dtype) 

486 log("process clipboard contents, selection=%s, type=%s, format=%s", selection, dtype, dformat) 

487 raw_data = self._munge_wire_selection_to_raw(wire_encoding, dtype, dformat, wire_data) 

488 if log.is_debug_enabled(): 

489 r = ellipsizer 

490 log("clipboard wire -> raw: %s -> %s", (dtype, dformat, wire_encoding, r(wire_data)), r(raw_data)) 

491 self._clipboard_got_contents(request_id, dtype, dformat, raw_data) 

492 

493 def _process_clipboard_contents_none(self, packet): 

494 log("process clipboard contents none") 

495 request_id = packet[1] 

496 self._clipboard_got_contents(request_id, None, None, None) 

497 

498 def _clipboard_got_contents(self, request_id, dtype, dformat, data): 

499 raise NotImplementedError() 

500 

501 

502 def progress(self): 

503 if self.progress_cb: 

504 self.progress_cb(len(self._clipboard_outstanding_requests), None) 

505 

506 

507 def _process_clipboard_pending_requests(self, packet): 

508 pending = packet[1] 

509 if self.progress_cb: 

510 self.progress_cb(None, pending) 

511 

512 def _process_clipboard_enable_selections(self, packet): 

513 selections = tuple(bytestostr(x) for x in packet[1]) 

514 self.enable_selections(selections) 

515 

516 def _process_clipboard_loop_uuids(self, packet): 

517 loop_uuids = packet[1] 

518 self.verify_remote_loop_uuids(loop_uuids) 

519 

520 

521 def process_clipboard_packet(self, packet): 

522 packet_type = bytestostr(packet[0]) 

523 handler = self._packet_handlers.get(packet_type) 

524 if handler: 

525 #log("process clipboard handler(%s)=%s", packet_type, handler) 

526 handler(packet) 

527 else: 

528 log.warn("Warning: no clipboard packet handler for '%s'", packet_type) 

529 

530 

531 

532class ClipboardProxyCore: 

533 def __init__(self, selection): 

534 self._selection = selection 

535 self._enabled = True 

536 self._have_token = False 

537 #enabled later during setup 

538 self._can_send = False 

539 self._can_receive = False 

540 #clients that need a new token for every owner-change: (ie: win32 and osx) 

541 #(forces the client to request new contents - prevents stale clipboard data) 

542 self._greedy_client = False 

543 self._want_targets = False 

544 #semaphore to block the sending of the token when we change the owner ourselves: 

545 self._block_owner_change = False 

546 self._last_emit_token = 0 

547 self._emit_token_timer = None 

548 #counters for info: 

549 self._selection_request_events = 0 

550 self._selection_get_events = 0 

551 self._selection_clear_events = 0 

552 self._sent_token_events = 0 

553 self._got_token_events = 0 

554 self._get_contents_events = 0 

555 self._request_contents_events = 0 

556 self._last_targets = () 

557 self.preferred_targets = [] 

558 

559 self._loop_uuid = "" 

560 

561 def init_uuid(self): 

562 self._loop_uuid = LOOP_PREFIX+get_hex_uuid() 

563 log("init_uuid() %s uuid=%s", self._selection, self._loop_uuid) 

564 

565 def set_direction(self, can_send : bool, can_receive : bool): 

566 self._can_send = can_send 

567 self._can_receive = can_receive 

568 

569 def set_want_targets(self, want_targets): 

570 self._want_targets = want_targets 

571 

572 

573 def get_info(self) -> dict: 

574 info = { 

575 "have_token" : self._have_token, 

576 "enabled" : self._enabled, 

577 "greedy_client" : self._greedy_client, 

578 "preferred-targets" : self.preferred_targets, 

579 "blocked_owner_change" : self._block_owner_change, 

580 "last-targets" : self._last_targets, 

581 "loop-uuid" : self._loop_uuid, 

582 "event" : { 

583 "selection_request" : self._selection_request_events, 

584 "selection_get" : self._selection_get_events, 

585 "selection_clear" : self._selection_clear_events, 

586 "got_token" : self._got_token_events, 

587 "sent_token" : self._sent_token_events, 

588 "get_contents" : self._get_contents_events, 

589 "request_contents" : self._request_contents_events, 

590 }, 

591 } 

592 return info 

593 

594 def cleanup(self): 

595 self._enabled = False 

596 self.cancel_emit_token() 

597 

598 def is_enabled(self) -> bool: 

599 return self._enabled 

600 

601 def set_enabled(self, enabled : bool): 

602 log("%s.set_enabled(%s)", self, enabled) 

603 self._enabled = enabled 

604 

605 def set_greedy_client(self, greedy : bool): 

606 log("%s.set_greedy_client(%s)", self, greedy) 

607 self._greedy_client = greedy 

608 

609 def set_preferred_targets(self, preferred_targets): 

610 self.preferred_targets = preferred_targets 

611 

612 

613 def __repr__(self): 

614 return "ClipboardProxyCore(%s)" % self._selection 

615 

616 def do_owner_changed(self, *_args): 

617 #an application on our side owns the clipboard selection 

618 #(they are ready to provide something via the clipboard) 

619 log("clipboard: %s owner_changed, enabled=%s, "+ 

620 "can-send=%s, can-receive=%s, have_token=%s, greedy_client=%s, block_owner_change=%s", 

621 bytestostr(self._selection), self._enabled, self._can_send, self._can_receive, 

622 self._have_token, self._greedy_client, self._block_owner_change) 

623 if not self._enabled or self._block_owner_change: 

624 return 

625 if self._have_token or ((self._greedy_client or self._want_targets) and self._can_send): 

626 self.schedule_emit_token() 

627 

628 def schedule_emit_token(self): 

629 if self._have_token or (not self._want_targets and not self._greedy_client) or DELAY_SEND_TOKEN<0: 

630 #token ownership will change or told not to wait 

631 GLib.idle_add(self.emit_token) 

632 elif not self._emit_token_timer: 

633 #we already had sent the token, 

634 #or sending it is expensive, so wait a bit: 

635 self.do_schedule_emit_token() 

636 

637 def do_schedule_emit_token(self): 

638 now = monotonic_time() 

639 elapsed = int((now-self._last_emit_token)*1000) 

640 log("do_schedule_emit_token() selection=%s, elapsed=%i (max=%i)", self._selection, elapsed, DELAY_SEND_TOKEN) 

641 if elapsed>=DELAY_SEND_TOKEN: 

642 #enough time has passed 

643 self.emit_token() 

644 else: 

645 self._emit_token_timer = GLib.timeout_add(DELAY_SEND_TOKEN-elapsed, self.emit_token) 

646 

647 def emit_token(self): 

648 self._emit_token_timer = None 

649 boc = self._block_owner_change 

650 self._block_owner_change = True 

651 self._have_token = False 

652 self._last_emit_token = monotonic_time() 

653 self.do_emit_token() 

654 self._sent_token_events += 1 

655 if boc is False: 

656 GLib.idle_add(self.remove_block) 

657 

658 def do_emit_token(self): 

659 #self.emit("send-clipboard-token") 

660 pass 

661 

662 def cancel_emit_token(self): 

663 ett = self._emit_token_timer 

664 if ett: 

665 self._emit_token_timer = None 

666 GLib.source_remove(ett) 

667 

668 

669 #def do_selection_request_event(self, event): 

670 # pass 

671 

672 #def do_selection_get(self, selection_data, info, time): 

673 # pass 

674 

675 #def do_selection_clear_event(self, event): 

676 # pass 

677 

678 def remove_block(self, *_args): 

679 log("remove_block: %s", self._selection) 

680 self._block_owner_change = False 

681 

682 def claim(self): 

683 pass 

684 

685 # This function is called by the xpra core when the peer has requested the 

686 # contents of this clipboard: 

687 def get_contents(self, target, cb): 

688 pass 

689 

690 

691 def filter_data(self, dtype=None, dformat=None, data=None, trusted=False, output_dtype=None): 

692 log("filter_data(%s, %s, %i %s, %s, %s)", 

693 dtype, dformat, len(data), type(data), trusted, output_dtype) 

694 if not data: 

695 return data 

696 IMAGE_OVERLAY = os.environ.get("XPRA_CLIPBOARD_IMAGE_OVERLAY", None) 

697 if IMAGE_OVERLAY and not os.path.exists(IMAGE_OVERLAY): 

698 IMAGE_OVERLAY = None 

699 IMAGE_STAMP = envbool("XPRA_CLIPBOARD_IMAGE_STAMP", False) 

700 SANITIZE_IMAGES = envbool("XPRA_SANITIZE_IMAGES", True) 

701 if dtype in ("image/png", "image/jpeg", "image/tiff") and ( 

702 (output_dtype is not None and dtype!=output_dtype) or 

703 IMAGE_STAMP or 

704 IMAGE_OVERLAY or 

705 (SANITIZE_IMAGES and not trusted) 

706 ): 

707 from xpra.codecs.pillow.decoder import open_only 

708 img_type = dtype.split("/")[-1] 

709 img = open_only(data, (img_type, )) 

710 has_alpha = img.mode=="RGBA" 

711 if not has_alpha and IMAGE_OVERLAY: 

712 img = img.convert("RGBA") 

713 w, h = img.size 

714 if IMAGE_OVERLAY: 

715 from PIL import Image #@UnresolvedImport 

716 overlay = Image.open(IMAGE_OVERLAY) 

717 if overlay.mode!="RGBA": 

718 log.warn("Warning: cannot use overlay image '%s'", IMAGE_OVERLAY) 

719 log.warn(" invalid mode '%s'", overlay.mode) 

720 else: 

721 log("adding clipboard image overlay to %s", dtype) 

722 overlay_resized = overlay.resize((w, h), Image.ANTIALIAS) 

723 composite = Image.alpha_composite(img, overlay_resized) 

724 if not has_alpha and img.mode=="RGBA": 

725 composite = composite.convert("RGB") 

726 img = composite 

727 if IMAGE_STAMP: 

728 log("adding clipboard image stamp to %s", dtype) 

729 from datetime import datetime 

730 from PIL import ImageDraw 

731 img_draw = ImageDraw.Draw(img) 

732 w, h = img.size 

733 img_draw.text((10, max(0, h//2-16)), 'via Xpra, %s' % datetime.now().isoformat(), fill='black') 

734 #now save it: 

735 img_type = (output_dtype or dtype).split("/")[-1] 

736 buf = BytesIO() 

737 img.save(buf, img_type.upper()) #ie: "PNG" 

738 data = buf.getvalue() 

739 buf.close() 

740 return data