xref: /qemu/ui/dbus-clipboard.c (revision ca61e750)
1 /*
2  * QEMU DBus display
3  *
4  * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com>
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22  * THE SOFTWARE.
23  */
24 #include "qemu/osdep.h"
25 #include "qemu/dbus.h"
26 #include "qemu/main-loop.h"
27 #include "qom/object_interfaces.h"
28 #include "sysemu/sysemu.h"
29 #include "qapi/error.h"
30 #include "trace.h"
31 
32 #include "dbus.h"
33 
34 #define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
35 
36 static void
37 dbus_clipboard_complete_request(
38     DBusDisplay *dpy,
39     GDBusMethodInvocation *invocation,
40     QemuClipboardInfo *info,
41     QemuClipboardType type)
42 {
43     GVariant *v_data = g_variant_new_from_data(
44         G_VARIANT_TYPE("ay"),
45         info->types[type].data,
46         info->types[type].size,
47         TRUE,
48         (GDestroyNotify)qemu_clipboard_info_unref,
49         qemu_clipboard_info_ref(info));
50 
51     qemu_dbus_display1_clipboard_complete_request(
52         dpy->clipboard, invocation,
53         MIME_TEXT_PLAIN_UTF8, v_data);
54 }
55 
56 static void
57 dbus_clipboard_update_info(DBusDisplay *dpy, QemuClipboardInfo *info)
58 {
59     bool self_update = info->owner == &dpy->clipboard_peer;
60     const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };
61     DBusClipboardRequest *req;
62     int i = 0;
63 
64     if (info->owner == NULL) {
65         if (dpy->clipboard_proxy) {
66             qemu_dbus_display1_clipboard_call_release(
67                 dpy->clipboard_proxy,
68                 info->selection,
69                 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
70         }
71         return;
72     }
73 
74     if (self_update || !info->has_serial) {
75         return;
76     }
77 
78     req = &dpy->clipboard_request[info->selection];
79     if (req->invocation && info->types[req->type].data) {
80         dbus_clipboard_complete_request(dpy, req->invocation, info, req->type);
81         g_clear_object(&req->invocation);
82         g_source_remove(req->timeout_id);
83         req->timeout_id = 0;
84         return;
85     }
86 
87     if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
88         mime[i++] = MIME_TEXT_PLAIN_UTF8;
89     }
90 
91     if (i > 0) {
92         if (dpy->clipboard_proxy) {
93             qemu_dbus_display1_clipboard_call_grab(
94                 dpy->clipboard_proxy,
95                 info->selection,
96                 info->serial,
97                 mime,
98                 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
99         }
100     }
101 }
102 
103 static void
104 dbus_clipboard_reset_serial(DBusDisplay *dpy)
105 {
106     if (dpy->clipboard_proxy) {
107         qemu_dbus_display1_clipboard_call_register(
108             dpy->clipboard_proxy,
109             G_DBUS_CALL_FLAGS_NONE,
110             -1, NULL, NULL, NULL);
111     }
112 }
113 
114 static void
115 dbus_clipboard_notify(Notifier *notifier, void *data)
116 {
117     DBusDisplay *dpy =
118         container_of(notifier, DBusDisplay, clipboard_peer.notifier);
119     QemuClipboardNotify *notify = data;
120 
121     switch (notify->type) {
122     case QEMU_CLIPBOARD_UPDATE_INFO:
123         dbus_clipboard_update_info(dpy, notify->info);
124         return;
125     case QEMU_CLIPBOARD_RESET_SERIAL:
126         dbus_clipboard_reset_serial(dpy);
127         return;
128     }
129 }
130 
131 static void
132 dbus_clipboard_qemu_request(QemuClipboardInfo *info,
133                             QemuClipboardType type)
134 {
135     DBusDisplay *dpy = container_of(info->owner, DBusDisplay, clipboard_peer);
136     g_autofree char *mime = NULL;
137     g_autoptr(GVariant) v_data = NULL;
138     g_autoptr(GError) err = NULL;
139     const char *data = NULL;
140     const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };
141     size_t n;
142 
143     if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
144         /* unsupported atm */
145         return;
146     }
147 
148     if (dpy->clipboard_proxy) {
149         if (!qemu_dbus_display1_clipboard_call_request_sync(
150                 dpy->clipboard_proxy,
151                 info->selection,
152                 mimes,
153                 G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {
154             error_report("Failed to request clipboard: %s", err->message);
155             return;
156         }
157 
158         if (g_strcmp0(mime, MIME_TEXT_PLAIN_UTF8)) {
159             error_report("Unsupported returned MIME: %s", mime);
160             return;
161         }
162 
163         data = g_variant_get_fixed_array(v_data, &n, 1);
164         qemu_clipboard_set_data(&dpy->clipboard_peer, info, type,
165                                 n, data, true);
166     }
167 }
168 
169 static void
170 dbus_clipboard_request_cancelled(DBusClipboardRequest *req)
171 {
172     if (!req->invocation) {
173         return;
174     }
175 
176     g_dbus_method_invocation_return_error(
177         req->invocation,
178         DBUS_DISPLAY_ERROR,
179         DBUS_DISPLAY_ERROR_FAILED,
180         "Cancelled clipboard request");
181 
182     g_clear_object(&req->invocation);
183     g_source_remove(req->timeout_id);
184     req->timeout_id = 0;
185 }
186 
187 static void
188 dbus_clipboard_unregister_proxy(DBusDisplay *dpy)
189 {
190     const char *name = NULL;
191     int i;
192 
193     for (i = 0; i < G_N_ELEMENTS(dpy->clipboard_request); ++i) {
194         dbus_clipboard_request_cancelled(&dpy->clipboard_request[i]);
195     }
196 
197     if (!dpy->clipboard_proxy) {
198         return;
199     }
200 
201     name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
202     trace_dbus_clipboard_unregister(name);
203     g_clear_object(&dpy->clipboard_proxy);
204 }
205 
206 static void
207 dbus_on_clipboard_proxy_name_owner_changed(
208     DBusDisplay *dpy,
209     GObject *object,
210     GParamSpec *pspec)
211 {
212     dbus_clipboard_unregister_proxy(dpy);
213 }
214 
215 static gboolean
216 dbus_clipboard_register(
217     DBusDisplay *dpy,
218     GDBusMethodInvocation *invocation)
219 {
220     g_autoptr(GError) err = NULL;
221     const char *name = NULL;
222 
223     if (dpy->clipboard_proxy) {
224         g_dbus_method_invocation_return_error(
225             invocation,
226             DBUS_DISPLAY_ERROR,
227             DBUS_DISPLAY_ERROR_FAILED,
228             "Clipboard peer already registered!");
229         return DBUS_METHOD_INVOCATION_HANDLED;
230     }
231 
232     dpy->clipboard_proxy =
233         qemu_dbus_display1_clipboard_proxy_new_sync(
234             g_dbus_method_invocation_get_connection(invocation),
235             G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
236             g_dbus_method_invocation_get_sender(invocation),
237             "/org/qemu/Display1/Clipboard",
238             NULL,
239             &err);
240     if (!dpy->clipboard_proxy) {
241         g_dbus_method_invocation_return_error(
242             invocation,
243             DBUS_DISPLAY_ERROR,
244             DBUS_DISPLAY_ERROR_FAILED,
245             "Failed to setup proxy: %s", err->message);
246         return DBUS_METHOD_INVOCATION_HANDLED;
247     }
248 
249     name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
250     trace_dbus_clipboard_register(name);
251 
252     g_object_connect(dpy->clipboard_proxy,
253                      "swapped-signal::notify::g-name-owner",
254                      dbus_on_clipboard_proxy_name_owner_changed, dpy,
255                      NULL);
256     qemu_clipboard_reset_serial();
257 
258     qemu_dbus_display1_clipboard_complete_register(dpy->clipboard, invocation);
259     return DBUS_METHOD_INVOCATION_HANDLED;
260 }
261 
262 static gboolean
263 dbus_clipboard_check_caller(DBusDisplay *dpy, GDBusMethodInvocation *invocation)
264 {
265     if (!dpy->clipboard_proxy ||
266         g_strcmp0(g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)),
267                   g_dbus_method_invocation_get_sender(invocation))) {
268         g_dbus_method_invocation_return_error(
269             invocation,
270             DBUS_DISPLAY_ERROR,
271             DBUS_DISPLAY_ERROR_FAILED,
272             "Unregistered caller");
273         return FALSE;
274     }
275 
276     return TRUE;
277 }
278 
279 static gboolean
280 dbus_clipboard_unregister(
281     DBusDisplay *dpy,
282     GDBusMethodInvocation *invocation)
283 {
284     if (!dbus_clipboard_check_caller(dpy, invocation)) {
285         return DBUS_METHOD_INVOCATION_HANDLED;
286     }
287 
288     dbus_clipboard_unregister_proxy(dpy);
289 
290     qemu_dbus_display1_clipboard_complete_unregister(
291         dpy->clipboard, invocation);
292 
293     return DBUS_METHOD_INVOCATION_HANDLED;
294 }
295 
296 static gboolean
297 dbus_clipboard_grab(
298     DBusDisplay *dpy,
299     GDBusMethodInvocation *invocation,
300     gint arg_selection,
301     guint arg_serial,
302     const gchar *const *arg_mimes)
303 {
304     QemuClipboardSelection s = arg_selection;
305     g_autoptr(QemuClipboardInfo) info = NULL;
306 
307     if (!dbus_clipboard_check_caller(dpy, invocation)) {
308         return DBUS_METHOD_INVOCATION_HANDLED;
309     }
310 
311     if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
312         g_dbus_method_invocation_return_error(
313             invocation,
314             DBUS_DISPLAY_ERROR,
315             DBUS_DISPLAY_ERROR_FAILED,
316             "Invalid clipboard selection: %d", arg_selection);
317         return DBUS_METHOD_INVOCATION_HANDLED;
318     }
319 
320     info = qemu_clipboard_info_new(&dpy->clipboard_peer, s);
321     if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {
322         info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
323     }
324     info->serial = arg_serial;
325     info->has_serial = true;
326     if (qemu_clipboard_check_serial(info, true)) {
327         qemu_clipboard_update(info);
328     } else {
329         trace_dbus_clipboard_grab_failed();
330     }
331 
332     qemu_dbus_display1_clipboard_complete_grab(dpy->clipboard, invocation);
333     return DBUS_METHOD_INVOCATION_HANDLED;
334 }
335 
336 static gboolean
337 dbus_clipboard_release(
338     DBusDisplay *dpy,
339     GDBusMethodInvocation *invocation,
340     gint arg_selection)
341 {
342     if (!dbus_clipboard_check_caller(dpy, invocation)) {
343         return DBUS_METHOD_INVOCATION_HANDLED;
344     }
345 
346     qemu_clipboard_peer_release(&dpy->clipboard_peer, arg_selection);
347 
348     qemu_dbus_display1_clipboard_complete_release(dpy->clipboard, invocation);
349     return DBUS_METHOD_INVOCATION_HANDLED;
350 }
351 
352 static gboolean
353 dbus_clipboard_request_timeout(gpointer user_data)
354 {
355     dbus_clipboard_request_cancelled(user_data);
356     return G_SOURCE_REMOVE;
357 }
358 
359 static gboolean
360 dbus_clipboard_request(
361     DBusDisplay *dpy,
362     GDBusMethodInvocation *invocation,
363     gint arg_selection,
364     const gchar *const *arg_mimes)
365 {
366     QemuClipboardSelection s = arg_selection;
367     QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;
368     QemuClipboardInfo *info = NULL;
369 
370     if (!dbus_clipboard_check_caller(dpy, invocation)) {
371         return DBUS_METHOD_INVOCATION_HANDLED;
372     }
373 
374     if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
375         g_dbus_method_invocation_return_error(
376             invocation,
377             DBUS_DISPLAY_ERROR,
378             DBUS_DISPLAY_ERROR_FAILED,
379             "Invalid clipboard selection: %d", arg_selection);
380         return DBUS_METHOD_INVOCATION_HANDLED;
381     }
382 
383     if (dpy->clipboard_request[s].invocation) {
384         g_dbus_method_invocation_return_error(
385             invocation,
386             DBUS_DISPLAY_ERROR,
387             DBUS_DISPLAY_ERROR_FAILED,
388             "Pending request");
389         return DBUS_METHOD_INVOCATION_HANDLED;
390     }
391 
392     info = qemu_clipboard_info(s);
393     if (!info || !info->owner || info->owner == &dpy->clipboard_peer) {
394         g_dbus_method_invocation_return_error(
395             invocation,
396             DBUS_DISPLAY_ERROR,
397             DBUS_DISPLAY_ERROR_FAILED,
398             "Empty clipboard");
399         return DBUS_METHOD_INVOCATION_HANDLED;
400     }
401 
402     if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||
403         !info->types[type].available) {
404         g_dbus_method_invocation_return_error(
405             invocation,
406             DBUS_DISPLAY_ERROR,
407             DBUS_DISPLAY_ERROR_FAILED,
408             "Unhandled MIME types requested");
409         return DBUS_METHOD_INVOCATION_HANDLED;
410     }
411 
412     if (info->types[type].data) {
413         dbus_clipboard_complete_request(dpy, invocation, info, type);
414     } else {
415         qemu_clipboard_request(info, type);
416 
417         dpy->clipboard_request[s].invocation = g_object_ref(invocation);
418         dpy->clipboard_request[s].type = type;
419         dpy->clipboard_request[s].timeout_id =
420             g_timeout_add_seconds(5, dbus_clipboard_request_timeout,
421                                   &dpy->clipboard_request[s]);
422     }
423 
424     return DBUS_METHOD_INVOCATION_HANDLED;
425 }
426 
427 void
428 dbus_clipboard_init(DBusDisplay *dpy)
429 {
430     g_autoptr(GDBusObjectSkeleton) clipboard = NULL;
431 
432     assert(!dpy->clipboard);
433 
434     clipboard = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Clipboard");
435     dpy->clipboard = qemu_dbus_display1_clipboard_skeleton_new();
436     g_object_connect(dpy->clipboard,
437                      "swapped-signal::handle-register",
438                      dbus_clipboard_register, dpy,
439                      "swapped-signal::handle-unregister",
440                      dbus_clipboard_unregister, dpy,
441                      "swapped-signal::handle-grab",
442                      dbus_clipboard_grab, dpy,
443                      "swapped-signal::handle-release",
444                      dbus_clipboard_release, dpy,
445                      "swapped-signal::handle-request",
446                      dbus_clipboard_request, dpy,
447                      NULL);
448 
449     g_dbus_object_skeleton_add_interface(
450         G_DBUS_OBJECT_SKELETON(clipboard),
451         G_DBUS_INTERFACE_SKELETON(dpy->clipboard));
452     g_dbus_object_manager_server_export(dpy->server, clipboard);
453     dpy->clipboard_peer.name = "dbus";
454     dpy->clipboard_peer.notifier.notify = dbus_clipboard_notify;
455     dpy->clipboard_peer.request = dbus_clipboard_qemu_request;
456     qemu_clipboard_peer_register(&dpy->clipboard_peer);
457 }
458