Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/clipboard/clipboard_core.py : 37%
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.
7import os
8import struct
9import re
10from io import BytesIO
11from gi.repository import GLib
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
19log = Logger("clipboard")
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)
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
34TEST_DROP_CLIPBOARD_REQUESTS = envint("XPRA_TEST_DROP_CLIPBOARD")
35DELAY_SEND_TOKEN = envint("XPRA_DELAY_SEND_TOKEN", 100)
37LOOP_DISABLE = envbool("XPRA_CLIPBOARD_LOOP_DISABLE", True)
38LOOP_PREFIX = os.environ.get("XPRA_CLIPBOARD_LOOP_PREFIX", "Xpra-Clipboard-Loop-Detection:")
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))
67TEXT_TARGETS = ("UTF8_STRING", "TEXT", "STRING", "text/plain")
69TRANSLATED_TARGETS = {
70 "application/x-moz-nativehtml" : "UTF8_STRING"
71 }
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
79def must_discard(target):
80 return any(x for x in DISCARD_TARGETS if x.match(target))
82def must_discard_extra(target):
83 return any(x for x in DISCARD_EXTRA_TARGETS if x.match(target))
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
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))
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)
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
148 def local_to_remote(self, selection):
149 return self._local_to_remote.get(selection, selection)
151 def remote_to_local(self, selection):
152 return self._remote_to_local.get(selection, selection)
154 def __repr__(self):
155 return "ClipboardProtocolHelperCore"
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
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 = {}
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)
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
199 def verify_remote_loop_uuids(self, uuids):
200 log("verify_remote_loop_uuids(%s)", uuids)
202 def _verify_remote_loop_uuids(self, clipboard, value, user_data):
203 pass
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)
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
218 def set_clipboard_contents_slice_fix(self, v):
219 self.clipboard_contents_slice_fix = v
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)
229 def set_greedy_client(self, greedy):
230 for proxy in self._clipboard_proxies.values():
231 proxy.set_greedy_client(greedy)
233 def set_want_targets_client(self, want_targets):
234 log("set_want_targets_client(%s)", want_targets)
235 self._want_targets = want_targets
237 def set_preferred_targets(self, preferred_targets):
238 for proxy in self._clipboard_proxies.values():
239 proxy.set_preferred_targets(preferred_targets)
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 }
253 def make_proxy(self, selection):
254 raise NotImplementedError()
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)
263 def init_proxies_uuid(self):
264 for proxy in self._clipboard_proxies.values():
265 proxy.init_uuid()
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()
276 def send_all_tokens(self):
277 self.send_tokens(CLIPBOARDS)
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)
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
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
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),))
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)
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)
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
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)
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)
498 def _clipboard_got_contents(self, request_id, dtype, dformat, data):
499 raise NotImplementedError()
502 def progress(self):
503 if self.progress_cb:
504 self.progress_cb(len(self._clipboard_outstanding_requests), None)
507 def _process_clipboard_pending_requests(self, packet):
508 pending = packet[1]
509 if self.progress_cb:
510 self.progress_cb(None, pending)
512 def _process_clipboard_enable_selections(self, packet):
513 selections = tuple(bytestostr(x) for x in packet[1])
514 self.enable_selections(selections)
516 def _process_clipboard_loop_uuids(self, packet):
517 loop_uuids = packet[1]
518 self.verify_remote_loop_uuids(loop_uuids)
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)
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 = []
559 self._loop_uuid = ""
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)
565 def set_direction(self, can_send : bool, can_receive : bool):
566 self._can_send = can_send
567 self._can_receive = can_receive
569 def set_want_targets(self, want_targets):
570 self._want_targets = want_targets
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
594 def cleanup(self):
595 self._enabled = False
596 self.cancel_emit_token()
598 def is_enabled(self) -> bool:
599 return self._enabled
601 def set_enabled(self, enabled : bool):
602 log("%s.set_enabled(%s)", self, enabled)
603 self._enabled = enabled
605 def set_greedy_client(self, greedy : bool):
606 log("%s.set_greedy_client(%s)", self, greedy)
607 self._greedy_client = greedy
609 def set_preferred_targets(self, preferred_targets):
610 self.preferred_targets = preferred_targets
613 def __repr__(self):
614 return "ClipboardProxyCore(%s)" % self._selection
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()
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()
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)
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)
658 def do_emit_token(self):
659 #self.emit("send-clipboard-token")
660 pass
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)
669 #def do_selection_request_event(self, event):
670 # pass
672 #def do_selection_get(self, selection_data, info, time):
673 # pass
675 #def do_selection_clear_event(self, event):
676 # pass
678 def remove_block(self, *_args):
679 log("remove_block: %s", self._selection)
680 self._block_owner_change = False
682 def claim(self):
683 pass
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
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