diff --git a/.gitignore b/.gitignore index f32443a..aef7f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ /xdg-desktop-portal-1.18.1.tar.xz /xdg-desktop-portal-1.18.2.tar.xz /xdg-desktop-portal-1.18.4.tar.xz +/xdg-desktop-portal-1.19.1.tar.xz diff --git a/sources b/sources index 9d1b93e..c9d8fed 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (xdg-desktop-portal-1.18.4.tar.xz) = 482676777e8180752e13ff485acab91de117f4ecf2aefd313a6f596df800f37d7004ee4ba1040009eb69a6efc780efc7dcd7e46b3f59c47bd9f21aef48d346de +SHA512 (xdg-desktop-portal-1.19.1.tar.xz) = c480cdc6fbbdeedbfe0e37ac463b6afa9127efa1b3e6c665c28e272c23f5d41e6f3f0e8e3c69c31d0829032a009bcf04643ba7f782332a3eccf591eca5c6dff4 diff --git a/xdg-desktop-portal-webextensions.patch b/xdg-desktop-portal-webextensions.patch new file mode 100644 index 0000000..e45f82c --- /dev/null +++ b/xdg-desktop-portal-webextensions.patch @@ -0,0 +1,1986 @@ +From b59c90fbd7bfd23bca1ce866e3c88e93fc678381 Mon Sep 17 00:00:00 2001 +From: Jan Horak +Date: Tue, 17 Dec 2024 17:18:33 +0100 +Subject: [PATCH] webextensions: add a portal for managing WebExtensions native + messaging servers. + +Rebase, fix and continue work on webextensions: add a portal for managing WebExtensions +native messaging servers: +https://github.com/flatpak/xdg-desktop-portal/pull/705 + +This commit builds on the work done in the original MR authored by @jhenstridge +but resolves pending items and brings it closer to completion. + +This is intended to provide a way for a confined web browser to start +native code helpers for their extensions. At present it can start the +servers installed on the host system. But in future this could be +extended to cover sandboxed native messaging servers too. +--- + data/meson.build | 1 + + data/org.freedesktop.portal.WebExtensions.xml | 158 ++++ + meson.build | 1 + + po/POTFILES.in | 1 + + src/meson.build | 1 + + src/web-extensions.c | 875 ++++++++++++++++++ + src/web-extensions.h | 24 + + src/xdg-desktop-portal.c | 4 + + src/xdp-request.c | 12 + + tests/meson.build | 3 + + tests/native-messaging-hosts/meson.build | 28 + + .../org.example.testing.json.in | 9 + + tests/native-messaging-hosts/server.sh | 3 + + tests/test-portals.c | 7 + + tests/web-extensions.c | 632 +++++++++++++ + tests/web-extensions.h | 2 + + 16 files changed, 1761 insertions(+) + create mode 100644 data/org.freedesktop.portal.WebExtensions.xml + create mode 100644 src/web-extensions.c + create mode 100644 src/web-extensions.h + create mode 100644 tests/native-messaging-hosts/meson.build + create mode 100644 tests/native-messaging-hosts/org.example.testing.json.in + create mode 100755 tests/native-messaging-hosts/server.sh + create mode 100644 tests/web-extensions.c + create mode 100644 tests/web-extensions.h + +diff --git a/data/meson.build b/data/meson.build +index 66ad68d16..11c36a053 100644 +--- a/data/meson.build ++++ b/data/meson.build +@@ -37,6 +37,7 @@ portal_sources = files( + 'org.freedesktop.portal.Trash.xml', + 'org.freedesktop.portal.Usb.xml', + 'org.freedesktop.portal.Wallpaper.xml', ++ 'org.freedesktop.portal.WebExtensions.xml', + ) + + portal_impl_sources = files( +diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml +new file mode 100644 +index 000000000..2725c0234 +--- /dev/null ++++ b/data/org.freedesktop.portal.WebExtensions.xml +@@ -0,0 +1,158 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/meson.build b/meson.build +index 876a531a1..55342e2a6 100644 +--- a/meson.build ++++ b/meson.build +@@ -82,6 +82,7 @@ config_h = configuration_data() + config_h.set('_GNU_SOURCE', 1) + config_h.set_quoted('G_LOG_DOMAIN', 'xdg-desktop-portal') + config_h.set_quoted('DATADIR', datadir) ++config_h.set_quoted('LIBDIR', libdir) + config_h.set_quoted('LIBEXECDIR', libexecdir) + config_h.set_quoted('LOCALEDIR', localedir) + config_h.set_quoted('SYSCONFDIR', sysconfdir) +diff --git a/po/POTFILES.in b/po/POTFILES.in +index d514b2167..fd3232518 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -7,3 +7,4 @@ src/screenshot.c + src/settings.c + src/usb.c + src/wallpaper.c ++src/web-extensions.c +diff --git a/src/meson.build b/src/meson.build +index 5b6d553b6..e306b3eab 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -96,6 +96,7 @@ xdg_desktop_portal_sources = files( + 'settings.c', + 'trash.c', + 'wallpaper.c', ++ 'web-extensions.c', + 'xdg-desktop-portal.c', + 'xdp-app-launch-context.c', + 'xdp-background-monitor.c', +diff --git a/src/web-extensions.c b/src/web-extensions.c +new file mode 100644 +index 000000000..a33fee3e1 +--- /dev/null ++++ b/src/web-extensions.c +@@ -0,0 +1,875 @@ ++/* ++ * Copyright © 2022 Canonical Ltd ++ * ++ * This program 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 . ++ * ++ */ ++ ++#include "config.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "gio/gdesktopappinfo.h" ++#include "gio/gio.h" ++#include "glib.h" ++#include "xdp-session.h" ++#include "web-extensions.h" ++#include "xdp-request.h" ++#include "xdp-permissions.h" ++#include "xdp-dbus.h" ++#include "xdp-impl-dbus.h" ++#include "xdp-utils.h" ++#include "xdp-app-info.h" ++ ++#define PERMISSION_TABLE "webextensions" ++ ++typedef struct _WebExtensions WebExtensions; ++typedef struct _WebExtensionsClass WebExtensionsClass; ++ ++struct _WebExtensions ++{ ++ XdpDbusWebExtensionsSkeleton parent_instance; ++}; ++ ++struct _WebExtensionsClass ++{ ++ XdpDbusWebExtensionsSkeletonClass parent_class; ++}; ++ ++static XdpDbusImplAccess *access_impl; ++static WebExtensions *web_extensions; ++ ++GType web_extensions_get_type (void); ++static void web_extensions_iface_init (XdpDbusWebExtensionsIface *iface); ++ ++G_DEFINE_TYPE_WITH_CODE (WebExtensions, web_extensions, XDP_DBUS_TYPE_WEB_EXTENSIONS_SKELETON, ++ G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_WEB_EXTENSIONS, ++ web_extensions_iface_init)); ++ ++typedef enum _WebExtensionsSessionMode ++{ ++ WEB_EXTENSIONS_SESSION_MODE_CHROMIUM, ++ WEB_EXTENSIONS_SESSION_MODE_MOZILLA, ++} WebExtensionsSessionMode; ++ ++typedef enum _WebExtensionsSessionState ++{ ++ WEB_EXTENSIONS_SESSION_STATE_INIT, ++ WEB_EXTENSIONS_SESSION_STATE_STARTING, ++ WEB_EXTENSIONS_SESSION_STATE_STARTED, ++ WEB_EXTENSIONS_SESSION_STATE_CLOSED, ++} WebExtensionsSessionState; ++ ++typedef struct _WebExtensionsSession ++{ ++ XdpSession parent; ++ ++ WebExtensionsSessionMode mode; ++ WebExtensionsSessionState state; ++ ++ GPid child_pid; ++ guint child_watch_id; ++ ++ int standard_input; ++ int standard_output; ++ int standard_error; ++} WebExtensionsSession; ++ ++typedef struct _WebExtensionsSessionClass ++{ ++ XdpSessionClass parent_class; ++} WebExtensionsSessionClass; ++ ++GType web_extensions_session_get_type (void); ++ ++G_DEFINE_TYPE (WebExtensionsSession, web_extensions_session, xdp_session_get_type ()); ++ ++static void ++web_extensions_session_init (WebExtensionsSession *session) ++{ ++ session->child_pid = -1; ++ session->child_watch_id = 0; ++ ++ session->standard_input = -1; ++ session->standard_output = -1; ++ session->standard_error = -1; ++} ++ ++static void ++web_extensions_session_close (XdpSession *session) ++{ ++ WebExtensionsSession *web_extensions_session = (WebExtensionsSession *)session; ++ ++ /* This function can be called repeatedly, e.g. by an explicit ++ "org.freedesktop.portal.Session::Close" message followed by ++ a call to finalize due to session's refcount reaching zero. ++ */ ++ if (web_extensions_session->state == WEB_EXTENSIONS_SESSION_STATE_CLOSED) return; ++ ++ /* We can assume that it is safe to transition to ++ WEB_EXTENSIONS_SESSION_STATE_CLOSED here, because we arrive ++ at web_extensions_session_close via one of two ways: ++ ++ 1. via the session_class->close function pointer from the ++ session_close function in src/session.c, which expects ++ its caller to lock the session's mutex (usually via the ++ SESSION_AUTOLOCK_UNREF macro); or ++ 2. from web_extensions_session_finalize, at which point ++ the last reference to the session has been released. ++ */ ++ web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_CLOSED; ++ ++ if (web_extensions_session->child_watch_id != 0) ++ { ++ g_source_remove (web_extensions_session->child_watch_id); ++ web_extensions_session->child_watch_id = 0; ++ } ++ ++ if (web_extensions_session->child_pid > 0) ++ { ++ /* The responsibility of gracefully killing the process is ++ delegated to the browser, and the following SIGKILL is ++ a final attempt to clean up if necessary. ++ */ ++ kill (web_extensions_session->child_pid, SIGKILL); ++ waitpid (web_extensions_session->child_pid, NULL, 0); ++ g_spawn_close_pid (web_extensions_session->child_pid); ++ web_extensions_session->child_pid = -1; ++ } ++ ++ if (web_extensions_session->standard_input >= 0) ++ { ++ close (web_extensions_session->standard_input); ++ web_extensions_session->standard_input = -1; ++ } ++ if (web_extensions_session->standard_output >= 0) ++ { ++ close (web_extensions_session->standard_output); ++ web_extensions_session->standard_output = -1; ++ } ++ if (web_extensions_session->standard_error >= 0) ++ { ++ close (web_extensions_session->standard_error); ++ web_extensions_session->standard_error = -1; ++ } ++} ++ ++static void ++web_extensions_session_finalize (GObject *object) ++{ ++ XdpSession *session = (XdpSession *)object; ++ ++ web_extensions_session_close (session); ++ G_OBJECT_CLASS (web_extensions_session_parent_class)->finalize (object); ++} ++ ++static void ++web_extensions_session_class_init (WebExtensionsSessionClass *klass) ++{ ++ /* Called at the first instantiation of WebExtensionsSession, ++ i.e. in web_extensions_session_new with the call to ++ g_initable_new. ++ https://docs.gtk.org/gobject/concepts.html#object-instantiation ++ https://docs.gtk.org/gio/type_func.Initable.new.html ++ */ ++ GObjectClass *object_class; ++ XdpSessionClass *session_class; ++ ++ object_class = G_OBJECT_CLASS (klass); ++ /* finalize is called when the session refcount reaches zero. ++ https://docs.gtk.org/gobject/concepts.html#reference-count ++ */ ++ object_class->finalize = web_extensions_session_finalize; ++ ++ session_class = (XdpSessionClass *)klass; ++ /* Register handler for org.freedesktop.portal.Session::Close */ ++ session_class->close = web_extensions_session_close; ++} ++ ++static WebExtensionsSession * ++web_extensions_session_new (GVariant *options, ++ XdpCall *call, ++ GDBusConnection *connection, ++ GError **error) ++{ ++ XdpSession *session; ++ WebExtensionsSession *web_extensions_session; ++ WebExtensionsSessionMode mode = WEB_EXTENSIONS_SESSION_MODE_MOZILLA; ++ const char *mode_str = NULL; ++ const char *session_token; ++ ++ g_variant_lookup (options, "mode", "&s", &mode_str); ++ if (mode_str != NULL) ++ { ++ if (!strcmp(mode_str, "chromium")) ++ mode = WEB_EXTENSIONS_SESSION_MODE_CHROMIUM; ++ else if (!strcmp(mode_str, "mozilla")) ++ mode = WEB_EXTENSIONS_SESSION_MODE_MOZILLA; ++ else ++ { ++ g_set_error (error, ++ XDG_DESKTOP_PORTAL_ERROR, ++ XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, ++ "Invalid mode"); ++ return NULL; ++ } ++ } ++ ++ session_token = lookup_session_token (options); ++ session = g_initable_new (web_extensions_session_get_type (), NULL, error, ++ "sender", call->sender, ++ "app-id", xdp_app_info_get_id (call->app_info), ++ "token", session_token, ++ "connection", connection, ++ NULL); ++ ++ if (session) ++ g_debug ("webextensions session owned by '%s' created", session->sender); ++ ++ if (!session) ++ { ++ g_warning ("Could not create WebExtensions session: %s", (*error)->message); ++ return NULL; ++ } ++ web_extensions_session = (WebExtensionsSession *)session; ++ web_extensions_session->mode = mode; ++ return web_extensions_session; ++} ++ ++static gboolean ++handle_create_session (XdpDbusWebExtensions *object, ++ GDBusMethodInvocation *invocation, ++ GVariant *arg_options) ++{ ++ XdpCall *call = xdp_call_from_invocation (invocation); ++ GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); ++ g_autoptr(GError) error = NULL; ++ XdpSession *session; ++ ++ session = (XdpSession *)web_extensions_session_new (arg_options, call, connection, &error); ++ if (!session) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ return TRUE; ++ } ++ if (!xdp_session_export (session, &error)) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ xdp_session_close (session, FALSE); ++ return TRUE; ++ } ++ xdp_session_register (session); ++ ++ xdp_dbus_web_extensions_complete_create_session (object, invocation, session->id); ++ ++ return TRUE; ++} ++ ++static void ++on_host_exited (GPid pid, ++ gint status, ++ gpointer user_data) ++{ ++ XdpSession *session = user_data; ++ WebExtensionsSession *web_extensions_session = (WebExtensionsSession *)session; ++ ++ SESSION_AUTOLOCK (session); ++ web_extensions_session->child_pid = -1; ++ web_extensions_session->child_watch_id = 0; ++ xdp_session_close (session, TRUE); ++} ++ ++static gboolean ++array_contains (JsonArray *array, ++ const char *value) ++{ ++ guint length, i; ++ ++ if (array == NULL) ++ return FALSE; ++ ++ length = json_array_get_length (array); ++ for (i = 0; i < length; i++) ++ { ++ const char *element = json_array_get_string_element (array, i); ++ if (g_strcmp0 (element, value) == 0) ++ return TRUE; ++ } ++ return FALSE; ++} ++ ++static gboolean ++is_valid_name (const char *name) ++{ ++ /* This regexp comes from the Mozilla documentation on valid native ++ messaging host names: ++ ++ https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests ++ ++ That is, one or more dot-separated groups composed of ++ alphanumeric characters and underscores. ++ */ ++ return g_regex_match_simple ("^\\w+(\\.\\w+)*$", name, 0, 0); ++} ++ ++static GStrv ++get_manifest_search_path (WebExtensionsSessionMode mode) ++{ ++ /* IMPORTANT: ++ The safety model depends on the inability of the sandboxed ++ browser to write to the search locations specified below. ++ ++ As this portal allows browser extensions to run a native ++ messaging application outside of the sandbox through the portal, ++ the sandboxing mechanism must ensure that these locations are ++ inaccessible to the browser. If the locations are both readable ++ AND writable by the sandboxed browser, then a vulnerability ++ resulting in a file writing primitive within the sandbox could ++ result in arbitrary code execution outside of the sandbox through ++ the portal. ++ ++ For example, the Firefox Snap package meets this criterion, ++ because all strictly confined Snap packages (including Firefox) ++ are prohibited by AppArmor from accessing most directories and ++ files in the user's home directory, except where explicitly ++ specified, for instance using the 'personal-files' interface. ++ https://snapcraft.io/docs/personal-files-interface ++ */ ++ const char *hosts_path_str; ++ g_autoptr(GPtrArray) search_path = NULL; ++ ++ hosts_path_str = g_getenv ("XDG_DESKTOP_PORTAL_WEB_EXTENSIONS_PATH"); ++ if (hosts_path_str != NULL) ++ return g_strsplit (hosts_path_str, ":", -1); ++ ++ search_path = g_ptr_array_new_with_free_func (g_free); ++ switch (mode) ++ { ++ case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: ++ /* Chrome and Chromium search paths documented here: ++ * https://developer.chrome.com/docs/extensions/nativeMessaging/#native-messaging-host-location ++ */ ++ /* Add per-user directories */ ++ g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "google-chrome", "NativeMessagingHosts", NULL)); ++ g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "chromium", "NativeMessagingHosts", NULL)); ++ /* Add system wide directories */ ++ g_ptr_array_add (search_path, g_strdup ("/etc/opt/chrome/native-messaging-hosts")); ++ g_ptr_array_add (search_path, g_strdup ("/etc/chromium/native-messaging-hosts")); ++ /* And the same for xdg-desktop-portal's configured prefix */ ++ g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "/opt/chrome/native-messaging-hosts")); ++ g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "/chromium/native-messaging-hosts")); ++ break; ++ ++ case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: ++ /* Firefox search paths documented here: ++ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#manifest_location ++ */ ++ /* Add per-user directories */ ++ g_ptr_array_add (search_path, g_build_filename (g_get_home_dir (), ".mozilla", "native-messaging-hosts", NULL)); ++ g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "mozilla", "native-messaging-hosts", NULL)); ++ /* Add system wide directories */ ++ g_ptr_array_add (search_path, g_strdup ("/usr/lib/mozilla/native-messaging-hosts")); ++ g_ptr_array_add (search_path, g_strdup ("/usr/lib64/mozilla/native-messaging-hosts")); ++ /* And the same for xdg-desktop-portal's configured prefix. ++ This is helpful on Debian-based systems where LIBDIR is ++ suffixed with 'dpkg-architecture -qDEB_HOST_MULTIARCH', ++ e.g. '/usr/lib/x86_64-linux-gnu'. ++ https://salsa.debian.org/debian/debhelper/-/blob/5b96b19b456fe5e094f2870327a753b4b3ece0dc/lib/Debian/Debhelper/Buildsystem/meson.pm#L78 ++ */ ++ g_ptr_array_add (search_path, g_strdup (LIBDIR "/mozilla/native-messaging-hosts")); ++ break; ++ } ++ ++ g_ptr_array_add (search_path, NULL); ++ return (GStrv)g_ptr_array_free (g_steal_pointer (&search_path), FALSE); ++} ++ ++static char * ++find_messaging_host (WebExtensionsSessionMode mode, ++ const char *messaging_host_name, ++ const char *extension_or_origin, ++ char **out_description, ++ char **out_manifest_filename, ++ char **out_json_manifest, ++ GError **error) ++{ ++ g_auto(GStrv) search_path = NULL; ++ g_autoptr(JsonParser) parser = NULL; ++ g_autofree char *metadata_basename = NULL; ++ int i; ++ ++ /* Check that the we have a valid native messaging host name */ ++ if (!is_valid_name (messaging_host_name)) ++ { ++ g_set_error (error, ++ XDG_DESKTOP_PORTAL_ERROR, ++ XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, ++ "Invalid native messaging host name"); ++ return NULL; ++ } ++ ++ search_path = get_manifest_search_path (mode); ++ parser = json_parser_new (); ++ metadata_basename = g_strconcat (messaging_host_name, ".json", NULL); ++ ++ for (i = 0; search_path[i] != NULL; i++) ++ { ++ g_autofree char *metadata_filename = NULL; ++ g_autoptr(GError) load_error = NULL; ++ JsonObject *metadata_root; ++ const char *host_path; ++ ++ metadata_filename = g_build_filename (search_path[i], metadata_basename, NULL); ++ if (!json_parser_load_from_file (parser, metadata_filename, &load_error)) ++ { ++ /* If the file doesn't exist, continue searching. Error out ++ on anything else. */ ++ if (g_error_matches (load_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) ++ continue; ++ g_propagate_error (error, g_steal_pointer (&load_error)); ++ return NULL; ++ } ++ ++ metadata_root = json_node_get_object (json_parser_get_root (parser)); ++ ++ /* Skip if metadata contains an unexpected name */ ++ if (g_strcmp0 (json_object_get_string_member (metadata_root, "name"), messaging_host_name) != 0) ++ continue; ++ ++ /* Skip if this is not a "stdio" type native messaging host */ ++ if (g_strcmp0 (json_object_get_string_member (metadata_root, "type"), "stdio") != 0) ++ continue; ++ ++ /* Skip if this host isn't available to the extension. Note ++ * that this ID is provided by the sandboxed browser, so this ++ * check is just to help implement its security policy. */ ++ switch (mode) ++ { ++ case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: ++ if (!array_contains (json_object_get_array_member (metadata_root, "allowed_origins"), extension_or_origin)) ++ continue; ++ break; ++ case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: ++ if (!array_contains (json_object_get_array_member (metadata_root, "allowed_extensions"), extension_or_origin)) ++ continue; ++ break; ++ } ++ ++ host_path = json_object_get_string_member (metadata_root, "path"); ++ if (!g_path_is_absolute (host_path)) ++ { ++ g_set_error (error, ++ XDG_DESKTOP_PORTAL_ERROR, ++ XDG_DESKTOP_PORTAL_ERROR_FAILED, ++ "Native messaging host path is not absolute"); ++ return NULL; ++ } ++ ++ /* Host matches: return its executable path and description */ ++ if (out_description != NULL) ++ *out_description = g_strdup (json_object_get_string_member (metadata_root, "description")); ++ if (out_manifest_filename != NULL) ++ *out_manifest_filename = g_steal_pointer (&metadata_filename); ++ if (out_json_manifest != NULL) ++ *out_json_manifest = json_to_string (json_parser_get_root (parser), FALSE); ++ return g_strdup (host_path); ++ } ++ ++ g_set_error (error, ++ XDG_DESKTOP_PORTAL_ERROR, ++ XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, ++ "Could not find native messaging host"); ++ return NULL; ++} ++ ++static gboolean ++handle_get_manifest (XdpDbusWebExtensions *object, ++ GDBusMethodInvocation *invocation, ++ const char *arg_session_handle, ++ const char *arg_name, ++ const char *arg_extension_or_origin) ++{ ++ XdpCall *call = xdp_call_from_invocation (invocation); ++ XdpSession *session; ++ WebExtensionsSession *web_extensions_session; ++ g_autofree char *host_path = NULL; ++ g_autofree char *json_manifest = NULL; ++ g_autoptr(GError) error = NULL; ++ ++ session = xdp_session_from_call (arg_session_handle, call); ++ if (!session) ++ { ++ g_dbus_method_invocation_return_error (invocation, ++ G_DBUS_ERROR, ++ G_DBUS_ERROR_ACCESS_DENIED, ++ "Invalid session"); ++ return TRUE; ++ } ++ ++ SESSION_AUTOLOCK_UNREF (session); ++ web_extensions_session = (WebExtensionsSession *)session; ++ ++ if (web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_INIT) ++ { ++ g_dbus_method_invocation_return_error (invocation, ++ G_DBUS_ERROR, ++ G_DBUS_ERROR_FAILED, ++ "Session already started"); ++ return TRUE; ++ } ++ ++ host_path = find_messaging_host (web_extensions_session->mode, ++ arg_name, arg_extension_or_origin, ++ NULL, NULL, &json_manifest, &error); ++ if (!host_path) ++ { ++ g_dbus_method_invocation_return_gerror (invocation, error); ++ return TRUE; ++ } ++ ++ xdp_dbus_web_extensions_complete_get_manifest (object, invocation, json_manifest); ++ return TRUE; ++} ++ ++static void ++handle_start_in_thread (GTask *task, ++ gpointer source_object, ++ gpointer task_data, ++ GCancellable *cancellable) ++{ ++ XdpRequest *request = (XdpRequest *)task_data; ++ XdpSession *session; ++ WebExtensionsSession *web_extensions_session; ++ const char *arg_name; ++ char *arg_extension_or_origin; ++ const char *app_id; ++ g_autofree char *host_path = NULL; ++ g_autofree char *description = NULL; ++ g_autofree char *manifest_filename = NULL; ++ guint response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; ++ gboolean should_close_session; ++ XdpPermission permission; ++ gboolean allowed; ++ char *argv[] = {NULL, NULL, NULL, NULL}; ++ g_autoptr(GError) error = NULL; ++ ++ REQUEST_AUTOLOCK (request); ++ session = g_object_get_data (G_OBJECT (request), "session"); ++ SESSION_AUTOLOCK_UNREF (g_object_ref (session)); ++ g_object_set_data (G_OBJECT (request), "session", NULL); ++ web_extensions_session = (WebExtensionsSession *)session; ++ ++ if (!request->exported || web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTING) ++ goto out; ++ ++ arg_name = g_object_get_data (G_OBJECT (request), "name"); ++ arg_extension_or_origin = g_object_get_data (G_OBJECT (request), "extension-or-origin"); ++ ++ host_path = find_messaging_host (web_extensions_session->mode, ++ arg_name, arg_extension_or_origin, ++ &description, &manifest_filename, NULL, ++ &error); ++ if (host_path == NULL) ++ { ++ g_warning ("Could not find WebExtensions backend: %s", error->message); ++ fflush(stderr); ++ fflush(stdout); ++ goto out; ++ } ++ ++ app_id = xdp_app_info_get_id (request->app_info); ++ permission = xdp_get_permission_sync (app_id, PERMISSION_TABLE, arg_name); ++ if (permission == XDP_PERMISSION_ASK || permission == XDP_PERMISSION_UNSET) ++ { ++ guint access_response = 2; ++ g_autoptr(GVariant) access_results = NULL; ++ GVariantBuilder opt_builder; ++ const char *display_name; ++ g_autofree gchar *app_info_id = NULL; ++ g_autofree gchar *title = NULL; ++ g_autofree gchar *subtitle = NULL; ++ g_autofree gchar *body = NULL; ++ GAppInfo* info = xdp_app_info_get_gappinfo (request->app_info); ++ ++ if (info) ++ { ++ g_auto(GStrv) app_id_components = g_strsplit (g_app_info_get_id (info), ".desktop", 2); ++ app_info_id = g_strdup (app_id_components[0]); ++ } ++ display_name = info ? g_app_info_get_display_name (info) : app_id; ++ title = g_strdup_printf (_("Allow %s to start WebExtension backend?"), display_name); ++ subtitle = g_strdup_printf (_("%s is requesting to launch \"%s\" (%s)."), display_name, description, arg_name); ++ body = g_strdup (_("This permission can be changed at any time from the privacy settings.")); ++ ++ g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); ++ g_variant_builder_add (&opt_builder, "{sv}", "deny_label", g_variant_new_string (_("Don't allow"))); ++ g_variant_builder_add (&opt_builder, "{sv}", "grant_label", g_variant_new_string (_("Allow"))); ++ if (!xdp_dbus_impl_access_call_access_dialog_sync (access_impl, ++ request->id, ++ app_info_id ? app_info_id : app_id, ++ "", ++ title, ++ subtitle, ++ body, ++ g_variant_builder_end (&opt_builder), ++ &access_response, ++ &access_results, ++ cancellable, ++ &error)) ++ { ++ g_warning ("AccessDialog call failed: %s", error->message); ++ g_clear_error (&error); ++ } ++ allowed = access_response == 0; ++ ++ // access_response == 2 means dialog has been closed ++ if (permission == XDP_PERMISSION_UNSET && access_response != 2) ++ xdp_set_permission_sync (app_id, PERMISSION_TABLE, arg_name, allowed ? XDP_PERMISSION_YES : XDP_PERMISSION_NO); ++ } ++ else ++ { ++ allowed = permission == XDP_PERMISSION_YES ? TRUE : FALSE; ++ } ++ ++ if (!allowed) ++ { ++ response = XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED; ++ goto out; ++ } ++ ++ argv[0] = host_path; ++ switch (web_extensions_session->mode) ++ { ++ case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: ++ /* Pass the origin ++ https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging ++ */ ++ argv[1] = arg_extension_or_origin; ++ break; ++ case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: ++ /* Pass the manifest filename and extension ID ++ https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging ++ https://searchfox.org/mozilla-central/rev/9fcc11127fbfbdc88cbf37489dac90542e141c77/toolkit/components/extensions/NativeMessaging.sys.mjs#104-110 ++ */ ++ argv[1] = manifest_filename; ++ argv[2] = arg_extension_or_origin; ++ break; ++ } ++ if (!g_spawn_async_with_pipes (NULL, /* working_directory */ ++ argv, ++ NULL, /* envp */ ++ G_SPAWN_DO_NOT_REAP_CHILD, ++ NULL, /* child_setup */ ++ NULL, /* user_data */ ++ &web_extensions_session->child_pid, ++ &web_extensions_session->standard_input, ++ &web_extensions_session->standard_output, ++ &web_extensions_session->standard_error, ++ &error)) ++ { ++ web_extensions_session->child_pid = -1; ++ goto out; ++ } ++ ++ web_extensions_session->child_watch_id = g_child_watch_add_full (G_PRIORITY_DEFAULT, ++ web_extensions_session->child_pid, ++ on_host_exited, ++ g_object_ref (web_extensions_session), ++ g_object_unref); ++ web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTED; ++ ++ response = XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS; ++ ++out: ++ should_close_session = !request->exported || response != XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS; ++ ++ if (request->exported) ++ { ++ GVariantBuilder results; ++ ++ g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT); ++ xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, g_variant_builder_end (&results)); ++ xdp_request_unexport (request); ++ } ++ ++ if (should_close_session) ++ xdp_session_close (session, TRUE); ++} ++ ++static gboolean ++handle_start (XdpDbusWebExtensions *object, ++ GDBusMethodInvocation *invocation, ++ const char *arg_session_handle, ++ const char *arg_name, ++ const char *arg_extension_or_origin, ++ GVariant *arg_options) ++{ ++ XdpRequest *request = xdp_request_from_invocation (invocation); ++ XdpSession *session; ++ WebExtensionsSession *web_extensions_session; ++ g_autoptr(GTask) task = NULL; ++ ++ REQUEST_AUTOLOCK (request); ++ ++ session = xdp_session_from_request (arg_session_handle, request); ++ if (!session) ++ { ++ g_dbus_method_invocation_return_error (invocation, ++ G_DBUS_ERROR, ++ G_DBUS_ERROR_ACCESS_DENIED, ++ "Invalid session"); ++ return TRUE; ++ } ++ ++ SESSION_AUTOLOCK_UNREF (session); ++ web_extensions_session = (WebExtensionsSession *)session; ++ ++ if (web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_INIT) ++ { ++ g_dbus_method_invocation_return_error (invocation, ++ G_DBUS_ERROR, ++ G_DBUS_ERROR_FAILED, ++ "Session already started"); ++ return TRUE; ++ } ++ ++ web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTING; ++ g_object_set_data_full (G_OBJECT (request), "session", g_object_ref (session), g_object_unref); ++ g_object_set_data_full (G_OBJECT (request), "name", g_strdup (arg_name), g_free); ++ g_object_set_data_full (G_OBJECT (request), "extension-or-origin", g_strdup (arg_extension_or_origin), g_free); ++ ++ xdp_request_export (request, g_dbus_method_invocation_get_connection (invocation)); ++ xdp_dbus_web_extensions_complete_start (object, invocation, request->id); ++ ++ task = g_task_new (object, NULL, NULL, NULL); ++ g_task_set_task_data (task, g_object_ref (request), g_object_unref); ++ g_task_run_in_thread (task, handle_start_in_thread); ++ ++ return TRUE; ++} ++ ++ ++static gboolean ++handle_get_pipes (XdpDbusWebExtensions *object, ++ GDBusMethodInvocation *invocation, ++ GUnixFDList *fd_list, ++ const char *arg_session_handle, ++ GVariant *arg_options) ++{ ++ XdpCall *call = xdp_call_from_invocation (invocation); ++ XdpSession *session; ++ WebExtensionsSession *web_extensions_session; ++ int fds[3]; ++ g_autoptr(GUnixFDList) out_fd_list = NULL; ++ ++ session = xdp_session_from_call (arg_session_handle, call); ++ if (!session) ++ { ++ g_dbus_method_invocation_return_error (invocation, ++ G_DBUS_ERROR, ++ G_DBUS_ERROR_ACCESS_DENIED, ++ "Invalid session"); ++ return TRUE; ++ } ++ ++ SESSION_AUTOLOCK_UNREF (session); ++ web_extensions_session = (WebExtensionsSession *)session; ++ ++ if (web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTED) ++ { ++ g_dbus_method_invocation_return_error (invocation, ++ G_DBUS_ERROR, ++ G_DBUS_ERROR_FAILED, ++ "Session not started"); ++ return TRUE; ++ } ++ ++ if (web_extensions_session->standard_input < 0 || ++ web_extensions_session->standard_output < 0 || ++ web_extensions_session->standard_error < 0) ++ { ++ g_dbus_method_invocation_return_error (invocation, ++ G_DBUS_ERROR, ++ G_DBUS_ERROR_FAILED, ++ "GetPipes already called"); ++ return TRUE; ++ } ++ ++ fds[0] = web_extensions_session->standard_input; ++ fds[1] = web_extensions_session->standard_output; ++ fds[2] = web_extensions_session->standard_error; ++ out_fd_list = g_unix_fd_list_new_from_array (fds, G_N_ELEMENTS (fds)); ++ /* out_fd_list now owns the file descriptors */ ++ web_extensions_session->standard_input = -1; ++ web_extensions_session->standard_output = -1; ++ web_extensions_session->standard_error = -1; ++ ++ xdp_dbus_web_extensions_complete_get_pipes (object, invocation, out_fd_list, ++ g_variant_new_handle (0), ++ g_variant_new_handle (1), ++ g_variant_new_handle (2)); ++ return TRUE; ++} ++ ++static void ++web_extensions_iface_init (XdpDbusWebExtensionsIface *iface) ++{ ++ iface->handle_create_session = handle_create_session; ++ iface->handle_get_manifest = handle_get_manifest; ++ iface->handle_start = handle_start; ++ iface->handle_get_pipes = handle_get_pipes; ++} ++ ++static void ++web_extensions_init (WebExtensions *web_extensions) ++{ ++ xdp_dbus_web_extensions_set_version (XDP_DBUS_WEB_EXTENSIONS (web_extensions), 1); ++} ++ ++static void ++web_extensions_class_init (WebExtensionsClass *klass) ++{ ++ /* Called at the first instantiation of WebExtensions, ++ i.e. in web_extensions_create with the call to g_object_new. ++ https://docs.gtk.org/gobject/concepts.html#object-instantiation ++ */ ++} ++ ++GDBusInterfaceSkeleton * ++web_extensions_create (GDBusConnection *connection, ++ const char *dbus_name_access) ++{ ++ g_autoptr(GError) error = NULL; ++ ++ web_extensions = g_object_new (web_extensions_get_type (), NULL); ++ ++ access_impl = xdp_dbus_impl_access_proxy_new_sync (connection, ++ G_DBUS_PROXY_FLAGS_NONE, ++ dbus_name_access, ++ DESKTOP_PORTAL_OBJECT_PATH, ++ NULL, ++ &error); ++ if (access_impl == NULL) ++ { ++ g_warning ("Failed to create access proxy: %s", error->message); ++ return NULL; ++ } ++ ++ return G_DBUS_INTERFACE_SKELETON (web_extensions); ++} +diff --git a/src/web-extensions.h b/src/web-extensions.h +new file mode 100644 +index 000000000..72b947a31 +--- /dev/null ++++ b/src/web-extensions.h +@@ -0,0 +1,24 @@ ++/* ++ * Copyright © 2022 Canonical Ltd ++ * ++ * This program 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 . ++ * ++ */ ++ ++#pragma once ++ ++#include ++ ++GDBusInterfaceSkeleton *web_extensions_create (GDBusConnection *connection, ++ const char *access_dbus_name); +diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c +index 55d44f9d0..e1559179f 100644 +--- a/src/xdg-desktop-portal.c ++++ b/src/xdg-desktop-portal.c +@@ -69,6 +69,7 @@ + #include "trash.h" + #include "usb.h" + #include "wallpaper.h" ++#include "web-extensions.h" + + static int global_exit_status = 0; + static GMainLoop *loop = NULL; +@@ -285,6 +286,9 @@ on_bus_acquired (GDBusConnection *connection, + { + XdpPortalImplementation *tmp; + ++ export_portal_implementation (connection, ++ web_extensions_create (connection, ++ access_impl->dbus_name)); + #ifdef HAVE_GEOCLUE + export_portal_implementation (connection, + location_create (connection, +diff --git a/src/xdp-request.c b/src/xdp-request.c +index 6aaa7ed23..46ceca861 100644 +--- a/src/xdp-request.c ++++ b/src/xdp-request.c +@@ -188,6 +188,18 @@ get_token (GDBusMethodInvocation *invocation) + if (method_info->option_arg >= 0) + options = g_variant_get_child_value (parameters, method_info->option_arg); + } ++ else if (strcmp (interface, "org.freedesktop.portal.WebExtensions") == 0) ++ { ++ if (strcmp (method, "Start") == 0) ++ { ++ options = g_variant_get_child_value (parameters, 3); ++ } ++ else ++ { ++ g_warning ("Support for %s::%s missing in %s", ++ interface, method, G_STRLOC); ++ } ++ } + else + { + g_warning ("Support for %s::%s missing in %s", +diff --git a/tests/meson.build b/tests/meson.build +index d3a46dc5f..df1ec61c4 100644 +--- a/tests/meson.build ++++ b/tests/meson.build +@@ -18,6 +18,7 @@ subdir('dbs') + subdir('portals') + subdir('services') + subdir('share') ++subdir('native-messaging-hosts') + + test_db = executable( + 'testdb', +@@ -106,6 +107,7 @@ if have_libportal + 'screenshot.c', + 'trash.c', + 'wallpaper.c', ++ 'web-extensions.c', + 'glib-backports.c', + ) + endif +@@ -174,6 +176,7 @@ portal_tests = [ + 'settings', + 'trash', + 'wallpaper', ++ 'webextensions', + ] + + test_env = env_tests +diff --git a/tests/native-messaging-hosts/meson.build b/tests/native-messaging-hosts/meson.build +new file mode 100644 +index 000000000..5ee790531 +--- /dev/null ++++ b/tests/native-messaging-hosts/meson.build +@@ -0,0 +1,28 @@ ++configure_file(input: 'server.sh', ++ output: '@PLAINNAME@', ++ copy: true, ++ install_mode: 'rwxr-xr-x', ++ install: enable_installed_tests, ++ install_dir: installed_tests_dir / 'native-messaging-hosts', ++) ++ ++config = configuration_data() ++config.set('server_path', meson.current_build_dir() / 'server.sh') ++configure_file(input: 'org.example.testing.json.in', ++ output: '@BASENAME@', ++ configuration: config, ++) ++ ++# create a second version to be installed ++if enable_installed_tests ++ config = configuration_data() ++ config.set('server_path', installed_tests_dir / 'native-messaging-hosts/server.sh') ++ configure_file(input: 'org.example.testing.json.in', ++ output: 'installed-org.example.testing.json', ++ configuration: config, ++ ) ++ install_data(meson.current_build_dir() / 'installed-org.example.testing.json', ++ rename: ['org.example.testing.json'], ++ install_dir: installed_tests_dir / 'native-messaging-hosts', ++ ) ++endif +diff --git a/tests/native-messaging-hosts/org.example.testing.json.in b/tests/native-messaging-hosts/org.example.testing.json.in +new file mode 100644 +index 000000000..58cd31029 +--- /dev/null ++++ b/tests/native-messaging-hosts/org.example.testing.json.in +@@ -0,0 +1,9 @@ ++{ ++ "name": "org.example.testing", ++ "description": "Test native messaging host", ++ "path": "@server_path@", ++ "type": "stdio", ++ "allowed_extensions": [ ++ "some-extension@example.org" ++ ] ++} +diff --git a/tests/native-messaging-hosts/server.sh b/tests/native-messaging-hosts/server.sh +new file mode 100755 +index 000000000..b0f3e402d +--- /dev/null ++++ b/tests/native-messaging-hosts/server.sh +@@ -0,0 +1,3 @@ ++#!/bin/sh ++ ++exec cat +diff --git a/tests/test-portals.c b/tests/test-portals.c +index bf56ea977..7593c84b3 100644 +--- a/tests/test-portals.c ++++ b/tests/test-portals.c +@@ -22,6 +22,7 @@ + #include "screenshot.h" + #include "trash.h" + #include "wallpaper.h" ++#include "web-extensions.h" + #endif + + #include "utils.h" +@@ -133,6 +134,7 @@ global_setup (void) + g_autofree gchar *backends_executable = NULL; + g_autofree gchar *services = NULL; + g_autofree gchar *portal_dir = NULL; ++ g_autofree gchar *web_extensions_dir = NULL; + g_autofree gchar *argv0 = NULL; + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GSubprocess) subprocess = NULL; +@@ -281,12 +283,14 @@ global_setup (void) + NULL); + + portal_dir = g_test_build_filename (G_TEST_BUILT, "portals", "test", NULL); ++ web_extensions_dir = g_test_build_filename (G_TEST_BUILT, "native-messaging-hosts", NULL); + + g_clear_object (&launcher); + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE); + g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DESKTOP_PORTAL_DIR", portal_dir, TRUE); ++ g_subprocess_launcher_setenv (launcher, "XDG_DESKTOP_PORTAL_WEB_EXTENSIONS_PATH", web_extensions_dir, TRUE); + g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE); + g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE); + g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO)); +@@ -593,6 +597,9 @@ main (int argc, char **argv) + g_test_add_func ("/portal/notification/display-hint", test_notification_display_hint); + g_test_add_func ("/portal/notification/category", test_notification_category); + g_test_add_func ("/portal/notification/supported-properties", test_notification_supported_properties); ++ ++ g_test_add_func ("/portal/webextensions/basic", test_web_extensions_basic); ++ g_test_add_func ("/portal/webextensions/bad-name", test_web_extensions_bad_name); + #endif + + global_setup (); +diff --git a/tests/web-extensions.c b/tests/web-extensions.c +new file mode 100644 +index 000000000..044d536a4 +--- /dev/null ++++ b/tests/web-extensions.c +@@ -0,0 +1,632 @@ ++#include ++ ++#include "web-extensions.h" ++#include "xdp-utils.h" ++ ++#include ++#include ++#include "xdp-impl-dbus.h" ++ ++extern char outdir[]; ++ ++// TODO: convert these simple client wrappers to proper libportal APIs ++ ++static void ++create_session_returned (GObject *object, ++ GAsyncResult *result, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = data; ++ g_autoptr(GVariant) ret = NULL; ++ GError *error = NULL; ++ g_autofree char *session_handle = NULL; ++ ++ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); ++ if (!ret) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ g_variant_get (ret, "(o)", &session_handle); ++ g_task_return_pointer (task, g_steal_pointer (&session_handle), g_free); ++} ++ ++static void ++create_session (GCancellable *cancellable, ++ GAsyncReadyCallback callback, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = NULL; ++ g_autoptr(GDBusConnection) session_bus = NULL; ++ GError *error = NULL; ++ g_autofree char *session_token = NULL; ++ GVariantBuilder options; ++ ++ task = g_task_new (NULL, cancellable, callback, data); ++ ++ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); ++ if (session_bus == NULL) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ ++ session_token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT)); ++ g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); ++ g_variant_builder_add (&options, "{sv}", "mode", g_variant_new_string ("mozilla")); ++ g_variant_builder_add (&options, "{sv}", "session_handle_token", g_variant_new_string (session_token)); ++ g_dbus_connection_call (session_bus, ++ "org.freedesktop.portal.Desktop", ++ "/org/freedesktop/portal/desktop", ++ "org.freedesktop.portal.WebExtensions", ++ "CreateSession", ++ g_variant_new ("(a{sv})", &options), ++ NULL, ++ G_DBUS_CALL_FLAGS_NONE, ++ -1, ++ cancellable, ++ create_session_returned, ++ g_steal_pointer (&task)); ++} ++ ++static char * ++create_session_finish (GAsyncResult *result, GError **error) ++{ ++ return g_task_propagate_pointer (G_TASK (result), error); ++} ++ ++static void ++get_manifest_returned (GObject *object, ++ GAsyncResult *result, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = data; ++ g_autoptr(GVariant) ret = NULL; ++ GError *error = NULL; ++ g_autofree char *json_manifest = NULL; ++ ++ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); ++ if (!ret) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ g_variant_get (ret, "(s)", &json_manifest); ++ g_task_return_pointer (task, g_steal_pointer (&json_manifest), g_free); ++} ++ ++static void ++get_manifest (const char *session_handle, ++ const char *name, ++ const char *extension_or_origin, ++ GCancellable *cancellable, ++ GAsyncReadyCallback callback, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = NULL; ++ g_autoptr(GDBusConnection) session_bus = NULL; ++ GError *error = NULL; ++ ++ task = g_task_new (NULL, cancellable, callback, data); ++ ++ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); ++ if (session_bus == NULL) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ ++ g_dbus_connection_call (session_bus, ++ "org.freedesktop.portal.Desktop", ++ "/org/freedesktop/portal/desktop", ++ "org.freedesktop.portal.WebExtensions", ++ "GetManifest", ++ g_variant_new ("(oss)", session_handle, name, extension_or_origin), ++ NULL, ++ G_DBUS_CALL_FLAGS_NONE, ++ -1, ++ cancellable, ++ get_manifest_returned, ++ g_steal_pointer (&task)); ++} ++ ++static char * ++get_manifest_finish (GAsyncResult *result, GError **error) ++{ ++ return g_task_propagate_pointer (G_TASK (result), error); ++} ++ ++static void ++start_returned (GObject *object, ++ GAsyncResult *result, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = data; ++ g_autoptr(GVariant) ret = NULL; ++ GError *error = NULL; ++ ++ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); ++ if (!ret) ++ { ++ guint signal_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "response-signal-id")); ++ g_dbus_connection_signal_unsubscribe (G_DBUS_CONNECTION (object), signal_id); ++ g_task_return_error (task, error); ++ return; ++ } ++} ++ ++static void ++start_completed (GDBusConnection *session_bus, ++ const char *sender_name, ++ const char *object_path, ++ const char *interface_name, ++ const char *signal_name, ++ GVariant *parameters, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = g_object_ref (data); ++ guint signal_id; ++ guint32 response; ++ g_autoptr(GVariant) ret = NULL; ++ ++ signal_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "response-signal-id")); ++ g_dbus_connection_signal_unsubscribe (session_bus, signal_id); ++ ++ g_variant_get (parameters, "(u@a{sv})", &response, &ret); ++ switch (response) ++ { ++ case 0: ++ g_task_return_boolean (task, TRUE); ++ break; ++ case 1: ++ g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Start cancelled"); ++ break; ++ case 2: ++ default: ++ g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Start failed"); ++ break; ++ } ++} ++ ++static void ++start (const char *session_handle, ++ const char *name, ++ const char *extension_or_origin, ++ GCancellable *cancellable, ++ GAsyncReadyCallback callback, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = NULL; ++ g_autoptr(GDBusConnection) session_bus = NULL; ++ GError *error = NULL; ++ g_autofree char *token = NULL; ++ g_autofree char *sender = NULL; ++ g_autofree char *request_path = NULL; ++ int i; ++ guint signal_id; ++ GVariantBuilder options; ++ ++ task = g_task_new (NULL, cancellable, callback, data); ++ ++ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); ++ if (session_bus == NULL) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ ++ token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT)); ++ sender = g_strdup (g_dbus_connection_get_unique_name (session_bus) + 1); ++ for (i = 0; sender[i]; i++) ++ if (sender[i] == '.') ++ sender[i] = '_'; ++ request_path = g_strconcat ("/org/freedesktop/portal/desktop/request/", sender, "/", token, NULL); ++ signal_id = g_dbus_connection_signal_subscribe (session_bus, ++ "org.freedesktop.portal.Desktop", ++ "org.freedesktop.portal.Request", ++ "Response", ++ request_path, ++ NULL, ++ G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, ++ start_completed, ++ g_object_ref (task), ++ g_object_unref); ++ g_object_set_data (G_OBJECT (task), "response-signal-id", GUINT_TO_POINTER (signal_id)); ++ ++ g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); ++ g_variant_builder_add (&options, "{sv}", "handle_token", g_variant_new_string (token)); ++ g_dbus_connection_call (session_bus, ++ "org.freedesktop.portal.Desktop", ++ "/org/freedesktop/portal/desktop", ++ "org.freedesktop.portal.WebExtensions", ++ "Start", ++ g_variant_new ("(ossa{sv})", session_handle, name, extension_or_origin, &options), ++ NULL, ++ G_DBUS_CALL_FLAGS_NONE, ++ -1, ++ cancellable, ++ start_returned, ++ g_steal_pointer (&task)); ++} ++ ++static gboolean ++start_finish (GAsyncResult *result, GError **error) ++{ ++ return g_task_propagate_boolean (G_TASK (result), error); ++} ++ ++static void ++get_pipes_returned (GObject *object, ++ GAsyncResult *result, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = data; ++ g_autoptr(GVariant) ret = NULL; ++ g_autoptr(GUnixFDList) fd_list = NULL; ++ GError *error = NULL; ++ ++ ret = g_dbus_connection_call_with_unix_fd_list_finish (G_DBUS_CONNECTION (object), &fd_list, result, &error); ++ if (!ret) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ g_task_return_pointer (task, g_steal_pointer (&fd_list), g_object_unref); ++} ++ ++static void ++get_pipes (const char *session_handle, ++ GCancellable *cancellable, ++ GAsyncReadyCallback callback, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = NULL; ++ g_autoptr(GDBusConnection) session_bus = NULL; ++ GError *error = NULL; ++ ++ task = g_task_new (NULL, cancellable, callback, data); ++ ++ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); ++ if (session_bus == NULL) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ ++ g_dbus_connection_call_with_unix_fd_list (session_bus, ++ "org.freedesktop.portal.Desktop", ++ "/org/freedesktop/portal/desktop", ++ "org.freedesktop.portal.WebExtensions", ++ "GetPipes", ++ g_variant_new ("(oa{sv})", session_handle, NULL), ++ NULL, ++ G_DBUS_CALL_FLAGS_NONE, ++ -1, ++ NULL, ++ cancellable, ++ get_pipes_returned, ++ g_steal_pointer (&task)); ++} ++ ++static gboolean ++get_pipes_finish (int *stdin_fileno, int *stdout_fileno, int *stderr_fileno, GAsyncResult *result, GError **error) ++{ ++ g_autoptr(GUnixFDList) fd_list = NULL; ++ ++ fd_list = g_task_propagate_pointer (G_TASK (result), error); ++ if (fd_list == NULL) ++ return FALSE; ++ ++ if (stdin_fileno != NULL) ++ { ++ *stdin_fileno = g_unix_fd_list_get (fd_list, 0, error); ++ if (*stdin_fileno < 0) ++ return FALSE; ++ } ++ if (stdout_fileno != NULL) ++ { ++ *stdout_fileno = g_unix_fd_list_get (fd_list, 1, error); ++ if (*stdout_fileno < 0) ++ return FALSE; ++ } ++ if (stderr_fileno != NULL) ++ { ++ *stderr_fileno = g_unix_fd_list_get (fd_list, 2, error); ++ if (*stderr_fileno < 0) ++ return FALSE; ++ } ++ return TRUE; ++} ++ ++static void ++close_session_returned (GObject *object, ++ GAsyncResult *result, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = data; ++ g_autoptr(GVariant) ret = NULL; ++ GError *error = NULL; ++ ++ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); ++ if (!ret) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ g_task_return_boolean (task, TRUE); ++} ++ ++static void ++close_session (const char *session_handle, ++ GCancellable *cancellable, ++ GAsyncReadyCallback callback, ++ gpointer data) ++{ ++ g_autoptr(GTask) task = NULL; ++ g_autoptr(GDBusConnection) session_bus = NULL; ++ GError *error = NULL; ++ ++ task = g_task_new (NULL, cancellable, callback, data); ++ ++ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); ++ if (session_bus == NULL) ++ { ++ g_task_return_error (task, error); ++ return; ++ } ++ ++ g_dbus_connection_call (session_bus, ++ "org.freedesktop.portal.Desktop", ++ session_handle, ++ "org.freedesktop.portal.Session", ++ "Close", ++ NULL, ++ NULL, ++ G_DBUS_CALL_FLAGS_NONE, ++ -1, ++ cancellable, ++ close_session_returned, ++ g_steal_pointer (&task)); ++} ++ ++static gboolean ++close_session_finish (GAsyncResult *result, GError **error) ++{ ++ return g_task_propagate_boolean (G_TASK (result), error); ++} ++ ++ ++static int got_info = 0; ++ ++extern XdpDbusImplPermissionStore *permission_store; ++ ++static void ++set_web_extensions_permissions (const char *permission) ++{ ++ const char *permissions[2] = { NULL, NULL }; ++ g_autoptr(GError) error = NULL; ++ ++ permissions[0] = permission; ++ xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store, ++ "webextensions", ++ TRUE, ++ "org.example.testing", ++ "", ++ permissions, ++ NULL, ++ &error); ++ g_assert_no_error (error); ++} ++ ++static gboolean ++cancel_call (gpointer data) ++{ ++ GCancellable *cancellable = data; ++ ++ g_debug ("cancel call"); ++ g_cancellable_cancel (cancellable); ++ ++ return G_SOURCE_REMOVE; ++} ++ ++typedef struct { ++ GCancellable *cancellable; ++ char *session_handle; ++ const char *messaging_host_name; ++} TestData; ++ ++static void ++close_session_cb (GObject *object, GAsyncResult *result, gpointer data) ++{ ++ g_autoptr(GError) error = NULL; ++ gboolean ret; ++ ++ ret = close_session_finish (result, &error); ++ if (ret) ++ { ++ g_assert_no_error (error); ++ } ++ else ++ { ++ /* The native messaging host may have closed before we tried to ++ close it. */ ++ g_assert_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD); ++ } ++ ++ got_info++; ++ g_main_context_wakeup (NULL); ++} ++ ++static void ++get_pipes_cb (GObject *object, GAsyncResult *result, gpointer data) ++{ ++ TestData *test_data = data; ++ g_autoptr(GError) error = NULL; ++ gboolean ret; ++ int stdin_fileno = -1, stdout_fileno = -1, stderr_fileno = -1; ++ ++ ret = get_pipes_finish (&stdin_fileno, &stdout_fileno, &stderr_fileno, result, &error); ++ g_assert_no_error (error); ++ g_assert_true (ret); ++ g_assert_cmpint (stdin_fileno, >, 0); ++ g_assert_cmpint (stdout_fileno, >, 0); ++ g_assert_cmpint (stderr_fileno, >, 0); ++ ++ close (stdin_fileno); ++ close (stdout_fileno); ++ close (stderr_fileno); ++ ++ close_session (test_data->session_handle, ++ test_data->cancellable, ++ close_session_cb, ++ test_data); ++} ++ ++static void ++start_cb (GObject *object, GAsyncResult *result, gpointer data) ++{ ++ TestData *test_data = data; ++ g_autoptr(GError) error = NULL; ++ gboolean ret; ++ ++ ret = start_finish (result, &error); ++ g_assert_no_error (error); ++ g_assert_true (ret); ++ ++ get_pipes (test_data->session_handle, ++ test_data->cancellable, ++ get_pipes_cb, ++ test_data); ++} ++ ++ ++static void ++get_manifest_cb (GObject *object, GAsyncResult *result, gpointer data) ++{ ++ TestData *test_data = data; ++ g_autoptr(GError) error = NULL; ++ g_autofree char *json_manifest = NULL; ++ g_autofree char *host_path = NULL; ++ g_autofree char *expected = NULL; ++ ++ host_path = g_test_build_filename (G_TEST_BUILT, "native-messaging-hosts", "server.sh", NULL); ++ expected = g_strdup_printf ("{\"name\":\"org.example.testing\",\"description\":\"Test native messaging host\",\"path\":\"%s\",\"type\":\"stdio\",\"allowed_extensions\":[\"some-extension@example.org\"]}", host_path); ++ ++ json_manifest = get_manifest_finish (result, &error); ++ g_assert_no_error (error); ++ g_assert_cmpstr (json_manifest, ==, expected); ++ ++ start (test_data->session_handle, ++ "org.example.testing", ++ "some-extension@example.org", ++ test_data->cancellable, ++ start_cb, ++ test_data); ++} ++ ++static void ++create_session_cb (GObject *object, GAsyncResult *result, gpointer data) ++{ ++ TestData *test_data = data; ++ g_autoptr(GError) error = NULL; ++ ++ test_data->session_handle = create_session_finish (result, &error); ++ g_assert_no_error (error); ++ g_assert_nonnull (test_data->session_handle); ++ ++ get_manifest (test_data->session_handle, ++ "org.example.testing", ++ "some-extension@example.org", ++ test_data->cancellable, ++ get_manifest_cb, ++ test_data); ++} ++ ++void ++test_web_extensions_basic (void) ++{ ++ g_autoptr(GCancellable) cancellable = NULL; ++ TestData test_data = { cancellable, NULL }; ++ g_autoptr(GKeyFile) keyfile = NULL; ++ g_autofree char *path = NULL; ++ g_autoptr(GError) error = NULL; ++ ++ got_info = 0; ++ keyfile = g_key_file_new (); ++ ++ g_key_file_set_integer (keyfile, "backend", "delay", 0); ++ g_key_file_set_integer (keyfile, "backend", "response", 0); ++ g_key_file_set_integer (keyfile, "result", "response", 0); ++ ++ path = g_build_filename (outdir, "access", NULL); ++ g_key_file_save_to_file (keyfile, path, &error); ++ g_assert_no_error (error); ++ ++ g_key_file_unref (keyfile); ++ ++ set_web_extensions_permissions ("yes"); ++ create_session (cancellable, create_session_cb, &test_data); ++ ++ g_timeout_add (100, cancel_call, cancellable); ++ while (!got_info) ++ g_main_context_iteration (NULL, TRUE); ++ g_free (test_data.session_handle); ++} ++ ++static void ++start_bad_name_cb (GObject *object, GAsyncResult *result, gpointer data) ++{ ++ g_autoptr(GError) error = NULL; ++ gboolean ret; ++ ++ ret = start_finish (result, &error); ++ g_assert_false (ret); ++ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); ++ ++ got_info++; ++ g_main_context_wakeup (NULL); ++} ++ ++static void ++create_session_bad_name_cb (GObject *object, GAsyncResult *result, gpointer data) ++{ ++ TestData *test_data = data; ++ g_autoptr(GError) error = NULL; ++ ++ test_data->session_handle = create_session_finish (result, &error); ++ g_assert_no_error (error); ++ g_assert_nonnull (test_data->session_handle); ++ ++ start (test_data->session_handle, ++ test_data->messaging_host_name, ++ "some-extension@example.org", ++ test_data->cancellable, ++ start_bad_name_cb, ++ test_data); ++} ++ ++void ++test_web_extensions_bad_name (void) ++{ ++ const char *messaging_host_name[] = { ++ "no-dashes", ++ "../foo", ++ "no_trailing_dot.", ++ }; ++ int i; ++ ++ for (i = 0; i < G_N_ELEMENTS (messaging_host_name); i++) ++ { ++ g_autoptr(GCancellable) cancellable = NULL; ++ TestData test_data = { cancellable, NULL, messaging_host_name[i] }; ++ ++ got_info = 0; ++ set_web_extensions_permissions ("yes"); ++ create_session (cancellable, create_session_bad_name_cb, &test_data); ++ ++ g_timeout_add (100, cancel_call, cancellable); ++ while (!got_info) ++ g_main_context_iteration (NULL, TRUE); ++ g_free (test_data.session_handle); ++ } ++} +diff --git a/tests/web-extensions.h b/tests/web-extensions.h +new file mode 100644 +index 000000000..0090184b9 +--- /dev/null ++++ b/tests/web-extensions.h +@@ -0,0 +1,2 @@ ++void test_web_extensions_basic (void); ++void test_web_extensions_bad_name (void); diff --git a/xdg-desktop-portal.spec b/xdg-desktop-portal.spec index 572b9bc..a8793b8 100644 --- a/xdg-desktop-portal.spec +++ b/xdg-desktop-portal.spec @@ -1,11 +1,14 @@ +%bcond docs %{undefined rhel} + %global flatpak_version 1.5.0 %global geoclue_version 2.5.2 -%global glib_version 2.69.1 +%global glib_version 2.72.0 +%global libportal_version 0.9.0 %global low_memory_monitor_version 2.0 %global pipewire_version 0.2.90 Name: xdg-desktop-portal -Version: 1.18.4 +Version: 1.19.1 Release: %autorelease Summary: Portal frontend service to flatpak @@ -14,6 +17,10 @@ License: LGPL-2.1-or-later URL: https://github.com/flatpak/xdg-desktop-portal/ Source0: https://github.com/flatpak/xdg-desktop-portal/releases/download/%{version}/%{name}-%{version}.tar.xz +# Backport of webextensions portal +# PR: https://github.com/flatpak/xdg-desktop-portal/pull/1537 +Patch0: xdg-desktop-portal-webextensions.patch + BuildRequires: gcc BuildRequires: gettext BuildRequires: meson @@ -22,23 +29,34 @@ BuildRequires: pkgconfig(flatpak) >= %{flatpak_version} BuildRequires: pkgconfig(fuse3) BuildRequires: pkgconfig(gdk-pixbuf-2.0) BuildRequires: pkgconfig(gio-unix-2.0) >= %{glib_version} +BuildRequires: pkgconfig(gstreamer-pbutils-1.0) BuildRequires: pkgconfig(json-glib-1.0) BuildRequires: pkgconfig(libgeoclue-2.0) >= %{geoclue_version} BuildRequires: pkgconfig(libpipewire-0.3) >= %{pipewire_version} -BuildRequires: pkgconfig(libportal) +BuildRequires: pkgconfig(libportal) >= %{libportal_version} BuildRequires: pkgconfig(libsystemd) +BuildRequires: pkgconfig(umockdev-1.0) BuildRequires: python3-dbusmock BuildRequires: python3-gobject-base BuildRequires: python3-pytest %if %{undefined rhel} BuildRequires: python3-pytest-xdist %endif +%if %{with docs} +BuildRequires: python3-furo +BuildRequires: python3-sphinx-copybutton +BuildRequires: python3-sphinxext-opengraph +BuildRequires: /usr/bin/sphinx-build +%endif +BuildRequires: /usr/bin/gst-inspect-1.0 +BuildRequires: gstreamer1-plugins-good +# for man-pages BuildRequires: /usr/bin/rst2man -BuildRequires: /usr/bin/xmlto Requires: dbus Requires: geoclue2 >= %{geoclue_version} Requires: glib2%{?_isa} >= %{glib_version} +Requires: libportal%{?_isa} >= %{libportal_version} Recommends: pipewire >= %{pipewire_version} Requires: pipewire-libs%{?_isa} >= %{pipewire_version} # Required for the document portal. @@ -70,7 +88,7 @@ The pkg-config file for %{name}. %build -%meson +%meson %{!?with_docs:-Ddocumentation=disabled} %meson_build @@ -108,6 +126,7 @@ install -dm 755 %{buildroot}/%{_datadir}/xdg-desktop-portal/portals %{_libexecdir}/xdg-desktop-portal %{_libexecdir}/xdg-desktop-portal-rewrite-launchers %{_libexecdir}/xdg-desktop-portal-validate-icon +%{_libexecdir}/xdg-desktop-portal-validate-sound %{_libexecdir}/xdg-document-portal %{_libexecdir}/xdg-permission-store %{_mandir}/man5/portals.conf.5*