[epiphany] Add a WebAppProvider D-Bus interface



commit 7547094e246e1e9277a6fbc66f1a7fd6db7b5b6e
Author: Phaedrus Leeds <mwleeds protonmail com>
Date:   Mon Nov 29 17:53:02 2021 -0800

    Add a WebAppProvider D-Bus interface
    
    This commit adds a D-Bus service that exposes the web apps managed by
    Epiphany so that a client can enumerate them, install a new one, or
    remove an installed one. The intended client is GNOME Software, since I
    am soon going to add back a webapp plugin to Software which will make
    use of this interface. This is part of a larger effort to improve the
    support for Progressive Web Apps in GNOME (though Epiphany's web apps do
    not support PWA manifest features).
    
    The great thing about having this be a service exposed by Epiphany,
    rather than having Software try to install Epiphany-compatible web apps
    on its own as it used to do, is that the implementation of how to create
    and manage the web apps can stay in Epiphany, and there can never be
    disagreement between Epiphany and Software about the proper on-disk
    format for them (e.g. the algorithm used for generating the app ID from
    the name).
    
    The goal for the PWA project is to support Flatpak'd Epiphany, whereas
    currently only non-Flatpak Epiphany can do web apps. This will be
    accomplished with new portals to allow installing and removing the
    .desktop launchers. The Flatpak support requirement is reflected in the
    design of the API here, specifically the install_token parameter for the
    Install() method. This token is to be acquired by the client of the
    D-Bus interface (Software) so that the installation can be achieved
    without any additional user interaction since the user would've already
    clicked "Install" in Software. Web app installation directly via
    Flatpak'd Epiphany's UI would involve a dialog created by the portal;
    this is because we don't want sandboxed applications in general to be
    able to create desktop launchers without user interaction.
    
    The Uninstall() method by contrast doesn't require such a token,
    because the portal can ensure that only apps created by an application
    are deleted by that application.
    
    The GetInstalledApps() method is implemented by looking at the profile
    directories of the apps, because we don't want to have to poke a sandbox
    hole to allow access to the actual desktop files.

 data/meson.build                                   |   7 +
 data/org.gnome.Epiphany.WebAppProvider.service.in  |   3 +
 embed/ephy-embed-shell.c                           |   2 +-
 lib/ephy-web-app-utils.c                           | 154 +++++++++--
 lib/ephy-web-app-utils.h                           |  24 +-
 org.gnome.Epiphany.Canary.json.in                  |   3 +-
 org.gnome.Epiphany.json                            |   3 +-
 src/ephy-main.c                                    |   2 +-
 src/meson.build                                    |  23 +-
 src/webapp-provider/ephy-webapp-provider-main.c    |  61 +++++
 src/webapp-provider/ephy-webapp-provider.c         | 282 +++++++++++++++++++++
 src/webapp-provider/ephy-webapp-provider.h         |  36 +++
 .../org.gnome.Epiphany.WebAppProvider.xml          |  85 +++++++
 src/window-commands.c                              |   3 +-
 tests/ephy-web-app-utils-test.c                    |   4 +-
 15 files changed, 659 insertions(+), 33 deletions(-)
---
diff --git a/data/meson.build b/data/meson.build
index eac6b8224..3108a0060 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -96,6 +96,13 @@ configure_file(
   install_dir: servicedir
 )
 
+configure_file(
+  input: 'org.gnome.Epiphany.WebAppProvider.service.in',
+  output: 'org.gnome.Epiphany.WebAppProvider.service',
+  configuration: service_conf,
+  install_dir: servicedir
+)
+
 search_provider_conf = configuration_data()
 search_provider_conf.set('appid', application_id)
 search_provider_conf.set('profile', profile != '' ? '/' + profile : '')
diff --git a/data/org.gnome.Epiphany.WebAppProvider.service.in 
b/data/org.gnome.Epiphany.WebAppProvider.service.in
new file mode 100644
index 000000000..1ab1699a6
--- /dev/null
+++ b/data/org.gnome.Epiphany.WebAppProvider.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.gnome.Epiphany.WebAppProvider
+Exec=@libexecdir@/epiphany-webapp-provider
diff --git a/embed/ephy-embed-shell.c b/embed/ephy-embed-shell.c
index 99d1fb0ef..cc5f9832c 100644
--- a/embed/ephy-embed-shell.c
+++ b/embed/ephy-embed-shell.c
@@ -340,7 +340,7 @@ web_process_extension_about_apps_message_received_cb (WebKitUserContentManager *
   g_autofree char *app_id = NULL;
 
   app_id = jsc_value_to_string (webkit_javascript_result_get_js_value (message));
-  ephy_web_application_delete (app_id);
+  ephy_web_application_delete (app_id, NULL);
 }
 
 static char *
diff --git a/lib/ephy-web-app-utils.c b/lib/ephy-web-app-utils.c
index dd2291833..233999942 100644
--- a/lib/ephy-web-app-utils.c
+++ b/lib/ephy-web-app-utils.c
@@ -242,6 +242,21 @@ ephy_web_application_get_profile_directory (const char *id)
   return ephy_web_application_get_directory_under (id, g_get_user_data_dir ());
 }
 
+/**
+ * ephy_web_application_get_desktop_path:
+ * @app: the #EphyWebApplication
+ *
+ * Gets the path to the .desktop file for @app
+ *
+ * Returns: (transfer full): A newly allocated string.
+ **/
+char *
+ephy_web_application_get_desktop_path (EphyWebApplication *app)
+{
+  g_autofree char *profile_dir = ephy_web_application_get_profile_directory (app->id);
+  return g_build_filename (profile_dir, app->desktop_file, NULL);
+}
+
 static char *
 ephy_web_application_get_cache_directory (const char *id)
 {
@@ -256,7 +271,10 @@ ephy_web_application_get_config_directory (const char *id)
 
 /**
  * ephy_web_application_delete:
- * @id: the identifier of the web application do delete
+ * @id: the identifier of the web application to delete
+ * @out_app_found: return location for a #EphyWebAppFound. This will be set to
+ *   %EPHY_WEB_APP_NOT_FOUND if deleting the app failed due to it not being
+ *   installed, and %EPHY_WEB_APP_FOUND otherwise.
  *
  * Deletes all the data associated with a Web Application created by
  * Epiphany.
@@ -264,7 +282,8 @@ ephy_web_application_get_config_directory (const char *id)
  * Returns: %TRUE if the web app was succesfully deleted, %FALSE otherwise
  **/
 gboolean
-ephy_web_application_delete (const char *id)
+ephy_web_application_delete (const char      *id,
+                             EphyWebAppFound *out_app_found)
 {
   g_autofree char *profile_dir = NULL;
   g_autofree char *cache_dir = NULL;
@@ -276,6 +295,9 @@ ephy_web_application_delete (const char *id)
 
   g_assert (id);
 
+  if (out_app_found)
+    *out_app_found = EPHY_WEB_APP_FOUND;
+
   profile_dir = ephy_web_application_get_profile_directory (id);
   if (!profile_dir)
     return FALSE;
@@ -284,6 +306,8 @@ ephy_web_application_delete (const char *id)
    * exist. */
   if (!g_file_test (profile_dir, G_FILE_TEST_IS_DIR)) {
     g_warning ("No application with id '%s' is installed.\n", id);
+    if (out_app_found)
+      *out_app_found = EPHY_WEB_APP_NOT_FOUND;
     return FALSE;
   }
 
@@ -330,12 +354,43 @@ ephy_web_application_delete (const char *id)
   return TRUE;
 }
 
+/**
+ * ephy_web_application_delete_by_desktop_file_id:
+ * @desktop_file_id: the .desktop file name for the web app to be deleted, with
+ *   the extension
+ * @out_app_found: return location for a #EphyWebAppFound. This will be set to
+ *   %EPHY_WEB_APP_NOT_FOUND if deleting the app failed due to it not being
+ *   installed, and %EPHY_WEB_APP_FOUND otherwise.
+ *
+ * Deletes all the data associated with a Web Application created by
+ * Epiphany.
+ *
+ * Returns: %TRUE if the web app was succesfully deleted, %FALSE otherwise
+ **/
+gboolean
+ephy_web_application_delete_by_desktop_file_id (const char      *desktop_file_id,
+                                                EphyWebAppFound *out_app_found)
+{
+  const char *id;
+  g_autofree char *gapp_id = NULL;
+
+  g_assert (desktop_file_id);
+
+  gapp_id = g_strdup (desktop_file_id);
+  if (g_str_has_suffix (desktop_file_id, ".desktop"))
+    gapp_id[strlen (desktop_file_id) - strlen (".desktop")] = '\0';
+
+  id = get_app_id_from_gapplication_id (gapp_id);
+
+  return ephy_web_application_delete (id, out_app_found);
+}
+
 static char *
 create_desktop_file (const char *id,
                      const char *name,
                      const char *address,
                      const char *profile_dir,
-                     GdkPixbuf  *icon)
+                     const char *icon_path)
 {
   g_autofree char *filename = NULL;
   g_autoptr (GKeyFile) file = NULL;
@@ -365,18 +420,8 @@ create_desktop_file (const char *id,
   g_key_file_set_value (file, "Desktop Entry", "Type", "Application");
   g_key_file_set_value (file, "Desktop Entry", "Categories", "GNOME;GTK;");
 
-  if (icon) {
-    g_autoptr (GOutputStream) stream = NULL;
-    g_autofree char *path = NULL;
-    g_autoptr (GFile) image = NULL;
-
-    path = g_build_filename (profile_dir, EPHY_WEB_APP_ICON_NAME, NULL);
-    image = g_file_new_for_path (path);
-
-    stream = (GOutputStream *)g_file_create (image, 0, NULL, NULL);
-    gdk_pixbuf_save_to_stream (icon, stream, "png", NULL, NULL, NULL);
-    g_key_file_set_value (file, "Desktop Entry", "Icon", path);
-  }
+  if (icon_path)
+    g_key_file_set_value (file, "Desktop Entry", "Icon", icon_path);
 
   wm_class = g_strconcat (EPHY_WEB_APP_GAPPLICATION_ID_PREFIX, id, NULL);
   g_key_file_set_value (file, "Desktop Entry", "StartupWMClass", wm_class);
@@ -409,7 +454,10 @@ create_desktop_file (const char *id,
  * @id: the identifier for the new web application
  * @address: the address of the new web application
  * @name: the name for the new web application
- * @icon: the icon for the new web application
+ * @icon_pixbuf: the icon for the new web application as a #GdkPixbuf
+ * @icon_path: the path to the icon, used instead of @icon_pixbuf
+ * @install_token: the install token acquired via portal, used for
+ *   non-interactive sandboxed installation
  * @options: the options for the new web application
  *
  * Creates a new Web Application for @address.
@@ -420,14 +468,19 @@ char *
 ephy_web_application_create (const char                *id,
                              const char                *address,
                              const char                *name,
-                             GdkPixbuf                 *icon,
+                             GdkPixbuf                 *icon_pixbuf,
+                             const char                *icon_path,
+                             const char                *install_token,
                              EphyWebApplicationOptions  options)
 {
   g_autofree char *app_file = NULL;
   g_autofree char *profile_dir = NULL;
   g_autofree char *desktop_file_path = NULL;
+  g_autofree char *icon_path_owned = NULL;
   int fd;
 
+  g_return_val_if_fail (!icon_pixbuf || !icon_path, NULL);
+
   /* If there's already a WebApp profile for the contents of this
    * view, do nothing. */
   profile_dir = ephy_web_application_get_profile_directory (id);
@@ -454,8 +507,22 @@ ephy_web_application_create (const char                *id,
   }
   close (fd);
 
+  /* Write the icon to a file */
+  if (icon_pixbuf) {
+    g_autoptr (GOutputStream) stream = NULL;
+    g_autoptr (GFile) image = NULL;
+
+    icon_path_owned = g_build_filename (profile_dir, EPHY_WEB_APP_ICON_NAME, NULL);
+    image = g_file_new_for_path (icon_path_owned);
+
+    stream = (GOutputStream *)g_file_create (image, 0, NULL, NULL);
+    gdk_pixbuf_save_to_stream (icon_pixbuf, stream, "png", NULL, NULL, NULL);
+  } else {
+    icon_path_owned = g_strdup (icon_path);
+  }
+
   /* Create the deskop file. */
-  desktop_file_path = create_desktop_file (id, name, address, profile_dir, icon);
+  desktop_file_path = create_desktop_file (id, name, address, profile_dir, icon_path_owned);
   if (desktop_file_path)
     ephy_web_application_initialize_settings (profile_dir, options);
 
@@ -586,7 +653,6 @@ ephy_web_application_for_profile_directory (const char *profile_dir)
   g_auto (GStrv) argv = NULL;
   g_autoptr (GFile) file = NULL;
   g_autoptr (GFileInfo) file_info = NULL;
-  guint64 created;
   g_autoptr (GDate) date = NULL;
 
   id = get_app_id_from_profile_directory (profile_dir);
@@ -614,10 +680,10 @@ ephy_web_application_for_profile_directory (const char *profile_dir)
 
   /* FIXME: this should use TIME_CREATED but it does not seem to be working. */
   file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED, 0, NULL, NULL);
-  created = g_file_info_get_attribute_uint64 (file_info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+  app->install_date_uint64 = g_file_info_get_attribute_uint64 (file_info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
 
   date = g_date_new ();
-  g_date_set_time_t (date, (time_t)created);
+  g_date_set_time_t (date, (time_t)app->install_date_uint64);
   g_date_strftime (app->install_date, 127, "%x", date);
 
   return g_steal_pointer (&app);
@@ -705,7 +771,6 @@ ephy_web_application_get_legacy_application_list (void)
   return ephy_web_application_get_application_list_internal (TRUE);
 }
 
-
 /**
  * ephy_web_application_free_application_list:
  * @list: an #EphyWebApplication GList
@@ -718,6 +783,51 @@ ephy_web_application_free_application_list (GList *list)
   g_list_free_full (list, (GDestroyNotify)ephy_web_application_free);
 }
 
+/**
+ * ephy_web_application_get_desktop_id_list:
+ *
+ * Gets a list of the currently installed web applications' .desktop filenames.
+ * This is useful even though we don't have access to the actual .desktop files
+ * when running under Flatpak, because we return it over D-Bus in the
+ * WebAppProvider service.
+ *
+ * Returns: (transfer-full): a %NULL-terminated array of strings
+ **/
+char **
+ephy_web_application_get_desktop_id_list (void)
+{
+  g_autoptr (GFileEnumerator) children = NULL;
+  g_autoptr (GFile) parent_directory = NULL;
+  GPtrArray *desktop_file_ids;
+
+  parent_directory = g_file_new_for_path (g_get_user_data_dir ());
+  children = g_file_enumerate_children (parent_directory,
+                                        "standard::name",
+                                        0, NULL, NULL);
+  if (!children)
+    return NULL;
+
+  desktop_file_ids = g_ptr_array_new_with_free_func (g_free);
+  for (;;) {
+    g_autoptr (GFileInfo) info = g_file_enumerator_next_file (children, NULL, NULL);
+    const char *name;
+
+    if (!info)
+      break;
+
+    name = g_file_info_get_name (info);
+    if (g_str_has_prefix (name, get_gapplication_id_prefix ())) {
+      g_autofree char *desktop_file_id = NULL;
+      desktop_file_id = g_strconcat (name, ".desktop", NULL);
+      g_ptr_array_add (desktop_file_ids, g_steal_pointer (&desktop_file_id));
+    }
+  }
+
+  g_ptr_array_add (desktop_file_ids, NULL);
+
+  return (char **)g_ptr_array_free (desktop_file_ids, FALSE);
+}
+
 /**
  * ephy_web_application_exists:
  * @id: the potential identifier of the web application
diff --git a/lib/ephy-web-app-utils.h b/lib/ephy-web-app-utils.h
index 4f98fc678..900b101f5 100644
--- a/lib/ephy-web-app-utils.h
+++ b/lib/ephy-web-app-utils.h
@@ -33,6 +33,7 @@ typedef struct {
   char *url;
   char *desktop_file;
   char install_date[128];
+  guint64 install_date_uint64;
 } EphyWebApplication;
 
 /**
@@ -51,17 +52,32 @@ typedef enum {
   EPHY_WEB_APPLICATION_SYSTEM,
 } EphyWebApplicationOptions;
 
+typedef enum {
+  EPHY_WEB_APP_FOUND,
+  EPHY_WEB_APP_NOT_FOUND,
+} EphyWebAppFound;
+
 #define EPHY_WEB_APP_ICON_NAME "app-icon.png"
 
 char               *ephy_web_application_get_app_id_from_name (const char *name);
 
 const char         *ephy_web_application_get_gapplication_id_from_profile_directory (const char 
*profile_dir);
 
-char               *ephy_web_application_create (const char *id, const char *address, const char *name, 
GdkPixbuf *icon, EphyWebApplicationOptions options);
+char               *ephy_web_application_create (const char                *id,
+                                                 const char                *address,
+                                                 const char                *name,
+                                                 GdkPixbuf                 *icon_pixbuf,
+                                                 const char                *icon_path,
+                                                 const char                *install_token,
+                                                 EphyWebApplicationOptions  options);
 
 char               *ephy_web_application_ensure_for_app_info (GAppInfo *app_info);
 
-gboolean            ephy_web_application_delete (const char *id);
+gboolean            ephy_web_application_delete (const char      *id,
+                                                 EphyWebAppFound *out_app_found);
+
+gboolean            ephy_web_application_delete_by_desktop_file_id (const char      *desktop_file_id,
+                                                                    EphyWebAppFound *out_app_found);
 
 void                ephy_web_application_setup_from_profile_directory (const char *profile_directory);
 
@@ -69,6 +85,8 @@ void                ephy_web_application_setup_from_desktop_file (GDesktopAppInf
 
 char               *ephy_web_application_get_profile_directory (const char *id);
 
+char               *ephy_web_application_get_desktop_path (EphyWebApplication *app);
+
 EphyWebApplication *ephy_web_application_for_profile_directory (const char *profile_dir);
 
 void                ephy_web_application_free (EphyWebApplication *app);
@@ -79,6 +97,8 @@ GList              *ephy_web_application_get_application_list (void);
 
 GList              *ephy_web_application_get_legacy_application_list (void);
 
+char              **ephy_web_application_get_desktop_id_list (void);
+
 void                ephy_web_application_free_application_list (GList *list);
 
 void                ephy_web_application_initialize_settings (const char *profile_directory, 
EphyWebApplicationOptions options);
diff --git a/org.gnome.Epiphany.Canary.json.in b/org.gnome.Epiphany.Canary.json.in
index 17fc14086..bade6e118 100644
--- a/org.gnome.Epiphany.Canary.json.in
+++ b/org.gnome.Epiphany.Canary.json.in
@@ -17,7 +17,8 @@
         "--socket=fallback-x11",
         "--socket=pulseaudio",
         "--socket=wayland",
-        "--system-talk-name=org.freedesktop.GeoClue2"
+        "--system-talk-name=org.freedesktop.GeoClue2",
+        "--own-name=org.gnome.Epiphany.WebAppProvider"
     ],
     "modules" : [
         {
diff --git a/org.gnome.Epiphany.json b/org.gnome.Epiphany.json
index 78ef33483..e0dddbf79 100644
--- a/org.gnome.Epiphany.json
+++ b/org.gnome.Epiphany.json
@@ -17,7 +17,8 @@
         "--socket=fallback-x11",
         "--socket=pulseaudio",
         "--socket=wayland",
-        "--system-talk-name=org.freedesktop.GeoClue2"
+        "--system-talk-name=org.freedesktop.GeoClue2",
+        "--own-name=org.gnome.Epiphany.WebAppProvider"
     ],
     "modules" : [
          {
diff --git a/src/ephy-main.c b/src/ephy-main.c
index 48b6c684b..bcf5ce703 100644
--- a/src/ephy-main.c
+++ b/src/ephy-main.c
@@ -370,7 +370,7 @@ main (int   argc,
   /* Delete the requested web application, if any. Must happen after
    * ephy_file_helpers_init (). */
   if (application_to_delete) {
-    ephy_web_application_delete (application_to_delete);
+    ephy_web_application_delete (application_to_delete, NULL);
     exit (0);
   }
 
diff --git a/src/meson.build b/src/meson.build
index eaee92180..50f6710b3 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -111,7 +111,7 @@ ephy_profile_migrator = executable('ephy-profile-migrator',
 )
 
 
-codegen = gnome.gdbus_codegen('ephy-shell-search-provider-generated',
+search_provider_codegen = gnome.gdbus_codegen('ephy-shell-search-provider-generated',
   'search-provider/org.gnome.ShellSearchProvider2.xml',
   interface_prefix: 'org.gnome',
   namespace: 'Ephy'
@@ -120,7 +120,7 @@ codegen = gnome.gdbus_codegen('ephy-shell-search-provider-generated',
 search_provider_sources = [
   'search-provider/ephy-search-provider.c',
   'search-provider/ephy-search-provider-main.c',
-  codegen
+  search_provider_codegen
 ]
 
 executable('epiphany-search-provider',
@@ -131,6 +131,25 @@ executable('epiphany-search-provider',
   install_rpath: pkglibdir
 )
 
+webapp_codegen = gnome.gdbus_codegen('ephy-webapp-provider-generated',
+  'webapp-provider/org.gnome.Epiphany.WebAppProvider.xml',
+  interface_prefix: 'org.gnome.Epiphany',
+  namespace: 'Ephy'
+)
+
+webapp_provider_sources = [
+  'webapp-provider/ephy-webapp-provider.c',
+  'webapp-provider/ephy-webapp-provider-main.c',
+  webapp_codegen
+]
+
+executable('epiphany-webapp-provider',
+  webapp_provider_sources,
+  dependencies: ephymain_dep,
+  install: true,
+  install_dir: libexecdir,
+  install_rpath: pkglibdir
+)
 
 resource_files = files('resources/epiphany.gresource.xml')
 resources = gnome.compile_resources('epiphany-resources',
diff --git a/src/webapp-provider/ephy-webapp-provider-main.c b/src/webapp-provider/ephy-webapp-provider-main.c
new file mode 100644
index 000000000..e4fdcfb55
--- /dev/null
+++ b/src/webapp-provider/ephy-webapp-provider-main.c
@@ -0,0 +1,61 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright (c) 2013 Igalia S.L.
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-webapp-provider.h"
+#include "ephy-file-helpers.h"
+
+#include <glib/gi18n.h>
+#include <locale.h>
+
+int
+main (int    argc,
+      char **argv)
+{
+  g_autoptr (EphyWebAppProviderService) webapp_provider = NULL;
+  int status;
+  GError *error = NULL;
+
+  g_setenv ("GIO_USE_VFS", "local", TRUE);
+
+  g_debug ("started %s", argv[0]);
+
+  /* Initialize the i18n stuff */
+  setlocale (LC_ALL, "");
+  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+  textdomain (GETTEXT_PACKAGE);
+
+  if (!ephy_file_helpers_init (NULL, 0, &error)) {
+    g_printerr ("%s\n", error->message);
+    g_error_free (error);
+    return 1;
+  }
+
+  webapp_provider = ephy_web_app_provider_service_new ();
+  status = g_application_run (G_APPLICATION (webapp_provider), argc, argv);
+
+  ephy_file_helpers_shutdown ();
+
+  g_debug ("stopping %s with status %d", argv[0], status);
+
+  return status;
+}
diff --git a/src/webapp-provider/ephy-webapp-provider.c b/src/webapp-provider/ephy-webapp-provider.c
new file mode 100644
index 000000000..af5fd7e05
--- /dev/null
+++ b/src/webapp-provider/ephy-webapp-provider.c
@@ -0,0 +1,282 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright (c) 2021 Matthew Leeds <mwleeds protonmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-webapp-provider.h"
+
+#include "ephy-web-app-utils.h"
+#include "ephy-flatpak-utils.h"
+
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+
+struct _EphyWebAppProviderService {
+  GApplication parent_instance;
+
+  EphyWebAppProvider *skeleton;
+};
+
+struct _EphyWebAppProviderServiceClass {
+  GApplicationClass parent_class;
+};
+
+G_DEFINE_TYPE (EphyWebAppProviderService, ephy_web_app_provider_service, G_TYPE_APPLICATION)
+
+#define INACTIVITY_TIMEOUT 60 * 1000 /* One minute, in milliseconds */
+
+typedef enum {
+  EPHY_WEBAPP_PROVIDER_ERROR_FAILED,
+  EPHY_WEBAPP_PROVIDER_ERROR_INVALID_ARGS,
+  EPHY_WEBAPP_PROVIDER_ERROR_NOT_INSTALLED,
+  EPHY_WEBAPP_PROVIDER_ERROR_LAST = EPHY_WEBAPP_PROVIDER_ERROR_NOT_INSTALLED, /*< skip >*/
+} EphyWebAppProviderError;
+
+static const GDBusErrorEntry ephy_webapp_provider_error_entries[] = {
+  { EPHY_WEBAPP_PROVIDER_ERROR_FAILED, "org.gnome.Epiphany.WebAppProvider.Error.Failed" },
+  { EPHY_WEBAPP_PROVIDER_ERROR_INVALID_ARGS, "org.gnome.Epiphany.WebAppProvider.Error.InvalidArgs" },
+  { EPHY_WEBAPP_PROVIDER_ERROR_NOT_INSTALLED, "org.gnome.Epiphany.WebAppProvider.Error.NotInstalled" },
+};
+
+/* Ensure that every error code has an associated D-Bus error name */
+G_STATIC_ASSERT (G_N_ELEMENTS (ephy_webapp_provider_error_entries) == EPHY_WEBAPP_PROVIDER_ERROR_LAST + 1);
+
+#define EPHY_WEBAPP_PROVIDER_ERROR (ephy_webapp_provider_error_quark ())
+GQuark
+ephy_webapp_provider_error_quark (void)
+{
+  static gsize quark = 0;
+  g_dbus_error_register_error_domain ("ephy-webapp-provider-error-quark",
+                                      &quark,
+                                      ephy_webapp_provider_error_entries,
+                                      G_N_ELEMENTS (ephy_webapp_provider_error_entries));
+  return (GQuark)quark;
+}
+
+static gboolean
+handle_get_installed_apps (EphyWebAppProvider        *skeleton,
+                           GDBusMethodInvocation     *invocation,
+                           EphyWebAppProviderService *self)
+{
+  g_auto (GStrv) desktop_ids = NULL;
+
+  g_debug ("%s", G_STRFUNC);
+
+  g_application_hold (G_APPLICATION (self));
+
+  desktop_ids = ephy_web_application_get_desktop_id_list ();
+
+  ephy_web_app_provider_complete_get_installed_apps (skeleton, invocation,
+                                                     (const gchar * const *)desktop_ids);
+
+  g_application_release (G_APPLICATION (self));
+
+  return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static gboolean
+handle_install (EphyWebAppProvider        *skeleton,
+                GDBusMethodInvocation     *invocation,
+                char                      *url,
+                char                      *name,
+                char                      *install_token,
+                EphyWebAppProviderService *self)
+{
+  g_autofree char *id = NULL;
+  g_autofree char *desktop_path = NULL;
+  g_autofree char *desktop_file_id = NULL;
+
+  g_debug ("%s", G_STRFUNC);
+
+  g_application_hold (G_APPLICATION (self));
+
+  /* We need an install token acquired by a trusted system component such as
+   * gnome-software because otherwise the Flatpak/Snap sandbox prevents us from
+   * installing the app without using a portal (which would not be appropriate
+   * since Epiphany is not the focused application). We use the same code path
+   * when not running under a sandbox too.
+   */
+  if (!install_token || *install_token == '\0') {
+    g_dbus_method_invocation_return_error (invocation, EPHY_WEBAPP_PROVIDER_ERROR,
+                                           EPHY_WEBAPP_PROVIDER_ERROR_INVALID_ARGS,
+                                           _("The install_token is required for the Install() method"));
+    goto out;
+  }
+  if (!g_uri_is_valid (url, G_URI_FLAGS_NONE, NULL)) {
+    g_dbus_method_invocation_return_error (invocation, EPHY_WEBAPP_PROVIDER_ERROR,
+                                           EPHY_WEBAPP_PROVIDER_ERROR_INVALID_ARGS,
+                                           _("The url passed was not valid: ‘%s’"), url);
+    goto out;
+  }
+  if (!name || *name == '\0') {
+    g_dbus_method_invocation_return_error (invocation, EPHY_WEBAPP_PROVIDER_ERROR,
+                                           EPHY_WEBAPP_PROVIDER_ERROR_INVALID_ARGS,
+                                           _("The name passed was not valid"));
+    goto out;
+  }
+
+  id = ephy_web_application_get_app_id_from_name (name);
+
+  desktop_path = ephy_web_application_create (id, url, name,
+                                              NULL, NULL, /* icon_pixbuf, icon_path */
+                                              install_token,
+                                              EPHY_WEB_APPLICATION_NONE);
+  if (!desktop_path) {
+    g_dbus_method_invocation_return_error (invocation, EPHY_WEBAPP_PROVIDER_ERROR,
+                                           EPHY_WEBAPP_PROVIDER_ERROR_FAILED,
+                                           _("Installing the web application ‘%s’ (%s) failed"),
+                                           name, url);
+    goto out;
+  }
+
+  desktop_file_id = g_path_get_basename (desktop_path);
+  ephy_web_app_provider_complete_install (skeleton, invocation, desktop_file_id);
+
+out:
+  g_application_release (G_APPLICATION (self));
+
+  return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static gboolean
+handle_uninstall (EphyWebAppProvider        *skeleton,
+                  GDBusMethodInvocation     *invocation,
+                  char                      *desktop_file_id,
+                  EphyWebAppProviderService *self)
+{
+  EphyWebAppFound app_found;
+
+  g_debug ("%s", G_STRFUNC);
+
+  g_application_hold (G_APPLICATION (self));
+
+  if (!desktop_file_id || !g_str_has_suffix (desktop_file_id, ".desktop")) {
+    g_dbus_method_invocation_return_error (invocation, EPHY_WEBAPP_PROVIDER_ERROR,
+                                           EPHY_WEBAPP_PROVIDER_ERROR_INVALID_ARGS,
+                                           _("The desktop file ID passed ‘%s’ was not valid"),
+                                           desktop_file_id ? desktop_file_id : "(null)");
+    goto out;
+  }
+
+  if (!ephy_web_application_delete_by_desktop_file_id (desktop_file_id, &app_found)) {
+    if (app_found == EPHY_WEB_APP_NOT_FOUND) {
+      g_dbus_method_invocation_return_error (invocation, EPHY_WEBAPP_PROVIDER_ERROR,
+                                             EPHY_WEBAPP_PROVIDER_ERROR_NOT_INSTALLED,
+                                             _("The web application ‘%s’ does not exist"),
+                                             desktop_file_id);
+    } else {
+      g_dbus_method_invocation_return_error (invocation, EPHY_WEBAPP_PROVIDER_ERROR,
+                                             EPHY_WEBAPP_PROVIDER_ERROR_FAILED,
+                                             _("The web application ‘%s’ could not be deleted"),
+                                             desktop_file_id);
+    }
+    goto out;
+  }
+
+  ephy_web_app_provider_complete_uninstall (skeleton, invocation);
+
+out:
+  g_application_release (G_APPLICATION (self));
+
+  return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static void
+ephy_web_app_provider_service_init (EphyWebAppProviderService *self)
+{
+  g_application_set_flags (G_APPLICATION (self), G_APPLICATION_IS_SERVICE);
+
+  g_application_set_inactivity_timeout (G_APPLICATION (self), INACTIVITY_TIMEOUT);
+}
+
+static gboolean
+ephy_web_app_provider_service_dbus_register (GApplication     *application,
+                                             GDBusConnection  *connection,
+                                             const gchar      *object_path,
+                                             GError          **error)
+{
+  EphyWebAppProviderService *self;
+
+  g_debug ("registering at object path %s", object_path);
+
+  if (!G_APPLICATION_CLASS (ephy_web_app_provider_service_parent_class)->dbus_register (application,
+                                                                                        connection,
+                                                                                        object_path,
+                                                                                        error))
+    return FALSE;
+
+  self = EPHY_WEB_APP_PROVIDER_SERVICE (application);
+  self->skeleton = ephy_web_app_provider_skeleton_new ();
+  ephy_web_app_provider_set_version (EPHY_WEB_APP_PROVIDER (self->skeleton), 1);
+
+  g_signal_connect (self->skeleton, "handle-get-installed-apps",
+                    G_CALLBACK (handle_get_installed_apps), self);
+  g_signal_connect (self->skeleton, "handle-install",
+                    G_CALLBACK (handle_install), self);
+  g_signal_connect (self->skeleton, "handle-uninstall",
+                    G_CALLBACK (handle_uninstall), self);
+
+  return g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->skeleton),
+                                           connection, object_path, error);
+}
+
+static void
+ephy_web_app_provider_service_dbus_unregister (GApplication    *application,
+                                               GDBusConnection *connection,
+                                               const gchar     *object_path)
+{
+  EphyWebAppProviderService *self;
+  GDBusInterfaceSkeleton *skeleton;
+
+  g_debug ("unregistering at object path %s", object_path);
+
+  self = EPHY_WEB_APP_PROVIDER_SERVICE (application);
+  skeleton = G_DBUS_INTERFACE_SKELETON (self->skeleton);
+  if (g_dbus_interface_skeleton_has_connection (skeleton, connection))
+    g_dbus_interface_skeleton_unexport_from_connection (skeleton, connection);
+
+  g_clear_object (&self->skeleton);
+
+  G_APPLICATION_CLASS (ephy_web_app_provider_service_parent_class)->dbus_unregister (application,
+                                                                                     connection,
+                                                                                     object_path);
+}
+
+static void
+ephy_web_app_provider_service_class_init (EphyWebAppProviderServiceClass *klass)
+{
+  GApplicationClass *application_class = G_APPLICATION_CLASS (klass);
+
+  application_class->dbus_register = ephy_web_app_provider_service_dbus_register;
+  application_class->dbus_unregister = ephy_web_app_provider_service_dbus_unregister;
+}
+
+EphyWebAppProviderService *
+ephy_web_app_provider_service_new (void)
+{
+  /* Note the application ID is constant for release/devel/canary builds
+   * because we want to always use the same well-known D-Bus name.
+   */
+  g_autofree gchar *app_id = g_strconcat ("org.gnome.Epiphany.WebAppProvider", NULL);
+
+  return g_object_new (EPHY_TYPE_WEB_APP_PROVIDER_SERVICE,
+                       "application-id", app_id,
+                       NULL);
+}
diff --git a/src/webapp-provider/ephy-webapp-provider.h b/src/webapp-provider/ephy-webapp-provider.h
new file mode 100644
index 000000000..1286cf0e0
--- /dev/null
+++ b/src/webapp-provider/ephy-webapp-provider.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright (c) 2021 Matthew Leeds <mwleeds protonmail com>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ephy-webapp-provider-generated.h"
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_WEB_APP_PROVIDER_SERVICE (ephy_web_app_provider_service_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyWebAppProviderService, ephy_web_app_provider_service, EPHY, 
WEB_APP_PROVIDER_SERVICE, GApplication)
+
+EphyWebAppProviderService *ephy_web_app_provider_service_new (void);
+
+G_END_DECLS
diff --git a/src/webapp-provider/org.gnome.Epiphany.WebAppProvider.xml 
b/src/webapp-provider/org.gnome.Epiphany.WebAppProvider.xml
new file mode 100644
index 000000000..6c2954dc3
--- /dev/null
+++ b/src/webapp-provider/org.gnome.Epiphany.WebAppProvider.xml
@@ -0,0 +1,85 @@
+<!DOCTYPE node PUBLIC
+'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
+'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
+<node>
+
+  <!--
+      org.gnome.Epiphany.WebAppProvider:
+      @short_description: Webapp provider interface
+
+      The interface used for handling Epiphany Webapps in GNOME Software, or other
+      clients (version 1).
+  -->
+  <interface name="org.gnome.Epiphany.WebAppProvider">
+    <!--
+        GetInstalledApps:
+        @desktop_file_ids: An array of .desktop file names, one for each
+          installed web app, with the .desktop suffix included
+
+        Returns the set of installed Epiphany web applications. The caller can
+        use them with g_desktop_app_info_new() if outside the sandbox.
+    -->
+    <method name="GetInstalledApps">
+      <arg type="as" name="webapps" direction="out" />
+    </method>
+
+    <!--
+        Install:
+        @url: the URL of the web app
+        @name: the human readable name of the web app
+        @install_token: the token acquired via org.freedesktop.portal.InstallDynamicLauncher
+        @desktop_file_id: the desktop file id of the installed app, with a
+          ".desktop" suffix
+
+        Installs a web app. This interface is expected to be used by trusted
+        system components such as GNOME Software, which can acquire an
+        @install_token using the portal method
+        org.freedesktop.portal.DynamicLauncher.RequestInstallToken(). This allows Epiphany
+        to install the web app without user interaction and despite being sandboxed.
+        This is desirable because the user would've already clicked "Install" in
+        Software; they should not have to confirm the operation again in a different
+        app (Epiphany).
+
+        The @install_token must be provided so that Epiphany can complete the
+        installation without a user-facing dialog. The icon given to
+        org.freedesktop.portal.InstallDynamicLauncher.RequestInstallToken() will
+        be used, and the name given to that method should match the @name given here.
+
+        If the arguments passed are invalid this method returns the error
+        `org.gnome.Epiphany.WebAppProvider.Error.InvalidArgs`, and otherwise
+        `org.gnome.Epiphany.WebAppProvider.Error.Failed`.
+    -->
+    <method name="Install">
+      <arg type="s" name="url" direction="in" />
+      <arg type="s" name="name" direction="in" />
+      <arg type="s" name="install_token" direction="in" />
+      <arg type="s" name="desktop_file_id" direction="out" />
+    </method>
+
+    <!--
+        Uninstall:
+        @desktop_file_id: the filename of the .desktop file for an installed web
+          app, with the .desktop suffix
+
+        Uninstalls a web app. Note that the @desktop_file_id is just a filename
+        not a full path, and it's the same one returned by the
+        GetInstalledWebApps() method.
+
+        The error `org.gnome.Epiphany.WebAppProvider.Error.NotInstalled` will be
+        returned if the specified web app is not installed. The other possible
+        error values are `org.gnome.Epiphany.WebAppProvider.Error.InvalidArgs`
+        and `org.gnome.Epiphany.WebAppProvider.Error.Failed`.
+    -->
+    <method name="Uninstall">
+      <arg type="s" name="desktop_file_id" direction="in" />
+    </method>
+    <!--
+        Version:
+
+        The API version number, to be incremented for backwards compatible
+        changes so clients can determine which features are available. For
+        backwards incompatible changes, the interface name will change.
+    -->
+    <property name="Version" type="u" access="read"/>
+  </interface>
+</node>
diff --git a/src/window-commands.c b/src/window-commands.c
index 91d62b14c..32f10091d 100644
--- a/src/window-commands.c
+++ b/src/window-commands.c
@@ -1793,6 +1793,7 @@ save_as_application_proceed (EphyApplicationDialogData *data)
                                               webkit_web_view_get_uri (WEBKIT_WEB_VIEW (data->view)),
                                               app_name,
                                               gtk_image_get_pixbuf (GTK_IMAGE (data->image)),
+                                              NULL, NULL, /* icon_path, install_token */
                                               data->webapp_options);
 
   if (desktop_file)
@@ -1842,7 +1843,7 @@ dialog_save_as_application_confirmation_cb (GtkDialog                 *dialog,
   gtk_widget_destroy (GTK_WIDGET (dialog));
 
   if (response == GTK_RESPONSE_OK) {
-    ephy_web_application_delete (app_id);
+    ephy_web_application_delete (app_id, NULL);
     save_as_application_proceed (data);
   }
 }
diff --git a/tests/ephy-web-app-utils-test.c b/tests/ephy-web-app-utils-test.c
index 5026388db..8f32570fe 100644
--- a/tests/ephy-web-app-utils-test.c
+++ b/tests/ephy-web-app-utils-test.c
@@ -68,7 +68,7 @@ test_web_app_lifetime (void)
 
     /* Test creation */
     id = ephy_web_application_get_app_id_from_name (test.name);
-    desktop_file = ephy_web_application_create (id, test.url, test.name, NULL, EPHY_WEB_APPLICATION_NONE);
+    desktop_file = ephy_web_application_create (id, test.url, test.name, NULL, NULL, NULL, 
EPHY_WEB_APPLICATION_NONE);
     g_assert_true (g_str_has_prefix (desktop_file, ephy_profile_dir ()));
     g_assert_true (g_file_test (desktop_file, G_FILE_TEST_EXISTS));
 
@@ -119,7 +119,7 @@ test_web_app_lifetime (void)
 
     /* Test delete API */
     g_test_message ("DELETE: %s", test.name);
-    g_assert_true (ephy_web_application_delete (id));
+    g_assert_true (ephy_web_application_delete (id, NULL));
 
     g_assert_false (g_file_test (desktop_link, G_FILE_TEST_EXISTS));
     g_assert_false (g_file_test (desktop_link, G_FILE_TEST_IS_SYMLINK));


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]