diff --git a/Makefile.am.inc b/Makefile.am.inc index 7be4c4f89..e3d466e58 100644 --- a/Makefile.am.inc +++ b/Makefile.am.inc @@ -1,6 +1,7 @@ PORTAL_IFACE_FILES =\ $(top_srcdir)/data/org.freedesktop.portal.Account.xml \ $(top_srcdir)/data/org.freedesktop.portal.Background.xml \ + $(top_srcdir)/data/org.freedesktop.portal.Clipboard.xml \ $(top_srcdir)/data/org.freedesktop.portal.Camera.xml \ $(top_srcdir)/data/org.freedesktop.portal.Device.xml \ $(top_srcdir)/data/org.freedesktop.portal.Documents.xml \ @@ -35,6 +36,7 @@ PORTAL_IMPL_IFACE_FILES =\ $(top_srcdir)/data/org.freedesktop.impl.portal.Account.xml \ $(top_srcdir)/data/org.freedesktop.impl.portal.AppChooser.xml \ $(top_srcdir)/data/org.freedesktop.impl.portal.Background.xml \ + $(top_srcdir)/data/org.freedesktop.impl.portal.Clipboard.xml \ $(top_srcdir)/data/org.freedesktop.impl.portal.DynamicLauncher.xml \ $(top_srcdir)/data/org.freedesktop.impl.portal.Email.xml \ $(top_srcdir)/data/org.freedesktop.impl.portal.FileChooser.xml \ diff --git a/data/meson.build b/data/meson.build index 489479f18..7762ebbbd 100644 --- a/data/meson.build +++ b/data/meson.build @@ -5,6 +5,7 @@ portal_sources = files( 'org.freedesktop.portal.Account.xml', 'org.freedesktop.portal.Background.xml', 'org.freedesktop.portal.Camera.xml', + 'org.freedesktop.portal.Clipboard.xml', 'org.freedesktop.portal.Device.xml', 'org.freedesktop.portal.Documents.xml', 'org.freedesktop.portal.DynamicLauncher.xml', @@ -38,6 +39,7 @@ portal_impl_sources = files( 'org.freedesktop.impl.portal.Account.xml', 'org.freedesktop.impl.portal.AppChooser.xml', 'org.freedesktop.impl.portal.Background.xml', + 'org.freedesktop.impl.portal.Clipboard.xml', 'org.freedesktop.impl.portal.DynamicLauncher.xml', 'org.freedesktop.impl.portal.Email.xml', 'org.freedesktop.impl.portal.FileChooser.xml', diff --git a/data/org.freedesktop.impl.portal.Clipboard.xml b/data/org.freedesktop.impl.portal.Clipboard.xml new file mode 100644 index 000000000..89a240b8b --- /dev/null +++ b/data/org.freedesktop.impl.portal.Clipboard.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Clipboard.xml b/data/org.freedesktop.portal.Clipboard.xml new file mode 100644 index 000000000..18d48d180 --- /dev/null +++ b/data/org.freedesktop.portal.Clipboard.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.RemoteDesktop.xml b/data/org.freedesktop.portal.RemoteDesktop.xml index 9d18b2225..707eed59e 100644 --- a/data/org.freedesktop.portal.RemoteDesktop.xml +++ b/data/org.freedesktop.portal.RemoteDesktop.xml @@ -38,8 +38,8 @@ A remote desktop session may only be started and stopped with this interface, but you can use the #org.freedesktop.portal.Session object created with this - method together with certain methods on the #org.freedesktop.portal.ScreenCast - interface. Specifically, you can call + method together with certain methods on the #org.freedesktop.portal.ScreenCast and + #org.freedesktop.portal.Clipboard interfaces. Specifically, you can call org.freedesktop.portal.ScreenCast.SelectSources() to also get screen content, and org.freedesktop.portal.ScreenCast.OpenPipewireRemote() to acquire a file descriptor for a PipeWire remote. See #org.freedesktop.portal.ScreenCast for diff --git a/doc/portal-docs.xml.in b/doc/portal-docs.xml.in index 721e6f448..6c8d4566b 100644 --- a/doc/portal-docs.xml.in +++ b/doc/portal-docs.xml.in @@ -93,6 +93,7 @@ + @@ -147,6 +148,7 @@ + diff --git a/src/Makefile.am.inc b/src/Makefile.am.inc index 86a8de0b9..1c8d82fa7 100644 --- a/src/Makefile.am.inc +++ b/src/Makefile.am.inc @@ -149,6 +149,8 @@ xdg_desktop_portal_SOURCES += \ src/pipewire.h \ src/camera.c \ src/camera.h \ + src/clipboard.c \ + src/clipboard.h \ $(NULL) endif diff --git a/src/clipboard.c b/src/clipboard.c new file mode 100644 index 000000000..819d264db --- /dev/null +++ b/src/clipboard.c @@ -0,0 +1,719 @@ +/* + * Copyright 2022 Google LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include +#include + +#include "config.h" +#include "remote-desktop.h" +#include "request.h" +#include "session.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _Clipboard Clipboard; +typedef struct _ClipboardClass ClipboardClass; + +struct _Clipboard { + XdpDbusClipboardSkeleton parent_instance; +}; + +struct _ClipboardClass { + XdpDbusClipboardSkeletonClass parent_class; +}; + +static XdpDbusImplClipboard *impl; +static Clipboard *clipboard; + +GType clipboard_get_type(void) G_GNUC_CONST; +static void clipboard_iface_init(XdpDbusClipboardIface *iface); + +static GQuark quark_request_session; + +G_DEFINE_TYPE_WITH_CODE(Clipboard, clipboard, XDP_DBUS_TYPE_CLIPBOARD_SKELETON, + G_IMPLEMENT_INTERFACE(XDP_DBUS_TYPE_CLIPBOARD, + clipboard_iface_init)) + +static XdpOptionKey clipboard_set_selection_options[] = {}; + +static XdpOptionKey clipboard_enable_options[] = {}; + +static void selection_write_done_done(GObject *source_object, GAsyncResult *res, + gpointer data) { + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK(request) + + session = g_object_get_qdata(G_OBJECT(request), quark_request_session); + SESSION_AUTOLOCK_UNREF(g_object_ref(session)); + g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL); + + if (!xdp_dbus_impl_clipboard_call_selection_write_done_finish( + impl, &response, &results, res, &error)) { + g_dbus_error_strip_remote_error(error); + g_warning("A backend call failed: %s:", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) { + if (!results) { + GVariantBuilder results_builder; + + g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_ref_sink(g_variant_builder_end(&results_builder)); + } + + xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response, + results); + request_unexport(request); + } + + if (should_close_session) { + session_close(session, TRUE); + } +} + +static void selection_read_done(GObject *source_object, GAsyncResult *res, + gpointer data) { + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + GVariantBuilder results_builder; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + g_autoptr(GUnixFDList) out_fd_list = NULL; + + REQUEST_AUTOLOCK(request); + session = g_object_get_qdata(G_OBJECT(request), quark_request_session); + + g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT); + SESSION_AUTOLOCK_UNREF(g_object_ref(session)); + g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL); + + if (!xdp_dbus_impl_clipboard_call_selection_read_finish( + impl, &response, &results, &out_fd_list, res, &error)) { + g_dbus_error_strip_remote_error(error); + g_warning("A backend call failed: %s:", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (results) { + int fd; + int fd_id; + g_autofree char *path = NULL; + + if (g_variant_lookup(results, "fd", "h", &fd_id)) { + fd = g_unix_fd_list_get(out_fd_list, fd_id, &error); + + path = xdp_app_info_get_path_for_fd(request->app_info, fd, 0, NULL, NULL, + &error); // TODO validate? + + // close(fd); // should I close this if I can't send the fd list? + + if (!path) { + g_warning("invalid file descriptor"); + } else { + g_variant_builder_add(&results_builder, "{sv}", "fd", + g_variant_new_int32(fd)); + // g_variant_new_handle(fd) // how to return fd list?? + } + } + } + + if (request->exported) { + xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response, + g_variant_builder_end(&results_builder)); + request_unexport(request); + } else { + g_variant_builder_clear(&results_builder); + } + + if (should_close_session) { + session_close(session, TRUE); + } +} + +static void set_selection_done(GObject *source_object, GAsyncResult *res, + gpointer data) { + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK(request); + + session = g_object_get_qdata(G_OBJECT(request), quark_request_session); + SESSION_AUTOLOCK_UNREF(g_object_ref(session)); + g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL); + + if (!xdp_dbus_impl_clipboard_call_set_selection_finish( + impl, &response, &results, res, &error)) { + g_dbus_error_strip_remote_error(error); + g_warning("A backend call failed: %s:", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) { + if (!results) { + GVariantBuilder results_builder; + + g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_ref_sink(g_variant_builder_end(&results_builder)); + } + + xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response, + results); + request_unexport(request); + + if (should_close_session) { + session_close(session, TRUE); + } + } +} + +static void selection_write_done(GObject *source_object, GAsyncResult *res, + gpointer data) { + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + GVariantBuilder results_builder; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + g_autoptr(GUnixFDList) out_fd_list = NULL; + + REQUEST_AUTOLOCK(request); + session = g_object_get_qdata(G_OBJECT(request), quark_request_session); + + g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT); + SESSION_AUTOLOCK_UNREF(g_object_ref(session)); + g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL); + + if (!xdp_dbus_impl_clipboard_call_selection_write_finish( + impl, &response, &results, &out_fd_list, res, &error)) { + g_dbus_error_strip_remote_error(error); + g_warning("A backend call failed: %s:", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (results) { + int fd; + int fd_id; + g_autofree char *path = NULL; + + if (g_variant_lookup(results, "fd", "h", &fd_id)) { + fd = g_unix_fd_list_get(out_fd_list, fd_id, &error); + + path = xdp_app_info_get_path_for_fd(request->app_info, fd, 0, NULL, NULL, + &error); // TODO validate? + // close(fd); // should I close this if I can't send the fd list? + + if (!path) { + g_warning("invalid file descriptor"); + } else { + g_variant_builder_add(&results_builder, "{sv}", "fd", + g_variant_new_int32(fd)); + // g_variant_new_handle(fd) // how to return fd list?? + } + } + } + + if (request->exported) { + xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response, + g_variant_builder_end(&results_builder)); + request_unexport(request); + } else { + g_variant_builder_clear(&results_builder); + } + + if (should_close_session) { + session_close(session, TRUE); + } +} + +static void enable_clipboard_done(GObject *source_object, GAsyncResult *res, + gpointer data) { + g_autoptr(Request) request = data; + Session *session; + guint response = 2; + gboolean should_close_session; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + + REQUEST_AUTOLOCK(request) + + session = g_object_get_qdata(G_OBJECT(request), quark_request_session); + SESSION_AUTOLOCK_UNREF(g_object_ref(session)); + g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL); + + if (!xdp_dbus_impl_clipboard_call_enable_clipboard_finish( + impl, &response, &results, res, &error)) { + g_dbus_error_strip_remote_error(error); + g_warning("A backend call failed: %s:", error->message); + } + + should_close_session = !request->exported || response != 0; + + if (request->exported) { + if (!results) { + GVariantBuilder results_builder; + + g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT); + results = g_variant_ref_sink(g_variant_builder_end(&results_builder)); + } + + xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response, + results); + request_unexport(request); + } + + if (should_close_session) { + session_close(session, TRUE); + } else if (!session->closed) { + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + remote_desktop_session_enabled_clipboard(remote_desktop_session); + } +} + +static gboolean handle_enable_clipboard(XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + const gchar *arg_session_handle, + GVariant *arg_options) { + Request *request = request_from_invocation(invocation); + Session *session; + RemoteDesktopSession *remote_desktop_session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + + session = acquire_session(arg_session_handle, request); + if (!session) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF(session); + + if (!is_remote_desktop_session(session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + remote_desktop_session = (RemoteDesktopSession *)session; + + if (!remote_desktop_session_can_enable_clipboard(remote_desktop_session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, "Invalid state"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync( + g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error); + if (!impl_request) { + g_dbus_method_invocation_return_gerror(invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request(request, impl_request); + request_export(request, g_dbus_method_invocation_get_connection(invocation)); + + g_object_set_qdata_full(G_OBJECT(request), quark_request_session, + g_object_ref(session), g_object_unref); + + g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options(arg_options, &options_builder, + clipboard_enable_options, + G_N_ELEMENTS(clipboard_enable_options), &error)) { + g_dbus_method_invocation_return_gerror(invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end(&options_builder); + + remote_desktop_session_enabling_clipboard(remote_desktop_session); + + xdp_dbus_impl_clipboard_call_enable_clipboard( + impl, request->id, arg_session_handle, + xdp_app_info_get_id(request->app_info), options, NULL, + enable_clipboard_done, g_object_ref(request)); + + xdp_dbus_clipboard_complete_enable_clipboard(object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean handle_selection_read(XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const gchar *arg_session_handle, + const gchar *arg_mime_type, + GVariant *arg_options) { + Request *request = request_from_invocation(invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + + session = acquire_session(arg_session_handle, request); + if (!session) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF(session); + + if (!is_remote_desktop_session(session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } else if (!remote_desktop_session_is_clipboard_enabled( + (RemoteDesktopSession *)session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync( + g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error); + if (!impl_request) { + g_dbus_method_invocation_return_gerror(invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request(request, impl_request); + request_export(request, g_dbus_method_invocation_get_connection(invocation)); + + g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); + options = g_variant_builder_end(&options_builder); + + g_object_set_qdata_full(G_OBJECT(request), quark_request_session, + g_object_ref(session), g_object_unref); + + xdp_dbus_impl_clipboard_call_selection_read( + impl, request->id, arg_session_handle, + xdp_app_info_get_id(request->app_info), arg_mime_type, options, NULL, + NULL, selection_read_done, g_object_ref(request)); + + xdp_dbus_clipboard_complete_selection_read(object, invocation, NULL, + request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean handle_selection_write(XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + const gchar *arg_session_handle, + guint arg_serial, + GVariant *arg_options) { + Request *request = request_from_invocation(invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + + session = acquire_session(arg_session_handle, request); + if (!session) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF(session); + + if (!is_remote_desktop_session(session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } else if (!remote_desktop_session_is_clipboard_enabled( + (RemoteDesktopSession *)session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync( + g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error); + if (!impl_request) { + g_dbus_method_invocation_return_gerror(invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request(request, impl_request); + request_export(request, g_dbus_method_invocation_get_connection(invocation)); + + g_object_set_qdata_full(G_OBJECT(request), quark_request_session, + g_object_ref(session), g_object_unref); + + g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); + options = g_variant_builder_end(&options_builder); + + xdp_dbus_impl_clipboard_call_selection_write( + impl, request->id, arg_session_handle, + xdp_app_info_get_id(request->app_info), arg_serial, options, NULL, NULL, + selection_write_done, g_object_ref(request)); + + xdp_dbus_clipboard_complete_selection_write(object, invocation, NULL, + request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean handle_set_selection(XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + const gchar *arg_session_handle, + GVariant *arg_options) { + Request *request = request_from_invocation(invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + + session = acquire_session(arg_session_handle, request); + if (!session) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF(session); + + if (!is_remote_desktop_session(session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } else if (!remote_desktop_session_is_clipboard_enabled( + (RemoteDesktopSession *)session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync( + g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error); + if (!impl_request) { + g_dbus_method_invocation_return_gerror(invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request(request, impl_request); + request_export(request, g_dbus_method_invocation_get_connection(invocation)); + + g_object_set_qdata_full(G_OBJECT(request), quark_request_session, + g_object_ref(session), g_object_unref); + + g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options( + arg_options, &options_builder, clipboard_set_selection_options, + G_N_ELEMENTS(clipboard_set_selection_options), &error)) { + g_dbus_method_invocation_return_gerror(invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end(&options_builder); + + xdp_dbus_impl_clipboard_call_set_selection( + impl, request->id, arg_session_handle, + xdp_app_info_get_id(request->app_info), options, NULL, set_selection_done, + g_object_ref(request)); + + xdp_dbus_clipboard_complete_set_selection(object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean handle_selection_write_done(XdpDbusClipboard *object, + GDBusMethodInvocation *invocation, + const gchar *arg_session_handle, + guint arg_serial, + gboolean arg_success, + GVariant *arg_options) { + Request *request = request_from_invocation(invocation); + Session *session; + GVariantBuilder options_builder; + GVariant *options; + g_autoptr(GError) error = NULL; + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + + session = acquire_session(arg_session_handle, request); + if (!session) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + SESSION_AUTOLOCK_UNREF(session); + + if (!is_remote_desktop_session(session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session type"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } else if (!remote_desktop_session_is_clipboard_enabled( + (RemoteDesktopSession *)session)) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync( + g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error); + if (!impl_request) { + g_dbus_method_invocation_return_gerror(invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request(request, impl_request); + request_export(request, g_dbus_method_invocation_get_connection(invocation)); + + g_object_set_qdata_full(G_OBJECT(request), quark_request_session, + g_object_ref(session), g_object_unref); + + g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); + options = g_variant_builder_end(&options_builder); + + xdp_dbus_impl_clipboard_call_selection_write_done( + impl, request->id, arg_session_handle, + xdp_app_info_get_id(request->app_info), arg_serial, arg_success, options, + NULL, selection_write_done_done, g_object_ref(request)); + + xdp_dbus_clipboard_complete_selection_write_done(object, invocation, + request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void clipboard_iface_init(XdpDbusClipboardIface *iface) { + iface->handle_enable_clipboard = handle_enable_clipboard; + + iface->handle_selection_read = handle_selection_read; + iface->handle_selection_write = handle_selection_write; + iface->handle_set_selection = handle_set_selection; + iface->handle_selection_write_done = handle_selection_write_done; +} + +static void clipboard_init(Clipboard *clipboard) { + xdp_dbus_clipboard_set_version(XDP_DBUS_CLIPBOARD(clipboard), 1); +} + +static void clipboard_class_init(ClipboardClass *klass) { + quark_request_session = + g_quark_from_static_string("-xdp-request-clipboard-session"); +} + +static void selection_transfer_cb(XdpDbusImplClipboard *impl, + const char *arg_session_handle, + const gchar *arg_mime_type, guint arg_serial, + gpointer data) { + GDBusConnection *connection = g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)); + g_autoptr(Session) session = lookup_session(arg_session_handle); + + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + + if (remote_desktop_session && + remote_desktop_session_is_clipboard_enabled(remote_desktop_session) && + !session->closed) { + g_dbus_connection_emit_signal( + connection, session->sender, "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Clipboard", "SelectionTransfer", + g_variant_new("(o@a{sv})", arg_session_handle, arg_mime_type, + arg_serial), + NULL); + } +} + +static void selection_owner_changed_cb(XdpDbusImplClipboard *impl, + const char *arg_session_handle, + GVariant *arg_options, gpointer data) { + GDBusConnection *connection = g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)); + g_autoptr(Session) session = lookup_session(arg_session_handle); + + RemoteDesktopSession *remote_desktop_session = + (RemoteDesktopSession *)session; + + if (remote_desktop_session && + remote_desktop_session_is_clipboard_enabled(remote_desktop_session) && + !session->closed) { + g_dbus_connection_emit_signal( + connection, session->sender, "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Clipboard", "SelectionTransfer", + g_variant_new("(o@a{sv})", arg_session_handle, arg_options), NULL); + } +} + +GDBusInterfaceSkeleton *clipboard_create(GDBusConnection *connection, + const char *dbus_name) { + g_autoptr(GError) error = NULL; + + impl = xdp_dbus_impl_clipboard_proxy_new_sync( + connection, G_DBUS_PROXY_FLAGS_NONE, dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, NULL, &error); + if (impl == NULL) { + g_warning("Failed to create clipboard: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(impl), G_MAXINT); + + clipboard = g_object_new(clipboard_get_type(), NULL); + + g_signal_connect(impl, "selection-transfer", + G_CALLBACK(selection_transfer_cb), clipboard); + + g_signal_connect(impl, "selection-owner-changed", + G_CALLBACK(selection_owner_changed_cb), clipboard); + + return G_DBUS_INTERFACE_SKELETON(clipboard); +} diff --git a/src/clipboard.h b/src/clipboard.h new file mode 100644 index 000000000..2e6e38fea --- /dev/null +++ b/src/clipboard.h @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Google LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include + +#include "session.h" + +GDBusInterfaceSkeleton *clipboard_create(GDBusConnection *connection, + const char *dbus_name); diff --git a/src/meson.build b/src/meson.build index 24227fe4f..04217c436 100644 --- a/src/meson.build +++ b/src/meson.build @@ -84,6 +84,7 @@ if have_pipewire 'remote-desktop.c', 'pipewire.c', 'camera.c', + 'clipboard.c', ) endif diff --git a/src/remote-desktop.c b/src/remote-desktop.c index f450d6965..55ca2a002 100644 --- a/src/remote-desktop.c +++ b/src/remote-desktop.c @@ -56,13 +56,14 @@ G_DEFINE_TYPE_WITH_CODE (RemoteDesktop, remote_desktop, G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_REMOTE_DESKTOP, remote_desktop_iface_init)) -typedef enum _RemoteDesktopSessionState -{ +typedef enum _RemoteDesktopSessionState { REMOTE_DESKTOP_SESSION_STATE_INIT, REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES, REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED, REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES, REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED, + REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD, + REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD, REMOTE_DESKTOP_SESSION_STATE_STARTING, REMOTE_DESKTOP_SESSION_STATE_STARTED, REMOTE_DESKTOP_SESSION_STATE_CLOSED @@ -85,6 +86,8 @@ typedef struct _RemoteDesktopSession DeviceType shared_devices; GList *streams; + + gboolean clipboard_enabled; } RemoteDesktopSession; typedef struct _RemoteDesktopSessionClass @@ -106,14 +109,15 @@ is_remote_desktop_session (Session *session) gboolean remote_desktop_session_can_select_sources (RemoteDesktopSession *session) { - switch (session->state) { case REMOTE_DESKTOP_SESSION_STATE_INIT: case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES: case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED: + case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD: return TRUE; case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES: + case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD: case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED: case REMOTE_DESKTOP_SESSION_STATE_STARTING: case REMOTE_DESKTOP_SESSION_STATE_STARTED: @@ -124,6 +128,26 @@ remote_desktop_session_can_select_sources (RemoteDesktopSession *session) g_assert_not_reached (); } +gboolean remote_desktop_session_can_enable_clipboard( + RemoteDesktopSession *session) { + switch (session->state) { + case REMOTE_DESKTOP_SESSION_STATE_INIT: + case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES: + case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED: + case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED: + return TRUE; + case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES: + case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD: + case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD: + case REMOTE_DESKTOP_SESSION_STATE_STARTING: + case REMOTE_DESKTOP_SESSION_STATE_STARTED: + case REMOTE_DESKTOP_SESSION_STATE_CLOSED: + return FALSE; + } + + g_assert_not_reached(); +} + GList * remote_desktop_session_get_streams (RemoteDesktopSession *session) { @@ -142,6 +166,20 @@ remote_desktop_session_sources_selected (RemoteDesktopSession *session) session->state = REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED; } +gboolean remote_desktop_session_is_clipboard_enabled( + RemoteDesktopSession *session) { + return session->clipboard_enabled; +} + +void remote_desktop_session_enabling_clipboard(RemoteDesktopSession *session) { + session->state = REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD; +} + +void remote_desktop_session_enabled_clipboard(RemoteDesktopSession *session) { + session->clipboard_enabled = true; + session->state = REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD; +} + static RemoteDesktopSession * remote_desktop_session_new (GVariant *options, Request *request, @@ -415,7 +453,9 @@ handle_select_devices (XdpDbusRemoteDesktop *object, case REMOTE_DESKTOP_SESSION_STATE_INIT: case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES: case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED: - break; + case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD: + case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD: + break; case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES: case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED: g_dbus_method_invocation_return_error (invocation, @@ -610,7 +650,8 @@ handle_start (XdpDbusRemoteDesktop *object, case REMOTE_DESKTOP_SESSION_STATE_INIT: case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED: case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED: - break; + case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD: + break; case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES: g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, @@ -623,6 +664,11 @@ handle_start (XdpDbusRemoteDesktop *object, G_DBUS_ERROR_FAILED, "Devices not selected"); return G_DBUS_METHOD_INVOCATION_HANDLED; + case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD: + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Clipboard not enabled"); + return G_DBUS_METHOD_INVOCATION_HANDLED; case REMOTE_DESKTOP_SESSION_STATE_STARTING: case REMOTE_DESKTOP_SESSION_STATE_STARTED: g_dbus_method_invocation_return_error (invocation, @@ -695,6 +741,8 @@ check_notify (Session *session, case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES: case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED: case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES: + case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD: + case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD: case REMOTE_DESKTOP_SESSION_STATE_STARTING: case REMOTE_DESKTOP_SESSION_STATE_CLOSED: return FALSE; diff --git a/src/remote-desktop.h b/src/remote-desktop.h index 4b75e9b47..aba7ca3e2 100644 --- a/src/remote-desktop.h +++ b/src/remote-desktop.h @@ -31,9 +31,19 @@ GList * remote_desktop_session_get_streams (RemoteDesktopSession *session); gboolean remote_desktop_session_can_select_sources (RemoteDesktopSession *session); +gboolean remote_desktop_session_can_enable_clipboard( + RemoteDesktopSession *session); + +gboolean remote_desktop_session_is_clipboard_enabled( + RemoteDesktopSession *session); + void remote_desktop_session_selecting_sources (RemoteDesktopSession *session); void remote_desktop_session_sources_selected (RemoteDesktopSession *session); +void remote_desktop_session_enabling_clipboard(RemoteDesktopSession *session); + +void remote_desktop_session_enabled_clipboard(RemoteDesktopSession *session); + GDBusInterfaceSkeleton * remote_desktop_create (GDBusConnection *connection, const char *dbus_name); diff --git a/src/request.c b/src/request.c index 5bd5016de..9af9fbf3c 100644 --- a/src/request.c +++ b/src/request.c @@ -268,8 +268,22 @@ get_token (GDBusMethodInvocation *invocation) g_warning ("Support for %s::%s missing in %s", interface, method, G_STRLOC); } - } - else if (strcmp (interface, "org.freedesktop.portal.Location") == 0) + } else if (strcmp(interface, "org.freedesktop.portal.Clipboard") == 0) { + if (strcmp(method, "EnableClipboard") == 0) { + options = g_variant_get_child_value(parameters, 1); + } else if (strcmp(method, "SetSelection") == 0) { + options = g_variant_get_child_value(parameters, 1); + } else if (strcmp(method, "SelectionWrite") == 0) { + options = g_variant_get_child_value(parameters, 2); + } else if (strcmp(method, "SelectionWriteDone") == 0) { + options = g_variant_get_child_value(parameters, 3); + } else if (strcmp(method, "SelectionRead") == 0) { + options = g_variant_get_child_value(parameters, 2); + } else { + g_warning("Support for %s::%s missing in %s", interface, method, + G_STRLOC); + } + } else if (strcmp (interface, "org.freedesktop.portal.Location") == 0) { if (strcmp (method, "CreateSession") == 0 ) { diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c index ad4d97075..43ee80736 100644 --- a/src/xdg-desktop-portal.c +++ b/src/xdg-desktop-portal.c @@ -60,6 +60,7 @@ #include "wallpaper.h" #include "realtime.h" #include "dynamic-launcher.h" +#include "clipboard.h" static GMainLoop *loop = NULL; @@ -276,6 +277,12 @@ on_bus_acquired (GDBusConnection *connection, export_portal_implementation (connection, notification_create (connection, implementation->dbus_name)); + implementation = + find_portal_implementation("org.freedesktop.impl.portal.Clipboard"); + if (implementation != NULL) + export_portal_implementation( + connection, clipboard_create(connection, implementation->dbus_name)); + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Inhibit"); if (implementation != NULL) export_portal_implementation (connection, diff --git a/tests/__init__.py b/tests/__init__.py index c64302e84..2e5ea02ab 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -294,6 +294,16 @@ def start_impl_portal(self, params=None, portal=None): self.start_dbus_monitor() + def add_template(self, portal, params={}): + """ + Add an additional template to the portal object + """ + self.obj_portal.AddTemplate( + f"tests/templates/{portal.lower()}.py", + params, + dbus_interface=dbusmock.MOCK_IFACE, + ) + def start_xdp(self): """ Start the xdg-desktop-portal process diff --git a/tests/portals/test.portal b/tests/portals/test.portal index e43f90e3d..29553d357 100644 --- a/tests/portals/test.portal +++ b/tests/portals/test.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.Test -Interfaces=org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Lockdown;org.freedesktop.impl.portal.Print;org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Settings; +Interfaces=org.freedesktop.impl.portal.RemoteDesktop;org.freedesktop.impl.portal.Clipboard;org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Lockdown;org.freedesktop.impl.portal.Print;org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Settings; UseIn=test diff --git a/tests/templates/clipboard.py b/tests/templates/clipboard.py new file mode 100644 index 000000000..48535ab3a --- /dev/null +++ b/tests/templates/clipboard.py @@ -0,0 +1,239 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests.templates import Response, init_template_logger, ImplRequest +import dbus.service +import dbus +import os, sys +from gi.repository import GLib + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.Clipboard" +VERSION = 1 + +logger = init_template_logger(__name__) + + +def load(mock, parameters=None): + logger.debug(f"Loading parameters: {parameters}") + + mock.delay: int = parameters.get("delay", 200) + mock.response: int = parameters.get("response", 0) + mock.expect_close: bool = parameters.get("expect-close", False) + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + } + ), + ) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def EnableClipboard( + self, handle, session_handle, app_id, options, cb_success, cb_error +): + try: + logger.debug( + f"EnableClipboard({handle}, {session_handle}, {app_id}, {options})" + ) + + response = Response(self.response, {}) + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"EnableClipboard Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"EnableClipboard with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def SetSelection(self, handle, session_handle, app_id, options, cb_success, cb_error): + try: + logger.debug( + f"EnableClipboard({handle}, {session_handle}, {app_id}, {options})" + ) + + response = Response(self.response, {}) + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"SetSelection Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"SetSelection with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosua{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def SelectionWrite( + self, handle, session_handle, app_id, serial, options, cb_success, cb_error +): + try: + logger.debug( + f"SelectionWrite({handle}, {session_handle}, {app_id}, {serial}, {options})" + ) + + response = Response(self.response, {}) + + request = ImplRequest(self, BUS_NAME, handle) + + # Open a file + fd = os.open("foo.txt", os.O_RDWR | os.O_CREAT) + response.results["fd"] = dbus.types.UnixFd(fd) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"SelectionWrite Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"SelectionWrite with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosuba{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def SelectionWriteDone( + self, handle, session_handle, app_id, serial, success, options, cb_success, cb_error +): + try: + logger.debug( + f"SelectionWriteDone({handle}, {session_handle}, {app_id}, {serial}, {success}, {options})" + ) + + response = Response(self.response, {}) + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"SelectionWriteDone Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"SelectionWriteDone with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oossa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def SelectionRead( + self, handle, session_handle, app_id, mime_type, options, cb_success, cb_error +): + try: + logger.debug( + f"SelectionRead({handle}, {session_handle}, {app_id}, {mime_type}, {options})" + ) + + response = Response(self.response, {}) + + request = ImplRequest(self, BUS_NAME, handle) + + # Open a file + fd = os.open("foo.txt", os.O_RDWR | os.O_CREAT) + response.results["fd"] = dbus.types.UnixFd(fd) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"SelectionRead Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"SelectionRead with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) diff --git a/tests/templates/remotedesktop.py b/tests/templates/remotedesktop.py new file mode 100644 index 000000000..0d4d987eb --- /dev/null +++ b/tests/templates/remotedesktop.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests.templates import Response, init_template_logger, ImplRequest +import dbus.service + +from gi.repository import GLib + + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.RemoteDesktop" +VERSION = 1 + +logger = init_template_logger(__name__) + + +def load(mock, parameters=None): + logger.debug(f"Loading parameters: {parameters}") + + mock.delay: int = parameters.get("delay", 200) + mock.response: int = parameters.get("response", 0) + mock.expect_close: bool = parameters.get("expect-close", False) + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + } + ), + ) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="oosa{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def CreateSession(self, handle, session_handle, app_id, options, cb_success, cb_error): + try: + logger.debug(f"CreateSession({handle}, {app_id}, {session_handle}, {options})") + + response = Response(self.response, {}) + + request = ImplRequest(self, BUS_NAME, handle) + + if self.expect_close: + + def closed_callback(): + response = Response(2, {}) + logger.debug(f"CreateSession Close() response {response}") + cb_success(response.response, response.results) + + request.export(closed_callback) + else: + request.export() + + def reply(): + logger.debug(f"CreateSession with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + except Exception as e: + logger.critical(e) + cb_error(e) diff --git a/tests/test_clipboard.py b/tests/test_clipboard.py new file mode 100644 index 000000000..ac59b9e2a --- /dev/null +++ b/tests/test_clipboard.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from logging import debug +from tests import Request, PortalTest +from gi.repository import GLib +import dbus + + +class TestClipboard(PortalTest): + def test_version(self): + self.check_version(1) + + def enable_clipboard(self, session_handle): + clipboard_interface = self.get_dbus_interface() + enable_clipboard_request = Request(self.dbus_con, clipboard_interface) + enable_clipboard_response = enable_clipboard_request.call( + "EnableClipboard", session_handle=session_handle, options={} + ) + assert enable_clipboard_response.response == 0 + + def create_session_handle(self, enableClipboard=True): + self.start_xdp() + self.start_impl_portal() + self.add_template("RemoteDesktop") + + remote_desktop_interface = self.get_dbus_interface("RemoteDesktop") + + create_session_request = Request(self.dbus_con, remote_desktop_interface) + create_session_response = create_session_request.call( + "CreateSession", options={"session_handle_token": "1234"} + ) + assert create_session_response.response == 0 + assert str(create_session_response.results["session_handle"]) + + session_handle = create_session_response.results["session_handle"] + + if enableClipboard: + self.enable_clipboard(session_handle) + + return create_session_response.results["session_handle"] + + def test_clipboard_set_selection(self): + session_handle = self.create_session_handle() + clipboard_interface = self.get_dbus_interface() + + set_selection_request = Request(self.dbus_con, clipboard_interface) + set_selection_response = set_selection_request.call( + "SetSelection", session_handle=session_handle, options={} + ) + assert set_selection_response.response == 0 + + def test_clipboard_selection_write(self): + session_handle = self.create_session_handle() + clipboard_interface = self.get_dbus_interface() + + selection_write_request = Request(self.dbus_con, clipboard_interface) + selection_write_response = selection_write_request.call( + "SelectionWrite", session_handle=session_handle, serial=4321, options={} + ) + assert selection_write_response.response == 0 + + selection_write_done_request = Request(self.dbus_con, clipboard_interface) + selection_write_done_response = selection_write_done_request.call( + "SelectionWriteDone", + session_handle=session_handle, + serial=4321, + success=True, + options={}, + ) + + assert selection_write_done_response.response == 0 + + def test_clipboard_selection_read(self): + session_handle = self.create_session_handle() + clipboard_interface = self.get_dbus_interface() + + selection_read_request = Request(self.dbus_con, clipboard_interface) + selection_read_response = selection_read_request.call( + "SelectionRead", session_handle=session_handle, mime_type="", options={} + ) + assert selection_read_response.response == 0 + + def test_clipboard_throws_without_enable(self): + session_handle = self.create_session_handle(False) # clipboard not enabled + clipboard_interface = self.get_dbus_interface() + + set_selection_request = Request(self.dbus_con, clipboard_interface) + with self.assertRaises(dbus.exceptions.DBusException): + set_selection_request.call( + "SetSelection", session_handle=session_handle, options={} + )