1 /*
2  * Copyright (C) 2018, Matthias Clasen
3  *
4  * This file is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU Lesser General Public License as
6  * published by the Free Software Foundation, version 3.0 of the
7  * License.
8  *
9  * This file is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * SPDX-License-Identifier: LGPL-3.0-only
18  */
19 
20 #include "config.h"
21 
22 #include "openuri.h"
23 
24 #define GNU_SOURCE 1
25 
26 #include <sys/types.h>
27 #include <sys/stat.h>
28 #include <fcntl.h>
29 
30 #include <glib/gstdio.h>
31 #include <gio/gunixfdlist.h>
32 
33 #include "portal-private.h"
34 
35 typedef struct {
36   XdpPortal *portal;
37   XdpParent *parent;
38   char *parent_handle;
39   char *uri;
40   gboolean ask;
41   gboolean writable;
42   gboolean open_dir;
43   guint signal_id;
44   GTask *task;
45   char *request_path;
46   guint cancelled_id;
47 } OpenCall;
48 
49 static void
open_call_free(OpenCall * call)50 open_call_free (OpenCall *call)
51 {
52   if (call->parent)
53     {
54       call->parent->parent_unexport (call->parent);
55       xdp_parent_free (call->parent);
56     }
57   g_free (call->parent_handle);
58 
59   if (call->signal_id)
60     g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
61 
62   if (call->cancelled_id)
63     g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
64 
65   g_free (call->request_path);
66 
67   g_object_unref (call->portal);
68   g_object_unref (call->task);
69   g_free (call->uri);
70 
71   g_free (call);
72 }
73 
74 static void
response_received(GDBusConnection * bus,const char * sender_name,const char * object_path,const char * interface_name,const char * signal_name,GVariant * parameters,gpointer data)75 response_received (GDBusConnection *bus,
76                    const char *sender_name,
77                    const char *object_path,
78                    const char *interface_name,
79                    const char *signal_name,
80                    GVariant *parameters,
81                    gpointer data)
82 {
83   OpenCall *call = data;
84   guint32 response;
85   g_autoptr(GVariant) ret = NULL;
86 
87   g_variant_get (parameters, "(u@a{sv})", &response, &ret);
88 
89   if (response == 0)
90     g_task_return_boolean (call->task, TRUE);
91   else if (response == 1)
92     g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "OpenURI canceled");
93   else
94     g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "OpenURI failed");
95 
96   open_call_free (call);
97 }
98 
99 static void do_open (OpenCall *call);
100 
101 static void
parent_exported(XdpParent * parent,const char * handle,gpointer data)102 parent_exported (XdpParent *parent,
103                  const char *handle,
104                  gpointer data)
105 {
106   OpenCall *call = data;
107   call->parent_handle = g_strdup (handle);
108   do_open (call);
109 }
110 
111 static void
cancelled_cb(GCancellable * cancellable,gpointer data)112 cancelled_cb (GCancellable *cancellable,
113               gpointer data)
114 {
115   OpenCall *call = data;
116 
117   g_dbus_connection_call (call->portal->bus,
118                           PORTAL_BUS_NAME,
119                           call->request_path,
120                           REQUEST_INTERFACE,
121                           "Close",
122                           NULL,
123                           NULL,
124                           G_DBUS_CALL_FLAGS_NONE,
125                           -1,
126                           NULL, NULL, NULL);
127 
128   g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "OpenURI call canceled by caller");
129 
130   open_call_free (call);
131 }
132 
133 static void
call_returned(GObject * object,GAsyncResult * result,gpointer data)134 call_returned (GObject *object,
135                GAsyncResult *result,
136                gpointer data)
137 {
138   OpenCall *call = data;
139   GError *error = NULL;
140   g_autoptr(GVariant) ret = NULL;
141   g_autoptr(GFile) file = NULL;
142 
143   file = g_file_new_for_uri (call->uri);
144   if (g_file_is_native (file))
145     ret = g_dbus_connection_call_with_unix_fd_list_finish (G_DBUS_CONNECTION (object), NULL, result, &error);
146   else
147     ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
148 
149   if (error)
150     {
151       g_task_return_error (call->task, error);
152       open_call_free (call);
153     }
154 }
155 
156 #ifndef O_PATH
157 #define O_PATH 0
158 #endif
159 
160 static void
do_open(OpenCall * call)161 do_open (OpenCall *call)
162 {
163   GVariantBuilder options;
164   g_autofree char *token = NULL;
165   g_autoptr(GFile) file = NULL;
166   GCancellable *cancellable;
167 
168   if (call->parent_handle == NULL)
169     {
170       call->parent->parent_export (call->parent, parent_exported, call);
171       return;
172     }
173 
174   token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
175   call->request_path = g_strconcat (REQUEST_PATH_PREFIX, call->portal->sender, "/", token, NULL);
176   call->signal_id = g_dbus_connection_signal_subscribe (call->portal->bus,
177                                                         PORTAL_BUS_NAME,
178                                                         REQUEST_INTERFACE,
179                                                         "Response",
180                                                         call->request_path,
181                                                         NULL,
182                                                         G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
183                                                         response_received,
184                                                         call,
185                                                         NULL);
186 
187   cancellable = g_task_get_cancellable (call->task);
188   if (cancellable)
189     call->cancelled_id = g_signal_connect (cancellable, "cancelled", G_CALLBACK (cancelled_cb), call);
190 
191   g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
192   g_variant_builder_add (&options, "{sv}", "handle_token", g_variant_new_string (token));
193   g_variant_builder_add (&options, "{sv}", "writable", g_variant_new_boolean (call->writable));
194   g_variant_builder_add (&options, "{sv}", "ask", g_variant_new_boolean (call->ask));
195 
196   file = g_file_new_for_uri (call->uri);
197 
198   if (g_file_is_native (file))
199     {
200       g_autoptr(GUnixFDList) fd_list = NULL;
201       g_autofree char *path = NULL;
202       int fd, fd_in, flags;
203 
204       path = g_file_get_path (file);
205 
206       if (call->writable)
207         flags = O_RDWR | O_CLOEXEC;
208       else
209         flags = O_RDONLY | O_CLOEXEC;
210 
211       fd = g_open (path, flags);
212       if (fd == -1)
213         {
214           g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to open '%s'", call->uri);
215           open_call_free (call);
216 
217           return;
218         }
219 
220       fd_list = g_unix_fd_list_new_from_array (&fd, 1);
221       fd = -1;
222       fd_in = 0;
223 
224       g_dbus_connection_call_with_unix_fd_list (call->portal->bus,
225                                                 PORTAL_BUS_NAME,
226                                                 PORTAL_OBJECT_PATH,
227                                                 "org.freedesktop.portal.OpenURI",
228                                                 call->open_dir ? "OpenDirectory" : "OpenFile",
229                                                 g_variant_new ("(sha{sv})", call->parent_handle, fd_in, &options),
230                                                 NULL,
231                                                 G_DBUS_CALL_FLAGS_NONE,
232                                                 -1,
233                                                 fd_list,
234                                                 NULL,
235                                                 call_returned,
236                                                 call);
237     }
238   else
239     {
240       g_dbus_connection_call (call->portal->bus,
241                               PORTAL_BUS_NAME,
242                               PORTAL_OBJECT_PATH,
243                               "org.freedesktop.portal.OpenURI",
244                               "OpenURI",
245                               g_variant_new ("(ssa{sv})", call->parent_handle, call->uri, &options),
246                               NULL,
247                               G_DBUS_CALL_FLAGS_NONE,
248                               -1,
249                               NULL,
250                               call_returned,
251                               call);
252     }
253 }
254 
255 /**
256  * xdp_portal_open_uri:
257  * @portal: a [class@Portal]
258  * @parent: parent window information
259  * @uri: the URI to open
260  * @flags: options for this call
261  * @cancellable: (nullable): optional [class@Gio.Cancellable]
262  * @callback: (scope async): a callback to call when the request is done
263  * @data: (closure): data to pass to @callback
264  *
265  * Opens @uri with an external hamdler.
266  */
267 void
xdp_portal_open_uri(XdpPortal * portal,XdpParent * parent,const char * uri,XdpOpenUriFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)268 xdp_portal_open_uri (XdpPortal *portal,
269                      XdpParent *parent,
270                      const char *uri,
271                      XdpOpenUriFlags flags,
272                      GCancellable *cancellable,
273                      GAsyncReadyCallback callback,
274                      gpointer data)
275 {
276   OpenCall *call = NULL;
277 
278   g_return_if_fail (XDP_IS_PORTAL (portal));
279   g_return_if_fail ((flags & ~(XDP_OPEN_URI_FLAG_ASK |
280                                XDP_OPEN_URI_FLAG_WRITABLE)) == 0);
281 
282   call = g_new0 (OpenCall, 1);
283   call->portal = g_object_ref (portal);
284   if (parent)
285     call->parent = xdp_parent_copy (parent);
286   else
287     call->parent_handle = g_strdup ("");
288   call->uri = g_strdup (uri);
289   call->ask = (flags & XDP_OPEN_URI_FLAG_ASK) != 0;
290   call->writable = (flags & XDP_OPEN_URI_FLAG_WRITABLE) != 0;
291   call->open_dir = FALSE;
292   call->task = g_task_new (portal, cancellable, callback, data);
293   g_task_set_source_tag (call->task, xdp_portal_open_uri);
294 
295   do_open (call);
296 }
297 
298 /**
299  * xdp_portal_open_uri_finish:
300  * @portal: a [class@Portal]
301  * @result: a [iface@Gio.AsyncResult]
302  * @error: return location for an error
303  *
304  * Finishes the open-uri request, and returns
305  * the result in the form of a boolean.
306  *
307  * Returns: `TRUE` if the call succeeded
308  */
309 gboolean
xdp_portal_open_uri_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)310 xdp_portal_open_uri_finish (XdpPortal *portal,
311                             GAsyncResult *result,
312                             GError **error)
313 {
314   g_return_val_if_fail (XDP_IS_PORTAL (portal), FALSE);
315   g_return_val_if_fail (g_task_is_valid (result, portal), FALSE);
316   g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_open_uri, FALSE);
317 
318   return g_task_propagate_boolean (G_TASK (result), error);
319 }
320 
321 /**
322  * xdp_portal_open_directory:
323  * @portal: a [class@Portal]
324  * @parent: parent window information
325  * @uri: the URI to open
326  * @flags: options for this call
327  * @cancellable: (nullable): optional [class@Gio.Cancellable]
328  * @callback: (scope async): a callback to call when the request is done
329  * @data: (closure): data to pass to @callback
330  *
331  * Opens the directory containing the file specified by the @uri. which
332  * must be a file: uri pointing to a file that the application has access
333  * to.
334  */
335 void
xdp_portal_open_directory(XdpPortal * portal,XdpParent * parent,const char * uri,XdpOpenUriFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)336 xdp_portal_open_directory (XdpPortal *portal,
337                            XdpParent *parent,
338                            const char *uri,
339                            XdpOpenUriFlags flags,
340                            GCancellable *cancellable,
341                            GAsyncReadyCallback callback,
342                            gpointer data)
343 {
344   OpenCall *call = NULL;
345 
346   g_return_if_fail (XDP_IS_PORTAL (portal));
347   g_return_if_fail ((flags & ~(XDP_OPEN_URI_FLAG_ASK)) == 0);
348 
349   call = g_new0 (OpenCall, 1);
350   call->portal = g_object_ref (portal);
351   if (parent)
352     call->parent = xdp_parent_copy (parent);
353   else
354     call->parent_handle = g_strdup ("");
355   call->uri = g_strdup (uri);
356   call->ask = (flags & XDP_OPEN_URI_FLAG_ASK) != 0;
357   call->writable = FALSE;
358   call->open_dir = TRUE;
359   call->task = g_task_new (portal, cancellable, callback, data);
360   g_task_set_source_tag (call->task, xdp_portal_open_directory);
361 
362   do_open (call);
363 }
364 
365 /**
366  * xdp_portal_open_directory_finish:
367  * @portal: a [class@Portal]
368  * @result: a [iface@Gio.AsyncResult]
369  * @error: return location for an error
370  *
371  * Finishes the open-directory request, and returns
372  * the result in the form of a boolean.
373  *
374  * Returns: `TRUE` if the call succeeded
375  */
376 gboolean
xdp_portal_open_directory_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)377 xdp_portal_open_directory_finish (XdpPortal     *portal,
378                                   GAsyncResult  *result,
379                                   GError       **error)
380 {
381   g_return_val_if_fail (XDP_IS_PORTAL (portal), FALSE);
382   g_return_val_if_fail (g_task_is_valid (result, portal), FALSE);
383   g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_open_directory, FALSE);
384 
385   return g_task_propagate_boolean (G_TASK (result), error);
386 }
387