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