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