selection: Support the file transfer portal
authorMatthias Clasen <mclasen@redhat.com>
Fri, 24 Feb 2023 14:37:59 +0000 (09:37 -0500)
committerMatthias Clasen <mclasen@redhat.com>
Sat, 25 Feb 2023 13:41:07 +0000 (08:41 -0500)
When the selection target is application/vnd.portal.files,
send files through the file transfer portal.

gtk/filetransferportal.c [new file with mode: 0644]
gtk/filetransferportalprivate.h [new file with mode: 0644]
gtk/gtkselection.c
gtk/meson.build

diff --git a/gtk/filetransferportal.c b/gtk/filetransferportal.c
new file mode 100644 (file)
index 0000000..30afaee
--- /dev/null
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2018 Matthias Clasen
+ *
+ * This library 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <gio/gio.h>
+
+#ifdef G_OS_UNIX
+
+#include <gio/gunixfdlist.h>
+
+#ifndef O_PATH
+#define O_PATH 0
+#endif
+
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#else
+#define HAVE_O_CLOEXEC 1
+#endif
+
+#include "filetransferportalprivate.h"
+
+static GDBusProxy *file_transfer_proxy = NULL;
+
+typedef struct {
+  GTask *task;
+  char **files;
+  int len;
+  int start;
+} AddFileData;
+
+static void
+free_add_file_data (gpointer data)
+{
+  AddFileData *afd = data;
+
+  g_object_unref (afd->task);
+  g_free (afd->files);
+  g_free (afd);
+}
+
+static void add_files (GDBusProxy  *proxy,
+                       AddFileData *afd);
+
+static void
+add_files_done (GObject      *object,
+                GAsyncResult *result,
+                gpointer      data)
+{
+  GDBusProxy *proxy = G_DBUS_PROXY (object);
+  AddFileData *afd = data;
+  GError *error = NULL;
+  GVariant *ret;
+
+  ret = g_dbus_proxy_call_with_unix_fd_list_finish (proxy, NULL, result, &error);
+  if (ret == NULL)
+    {
+      g_task_return_error (afd->task, error);
+      free_add_file_data (afd);
+      return;
+    }
+
+  g_variant_unref (ret);
+
+  if (afd->start >= afd->len)
+    {
+      g_task_return_boolean (afd->task, TRUE);
+      free_add_file_data (afd);
+      return;
+    }
+
+  add_files (proxy, afd);
+}
+
+/* We call AddFiles in chunks of 16 to avoid running into
+ * the per-message fd limit of the bus.
+ */
+static void
+add_files (GDBusProxy  *proxy,
+           AddFileData *afd)
+{
+  GUnixFDList *fd_list;
+  GVariantBuilder fds, options;
+  int i;
+  char *key;
+
+  g_variant_builder_init (&fds, G_VARIANT_TYPE ("ah"));
+  fd_list = g_unix_fd_list_new ();
+
+  for (i = 0; afd->files[afd->start + i]; i++)
+    {
+      int fd;
+      int fd_in;
+      GError *error = NULL;
+
+      if (i == 16)
+        break;
+
+      fd = open (afd->files[afd->start + i], O_PATH | O_CLOEXEC);
+      if (fd == -1)
+        {
+          g_task_return_new_error (afd->task, G_IO_ERROR, g_io_error_from_errno (errno),
+                                   "Failed to open %s", afd->files[afd->start + i]);
+          free_add_file_data (afd);
+          g_object_unref (fd_list);
+          return;
+         }
+
+#ifndef HAVE_O_CLOEXEC
+      fcntl (fd, F_SETFD, FD_CLOEXEC);
+#endif
+      fd_in = g_unix_fd_list_append (fd_list, fd, &error);
+      close (fd);
+
+      if (fd_in == -1)
+        {
+          g_task_return_error (afd->task, error);
+          free_add_file_data (afd);
+          g_object_unref (fd_list);
+          return;
+        }
+
+      g_variant_builder_add (&fds, "h", fd_in);
+    }
+
+   afd->start += 16;
+
+  key = (char *)g_object_get_data (G_OBJECT (afd->task), "key");
+
+  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+  g_dbus_proxy_call_with_unix_fd_list (proxy,
+                                       "AddFiles",
+                                       g_variant_new ("(saha{sv})", key, &fds, &options),
+                                       0, -1,
+                                       fd_list,
+                                       NULL,
+                                       add_files_done, afd);
+
+  g_object_unref (fd_list);
+}
+
+static void
+start_session_done (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      data)
+{
+  GDBusProxy *proxy = G_DBUS_PROXY (object);
+  AddFileData *afd = data;
+  GError *error = NULL;
+  GVariant *ret;
+  const char *key;
+
+  ret = g_dbus_proxy_call_finish (proxy, result, &error);
+  if (ret == NULL)
+    {
+      g_task_return_error (afd->task, error);
+      free_add_file_data (afd);
+      return;
+    }
+
+  g_variant_get (ret, "(&s)", &key);
+
+  g_object_set_data_full (G_OBJECT (afd->task), "key", g_strdup (key), g_free);
+
+  g_variant_unref (ret);
+
+  add_files (proxy, afd);
+}
+
+void
+file_transfer_portal_register_files (const char          **files,
+                                     gboolean              writable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              data)
+{
+  GTask *task;
+  AddFileData *afd;
+  GVariantBuilder options;
+
+  task = g_task_new (NULL, NULL, callback, data);
+
+  if (file_transfer_proxy == NULL)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                               "No portal found");
+      g_object_unref (task);
+      return;
+    }
+
+  afd = g_new (AddFileData, 1);
+  afd->task = task;
+  afd->files = g_strdupv ((char **)files);
+  afd->len = g_strv_length (afd->files);
+  afd->start = 0;
+
+  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add (&options, "{sv}", "writable", g_variant_new_boolean (writable));
+  g_variant_builder_add (&options, "{sv}", "autostop", g_variant_new_boolean (TRUE));
+
+  g_dbus_proxy_call (file_transfer_proxy, "StartTransfer",
+                     g_variant_new ("(a{sv})", &options),
+                     0, -1, NULL, start_session_done, afd);
+}
+
+gboolean
+file_transfer_portal_register_files_finish (GAsyncResult  *result,
+                                            char         **key,
+                                            GError       **error)
+{
+  if (g_task_propagate_boolean (G_TASK (result), error))
+    {
+      *key = g_strdup (g_object_get_data (G_OBJECT (result), "key"));
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+char *
+file_transfer_portal_register_files_sync (const char **files,
+                                          gboolean     writable,
+                                          GError     **error)
+{
+  const char *value;
+  char *key;
+  GUnixFDList *fd_list;
+  GVariantBuilder fds, options;
+  int i;
+  GVariant *ret;
+
+  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+  ret = g_dbus_proxy_call_sync (file_transfer_proxy,
+                                "StartTransfer",
+                                g_variant_new ("(a{sv})", &options),
+                                0,
+                                -1,
+                                NULL,
+                                error);
+  if (ret == NULL)
+    return NULL;
+
+  g_variant_get (ret, "(&s)", &value);
+  key = g_strdup (value);
+  g_variant_unref (ret);
+
+  fd_list = NULL;
+
+  for (i = 0; files[i]; i++)
+    {
+      int fd;
+      int fd_in;
+
+      if (fd_list == NULL)
+        {
+          g_variant_builder_init (&fds, G_VARIANT_TYPE ("ah"));
+          fd_list = g_unix_fd_list_new ();
+        }
+
+      fd = open (files[i], O_PATH | O_CLOEXEC);
+      if (fd == -1)
+        {
+          g_set_error (error,
+                       G_IO_ERROR, g_io_error_from_errno (errno),
+                       "Failed to open %s", files[i]);
+          g_variant_builder_clear (&fds);
+          g_object_unref (fd_list);
+          g_free (key);
+          return NULL;
+         }
+
+#ifndef HAVE_O_CLOEXEC
+      fcntl (fd, F_SETFD, FD_CLOEXEC);
+#endif
+      fd_in = g_unix_fd_list_append (fd_list, fd, error);
+      close (fd);
+
+      if (fd_in == -1)
+        {
+          g_variant_builder_clear (&fds);
+          g_object_unref (fd_list);
+          g_free (key);
+          return NULL;
+        }
+
+      g_variant_builder_add (&fds, "h", fd_in);
+
+      if ((i + 1) % 16 == 0 || files[i + 1] == NULL)
+        {
+          g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+          ret = g_dbus_proxy_call_with_unix_fd_list_sync (file_transfer_proxy,
+                                                          "AddFiles",
+                                                          g_variant_new ("(saha{sv})",
+                                                                         key,
+                                                                         &fds,
+                                                                         &options),
+                                                          0,
+                                                          -1,
+                                                          fd_list,
+                                                          NULL,
+                                                          NULL,
+                                                          error);
+          g_clear_object (&fd_list);
+
+          if (ret == NULL)
+            {
+              g_free (key);
+              return NULL;
+            }
+
+          g_variant_unref (ret);
+        }
+    }
+
+  return key;
+}
+
+static void
+retrieve_files_done (GObject      *object,
+                     GAsyncResult *result,
+                     gpointer      data)
+{
+  GDBusProxy *proxy = G_DBUS_PROXY (object);
+  GTask *task = data;
+  GError *error = NULL;
+  GVariant *ret;
+  char **files;
+
+  ret = g_dbus_proxy_call_finish (proxy, result, &error);
+  if (ret == NULL)
+    {
+      g_task_return_error (task, error);
+      g_object_unref (task);
+      return;
+    }
+
+  g_variant_get (ret, "(^a&s)", &files);
+
+  g_object_set_data_full (G_OBJECT (task), "files", g_strdupv (files), (GDestroyNotify)g_strfreev);
+
+  g_variant_unref (ret);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+void
+file_transfer_portal_retrieve_files (const char          *key,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             data)
+{
+  GTask *task;
+  GVariantBuilder options;
+
+  task = g_task_new (NULL, NULL, callback, data);
+
+  if (file_transfer_proxy == NULL)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                               "No portal found");
+      g_object_unref (task);
+      return;
+    }
+
+  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+  g_dbus_proxy_call (file_transfer_proxy,
+                     "RetrieveFiles",
+                     g_variant_new ("(sa{sv})", key, &options),
+                     0, -1, NULL,
+                     retrieve_files_done, task);
+}
+
+gboolean
+file_transfer_portal_retrieve_files_finish (GAsyncResult   *result,
+                                            char         ***files,
+                                            GError        **error)
+{
+  if (g_task_propagate_boolean (G_TASK (result), error))
+    {
+      *files = g_strdupv (g_object_get_data (G_OBJECT (result), "files"));
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+char **
+file_transfer_portal_retrieve_files_sync (const char  *key,
+                                          GError     **error)
+{
+  GVariantBuilder options;
+  GVariant *ret;
+  char **files = NULL;
+
+  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+  ret = g_dbus_proxy_call_sync (file_transfer_proxy,
+                                "RetrieveFiles",
+                                g_variant_new ("(sa{sv})", key, &options),
+                                0, -1, NULL,
+                                error);
+  if (ret)
+    {
+      const char **value;
+      g_variant_get (ret, "(^a&s)", &value);
+      files = g_strdupv ((char **)value);
+      g_variant_unref (ret);
+    }
+
+  return files;
+}
+
+static void
+connection_closed (GDBusConnection *connection,
+                   gboolean         remote_peer_vanished,
+                   GError          *error)
+{
+  g_clear_object (&file_transfer_proxy);
+}
+
+static void
+finish_registration (void)
+{
+  /* Free the singleton when the connection closes, important for test */
+  g_signal_connect (g_dbus_proxy_get_connection (G_DBUS_PROXY (file_transfer_proxy)),
+                    "closed", G_CALLBACK (connection_closed), NULL);
+}
+
+static gboolean
+proxy_has_owner (GDBusProxy *proxy)
+{
+  char *owner;
+
+  owner = g_dbus_proxy_get_name_owner (proxy);
+  if (owner)
+    {
+      g_free (owner);
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+void
+file_transfer_portal_register (void)
+{
+  static gboolean called;
+
+  if (!called)
+    {
+      called = TRUE;
+
+      file_transfer_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+                                G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS
+                                | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                                NULL,
+                                "org.freedesktop.portal.Documents",
+                                "/org/freedesktop/portal/documents",
+                                "org.freedesktop.portal.FileTransfer",
+                                NULL,
+                                NULL);
+
+      if (file_transfer_proxy && !proxy_has_owner (file_transfer_proxy))
+        g_clear_object (&file_transfer_proxy);
+
+      if (file_transfer_proxy)
+        finish_registration ();
+    }
+}
+
+gboolean
+file_transfer_portal_supported (void)
+{
+  file_transfer_portal_register ();
+
+  return file_transfer_proxy != NULL;
+}
+
+#endif /* G_OS_UNIX */
diff --git a/gtk/filetransferportalprivate.h b/gtk/filetransferportalprivate.h
new file mode 100644 (file)
index 0000000..d136b53
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 Matthias Clasen
+ *
+ * This library 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __FILE_TRANSFER_PROTOCOL_H__
+#define __FILE_TRANSFER_PROTOCOL_H__
+
+
+void     file_transfer_portal_register              (void);
+
+void     file_transfer_portal_register_files        (const char           **files,
+                                                     gboolean               writable,
+                                                     GAsyncReadyCallback    callback,
+                                                     gpointer               data);
+gboolean file_transfer_portal_register_files_finish (GAsyncResult          *result,
+                                                     char                 **key,
+                                                     GError               **error);
+
+void     file_transfer_portal_retrieve_files        (const char            *key,
+                                                     GAsyncReadyCallback    callback,
+                                                     gpointer               data);
+gboolean file_transfer_portal_retrieve_files_finish (GAsyncResult          *result,
+                                                     char                ***files,
+                                                     GError               **error);
+
+
+char *   file_transfer_portal_register_files_sync   (const char           **files,
+                                                     gboolean               writable,
+                                                     GError               **error);
+
+char **  file_transfer_portal_retrieve_files_sync   (const char            *key,
+                                                     GError               **error);
+
+gboolean file_transfer_portal_supported             (void);
+
+#endif
index 32d9de88bfae637258f005baa0674378c4f13b1d..048b4ad4964558d4c25e23fcdaff10a5bd2b4d81 100644 (file)
 #include "broadway/gdkbroadway.h"
 #endif
 
+#ifndef G_OS_WIN32
+#include "filetransferportalprivate.h"
+#endif
+
 #undef DEBUG_SELECTION
 
 /* Maximum size of a sent chunk, in bytes. Also the default size of
@@ -338,6 +342,7 @@ static GdkAtom text_plain_atom;
 static GdkAtom text_plain_utf8_atom;
 static GdkAtom text_plain_locale_atom;
 static GdkAtom text_uri_list_atom;
+static GdkAtom portal_files_atom;
 
 static void 
 init_atoms (void)
@@ -358,6 +363,7 @@ init_atoms (void)
       g_free (tmp);
 
       text_uri_list_atom = gdk_atom_intern_static_string ("text/uri-list");
+      portal_files_atom = gdk_atom_intern_static_string ("application/vnd.portal.files");
     }
 }
 
@@ -502,6 +508,10 @@ gtk_target_list_add_image_targets (GtkTargetList *list,
  * Appends the URI targets supported by #GtkSelectionData to
  * the target list. All targets are added with the same @info.
  * 
+ * Since 3.24.37, this includes the application/vnd.portal.files
+ * target when possible, to allow sending files between sandboxed
+ * apps via the FileTransfer portal.
+ *
  * Since: 2.6
  **/
 void 
@@ -512,7 +522,12 @@ gtk_target_list_add_uri_targets (GtkTargetList *list,
   
   init_atoms ();
 
-  gtk_target_list_add (list, text_uri_list_atom, 0, info);  
+  gtk_target_list_add (list, text_uri_list_atom, 0, info);
+
+#ifndef G_OS_WIN32
+  if (file_transfer_portal_supported ())
+    gtk_target_list_add (list, portal_files_atom, 0, info);
+#endif
 }
 
 /**
@@ -1835,6 +1850,9 @@ gtk_selection_data_get_pixbuf (const GtkSelectionData *selection_data)
  * Sets the contents of the selection from a list of URIs.
  * The string is converted to the form determined by
  * @selection_data->target.
+ *
+ * Since 3.24.37, this may involve using the FileTransfer
+ * portal to send files between sandboxed apps.
  * 
  * Returns: %TRUE if the selection was successfully set,
  *   otherwise %FALSE.
@@ -1880,6 +1898,57 @@ gtk_selection_data_set_uris (GtkSelectionData  *selection_data,
          return TRUE;
        }
     }
+#ifndef G_OS_WIN32
+  else if (selection_data->target == portal_files_atom &&
+           file_transfer_portal_supported ())
+    {
+      GPtrArray *a;
+      char **files;
+      char *key;
+      GError *error = NULL;
+
+      a = g_ptr_array_new ();
+
+      for (int i = 0; uris[i]; i++)
+        {
+          GFile *file;
+          char *path;
+
+          file = g_file_new_for_uri (uris[i]);
+          path = g_file_get_path (file);
+          g_object_unref (file);
+
+          if (path == NULL)
+            {
+              g_ptr_array_unref (a);
+              return FALSE;
+            }
+
+          g_ptr_array_add (a, path);
+        }
+
+      g_ptr_array_add (a, NULL);
+      files = (char **) g_ptr_array_free (a, FALSE);
+
+      key = file_transfer_portal_register_files_sync ((const char **)files, TRUE, &error);
+      if (key == NULL)
+        {
+          g_strfreev (files);
+          g_warning ("%s", error->message);
+          g_error_free (error);
+          return FALSE;
+        }
+
+      gtk_selection_data_set (selection_data,
+                              portal_files_atom,
+                              8, (guchar *)key, strlen (key));
+
+      g_strfreev (files);
+      g_free (key);
+
+      return TRUE;
+    }
+#endif
 
   return FALSE;
 }
@@ -1890,6 +1959,9 @@ gtk_selection_data_set_uris (GtkSelectionData  *selection_data,
  * 
  * Gets the contents of the selection data as array of URIs.
  *
+ * Since 3.24.37, this may involve using the FileTransfer
+ * portal to send files between sandboxed apps.
+ *
  * Returns:  (array zero-terminated=1) (element-type utf8) (transfer full): if
  *   the selection data contains a list of
  *   URIs, a newly allocated %NULL-terminated string array
@@ -1922,6 +1994,40 @@ gtk_selection_data_get_uris (const GtkSelectionData *selection_data)
       
       g_strfreev (list);
     }
+#ifndef G_OS_WIN32
+  else if (selection_data->length >= 0 &&
+           selection_data->type == portal_files_atom &&
+           file_transfer_portal_supported ())
+    {
+      char *key;
+      GError *error = NULL;
+      char **files;
+
+      key = g_strndup ((char *) selection_data->data, selection_data->length);
+      files = file_transfer_portal_retrieve_files_sync (key, &error);
+      if (error)
+        {
+          g_warning ("%s", error->message);
+          g_error_free (error);
+        }
+      g_free (key);
+
+      if (files)
+        {
+          GPtrArray *uris = g_ptr_array_new ();
+
+          for (int i = 0; files[i]; i++)
+            {
+              GFile *file = g_file_new_for_path (files[i]);
+              g_ptr_array_add (uris, g_file_get_uri (file));
+              g_object_unref (file);
+            }
+
+          g_ptr_array_add (uris, NULL);
+          result = (char **) g_ptr_array_free (uris, FALSE);
+        }
+    }
+#endif
 
   return result;
 }
@@ -2246,7 +2352,8 @@ gtk_targets_include_uri (GdkAtom *targets,
 
   for (i = 0; i < n_targets; i++)
     {
-      if (targets[i] == text_uri_list_atom)
+      if (targets[i] == text_uri_list_atom ||
+          targets[i] == portal_files_atom)
        {
          result = TRUE;
          break;
index 21540dd93ba64bd37d8f1e3c241ea0414703140f..4b7a453e11d28c88fa7e400714f7aed94067bf39 100644 (file)
@@ -659,6 +659,10 @@ if os_unix and tracker3_enabled
   gtk_unix_sources += 'gtksearchenginetracker3.c'
 endif
 
+if os_unix
+  gtk_unix_sources += 'filetransferportal.c'
+endif
+
 if os_unix
   gtk_sources += gtk_unix_sources
 endif