Branch data Line data Source code
1 : : /*
2 : : * Copyright © 2024 GNOME Foundation Inc.
3 : : *
4 : : * SPDX-License-Identifier: LGPL-2.1-or-later
5 : : *
6 : : * This library is free software; you can redistribute it and/or
7 : : * modify it under the terms of the GNU Lesser General Public
8 : : * License as published by the Free Software Foundation; either
9 : : * version 2.1 of the License, or (at your option) any later version.
10 : : *
11 : : * This library is distributed in the hope that it will be useful,
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : : * Lesser General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU Lesser General
17 : : * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Authors: Julian Sparber <jsparber@gnome.org>
20 : : * Philip Withnall <philip@tecnocode.co.uk>
21 : : */
22 : :
23 : : /* A stub implementation of xdg-desktop-portal */
24 : :
25 : : #include <glib.h>
26 : : #include <glib/gstdio.h>
27 : : #include <glib/glib-unix.h>
28 : : #include <gio/gio.h>
29 : : #include <gio/gunixfdlist.h>
30 : :
31 : :
32 : : #include "fake-desktop-portal.h"
33 : : #include "fake-openuri-portal-generated.h"
34 : : #include "fake-request-portal-generated.h"
35 : :
36 : : struct _GFakeDesktopPortalThread
37 : : {
38 : : GObject parent_instance;
39 : :
40 : : char *address; /* (not nullable) */
41 : : GCancellable *cancellable; /* (not nullable) (owned) */
42 : : GThread *thread; /* (not nullable) (owned) */
43 : : GCond cond; /* (mutex mutex) */
44 : : GMutex mutex;
45 : : gboolean ready; /* (mutex mutex) */
46 : :
47 : : char *request_activation_token; /* (mutex mutex) */
48 : : char *request_uri; /* (mutex mutex) */
49 : : } FakeDesktopPortalThread;
50 : :
51 : 38 : G_DEFINE_FINAL_TYPE (GFakeDesktopPortalThread, g_fake_desktop_portal_thread, G_TYPE_OBJECT)
52 : :
53 : : static void g_fake_desktop_portal_thread_finalize (GObject *object);
54 : :
55 : : static void
56 : 1 : g_fake_desktop_portal_thread_class_init (GFakeDesktopPortalThreadClass *klass)
57 : : {
58 : 1 : GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
59 : :
60 : 1 : gobject_class->finalize = g_fake_desktop_portal_thread_finalize;
61 : 1 : }
62 : :
63 : : static void
64 : 4 : g_fake_desktop_portal_thread_init (GFakeDesktopPortalThread *self)
65 : : {
66 : 4 : self->cancellable = g_cancellable_new ();
67 : 4 : g_cond_init (&self->cond);
68 : 4 : g_mutex_init (&self->mutex);
69 : 4 : }
70 : :
71 : : static void
72 : 4 : g_fake_desktop_portal_thread_finalize (GObject *object)
73 : : {
74 : 4 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (object);
75 : :
76 : 4 : g_assert (self->thread == NULL); /* should already have been joined */
77 : :
78 : 4 : g_mutex_clear (&self->mutex);
79 : 4 : g_cond_clear (&self->cond);
80 : 4 : g_clear_object (&self->cancellable);
81 : 4 : g_clear_pointer (&self->address, g_free);
82 : :
83 : 4 : g_clear_pointer (&self->request_activation_token, g_free);
84 : 4 : g_clear_pointer (&self->request_uri, g_free);
85 : :
86 : 4 : G_OBJECT_CLASS (g_fake_desktop_portal_thread_parent_class)->finalize (object);
87 : 4 : }
88 : :
89 : : static gboolean
90 : 0 : on_handle_close (FakeRequest *object,
91 : : GDBusMethodInvocation *invocation,
92 : : gpointer user_data)
93 : : {
94 : 0 : g_test_message ("Got close request");
95 : 0 : fake_request_complete_close (object, invocation);
96 : :
97 : 0 : return G_DBUS_METHOD_INVOCATION_HANDLED;
98 : : }
99 : :
100 : : static char*
101 : 4 : get_request_path (GDBusMethodInvocation *invocation,
102 : : const char *token)
103 : : {
104 : : char *request_obj_path;
105 : : char *sender;
106 : :
107 : 4 : sender = g_strdup (g_dbus_method_invocation_get_sender (invocation) + 1);
108 : :
109 : : /* The object path needs to be the specific format.
110 : : * See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request */
111 : 19 : for (size_t i = 0; sender[i]; i++)
112 : 15 : if (sender[i] == '.')
113 : 4 : sender[i] = '_';
114 : :
115 : 4 : request_obj_path = g_strdup_printf ("/org/freedesktop/portal/desktop/request/%s/%s", sender, token);
116 : 4 : g_free (sender);
117 : :
118 : 4 : return request_obj_path;
119 : : }
120 : :
121 : : static gboolean
122 : 4 : handle_request (GFakeDesktopPortalThread *self,
123 : : FakeOpenURI *object,
124 : : GDBusMethodInvocation *invocation,
125 : : const gchar *arg_parent_window,
126 : : const gchar *arg_uri,
127 : : gboolean open_file,
128 : : GVariant *arg_options)
129 : : {
130 : 4 : const char *activation_token = NULL;
131 : 4 : GError *error = NULL;
132 : : FakeRequest *interface_request;
133 : : GVariantBuilder opt_builder;
134 : : char *request_obj_path;
135 : 4 : const char *token = NULL;
136 : :
137 : 4 : if (arg_options)
138 : : {
139 : 4 : g_variant_lookup (arg_options, "activation_token", "&s", &activation_token);
140 : 4 : g_variant_lookup (arg_options, "handle_token", "&s", &token);
141 : : }
142 : :
143 : 4 : g_set_str (&self->request_activation_token, activation_token);
144 : 4 : g_set_str (&self->request_uri, arg_uri);
145 : :
146 : 4 : request_obj_path = get_request_path (invocation, token ? token : "t");
147 : :
148 : 4 : if (open_file)
149 : : {
150 : 4 : g_test_message ("Got open file request for %s", arg_uri);
151 : :
152 : 4 : fake_open_uri_complete_open_file (object,
153 : : invocation,
154 : : NULL,
155 : : request_obj_path);
156 : :
157 : : }
158 : : else
159 : : {
160 : 0 : g_test_message ("Got open URI request for %s", arg_uri);
161 : :
162 : 0 : fake_open_uri_complete_open_uri (object,
163 : : invocation,
164 : : request_obj_path);
165 : :
166 : : }
167 : :
168 : 4 : interface_request = fake_request_skeleton_new ();
169 : 4 : g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
170 : :
171 : 4 : g_signal_connect (interface_request,
172 : : "handle-close",
173 : : G_CALLBACK (on_handle_close),
174 : : NULL);
175 : :
176 : 4 : g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (interface_request),
177 : : g_dbus_method_invocation_get_connection (invocation),
178 : : request_obj_path,
179 : : &error);
180 : 4 : g_assert_no_error (error);
181 : 4 : g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (interface_request),
182 : : G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD);
183 : 4 : g_test_message ("Request skeleton exported at %s", request_obj_path);
184 : :
185 : : /* We can't use `fake_request_emit_response()` because it doesn't set the sender */
186 : 4 : g_dbus_connection_emit_signal (g_dbus_method_invocation_get_connection (invocation),
187 : : g_dbus_method_invocation_get_sender (invocation),
188 : : request_obj_path,
189 : : "org.freedesktop.portal.Request",
190 : : "Response",
191 : : g_variant_new ("(u@a{sv})",
192 : : 0, /* Success */
193 : : g_variant_builder_end (&opt_builder)),
194 : : NULL);
195 : :
196 : 4 : g_test_message ("Response emitted");
197 : :
198 : 4 : g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (interface_request));
199 : 4 : g_free (request_obj_path);
200 : 4 : g_object_unref (interface_request);
201 : :
202 : 4 : return G_DBUS_METHOD_INVOCATION_HANDLED;
203 : : }
204 : :
205 : : static char *
206 : 2 : handle_to_uri (GVariant *handle,
207 : : GUnixFDList *fd_list)
208 : : {
209 : 2 : int fd = -1;
210 : : int fd_id;
211 : : char *path;
212 : : char *uri;
213 : 2 : GError *local_error = NULL;
214 : :
215 : 2 : fd_id = g_variant_get_handle (handle);
216 : 2 : fd = g_unix_fd_list_get (fd_list, fd_id, NULL);
217 : :
218 : 2 : if (fd == -1)
219 : 0 : return NULL;
220 : :
221 : 2 : path = g_unix_fd_query_path (fd, &local_error);
222 : 2 : g_assert_no_error (local_error);
223 : :
224 : 2 : uri = g_filename_to_uri (path, NULL, NULL);
225 : 2 : g_free (path);
226 : 2 : close (fd);
227 : :
228 : 2 : return uri;
229 : : }
230 : :
231 : : static gboolean
232 : 2 : on_handle_open_file (FakeOpenURI *object,
233 : : GDBusMethodInvocation *invocation,
234 : : GUnixFDList *fd_list,
235 : : const gchar *arg_parent_window,
236 : : GVariant *arg_fd,
237 : : GVariant *arg_options,
238 : : gpointer user_data)
239 : : {
240 : 2 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
241 : 2 : char *uri = NULL;
242 : :
243 : 2 : uri = handle_to_uri (arg_fd, fd_list);
244 : 2 : handle_request (self,
245 : : object,
246 : : invocation,
247 : : arg_parent_window,
248 : : uri,
249 : : TRUE,
250 : : arg_options);
251 : 2 : g_free (uri);
252 : :
253 : 2 : return G_DBUS_METHOD_INVOCATION_HANDLED;
254 : : }
255 : :
256 : : static gboolean
257 : 2 : on_handle_open_uri (FakeOpenURI *object,
258 : : GDBusMethodInvocation *invocation,
259 : : const gchar *arg_parent_window,
260 : : const gchar *arg_uri,
261 : : GVariant *arg_options,
262 : : gpointer user_data)
263 : : {
264 : 2 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
265 : :
266 : 2 : handle_request (self,
267 : : object,
268 : : invocation,
269 : : arg_parent_window,
270 : : arg_uri,
271 : : TRUE,
272 : : arg_options);
273 : :
274 : 2 : return G_DBUS_METHOD_INVOCATION_HANDLED;
275 : : }
276 : :
277 : : static void
278 : 4 : on_name_acquired (GDBusConnection *connection,
279 : : const gchar *name,
280 : : gpointer user_data)
281 : : {
282 : 4 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
283 : :
284 : 4 : g_test_message ("Acquired the name %s", name);
285 : :
286 : 4 : g_mutex_lock (&self->mutex);
287 : 4 : self->ready = TRUE;
288 : 4 : g_cond_signal (&self->cond);
289 : 4 : g_mutex_unlock (&self->mutex);
290 : 4 : }
291 : :
292 : : static void
293 : 0 : on_name_lost (GDBusConnection *connection,
294 : : const gchar *name,
295 : : gpointer user_data)
296 : : {
297 : 0 : g_test_message ("Lost the name %s", name);
298 : 0 : }
299 : :
300 : : static gboolean
301 : 4 : cancelled_cb (GCancellable *cancellable,
302 : : gpointer user_data)
303 : : {
304 : 4 : g_test_message ("fake-desktop-portal cancelled");
305 : 4 : return G_SOURCE_CONTINUE;
306 : : }
307 : :
308 : : static gpointer
309 : 4 : fake_desktop_portal_thread_cb (gpointer user_data)
310 : : {
311 : 4 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
312 : 4 : GMainContext *context = NULL;
313 : 4 : GDBusConnection *connection = NULL;
314 : 4 : GSource *source = NULL;
315 : : guint id;
316 : : FakeOpenURI *interface_open_uri;
317 : 4 : GError *local_error = NULL;
318 : :
319 : 4 : context = g_main_context_new ();
320 : 4 : g_main_context_push_thread_default (context);
321 : :
322 : 4 : connection = g_dbus_connection_new_for_address_sync (self->address,
323 : : G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
324 : : G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
325 : : NULL,
326 : : self->cancellable,
327 : : &local_error);
328 : 4 : g_assert_no_error (local_error);
329 : :
330 : : /* Listen for cancellation. The source will wake up the context iteration
331 : : * which can then re-check its exit condition below. */
332 : 4 : source = g_cancellable_source_new (self->cancellable);
333 : 4 : g_source_set_callback (source, G_SOURCE_FUNC (cancelled_cb), NULL, NULL);
334 : 4 : g_source_attach (source, context);
335 : 4 : g_source_unref (source);
336 : :
337 : : /* Set up the interface */
338 : 4 : g_test_message ("Acquired a message bus connection");
339 : :
340 : 4 : interface_open_uri = fake_open_uri_skeleton_new ();
341 : :
342 : 4 : g_signal_connect (interface_open_uri,
343 : : "handle-open-file",
344 : : G_CALLBACK (on_handle_open_file),
345 : : self);
346 : 4 : g_signal_connect (interface_open_uri,
347 : : "handle-open-uri",
348 : : G_CALLBACK (on_handle_open_uri),
349 : : self);
350 : :
351 : 4 : g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (interface_open_uri),
352 : : connection,
353 : : "/org/freedesktop/portal/desktop",
354 : : &local_error);
355 : 4 : g_assert_no_error (local_error);
356 : :
357 : : /* Own the portal name */
358 : 4 : id = g_bus_own_name_on_connection (connection,
359 : : "org.freedesktop.portal.Desktop",
360 : : G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
361 : : G_BUS_NAME_OWNER_FLAGS_REPLACE,
362 : : on_name_acquired,
363 : : on_name_lost,
364 : : self,
365 : : NULL);
366 : :
367 : 20 : while (!g_cancellable_is_cancelled (self->cancellable))
368 : 16 : g_main_context_iteration (context, TRUE);
369 : :
370 : 4 : g_bus_unown_name (id);
371 : 4 : g_clear_object (&connection);
372 : 4 : g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (interface_open_uri));
373 : 4 : g_object_unref (interface_open_uri);
374 : 4 : g_main_context_pop_thread_default (context);
375 : 4 : g_clear_pointer (&context, g_main_context_unref);
376 : :
377 : 4 : return NULL;
378 : : }
379 : :
380 : : /* Get the activation token given to the most recent OpenURI request
381 : : *
382 : : * Returns: (transfer none) (nullable: an activation token
383 : : */
384 : : const gchar *
385 : 4 : g_fake_desktop_portal_thread_get_last_request_activation_token (GFakeDesktopPortalThread *self)
386 : : {
387 : 4 : g_return_val_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self), NULL);
388 : :
389 : 4 : return self->request_activation_token;
390 : : }
391 : :
392 : : /* Get the file or URI given to the most recent OpenURI request
393 : : *
394 : : * Returns: (transfer none) (nullable): an URI
395 : : */
396 : : const gchar *
397 : 4 : g_fake_desktop_portal_thread_get_last_request_uri (GFakeDesktopPortalThread *self)
398 : : {
399 : 4 : g_return_val_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self), NULL);
400 : :
401 : 4 : return self->request_uri;
402 : : }
403 : :
404 : : /*
405 : : * Create a new #GFakeDesktopPortalThread. The thread isn’t started until
406 : : * g_fake_desktop_portal_thread_run() is called on it.
407 : : *
408 : : * Returns: (transfer full): the new fake desktop portal wrapper
409 : : */
410 : : GFakeDesktopPortalThread *
411 : 4 : g_fake_desktop_portal_thread_new (const char *address)
412 : : {
413 : 4 : GFakeDesktopPortalThread *self = g_object_new (G_TYPE_FAKE_DESKTOP_PORTAL_THREAD, NULL);
414 : 4 : self->address = g_strdup (address);
415 : 4 : return g_steal_pointer (&self);
416 : : }
417 : :
418 : : /*
419 : : * Start a worker thread which will run a fake
420 : : * `org.freedesktop.portal.Desktops` portal on the bus at @address. This is
421 : : * intended to be used with #GTestDBus to mock up a portal from within a unit
422 : : * test process, rather than relying on D-Bus activation of a mock portal
423 : : * subprocess.
424 : : *
425 : : * It blocks until the thread has owned its D-Bus name and is ready to handle
426 : : * requests.
427 : : */
428 : : void
429 : 4 : g_fake_desktop_portal_thread_run (GFakeDesktopPortalThread *self)
430 : : {
431 : 4 : g_return_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self));
432 : 4 : g_return_if_fail (self->thread == NULL);
433 : :
434 : 4 : self->thread = g_thread_new ("fake-desktop-portal", fake_desktop_portal_thread_cb, self);
435 : :
436 : : /* Block until the thread is ready. */
437 : 4 : g_mutex_lock (&self->mutex);
438 : 8 : while (!self->ready)
439 : 4 : g_cond_wait (&self->cond, &self->mutex);
440 : 4 : g_mutex_unlock (&self->mutex);
441 : : }
442 : :
443 : : /* Stop and join a worker thread started with fake_desktop_portal_thread_run().
444 : : * Blocks until the thread has stopped and joined.
445 : : *
446 : : * Once this has been called, it’s safe to drop the final reference on @self. */
447 : : void
448 : 4 : g_fake_desktop_portal_thread_stop (GFakeDesktopPortalThread *self)
449 : : {
450 : 4 : g_return_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self));
451 : 4 : g_return_if_fail (self->thread != NULL);
452 : :
453 : 4 : g_cancellable_cancel (self->cancellable);
454 : 4 : g_thread_join (g_steal_pointer (&self->thread));
455 : : }
456 : :
457 : : /* Whether fake-desktop-portal is supported on this platform. This basically
458 : : * means whether g_unix_fd_query_path() will work at runtime. */
459 : : gboolean
460 : 4 : g_fake_desktop_portal_is_supported (void)
461 : : {
462 : : enum {
463 : : UNKNOWN,
464 : : SUPPORTED,
465 : : UNSUPPORTED,
466 : : };
467 : :
468 : : static size_t checked = UNKNOWN;
469 : :
470 : 4 : if g_once_init_enter (&checked)
471 : : {
472 : : char *fd_path;
473 : : int fd;
474 : :
475 : 1 : fd = g_open ("/dev/null", O_RDONLY);
476 : 1 : fd_path = g_unix_fd_query_path (fd, NULL);
477 : 1 : g_free (fd_path);
478 : 1 : g_clear_fd (&fd, NULL);
479 : :
480 : 1 : g_once_init_leave (&checked, fd_path != NULL ? SUPPORTED : UNSUPPORTED);
481 : : }
482 : :
483 : 4 : return checked == SUPPORTED;
484 : : }
|