summary refs log tree commit diff stats
path: root/ui/dbus-clipboard.c
diff options
context:
space:
mode:
Diffstat (limited to 'ui/dbus-clipboard.c')
-rw-r--r--ui/dbus-clipboard.c457
1 files changed, 457 insertions, 0 deletions
diff --git a/ui/dbus-clipboard.c b/ui/dbus-clipboard.c
new file mode 100644
index 0000000000..5843d26cd2
--- /dev/null
+++ b/ui/dbus-clipboard.c
@@ -0,0 +1,457 @@
+/*
+ * 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/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 void
+dbus_on_clipboard_proxy_name_owner_changed(
+    DBusDisplay *dpy,
+    GObject *object,
+    GParamSpec *pspec)
+{
+    dbus_clipboard_unregister_proxy(dpy);
+}
+
+static gboolean
+dbus_clipboard_register(
+    DBusDisplay *dpy,
+    GDBusMethodInvocation *invocation)
+{
+    g_autoptr(GError) err = NULL;
+    const char *name = NULL;
+
+    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(
+            g_dbus_method_invocation_get_connection(invocation),
+            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_on_clipboard_proxy_name_owner_changed, 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);
+}