[neon/backports-noble/xdg-desktop-portal-noble/Neon/unstable] debian/patches: refresh patches
Carlos De Maine
null at kde.org
Fri Dec 13 03:15:37 GMT 2024
Git commit a7100215cea644f2ed1dc2a111ce2e4698d2e842 by Carlos De Maine.
Committed on 13/12/2024 at 03:15.
Pushed by carlosdem into branch 'Neon/unstable'.
refresh patches
D +0 -38 debian/patches/debian/doc-Use-system-copy-of-Inter-Variable-font.patch
M +1 -3 debian/patches/series
A +1964 -0 debian/patches/webextensions-portal.patch
D +0 -31 debian/patches/xdp_validate_icon-Allow-sandboxing-of-the-validator-to-be.patch
D +0 -46 debian/patches/xdp_validate_icon-Assign-argument-indices-automatically.patch
https://invent.kde.org/neon/backports-noble/xdg-desktop-portal-noble/-/commit/a7100215cea644f2ed1dc2a111ce2e4698d2e842
diff --git a/debian/patches/debian/doc-Use-system-copy-of-Inter-Variable-font.patch b/debian/patches/debian/doc-Use-system-copy-of-Inter-Variable-font.patch
deleted file mode 100644
index 510b969..0000000
--- a/debian/patches/debian/doc-Use-system-copy-of-Inter-Variable-font.patch
+++ /dev/null
@@ -1,38 +0,0 @@
-From: Simon McVittie <smcv at debian.org>
-Date: Tue, 15 Oct 2024 14:25:07 +0100
-Subject: doc: Use system copy of Inter Variable font
-
-Forwarded: not-needed, Debian-specific
----
- doc/_static/xdg.css | 11 +----------
- 1 file changed, 1 insertion(+), 10 deletions(-)
-
-diff --git a/doc/_static/xdg.css b/doc/_static/xdg.css
-index 59728dd..298b982 100644
---- a/doc/_static/xdg.css
-+++ b/doc/_static/xdg.css
-@@ -6,15 +6,6 @@ https://pypi.org/project/furo/
-
- /* FONTS */
-
-- at font-face {
-- font-family: "Inter var";
-- font-weight: 100 900;
-- font-display: swap;
-- font-style: normal;
-- font-named-instance: "Regular";
-- src: url("inter.woff2") format("woff2");
--}
--
- :root {
- --rounded-corner: 12px;
- --blue1: rgb(153,193,241);
-@@ -97,7 +88,7 @@ html, body {
- margin: 0;
- padding: 0;
- font-size: 16px;
-- font-family: "Inter var", sans-serif;
-+ font-family: "Inter Variable", "Inter var", sans-serif;
- font-weight: 400;
- line-height: 1.6;
- }
diff --git a/debian/patches/series b/debian/patches/series
index d110aab..edf4c72 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,3 +1 @@
-xdp_validate_icon-Assign-argument-indices-automatically.patch
-xdp_validate_icon-Allow-sandboxing-of-the-validator-to-be.patch
-debian/doc-Use-system-copy-of-Inter-Variable-font.patch
+webextensions-portal.patch
diff --git a/debian/patches/webextensions-portal.patch b/debian/patches/webextensions-portal.patch
new file mode 100644
index 0000000..9f03aa8
--- /dev/null
+++ b/debian/patches/webextensions-portal.patch
@@ -0,0 +1,1964 @@
+From: James Henstridge <james at jamesh.id.au>
+Date: Tue, 1 Feb 2022 20:12:49 +0800
+Subject: webextensions: add a portal for managing WebExtensions native
+ messaging servers.
+
+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.
+
+Origin: https://github.com/flatpak/xdg-desktop-portal/pull/705
+Last-Update: 2024-08-21
+---
+ data/meson.build | 1 +
+ data/org.freedesktop.portal.WebExtensions.xml | 155 ++++
+ meson.build | 3 +
+ src/meson.build | 1 +
+ src/request.c | 12 +
+ src/web-extensions.c | 861 +++++++++++++++++++++
+ src/web-extensions.h | 24 +
+ src/xdg-desktop-portal.c | 11 +
+ 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 | 616 +++++++++++++++
+ tests/web-extensions.h | 2 +
+ 15 files changed, 1736 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 eea49ad..63ea5fb 100644
+--- a/data/meson.build
++++ b/data/meson.build
+@@ -37,6 +37,7 @@ portal_sources = files(
+ 'org.freedesktop.portal.Settings.xml',
+ 'org.freedesktop.portal.Trash.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 0000000..36caaaf
+--- /dev/null
++++ b/data/org.freedesktop.portal.WebExtensions.xml
+@@ -0,0 +1,155 @@
++<?xml version="1.0"?>
++<!--
++ Copyright (C) 2022 Canonical Ltd
++
++ This library 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 <http://www.gnu.org/licenses/>.
++-->
++
++<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
++ <!--
++ org.freedesktop.portal.WebExtensions:
++ @short_description: WebExtensions portal
++
++ The WebExtensions portal allows sandboxed web browsers to start
++ native messaging hosts installed on the host system.
++
++ Accompanying documentation for Firefox's implementation is
++ available: `Native messaging for a strictly-confined Firefox
++ <https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/native-messaging-portal-design.html>`_.
++
++ This documentation describes version 1 of this interface.
++ -->
++ <interface name="org.freedesktop.portal.WebExtensions">
++ <!--
++ CreateSession:
++ @options: Vardict with optional further information
++ @session_handle: Object path for the #org.freedesktop.portal.Session created by this call.
++
++ Create a web extensions session. A successfully created
++ session can at any time be closed using
++ org.freedesktop.portal.Session::Close, or may at any time be
++ closed by the portal implementation, which will be signalled
++ via org.freedesktop.portal.Session::Closed.
++
++ To close a session, the browser should:
++
++ 1. close the process's stdin/stdout/stderr file descriptors
++ obtained from the portal;
++ 2. wait for a D-Bus Closed signal from the portal on the
++ org.freedesktop.portal.Session object (which will be
++ triggered on SIGCHLD via the g_child_watch_add_full
++ handler); and
++ 3. if the Closed signal from the portal doesn't come in time,
++ call the Close method on the org.freedesktop.portal.Session
++ object.
++
++ Supported keys in the @options vardict include:
++ <variablelist>
++ <varlistentry>
++ <term>mode s</term>
++ <listitem><para>
++ A string indicating which behaviour the portal should
++ use when locating and starting native messaging
++ hosts. Valid values are "mozilla" and "chromium". By
++ default, mozilla behaviour is used.
++ </para></listitem>
++ </varlistentry>
++ <varlistentry>
++ <term>session_handle_token s</term>
++ <listitem><para>
++ A string that will be used as the last element of the session handle. Must be a valid
++ object path element. See the #org.freedesktop.portal.Session documentation for
++ more information about the session handle.
++ </para></listitem>
++ </varlistentry>
++ </variablelist>
++ -->
++ <method name="CreateSession">
++ <arg type="a{sv}" name="options" direction="in"/>
++ <arg type="o" name="session_handle" direction="out"/>
++ </method>
++ <!--
++ GetManifest:
++ @session_handle: Object path for the #org.freedesktop.portal.Session object
++ @name: name of the native messaging host
++ @extension_or_origin: extension ID or origin URI identifying the extension
++ @json_manifest: the JSON manifest for the native messaging host
++
++ Return the JSON manifest of the native messaging host that
++ Start would invoke.
++ -->
++ <method name="GetManifest">
++ <arg type="o" name="session_handle" direction="in"/>
++ <arg type="s" name="name" direction="in"/>
++ <arg type="s" name="extension_or_origin" direction="in"/>
++ <arg type="s" name="json_manifest" direction="out"/>
++ </method>
++ <!--
++ Start:
++ @session_handle: Object path for the #org.freedesktop.portal.Session object
++ @name: name of the native messaging host
++ @extension_or_origin: extension ID or origin URI identifying the extension
++ @options: Vardict with optional further information
++ @handle: Object path for the #org.freedesktop.portal.Request object representing this call
++
++ Start the named native messaging host. The caller must
++ indicate the requesting web extension (either by extension ID
++ for Firefox, or origin URI for Chrome), which will be matched
++ against the host's access control list.
++
++ If the host can't be started, or invalid data is provided,
++ the session will be closed.
++
++ Supported keys in the @options vardict include:
++ <variablelist>
++ <varlistentry>
++ <term>handle_token s</term>
++ <listitem><para>
++ A string that will be used as the last element of the @handle. Must be a valid
++ object path element. See the #org.freedesktop.portal.Request documentation for
++ more information about the @handle.
++ </para></listitem>
++ </varlistentry>
++ </variablelist>
++ -->
++ <method name="Start">
++ <arg type="o" name="session_handle" direction="in"/>
++ <arg type="s" name="name" direction="in"/>
++ <arg type="s" name="extension_or_origin" direction="in"/>
++ <arg type="a{sv}" name="options" direction="in"/>
++ <arg type="o" name="handle" direction="out"/>
++ </method>
++ <!--
++ GetPipes:
++ @session_handle: Object path for the #org.freedesktop.portal.Session object
++ @options: Vardict with optional further information
++ @stdin: File descriptor representing the hosts's stdin.
++ @stdout: File descriptor representing the host's stdout.
++ @stderr: File descriptor representing the host's stderr.
++
++ Retrieve file descriptors for the native messaging host
++ identified by the session. This method should only be called
++ after the Start request recveives a successful response.
++ -->
++ <method name="GetPipes">
++ <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
++ <arg type="o" name="session_handle" direction="in"/>
++ <arg type="a{sv}" name="options" direction="in"/>
++ <arg type="h" name="stdin" direction="out"/>
++ <arg type="h" name="stdout" direction="out"/>
++ <arg type="h" name="stderr" direction="out"/>
++ </method>
++ <property name="version" type="u" access="read"/>
++ </interface>
++</node>
+diff --git a/meson.build b/meson.build
+index c1ea053..893fe21 100644
+--- a/meson.build
++++ b/meson.build
+@@ -11,9 +11,11 @@ project(
+
+ prefix = get_option('prefix')
+ datadir = prefix / get_option('datadir')
++libdir = prefix / get_option('libdir')
+ libexecdir = prefix / get_option('libexecdir')
+ sysconfdir = prefix / get_option('sysconfdir')
+ localedir = prefix / get_option('localedir')
++sysconfdir = prefix / get_option('sysconfdir')
+ dbus_service_dir = get_option('dbus-service-dir')
+ if dbus_service_dir == ''
+ dbus_service_dir = prefix / datadir / 'dbus-1' / 'services'
+@@ -77,6 +79,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/src/meson.build b/src/meson.build
+index ab3a0d1..1ce4ee1 100644
+--- a/src/meson.build
++++ b/src/meson.build
+@@ -84,6 +84,7 @@ xdg_desktop_portal_sources = files(
+ 'settings.c',
+ 'trash.c',
+ 'wallpaper.c',
++ 'web-extensions.c',
+ 'xdg-desktop-portal.c',
+ 'xdp-utils.c',
+ )
+diff --git a/src/request.c b/src/request.c
+index 9deef53..4bcd9e1 100644
+--- a/src/request.c
++++ b/src/request.c
+@@ -371,6 +371,18 @@ get_token (GDBusMethodInvocation *invocation)
+ interface, method, G_STRLOC);
+ }
+ }
++ 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_print ("Support for %s missing in " G_STRLOC, interface);
+diff --git a/src/web-extensions.c b/src/web-extensions.c
+new file mode 100644
+index 0000000..b04395a
+--- /dev/null
++++ b/src/web-extensions.c
+@@ -0,0 +1,861 @@
++/*
++ * 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 <http://www.gnu.org/licenses/>.
++ *
++ */
++
++#include "config.h"
++
++#include <stdint.h>
++#include <sys/types.h>
++#include <sys/wait.h>
++#include <glib/gi18n.h>
++#include <gio/gunixfdlist.h>
++#include <json-glib/json-glib.h>
++
++#include "session.h"
++#include "web-extensions.h"
++#include "request.h"
++#include "permissions.h"
++#include "xdp-dbus.h"
++#include "xdp-impl-dbus.h"
++#include "xdp-utils.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
++{
++ Session 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
++{
++ SessionClass parent_class;
++} WebExtensionsSessionClass;
++
++GType web_extensions_session_get_type (void);
++
++G_DEFINE_TYPE (WebExtensionsSession, web_extensions_session, 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 (Session *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)
++{
++ Session *session = (Session *)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;
++ SessionClass *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 = (SessionClass *)klass;
++ /* Register handler for org.freedesktop.portal.Session::Close */
++ session_class->close = web_extensions_session_close;
++}
++
++static WebExtensionsSession *
++web_extensions_session_new (GVariant *options,
++ Call *call,
++ GDBusConnection *connection,
++ GError **error)
++{
++ Session *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);
++
++ 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)
++{
++ Call *call = call_from_invocation (invocation);
++ GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation);
++ g_autoptr(GError) error = NULL;
++ Session *session;
++
++ session = (Session *)web_extensions_session_new (arg_options, call, connection, &error);
++ if (!session)
++ {
++ g_dbus_method_invocation_return_gerror (invocation, error);
++ return TRUE;
++ }
++ if (!session_export (session, &error))
++ {
++ g_dbus_method_invocation_return_gerror (invocation, error);
++ session_close (session, FALSE);
++ return TRUE;
++ }
++ 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)
++{
++ Session *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;
++ 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)
++{
++ Call *call = call_from_invocation (invocation);
++ Session *session;
++ WebExtensionsSession *web_extensions_session;
++ g_autofree char *host_path = NULL;
++ g_autofree char *json_manifest = NULL;
++ g_autoptr(GError) error = NULL;
++
++ session = acquire_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)
++{
++ Request *request = (Request *)task_data;
++ Session *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;
++ Permission 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 = get_permission_sync (app_id, PERMISSION_TABLE, arg_name);
++ if (permission == PERMISSION_ASK || permission == PERMISSION_UNSET)
++ {
++ guint access_response = 2;
++ g_autoptr(GVariant) access_results = NULL;
++ GVariantBuilder opt_builder;
++ g_autoptr(GAppInfo) info = NULL;
++ 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;
++
++ info = xdp_app_info_load_app_info (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;
++
++ if (permission == PERMISSION_UNSET)
++ set_permission_sync (app_id, PERMISSION_TABLE, arg_name, allowed ? PERMISSION_YES : PERMISSION_NO);
++ }
++ else
++ {
++ allowed = permission == 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));
++ request_unexport (request);
++ }
++
++ if (should_close_session)
++ 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)
++{
++ Request *request = request_from_invocation (invocation);
++ Session *session;
++ WebExtensionsSession *web_extensions_session;
++ g_autoptr(GTask) task = NULL;
++
++ REQUEST_AUTOLOCK (request);
++
++ session = acquire_session (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);
++
++ 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)
++{
++ Call *call = call_from_invocation (invocation);
++ Session *session;
++ WebExtensionsSession *web_extensions_session;
++ int fds[3];
++ g_autoptr(GUnixFDList) out_fd_list = NULL;
++
++ session = acquire_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);
++
++ return G_DBUS_INTERFACE_SKELETON (web_extensions);
++}
+diff --git a/src/web-extensions.h b/src/web-extensions.h
+new file mode 100644
+index 0000000..72b947a
+--- /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 <http://www.gnu.org/licenses/>.
++ *
++ */
++
++#pragma once
++
++#include <gio/gio.h>
++
++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 e377a96..5eade15 100644
+--- a/src/xdg-desktop-portal.c
++++ b/src/xdg-desktop-portal.c
+@@ -64,6 +64,7 @@
+ #include "settings.h"
+ #include "trash.h"
+ #include "wallpaper.h"
++#include "web-extensions.h"
+
+ static GMainLoop *loop = NULL;
+
+@@ -163,6 +164,13 @@ method_needs_request (GDBusMethodInvocation *invocation)
+ else
+ return TRUE;
+ }
++ else if (strcmp (interface, "org.freedesktop.portal.WebExtensions") == 0)
++ {
++ if (strcmp (method, "Start") == 0)
++ return TRUE;
++ else
++ return FALSE;
++ }
+ else
+ {
+ return TRUE;
+@@ -309,6 +317,9 @@ on_bus_acquired (GDBusConnection *connection,
+ device_create (connection,
+ access_impl->dbus_name,
+ lockdown));
++ 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/tests/meson.build b/tests/meson.build
+index a2dafee..bb8a338 100644
+--- a/tests/meson.build
++++ b/tests/meson.build
+@@ -15,6 +15,7 @@ subdir('dbs')
+ subdir('portals')
+ subdir('services')
+ subdir('share')
++subdir('native-messaging-hosts')
+
+ test_db = executable(
+ 'testdb',
+@@ -114,6 +115,7 @@ if have_libportal
+ 'screenshot.c',
+ 'trash.c',
+ 'wallpaper.c',
++ 'web-extensions.c',
+ 'glib-backports.c',
+ )
+
+@@ -177,6 +179,7 @@ portal_tests = [
+ 'screenshot',
+ '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 0000000..5ee7905
+--- /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 0000000..58cd310
+--- /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 at example.org"
++ ]
++}
+diff --git a/tests/native-messaging-hosts/server.sh b/tests/native-messaging-hosts/server.sh
+new file mode 100755
+index 0000000..b0f3e40
+--- /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 3792953..1248485 100644
+--- a/tests/test-portals.c
++++ b/tests/test-portals.c
+@@ -23,6 +23,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;
+@@ -263,12 +265,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));
+@@ -586,6 +590,9 @@ main (int argc, char **argv)
+ g_test_add_func ("/portal/notification/bad-arg", test_notification_bad_arg);
+ g_test_add_func ("/portal/notification/bad-priority", test_notification_bad_priority);
+ g_test_add_func ("/portal/notification/bad-button", test_notification_bad_button);
++
++ 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 0000000..711a6ca
+--- /dev/null
++++ b/tests/web-extensions.c
+@@ -0,0 +1,616 @@
++#include <config.h>
++
++#include "web-extensions.h"
++#include "xdp-utils.h"
++
++#include <gio/gio.h>
++#include <gio/gunixfdlist.h>
++#include "xdp-impl-dbus.h"
++
++// 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 at 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 at 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 at 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 at 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 };
++
++ got_info = 0;
++
++ 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 at 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 0000000..0090184
+--- /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/debian/patches/xdp_validate_icon-Allow-sandboxing-of-the-validator-to-be.patch b/debian/patches/xdp_validate_icon-Allow-sandboxing-of-the-validator-to-be.patch
deleted file mode 100644
index 8e4fc45..0000000
--- a/debian/patches/xdp_validate_icon-Allow-sandboxing-of-the-validator-to-be.patch
+++ /dev/null
@@ -1,31 +0,0 @@
-From: Simon McVittie <smcv at debian.org>
-Date: Wed, 30 Oct 2024 17:41:39 +0000
-Subject: xdp_validate_icon: Allow sandboxing of the validator to be disabled
-
-OS distributions often compile packages and run their build-time tests
-in a chroot environment or an unprivileged container, and bubblewrap
-cannot usually work in either of those environments.
-
-Signed-off-by: Simon McVittie <smcv at debian.org>
-Bug: https://github.com/flatpak/xdg-desktop-portal/issues/1497
-Forwarded: https://github.com/flatpak/xdg-desktop-portal/pull/1498
----
- src/xdp-utils.c | 5 ++++-
- 1 file changed, 4 insertions(+), 1 deletion(-)
-
-diff --git a/src/xdp-utils.c b/src/xdp-utils.c
-index 8ecb84b..3fde49c 100644
---- a/src/xdp-utils.c
-+++ b/src/xdp-utils.c
-@@ -606,7 +606,10 @@ xdp_validate_icon (XdpSealedFd *icon,
-
- i = 0;
- args[i++] = icon_validator;
-- args[i++] = "--sandbox";
-+
-+ if (g_getenv ("XDP_VALIDATE_ICON_INSECURE") == NULL)
-+ args[i++] = "--sandbox";
-+
- args[i++] = "--fd";
- args[i++] = G_STRINGIFY (VALIDATOR_INPUT_FD);
- args[i++] = "--ruleset";
diff --git a/debian/patches/xdp_validate_icon-Assign-argument-indices-automatically.patch b/debian/patches/xdp_validate_icon-Assign-argument-indices-automatically.patch
deleted file mode 100644
index c5c907b..0000000
--- a/debian/patches/xdp_validate_icon-Assign-argument-indices-automatically.patch
+++ /dev/null
@@ -1,46 +0,0 @@
-From: Simon McVittie <smcv at debian.org>
-Date: Wed, 30 Oct 2024 17:40:29 +0000
-Subject: xdp_validate_icon: Assign argument indices automatically
-
-Signed-off-by: Simon McVittie <smcv at debian.org>
-Bug: https://github.com/flatpak/xdg-desktop-portal/issues/1497
-Forwarded: https://github.com/flatpak/xdg-desktop-portal/pull/1498
----
- src/xdp-utils.c | 17 ++++++++++-------
- 1 file changed, 10 insertions(+), 7 deletions(-)
-
-diff --git a/src/xdp-utils.c b/src/xdp-utils.c
-index 2219b36..8ecb84b 100644
---- a/src/xdp-utils.c
-+++ b/src/xdp-utils.c
-@@ -591,6 +591,7 @@ xdp_validate_icon (XdpSealedFd *icon,
- const char *icon_validator = LIBEXECDIR "/xdg-desktop-portal-validate-icon";
- const char *args[7];
- int size;
-+ size_t i;
- g_autofree char *output = NULL;
- g_autoptr(GKeyFile) key_file = NULL;
-
-@@ -603,13 +604,15 @@ xdp_validate_icon (XdpSealedFd *icon,
- return FALSE;
- }
-
-- args[0] = icon_validator;
-- args[1] = "--sandbox";
-- args[2] = "--fd";
-- args[3] = G_STRINGIFY (VALIDATOR_INPUT_FD);
-- args[4] = "--ruleset";
-- args[5] = icon_type_to_string (icon_type);
-- args[6] = NULL;
-+ i = 0;
-+ args[i++] = icon_validator;
-+ args[i++] = "--sandbox";
-+ args[i++] = "--fd";
-+ args[i++] = G_STRINGIFY (VALIDATOR_INPUT_FD);
-+ args[i++] = "--ruleset";
-+ args[i++] = icon_type_to_string (icon_type);
-+ g_assert (i < G_N_ELEMENTS (args));
-+ args[i++] = NULL;
-
- output = xdp_spawn_full (args, xdp_sealed_fd_dup_fd (icon), VALIDATOR_INPUT_FD, &error);
- if (!output)
More information about the Neon-commits
mailing list