|  | /* | 
|  | * 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 "sysemu/sysemu.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; | 
|  |  | 
|  | 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; | 
|  | } | 
|  |  | 
|  | 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); | 
|  | } |