[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