| /* | 
 |  * QEMU DBus display | 
 |  * | 
 |  * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com> | 
 |  * | 
 |  * Permission is hereby granted, free of charge, to any person obtaining a copy | 
 |  * of this software and associated documentation files (the "Software"), to deal | 
 |  * in the Software without restriction, including without limitation the rights | 
 |  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | 
 |  * copies of the Software, and to permit persons to whom the Software is | 
 |  * furnished to do so, subject to the following conditions: | 
 |  * | 
 |  * The above copyright notice and this permission notice shall be included in | 
 |  * all copies or substantial portions of the Software. | 
 |  * | 
 |  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | 
 |  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | 
 |  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | 
 |  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | 
 |  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 
 |  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | 
 |  * THE SOFTWARE. | 
 |  */ | 
 | #include "qemu/osdep.h" | 
 | #include "qemu/dbus.h" | 
 | #include "qemu/error-report.h" | 
 | #include "qemu/main-loop.h" | 
 | #include "qom/object_interfaces.h" | 
 | #include "system/system.h" | 
 | #include "qapi/error.h" | 
 | #include "trace.h" | 
 |  | 
 | #include "dbus.h" | 
 |  | 
 | #define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8" | 
 |  | 
 | static void | 
 | dbus_clipboard_complete_request( | 
 |     DBusDisplay *dpy, | 
 |     GDBusMethodInvocation *invocation, | 
 |     QemuClipboardInfo *info, | 
 |     QemuClipboardType type) | 
 | { | 
 |     GVariant *v_data = g_variant_new_from_data( | 
 |         G_VARIANT_TYPE("ay"), | 
 |         info->types[type].data, | 
 |         info->types[type].size, | 
 |         TRUE, | 
 |         (GDestroyNotify)qemu_clipboard_info_unref, | 
 |         qemu_clipboard_info_ref(info)); | 
 |  | 
 |     qemu_dbus_display1_clipboard_complete_request( | 
 |         dpy->clipboard, invocation, | 
 |         MIME_TEXT_PLAIN_UTF8, v_data); | 
 | } | 
 |  | 
 | static void | 
 | dbus_clipboard_update_info(DBusDisplay *dpy, QemuClipboardInfo *info) | 
 | { | 
 |     bool self_update = info->owner == &dpy->clipboard_peer; | 
 |     const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, }; | 
 |     DBusClipboardRequest *req; | 
 |     int i = 0; | 
 |  | 
 |     if (info->owner == NULL) { | 
 |         if (dpy->clipboard_proxy) { | 
 |             qemu_dbus_display1_clipboard_call_release( | 
 |                 dpy->clipboard_proxy, | 
 |                 info->selection, | 
 |                 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); | 
 |         } | 
 |         return; | 
 |     } | 
 |  | 
 |     if (self_update || !info->has_serial) { | 
 |         return; | 
 |     } | 
 |  | 
 |     req = &dpy->clipboard_request[info->selection]; | 
 |     if (req->invocation && info->types[req->type].data) { | 
 |         dbus_clipboard_complete_request(dpy, req->invocation, info, req->type); | 
 |         g_clear_object(&req->invocation); | 
 |         g_source_remove(req->timeout_id); | 
 |         req->timeout_id = 0; | 
 |         return; | 
 |     } | 
 |  | 
 |     if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { | 
 |         mime[i++] = MIME_TEXT_PLAIN_UTF8; | 
 |     } | 
 |  | 
 |     if (i > 0) { | 
 |         if (dpy->clipboard_proxy) { | 
 |             qemu_dbus_display1_clipboard_call_grab( | 
 |                 dpy->clipboard_proxy, | 
 |                 info->selection, | 
 |                 info->serial, | 
 |                 mime, | 
 |                 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | static void | 
 | dbus_clipboard_reset_serial(DBusDisplay *dpy) | 
 | { | 
 |     if (dpy->clipboard_proxy) { | 
 |         qemu_dbus_display1_clipboard_call_register( | 
 |             dpy->clipboard_proxy, | 
 |             G_DBUS_CALL_FLAGS_NONE, | 
 |             -1, NULL, NULL, NULL); | 
 |     } | 
 | } | 
 |  | 
 | static void | 
 | dbus_clipboard_notify(Notifier *notifier, void *data) | 
 | { | 
 |     DBusDisplay *dpy = | 
 |         container_of(notifier, DBusDisplay, clipboard_peer.notifier); | 
 |     QemuClipboardNotify *notify = data; | 
 |  | 
 |     switch (notify->type) { | 
 |     case QEMU_CLIPBOARD_UPDATE_INFO: | 
 |         dbus_clipboard_update_info(dpy, notify->info); | 
 |         return; | 
 |     case QEMU_CLIPBOARD_RESET_SERIAL: | 
 |         dbus_clipboard_reset_serial(dpy); | 
 |         return; | 
 |     } | 
 | } | 
 |  | 
 | static void | 
 | dbus_clipboard_qemu_request(QemuClipboardInfo *info, | 
 |                             QemuClipboardType type) | 
 | { | 
 |     DBusDisplay *dpy = container_of(info->owner, DBusDisplay, clipboard_peer); | 
 |     g_autofree char *mime = NULL; | 
 |     g_autoptr(GVariant) v_data = NULL; | 
 |     g_autoptr(GError) err = NULL; | 
 |     const char *data = NULL; | 
 |     const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL }; | 
 |     size_t n; | 
 |  | 
 |     trace_dbus_clipboard_qemu_request(type); | 
 |  | 
 |     if (type != QEMU_CLIPBOARD_TYPE_TEXT) { | 
 |         /* unsupported atm */ | 
 |         return; | 
 |     } | 
 |  | 
 |     if (dpy->clipboard_proxy) { | 
 |         if (!qemu_dbus_display1_clipboard_call_request_sync( | 
 |                 dpy->clipboard_proxy, | 
 |                 info->selection, | 
 |                 mimes, | 
 |                 G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) { | 
 |             error_report("Failed to request clipboard: %s", err->message); | 
 |             return; | 
 |         } | 
 |  | 
 |         if (g_strcmp0(mime, MIME_TEXT_PLAIN_UTF8)) { | 
 |             error_report("Unsupported returned MIME: %s", mime); | 
 |             return; | 
 |         } | 
 |  | 
 |         data = g_variant_get_fixed_array(v_data, &n, 1); | 
 |         qemu_clipboard_set_data(&dpy->clipboard_peer, info, type, | 
 |                                 n, data, true); | 
 |     } | 
 | } | 
 |  | 
 | static void | 
 | dbus_clipboard_request_cancelled(DBusClipboardRequest *req) | 
 | { | 
 |     if (!req->invocation) { | 
 |         return; | 
 |     } | 
 |  | 
 |     g_dbus_method_invocation_return_error( | 
 |         req->invocation, | 
 |         DBUS_DISPLAY_ERROR, | 
 |         DBUS_DISPLAY_ERROR_FAILED, | 
 |         "Cancelled clipboard request"); | 
 |  | 
 |     g_clear_object(&req->invocation); | 
 |     g_source_remove(req->timeout_id); | 
 |     req->timeout_id = 0; | 
 | } | 
 |  | 
 | static void | 
 | dbus_clipboard_unregister_proxy(DBusDisplay *dpy) | 
 | { | 
 |     const char *name = NULL; | 
 |     int i; | 
 |  | 
 |     for (i = 0; i < G_N_ELEMENTS(dpy->clipboard_request); ++i) { | 
 |         dbus_clipboard_request_cancelled(&dpy->clipboard_request[i]); | 
 |     } | 
 |  | 
 |     if (!dpy->clipboard_proxy) { | 
 |         return; | 
 |     } | 
 |  | 
 |     name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)); | 
 |     trace_dbus_clipboard_unregister(name); | 
 |     g_clear_object(&dpy->clipboard_proxy); | 
 | } | 
 |  | 
 | static gboolean | 
 | dbus_clipboard_register( | 
 |     DBusDisplay *dpy, | 
 |     GDBusMethodInvocation *invocation) | 
 | { | 
 |     g_autoptr(GError) err = NULL; | 
 |     const char *name = NULL; | 
 |     GDBusConnection *connection = g_dbus_method_invocation_get_connection(invocation); | 
 |  | 
 |     if (dpy->clipboard_proxy) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Clipboard peer already registered!"); | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     dpy->clipboard_proxy = | 
 |         qemu_dbus_display1_clipboard_proxy_new_sync( | 
 |             connection, | 
 |             G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, | 
 |             g_dbus_method_invocation_get_sender(invocation), | 
 |             "/org/qemu/Display1/Clipboard", | 
 |             NULL, | 
 |             &err); | 
 |     if (!dpy->clipboard_proxy) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Failed to setup proxy: %s", err->message); | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)); | 
 |     trace_dbus_clipboard_register(name); | 
 |  | 
 |     g_object_connect(dpy->clipboard_proxy, | 
 |                      "swapped-signal::notify::g-name-owner", | 
 |                      dbus_clipboard_unregister_proxy, dpy, | 
 |                      NULL); | 
 |     g_object_connect(connection, | 
 |                      "swapped-signal::closed", | 
 |                      dbus_clipboard_unregister_proxy, dpy, | 
 |                      NULL); | 
 |     qemu_clipboard_reset_serial(); | 
 |  | 
 |     qemu_dbus_display1_clipboard_complete_register(dpy->clipboard, invocation); | 
 |     return DBUS_METHOD_INVOCATION_HANDLED; | 
 | } | 
 |  | 
 | static gboolean | 
 | dbus_clipboard_check_caller(DBusDisplay *dpy, GDBusMethodInvocation *invocation) | 
 | { | 
 |     if (!dpy->clipboard_proxy || | 
 |         g_strcmp0(g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)), | 
 |                   g_dbus_method_invocation_get_sender(invocation))) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Unregistered caller"); | 
 |         return FALSE; | 
 |     } | 
 |  | 
 |     return TRUE; | 
 | } | 
 |  | 
 | static gboolean | 
 | dbus_clipboard_unregister( | 
 |     DBusDisplay *dpy, | 
 |     GDBusMethodInvocation *invocation) | 
 | { | 
 |     if (!dbus_clipboard_check_caller(dpy, invocation)) { | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     dbus_clipboard_unregister_proxy(dpy); | 
 |  | 
 |     qemu_dbus_display1_clipboard_complete_unregister( | 
 |         dpy->clipboard, invocation); | 
 |  | 
 |     return DBUS_METHOD_INVOCATION_HANDLED; | 
 | } | 
 |  | 
 | static gboolean | 
 | dbus_clipboard_grab( | 
 |     DBusDisplay *dpy, | 
 |     GDBusMethodInvocation *invocation, | 
 |     gint arg_selection, | 
 |     guint arg_serial, | 
 |     const gchar *const *arg_mimes) | 
 | { | 
 |     QemuClipboardSelection s = arg_selection; | 
 |     g_autoptr(QemuClipboardInfo) info = NULL; | 
 |  | 
 |     if (!dbus_clipboard_check_caller(dpy, invocation)) { | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     trace_dbus_clipboard_grab(arg_selection, arg_serial); | 
 |  | 
 |     if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Invalid clipboard selection: %d", arg_selection); | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     info = qemu_clipboard_info_new(&dpy->clipboard_peer, s); | 
 |     if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) { | 
 |         info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; | 
 |     } | 
 |     info->serial = arg_serial; | 
 |     info->has_serial = true; | 
 |     if (qemu_clipboard_check_serial(info, true)) { | 
 |         qemu_clipboard_update(info); | 
 |     } else { | 
 |         trace_dbus_clipboard_grab_failed(); | 
 |     } | 
 |  | 
 |     qemu_dbus_display1_clipboard_complete_grab(dpy->clipboard, invocation); | 
 |     return DBUS_METHOD_INVOCATION_HANDLED; | 
 | } | 
 |  | 
 | static gboolean | 
 | dbus_clipboard_release( | 
 |     DBusDisplay *dpy, | 
 |     GDBusMethodInvocation *invocation, | 
 |     gint arg_selection) | 
 | { | 
 |     if (!dbus_clipboard_check_caller(dpy, invocation)) { | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     qemu_clipboard_peer_release(&dpy->clipboard_peer, arg_selection); | 
 |  | 
 |     qemu_dbus_display1_clipboard_complete_release(dpy->clipboard, invocation); | 
 |     return DBUS_METHOD_INVOCATION_HANDLED; | 
 | } | 
 |  | 
 | static gboolean | 
 | dbus_clipboard_request_timeout(gpointer user_data) | 
 | { | 
 |     dbus_clipboard_request_cancelled(user_data); | 
 |     return G_SOURCE_REMOVE; | 
 | } | 
 |  | 
 | static gboolean | 
 | dbus_clipboard_request( | 
 |     DBusDisplay *dpy, | 
 |     GDBusMethodInvocation *invocation, | 
 |     gint arg_selection, | 
 |     const gchar *const *arg_mimes) | 
 | { | 
 |     QemuClipboardSelection s = arg_selection; | 
 |     QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT; | 
 |     QemuClipboardInfo *info = NULL; | 
 |  | 
 |     if (!dbus_clipboard_check_caller(dpy, invocation)) { | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Invalid clipboard selection: %d", arg_selection); | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     if (dpy->clipboard_request[s].invocation) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Pending request"); | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     info = qemu_clipboard_info(s); | 
 |     if (!info || !info->owner || info->owner == &dpy->clipboard_peer) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Empty clipboard"); | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) || | 
 |         !info->types[type].available) { | 
 |         g_dbus_method_invocation_return_error( | 
 |             invocation, | 
 |             DBUS_DISPLAY_ERROR, | 
 |             DBUS_DISPLAY_ERROR_FAILED, | 
 |             "Unhandled MIME types requested"); | 
 |         return DBUS_METHOD_INVOCATION_HANDLED; | 
 |     } | 
 |  | 
 |     if (info->types[type].data) { | 
 |         dbus_clipboard_complete_request(dpy, invocation, info, type); | 
 |     } else { | 
 |         qemu_clipboard_request(info, type); | 
 |  | 
 |         dpy->clipboard_request[s].invocation = g_object_ref(invocation); | 
 |         dpy->clipboard_request[s].type = type; | 
 |         dpy->clipboard_request[s].timeout_id = | 
 |             g_timeout_add_seconds(5, dbus_clipboard_request_timeout, | 
 |                                   &dpy->clipboard_request[s]); | 
 |     } | 
 |  | 
 |     return DBUS_METHOD_INVOCATION_HANDLED; | 
 | } | 
 |  | 
 | void | 
 | dbus_clipboard_init(DBusDisplay *dpy) | 
 | { | 
 |     g_autoptr(GDBusObjectSkeleton) clipboard = NULL; | 
 |  | 
 |     assert(!dpy->clipboard); | 
 |  | 
 |     clipboard = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Clipboard"); | 
 |     dpy->clipboard = qemu_dbus_display1_clipboard_skeleton_new(); | 
 |     g_object_connect(dpy->clipboard, | 
 |                      "swapped-signal::handle-register", | 
 |                      dbus_clipboard_register, dpy, | 
 |                      "swapped-signal::handle-unregister", | 
 |                      dbus_clipboard_unregister, dpy, | 
 |                      "swapped-signal::handle-grab", | 
 |                      dbus_clipboard_grab, dpy, | 
 |                      "swapped-signal::handle-release", | 
 |                      dbus_clipboard_release, dpy, | 
 |                      "swapped-signal::handle-request", | 
 |                      dbus_clipboard_request, dpy, | 
 |                      NULL); | 
 |  | 
 |     g_dbus_object_skeleton_add_interface( | 
 |         G_DBUS_OBJECT_SKELETON(clipboard), | 
 |         G_DBUS_INTERFACE_SKELETON(dpy->clipboard)); | 
 |     g_dbus_object_manager_server_export(dpy->server, clipboard); | 
 |     dpy->clipboard_peer.name = "dbus"; | 
 |     dpy->clipboard_peer.notifier.notify = dbus_clipboard_notify; | 
 |     dpy->clipboard_peer.request = dbus_clipboard_qemu_request; | 
 |     qemu_clipboard_peer_register(&dpy->clipboard_peer); | 
 | } |