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 "filechooser.h"
23 #include "portal-private.h"
24 
25 typedef struct {
26   XdpPortal *portal;
27   XdpParent *parent;
28   char *parent_handle;
29   char *method;
30   char *title;
31   gboolean multiple;
32   char *current_name;
33   char *current_folder;
34   char *current_file;
35   GVariant *files;
36   GVariant *filters;
37   GVariant *current_filter;
38   GVariant *choices;
39   guint signal_id;
40   GTask *task;
41   char *request_path;
42   guint cancelled_id;
43 } FileCall;
44 
45 static void
file_call_free(FileCall * call)46 file_call_free (FileCall *call)
47 {
48   if (call->parent)
49     {
50       call->parent->parent_unexport (call->parent);
51       xdp_parent_free (call->parent);
52     }
53   g_free (call->parent_handle);
54 
55   if (call->signal_id)
56     g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
57 
58   if (call->cancelled_id)
59     g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
60 
61   g_free (call->request_path);
62 
63   g_object_unref (call->portal);
64   g_object_unref (call->task);
65 
66   // call->method not free'ed as it is assigned from static strings
67   g_free (call->title);
68   g_free (call->current_name);
69   g_free (call->current_folder);
70   g_free (call->current_file);
71   if (call->files)
72     g_variant_unref (call->files);
73   if (call->filters)
74     g_variant_unref (call->filters);
75   if (call->current_filter)
76     g_variant_unref (call->current_filter);
77   if (call->choices)
78     g_variant_unref (call->choices);
79 
80   g_free (call);
81 }
82 
83 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)84 response_received (GDBusConnection *bus,
85                    const char *sender_name,
86                    const char *object_path,
87                    const char *interface_name,
88                    const char *signal_name,
89                    GVariant *parameters,
90                    gpointer data)
91 {
92   FileCall *call = data;
93   guint32 response;
94   g_autoptr(GVariant) ret = NULL;
95 
96   if (call->cancelled_id)
97     {
98       g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
99       call->cancelled_id = 0;
100     }
101 
102   g_variant_get (parameters, "(u@a{sv})", &response, &ret);
103 
104   if (response == 0)
105     g_task_return_pointer (call->task, g_variant_ref (ret), (GDestroyNotify)g_variant_unref);
106   else if (response == 1)
107     g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Filechooser canceled");
108   else
109     g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "Filechooser failed");
110 
111   file_call_free (call);
112 }
113 
114 static void open_file (FileCall *call);
115 
116 static void
parent_exported(XdpParent * parent,const char * handle,gpointer data)117 parent_exported (XdpParent *parent,
118                  const char *handle,
119                  gpointer data)
120 {
121   FileCall *call = data;
122   call->parent_handle = g_strdup (handle);
123   open_file (call);
124 }
125 
126 static void
cancelled_cb(GCancellable * cancellable,gpointer data)127 cancelled_cb (GCancellable *cancellable,
128               gpointer data)
129 {
130   FileCall *call = data;
131 
132   g_dbus_connection_call (call->portal->bus,
133                           PORTAL_BUS_NAME,
134                           call->request_path,
135                           REQUEST_INTERFACE,
136                           "Close",
137                           NULL,
138                           NULL,
139                           G_DBUS_CALL_FLAGS_NONE,
140                           -1,
141                           NULL, NULL, NULL);
142 
143   g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "OpenFile call canceled by caller");
144 
145   file_call_free (call);
146 }
147 
148 static void
call_returned(GObject * object,GAsyncResult * result,gpointer data)149 call_returned (GObject *object,
150                GAsyncResult *result,
151                gpointer data)
152 {
153   FileCall *call = data;
154   GError *error = NULL;
155   g_autoptr(GVariant) ret = NULL;
156 
157   ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
158   if (error)
159     {
160       g_task_return_error (call->task, error);
161       file_call_free (call);
162     }
163 }
164 
165 static void
open_file(FileCall * call)166 open_file (FileCall *call)
167 {
168   GVariantBuilder options;
169   g_autofree char *token = NULL;
170   GCancellable *cancellable;
171 
172   if (call->parent_handle == NULL)
173     {
174       call->parent->parent_export (call->parent, parent_exported, call);
175       return;
176     }
177 
178   token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
179   call->request_path = g_strconcat (REQUEST_PATH_PREFIX, call->portal->sender, "/", token, NULL);
180   call->signal_id = g_dbus_connection_signal_subscribe (call->portal->bus,
181                                                         PORTAL_BUS_NAME,
182                                                         REQUEST_INTERFACE,
183                                                         "Response",
184                                                         call->request_path,
185                                                         NULL,
186                                                         G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
187                                                         response_received,
188                                                         call,
189                                                         NULL);
190 
191   cancellable = g_task_get_cancellable (call->task);
192   if (cancellable)
193     call->cancelled_id = g_signal_connect (cancellable, "cancelled", G_CALLBACK (cancelled_cb), call);
194 
195   g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
196   g_variant_builder_add (&options, "{sv}", "handle_token", g_variant_new_string (token));
197   if (call->multiple)
198     g_variant_builder_add (&options, "{sv}", "multiple", g_variant_new_boolean (call->multiple));
199   if (call->files)
200     g_variant_builder_add (&options, "{sv}", "files", call->files);
201   if (call->filters)
202     g_variant_builder_add (&options, "{sv}", "filters", call->filters);
203   if (call->current_filter)
204     g_variant_builder_add (&options, "{sv}", "current_filter", call->current_filter);
205   if (call->choices)
206     g_variant_builder_add (&options, "{sv}", "choices", call->choices);
207   if (call->current_name)
208     g_variant_builder_add (&options, "{sv}", "current_name", g_variant_new_string (call->current_name));
209   if (call->current_folder)
210     g_variant_builder_add (&options, "{sv}", "current_folder", g_variant_new_bytestring (call->current_folder));
211   if (call->current_file)
212     g_variant_builder_add (&options, "{sv}", "current_file", g_variant_new_bytestring (call->current_file));
213 
214   g_dbus_connection_call (call->portal->bus,
215                           PORTAL_BUS_NAME,
216                           PORTAL_OBJECT_PATH,
217                           "org.freedesktop.portal.FileChooser",
218                           call->method,
219                           g_variant_new ("(ssa{sv})", call->parent_handle, call->title, &options),
220                           NULL,
221                           G_DBUS_CALL_FLAGS_NONE,
222                           -1,
223                           NULL,
224                           call_returned,
225                           call);
226 }
227 
228 /**
229  * xdp_portal_open_file:
230  * @portal: a [class@Portal]
231  * @parent: (nullable): parent window information
232  * @title: title for the file chooser dialog
233  * @filters: (nullable): a [struct@GLib.Variant] describing file filters
234  * @current_filter: (nullable): a [struct@GLib.Variant] describing the current file filter
235  * @choices: (nullable): a [struct@GLib.Variant] describing extra widgets
236  * @flags: options for this call
237  * @cancellable: (nullable): optional [class@Gio.Cancellable]
238  * @callback: (scope async): a callback to call when the request is done
239  * @data: (closure): data to pass to @callback
240  *
241  * Asks the user to open one or more files.
242  *
243  * The format for the @filters argument is a(sa(us)).
244  * Each item in the array specifies a single filter to offer to the user.
245  * The first string is a user-visible name for the filter. The a(us)
246  * specifies a list of filter strings, which can be either a glob pattern
247  * (indicated by 0) or a mimetype (indicated by 1).
248  *
249  * Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
250  *
251  * The format for the @choices argument is a(ssa(ss)s).
252  * For each element, the first string is an ID that will be returned
253  * with the response, te second string is a user-visible label. The
254  * a(ss) is the list of choices, each being a is an ID and a
255  * user-visible label. The final string is the initial selection,
256  * or "", to let the portal decide which choice will be initially selected.
257  * None of the strings, except for the initial selection, should be empty.
258  *
259  * As a special case, passing an empty array for the list of choices
260  * indicates a boolean choice that is typically displayed as a check
261  * button, using "true" and "false" as the choices.
262  *
263  * Example: [('encoding', 'Encoding', [('utf8', 'Unicode (UTF-8)'), ('latin15', 'Western')], 'latin15'), ('reencode', 'Reencode', [], 'false')]
264  *
265  * When the request is done, @callback will be called. You can then
266  * call [method@Portal.open_file_finish] to get the results.
267  */
268 void
xdp_portal_open_file(XdpPortal * portal,XdpParent * parent,const char * title,GVariant * filters,GVariant * current_filter,GVariant * choices,XdpOpenFileFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)269 xdp_portal_open_file (XdpPortal *portal,
270                       XdpParent *parent,
271                       const char *title,
272                       GVariant *filters,
273                       GVariant *current_filter,
274                       GVariant *choices,
275                       XdpOpenFileFlags flags,
276                       GCancellable *cancellable,
277                       GAsyncReadyCallback  callback,
278                       gpointer data)
279 {
280   FileCall *call;
281 
282   g_return_if_fail (XDP_IS_PORTAL (portal));
283   g_return_if_fail ((flags & ~(XDP_OPEN_FILE_FLAG_MULTIPLE)) == 0);
284 
285   call = g_new0 (FileCall, 1);
286   call->portal = g_object_ref (portal);
287   if (parent)
288     call->parent = xdp_parent_copy (parent);
289   else
290     call->parent_handle = g_strdup ("");
291   call->method = "OpenFile";
292   call->title = g_strdup (title);
293   call->multiple = (flags & XDP_OPEN_FILE_FLAG_MULTIPLE) != 0;
294   call->filters = filters ? g_variant_ref (filters) : NULL;
295   call->current_filter = current_filter ? g_variant_ref (current_filter) : NULL;
296   call->choices = choices ? g_variant_ref (choices) : NULL;
297   call->task = g_task_new (portal, cancellable, callback, data);
298   g_task_set_source_tag (call->task, xdp_portal_open_file);
299 
300   open_file (call);
301 }
302 
303 /**
304  * xdp_portal_open_file_finish:
305  * @portal: a [class@Portal]
306  * @result: a [iface@Gio.AsyncResult]
307  * @error: return location for an error
308  *
309  * Finishes the open-file request, and returns
310  * the result in the form of a [struct@GLib.Variant] dictionary containing
311  * the following fields:
312  *
313  * - uris `as`: an array of strings containing the uris of selected files
314  * - choices `a(ss)`: an array of pairs of strings, the first string being the
315  *     ID of a combobox that was passed into this call, the second string
316  *     being the selected option.
317  *
318  * Returns: (transfer full): a [struct@GLib.Variant] dictionary with the results
319  */
320 GVariant *
xdp_portal_open_file_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)321 xdp_portal_open_file_finish (XdpPortal *portal,
322                              GAsyncResult *result,
323                              GError **error)
324 {
325   GVariant *ret;
326 
327   g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
328   g_return_val_if_fail (g_task_is_valid (result, portal), NULL);
329   g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_open_file, NULL);
330 
331   ret = g_task_propagate_pointer (G_TASK (result), error);
332   return ret ? g_variant_ref (ret) : NULL;
333 }
334 
335 /**
336  * xdp_portal_save_file:
337  * @portal: a [class@Portal]
338  * @parent: (nullable): parent window information
339  * @title: title for the file chooser dialog
340  * @current_name: (nullable): suggested filename
341  * @current_folder: (nullable): suggested folder to save the file in
342  * @current_file: (nullable): the current file (when saving an existing file)
343  * @filters: (nullable): a [struct@GLib.Variant] describing file filters
344  * @current_filter: (nullable): a [struct@GLib.Variant] describing the current file filter
345  * @choices: (nullable): a [struct@GLib.Variant] describing extra widgets
346  * @flags: options for this call
347  * @cancellable: (nullable): optional [class@Gio.Cancellable]
348  * @callback: (scope async): a callback to call when the request is done
349  * @data: (closure): data to pass to @callback
350  *
351  * Asks the user for a location to save a file.
352  *
353  * The format for the @filters argument is the same as for [method@Portal.open_file].
354 
355  * The format for the @choices argument is the same as for [method@Portal.open_file].
356  *
357  * When the request is done, @callback will be called. You can then
358  * call [method@Portal.save_file_finish] to get the results.
359  */
360 void
xdp_portal_save_file(XdpPortal * portal,XdpParent * parent,const char * title,const char * current_name,const char * current_folder,const char * current_file,GVariant * filters,GVariant * current_filter,GVariant * choices,XdpSaveFileFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)361 xdp_portal_save_file (XdpPortal *portal,
362                       XdpParent *parent,
363                       const char *title,
364                       const char *current_name,
365                       const char *current_folder,
366                       const char *current_file,
367                       GVariant *filters,
368                       GVariant *current_filter,
369                       GVariant *choices,
370                       XdpSaveFileFlags flags,
371                       GCancellable *cancellable,
372                       GAsyncReadyCallback  callback,
373                       gpointer data)
374 {
375   FileCall *call;
376 
377   g_return_if_fail (XDP_IS_PORTAL (portal));
378   g_return_if_fail (flags == XDP_SAVE_FILE_FLAG_NONE);
379 
380   call = g_new0 (FileCall, 1);
381   call->portal = g_object_ref (portal);
382   if (parent)
383     call->parent = xdp_parent_copy (parent);
384   else
385     call->parent_handle = g_strdup ("");
386   call->method = "SaveFile";
387   call->title = g_strdup (title);
388   call->current_name = g_strdup (current_name);
389   call->current_folder = g_strdup (current_folder);
390   call->current_file = g_strdup (current_file);
391   call->filters = filters ? g_variant_ref (filters) : NULL;
392   call->current_filter = current_filter ? g_variant_ref (current_filter) : NULL;
393   call->choices = choices ? g_variant_ref (choices) : NULL;
394   call->task = g_task_new (portal, cancellable, callback, data);
395   g_task_set_source_tag (call->task, xdp_portal_save_file);
396 
397   open_file (call);
398 }
399 
400 /**
401  * xdp_portal_save_file_finish:
402  * @portal: a [class@Portal]
403  * @result: a [iface@Gio.AsyncResult]
404  * @error: return location for an error
405  *
406  * Finishes the save-file request, and returns
407  * the result in the form of a [struct@GLib.Variant] dictionary containing
408  * the following fields:
409  *
410  * - uris `(as)`: an array of strings containing the uri of the selected file
411  * - choices `a(ss)`: an array of pairs of strings, the first string being the
412  *   ID of a combobox that was passed into this call, the second string
413  *   being the selected option.
414  *
415  * Returns: (transfer full): a [struct@GLib.Variant] dictionary with the results
416  */
417 GVariant *
xdp_portal_save_file_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)418 xdp_portal_save_file_finish (XdpPortal *portal,
419                              GAsyncResult *result,
420                              GError **error)
421 {
422   GVariant *ret;
423 
424   g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
425   g_return_val_if_fail (g_task_is_valid (result, portal), NULL);
426   g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_save_file, NULL);
427 
428   ret = g_task_propagate_pointer (G_TASK (result), error);
429   return ret ? g_variant_ref (ret) : NULL;
430 }
431 
432 /**
433  * xdp_portal_save_files:
434  * @portal: a [class@Portal]
435  * @parent: (nullable): parent window information
436  * @title: title for the file chooser dialog
437  * @current_name: (nullable): suggested filename
438  * @current_folder: (nullable): suggested folder to save the file in
439  * @files: An array of file names to be saved
440  * @choices: (nullable): a [struct@GLib.Variant] describing extra widgets
441  * @flags: options for this call
442  * @cancellable: (nullable): optional [class@Gio.Cancellable]
443  * @callback: (scope async): a callback to call when the request is done
444  * @data: (closure): data to pass to @callback
445  *
446  * Asks for a folder as a location to save one or more files. The
447  * names of the files will be used as-is and appended to the selected
448  * folder's path in the list of returned files. If the selected folder
449  * already contains a file with one of the given names, the portal may
450  * prompt or take some other action to construct a unique file name and
451  * return that instead.
452  *
453  * The format for the @choices argument is the same as for [method@Portal.open_file].
454  *
455  * When the request is done, @callback will be called. You can then
456  * call [method@Portal.save_file_finish] to get the results.
457  */
458 void
xdp_portal_save_files(XdpPortal * portal,XdpParent * parent,const char * title,const char * current_name,const char * current_folder,GVariant * files,GVariant * choices,XdpSaveFileFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)459 xdp_portal_save_files (XdpPortal *portal,
460                        XdpParent *parent,
461                        const char *title,
462                        const char *current_name,
463                        const char *current_folder,
464                        GVariant *files,
465                        GVariant *choices,
466                        XdpSaveFileFlags flags,
467                        GCancellable *cancellable,
468                        GAsyncReadyCallback  callback,
469                        gpointer data)
470 {
471   FileCall *call;
472 
473   g_return_if_fail (XDP_IS_PORTAL (portal));
474   g_return_if_fail (files != NULL);
475   g_return_if_fail (flags == XDP_SAVE_FILE_FLAG_NONE);
476 
477   call = g_new0 (FileCall, 1);
478   call->portal = g_object_ref (portal);
479   if (parent)
480     call->parent = xdp_parent_copy (parent);
481   else
482     call->parent_handle = g_strdup ("");
483   call->method = "SaveFiles";
484   call->title = g_strdup (title);
485   call->current_name = g_strdup (current_name);
486   call->current_folder = g_strdup (current_folder);
487   call->files = g_variant_ref (files);
488   call->choices = choices ? g_variant_ref (choices) : NULL;
489   call->task = g_task_new (portal, cancellable, callback, data);
490   g_task_set_source_tag (call->task, xdp_portal_save_files);
491 
492   open_file (call);
493 }
494 
495 /**
496  * xdp_portal_save_files_finish:
497  * @portal: a [class@Portal]
498  * @result: a [iface@Gio.AsyncResult]
499  * @error: return location for an error
500  *
501  * Finishes the save-files request, and returns
502  * the result in the form of a [struct@GLib.Variant] dictionary containing
503  * the following fields:
504  *
505  * - uris `(as)`: an array of strings containing the uri corresponding to each
506  *   file passed to the save-files request, in the same order. Note that the
507  *   file names may have changed, for example if a file with the same name in
508  *   the selected folder already exists.
509  * - choices `a(ss)`: an array of pairs of strings, the first string being the
510  *   ID of a combobox that was passed into this call, the second string
511  *   being the selected option.
512  *
513  * Returns: (transfer full): a [struct@GLib.Variant] dictionary with the results
514  */
515 GVariant *
xdp_portal_save_files_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)516 xdp_portal_save_files_finish (XdpPortal *portal,
517                               GAsyncResult *result,
518                               GError **error)
519 {
520   GVariant *ret;
521 
522   g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
523   g_return_val_if_fail (g_task_is_valid (result, portal), NULL);
524   g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_save_files, NULL);
525 
526   ret = g_task_propagate_pointer (G_TASK (result), error);
527   return ret ? g_variant_ref (ret) : NULL;
528 }
529