Blame SOURCES/add-back-shell-extensions-support.patch

0e9714
From 82a85d6bbacb148e355680446c4bbd6a091ad2eb Mon Sep 17 00:00:00 2001
0e9714
From: Kalev Lember <klember@redhat.com>
0e9714
Date: Wed, 3 Jun 2020 15:50:41 +0200
0e9714
Subject: [PATCH 1/2] Revert "Remove support for Shell extensions"
0e9714
0e9714
This reverts commit 77bdc3855c26de0edc2ce5d73bb6be861721c37b.
0e9714
---
0e9714
 contrib/gnome-software.spec.in                |    1 +
0e9714
 meson_options.txt                             |    1 +
0e9714
 plugins/core/gs-appstream.c                   |   14 +-
0e9714
 plugins/core/gs-desktop-common.c              |    3 +
0e9714
 plugins/meson.build                           |    3 +
0e9714
 plugins/packagekit/gs-plugin-packagekit.c     |    3 +
0e9714
 plugins/shell-extensions/gs-appstream.c       |    1 +
0e9714
 plugins/shell-extensions/gs-appstream.h       |    1 +
0e9714
 .../gs-plugin-shell-extensions.c              | 1159 +++++++++++++++++
0e9714
 plugins/shell-extensions/gs-self-test.c       |  156 +++
0e9714
 plugins/shell-extensions/meson.build          |   47 +
0e9714
 src/gs-category-page.c                        |   25 +
0e9714
 src/gs-category-page.ui                       |   45 +
0e9714
 src/gs-details-page.c                         |   14 +-
0e9714
 src/gs-repo-row.c                             |    3 +-
0e9714
 src/gs-repos-dialog.c                         |    3 +-
0e9714
 src/gs-summary-tile.c                         |   15 +-
0e9714
 17 files changed, 1478 insertions(+), 16 deletions(-)
0e9714
 create mode 120000 plugins/shell-extensions/gs-appstream.c
0e9714
 create mode 120000 plugins/shell-extensions/gs-appstream.h
0e9714
 create mode 100644 plugins/shell-extensions/gs-plugin-shell-extensions.c
0e9714
 create mode 100644 plugins/shell-extensions/gs-self-test.c
0e9714
 create mode 100644 plugins/shell-extensions/meson.build
0e9714
0e9714
diff --git a/contrib/gnome-software.spec.in b/contrib/gnome-software.spec.in
0e9714
index 587a72e6..7f415f51 100644
0e9714
--- a/contrib/gnome-software.spec.in
0e9714
+++ b/contrib/gnome-software.spec.in
0e9714
@@ -183,6 +183,7 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
0e9714
 %{_libdir}/gs-plugins-%{gs_plugin_version}/libgs_plugin_provenance.so
0e9714
 %{_libdir}/gs-plugins-%{gs_plugin_version}/libgs_plugin_repos.so
0e9714
 %{_libdir}/gs-plugins-%{gs_plugin_version}/libgs_plugin_rewrite-resource.so
0e9714
+%{_libdir}/gs-plugins-%{gs_plugin_version}/libgs_plugin_shell-extensions.so
0e9714
 %{_libdir}/gs-plugins-%{gs_plugin_version}/libgs_plugin_systemd-updates.so
0e9714
 %{_sysconfdir}/xdg/autostart/gnome-software-service.desktop
0e9714
 %{_datadir}/app-info/xmls/org.gnome.Software.Featured.xml
0e9714
diff --git a/meson_options.txt b/meson_options.txt
0e9714
index f6068ba5..eba69ea2 100644
0e9714
--- a/meson_options.txt
0e9714
+++ b/meson_options.txt
0e9714
@@ -10,6 +10,7 @@ option('fwupd', type : 'boolean', value : true, description : 'enable fwupd supp
0e9714
 option('flatpak', type : 'boolean', value : true, description : 'enable Flatpak support')
0e9714
 option('malcontent', type : 'boolean', value : true, description : 'enable parental controls support using libmalcontent')
0e9714
 option('rpm_ostree', type : 'boolean', value : false, description : 'enable rpm-ostree support')
0e9714
+option('shell_extensions', type : 'boolean', value : true, description : 'enable shell extensions support')
0e9714
 option('odrs', type : 'boolean', value : true, description : 'enable ODRS support')
0e9714
 option('gudev', type : 'boolean', value : true, description : 'enable GUdev support')
0e9714
 option('snap', type : 'boolean', value : false, description : 'enable Snap support')
0e9714
diff --git a/plugins/core/gs-appstream.c b/plugins/core/gs-appstream.c
0e9714
index a387f2e0..e507ac0f 100644
0e9714
--- a/plugins/core/gs-appstream.c
0e9714
+++ b/plugins/core/gs-appstream.c
0e9714
@@ -30,15 +30,6 @@ gs_appstream_create_app (GsPlugin *plugin, XbSilo *silo, XbNode *component, GErr
0e9714
 	if (gs_app_has_quirk (app_new, GS_APP_QUIRK_IS_WILDCARD))
0e9714
 		return g_steal_pointer (&app_new);
0e9714
 
0e9714
-	/* no longer supported */
0e9714
-	if (gs_app_get_kind (app_new) == AS_APP_KIND_SHELL_EXTENSION) {
0e9714
-		g_set_error (error,
0e9714
-			     GS_PLUGIN_ERROR,
0e9714
-			     GS_PLUGIN_ERROR_NOT_SUPPORTED,
0e9714
-			     "shell extensions no longer supported");
0e9714
-		return NULL;
0e9714
-	}
0e9714
-
0e9714
 	/* look for existing object */
0e9714
 	app = gs_plugin_cache_lookup (plugin, gs_app_get_unique_id (app_new));
0e9714
 	if (app != NULL)
0e9714
@@ -1548,6 +1539,11 @@ gs_appstream_component_add_extra_info (GsPlugin *plugin, XbBuilderNode *componen
0e9714
 		gs_appstream_component_add_category (component, "Addon");
0e9714
 		gs_appstream_component_add_category (component, "Font");
0e9714
 		break;
0e9714
+	case AS_APP_KIND_SHELL_EXTENSION:
0e9714
+		gs_appstream_component_add_category (component, "Addon");
0e9714
+		gs_appstream_component_add_category (component, "ShellExtension");
0e9714
+		gs_appstream_component_add_icon (component, "application-x-addon-symbolic");
0e9714
+		break;
0e9714
 	case AS_APP_KIND_DRIVER:
0e9714
 		gs_appstream_component_add_category (component, "Addon");
0e9714
 		gs_appstream_component_add_category (component, "Driver");
0e9714
diff --git a/plugins/core/gs-desktop-common.c b/plugins/core/gs-desktop-common.c
0e9714
index 33ae3fa2..17ed029d 100644
0e9714
--- a/plugins/core/gs-desktop-common.c
0e9714
+++ b/plugins/core/gs-desktop-common.c
0e9714
@@ -203,6 +203,9 @@ static const GsDesktopMap map_addons[] = {
0e9714
 	{ "language-packs",	NC_("Menu of Add-ons", "Language Packs"),
0e9714
 					{ "Addon::LanguagePack",
0e9714
 					  NULL} },
0e9714
+	{ "shell-extensions",	NC_("Menu of Add-ons", "Shell Extensions"),
0e9714
+					{ "Addon::ShellExtension",
0e9714
+					  NULL} },
0e9714
 	{ "localization",	NC_("Menu of Add-ons", "Localization"),
0e9714
 					{ "Addon::Localization",
0e9714
 					  NULL} },
0e9714
diff --git a/plugins/meson.build b/plugins/meson.build
0e9714
index d749b3df..d30f14d4 100644
0e9714
--- a/plugins/meson.build
0e9714
+++ b/plugins/meson.build
0e9714
@@ -39,6 +39,9 @@ subdir('repos')
0e9714
 if get_option('rpm_ostree')
0e9714
   subdir('rpm-ostree')
0e9714
 endif
0e9714
+if get_option('shell_extensions')
0e9714
+  subdir('shell-extensions')
0e9714
+endif
0e9714
 if get_option('snap')
0e9714
   subdir('snap')
0e9714
 endif
0e9714
diff --git a/plugins/packagekit/gs-plugin-packagekit.c b/plugins/packagekit/gs-plugin-packagekit.c
0e9714
index d0bdabae..2c4e1644 100644
0e9714
--- a/plugins/packagekit/gs-plugin-packagekit.c
0e9714
+++ b/plugins/packagekit/gs-plugin-packagekit.c
0e9714
@@ -688,5 +688,8 @@ gs_plugin_launch (GsPlugin *plugin,
0e9714
 	if (g_strcmp0 (gs_app_get_management_plugin (app),
0e9714
 		       gs_plugin_get_name (plugin)) != 0)
0e9714
 		return TRUE;
0e9714
+	/* these are handled by the shell extensions plugin */
0e9714
+	if (gs_app_get_kind (app) == AS_APP_KIND_SHELL_EXTENSION)
0e9714
+		return TRUE;
0e9714
 	return gs_plugin_app_launch (plugin, app, error);
0e9714
 }
0e9714
diff --git a/plugins/shell-extensions/gs-appstream.c b/plugins/shell-extensions/gs-appstream.c
0e9714
new file mode 120000
0e9714
index 00000000..96326ab0
0e9714
--- /dev/null
0e9714
+++ b/plugins/shell-extensions/gs-appstream.c
0e9714
@@ -0,0 +1 @@
0e9714
+../core/gs-appstream.c
0e9714
\ No newline at end of file
0e9714
diff --git a/plugins/shell-extensions/gs-appstream.h b/plugins/shell-extensions/gs-appstream.h
0e9714
new file mode 120000
0e9714
index 00000000..4eabcb3c
0e9714
--- /dev/null
0e9714
+++ b/plugins/shell-extensions/gs-appstream.h
0e9714
@@ -0,0 +1 @@
0e9714
+../core/gs-appstream.h
0e9714
\ No newline at end of file
0e9714
diff --git a/plugins/shell-extensions/gs-plugin-shell-extensions.c b/plugins/shell-extensions/gs-plugin-shell-extensions.c
0e9714
new file mode 100644
0e9714
index 00000000..80a5d0eb
0e9714
--- /dev/null
0e9714
+++ b/plugins/shell-extensions/gs-plugin-shell-extensions.c
0e9714
@@ -0,0 +1,1159 @@
0e9714
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
0e9714
+ *
0e9714
+ * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com>
0e9714
+ * Copyright (C) 2018 Kalev Lember <klember@redhat.com>
0e9714
+ *
0e9714
+ * SPDX-License-Identifier: GPL-2.0+
0e9714
+ */
0e9714
+
0e9714
+#include <config.h>
0e9714
+
0e9714
+#include <errno.h>
0e9714
+#include <glib/gi18n.h>
0e9714
+#include <json-glib/json-glib.h>
0e9714
+#include <xmlb.h>
0e9714
+
0e9714
+#include <gnome-software.h>
0e9714
+
0e9714
+#include "gs-appstream.h"
0e9714
+
0e9714
+#define SHELL_EXTENSIONS_API_URI 		"https://extensions.gnome.org/"
0e9714
+
0e9714
+/*
0e9714
+ * Things we want from the API:
0e9714
+ *
0e9714
+ *  - Size on disk/download
0e9714
+ *  - Existing review data for each extension?
0e9714
+ *  - A local icon for an installed shell extension
0e9714
+ *
0e9714
+ * See https://git.gnome.org/browse/extensions-web/tree/sweettooth/extensions/views.py
0e9714
+ * for the source to the web application.
0e9714
+ */
0e9714
+
0e9714
+struct GsPluginData {
0e9714
+	GDBusProxy	*proxy;
0e9714
+	gchar		*shell_version;
0e9714
+	GsApp		*cached_origin;
0e9714
+	GSettings	*settings;
0e9714
+	XbSilo		*silo;
0e9714
+	GRWLock		 silo_lock;
0e9714
+};
0e9714
+
0e9714
+typedef enum {
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_ENABLED		= 1,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_DISABLED	= 2,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_ERROR		= 3,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_OUT_OF_DATE	= 4,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_DOWNLOADING	= 5,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_INITIALIZED	= 6,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_UNINSTALLED	= 99,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_STATE_LAST
0e9714
+} GsPluginShellExtensionState;
0e9714
+
0e9714
+typedef enum {
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_KIND_SYSTEM		= 1,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_KIND_PER_USER		= 2,
0e9714
+	GS_PLUGIN_SHELL_EXTENSION_KIND_LAST
0e9714
+} GsPluginShellExtensionKind;
0e9714
+
0e9714
+static gboolean _check_silo (GsPlugin *plugin, GCancellable *cancellable, GError **error);
0e9714
+
0e9714
+void
0e9714
+gs_plugin_initialize (GsPlugin *plugin)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
0e9714
+
0e9714
+	/* XbSilo needs external locking as we destroy the silo and build a new
0e9714
+	 * one when something changes */
0e9714
+	g_rw_lock_init (&priv->silo_lock);
0e9714
+
0e9714
+	/* add source */
0e9714
+	priv->cached_origin = gs_app_new (gs_plugin_get_name (plugin));
0e9714
+	gs_app_set_kind (priv->cached_origin, AS_APP_KIND_SOURCE);
0e9714
+	gs_app_set_origin_hostname (priv->cached_origin, SHELL_EXTENSIONS_API_URI);
0e9714
+	gs_app_set_origin (priv->cached_origin, _("GNOME"));
0e9714
+
0e9714
+	priv->settings = g_settings_new ("org.gnome.software");
0e9714
+
0e9714
+	/* add the source to the plugin cache which allows us to match the
0e9714
+	 * unique ID to a GsApp when creating an event */
0e9714
+	gs_plugin_cache_add (plugin,
0e9714
+			     gs_app_get_unique_id (priv->cached_origin),
0e9714
+			     priv->cached_origin);
0e9714
+}
0e9714
+
0e9714
+void
0e9714
+gs_plugin_destroy (GsPlugin *plugin)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_free (priv->shell_version);
0e9714
+	if (priv->proxy != NULL)
0e9714
+		g_object_unref (priv->proxy);
0e9714
+	if (priv->silo != NULL)
0e9714
+		g_object_unref (priv->silo);
0e9714
+	g_object_unref (priv->cached_origin);
0e9714
+	g_object_unref (priv->settings);
0e9714
+	g_rw_lock_clear (&priv->silo_lock);
0e9714
+}
0e9714
+
0e9714
+void
0e9714
+gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app)
0e9714
+{
0e9714
+	if (gs_app_get_kind (app) == AS_APP_KIND_SHELL_EXTENSION &&
0e9714
+	    gs_app_get_scope (app) == AS_APP_SCOPE_USER) {
0e9714
+		gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
0e9714
+	}
0e9714
+}
0e9714
+
0e9714
+static AsAppState
0e9714
+gs_plugin_shell_extensions_convert_state (guint value)
0e9714
+{
0e9714
+	switch (value) {
0e9714
+	case GS_PLUGIN_SHELL_EXTENSION_STATE_DISABLED:
0e9714
+	case GS_PLUGIN_SHELL_EXTENSION_STATE_DOWNLOADING:
0e9714
+	case GS_PLUGIN_SHELL_EXTENSION_STATE_ENABLED:
0e9714
+	case GS_PLUGIN_SHELL_EXTENSION_STATE_ERROR:
0e9714
+	case GS_PLUGIN_SHELL_EXTENSION_STATE_INITIALIZED:
0e9714
+	case GS_PLUGIN_SHELL_EXTENSION_STATE_OUT_OF_DATE:
0e9714
+		return AS_APP_STATE_INSTALLED;
0e9714
+	case GS_PLUGIN_SHELL_EXTENSION_STATE_UNINSTALLED:
0e9714
+		return AS_APP_STATE_AVAILABLE;
0e9714
+	default:
0e9714
+		g_warning ("unknown state %u", value);
0e9714
+	}
0e9714
+	return AS_APP_STATE_UNKNOWN;
0e9714
+}
0e9714
+
0e9714
+static GsApp *
0e9714
+gs_plugin_shell_extensions_parse_installed (GsPlugin *plugin,
0e9714
+                                            const gchar *uuid,
0e9714
+                                            GVariantIter *iter,
0e9714
+                                            GError **error)
0e9714
+{
0e9714
+	const gchar *tmp;
0e9714
+	gchar *str;
0e9714
+	GVariant *val;
0e9714
+	g_autofree gchar *id = NULL;
0e9714
+	g_autoptr(AsIcon) ic = NULL;
0e9714
+	g_autoptr(GsApp) app = NULL;
0e9714
+
0e9714
+	id = as_utils_appstream_id_build (uuid);
0e9714
+	app = gs_app_new (id);
0e9714
+	gs_app_set_metadata (app, "GnomeSoftware::Creator",
0e9714
+			     gs_plugin_get_name (plugin));
0e9714
+	gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
0e9714
+	gs_app_set_metadata (app, "shell-extensions::uuid", uuid);
0e9714
+	gs_app_set_kind (app, AS_APP_KIND_SHELL_EXTENSION);
0e9714
+	gs_app_set_license (app, GS_APP_QUALITY_NORMAL, "GPL-2.0+");
0e9714
+	gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "GNOME Shell Extension");
0e9714
+	gs_app_set_origin_hostname (app, SHELL_EXTENSIONS_API_URI);
0e9714
+	gs_app_set_origin (app, _("GNOME"));
0e9714
+	while (g_variant_iter_loop (iter, "{sv}", &str, &val)) {
0e9714
+		if (g_strcmp0 (str, "description") == 0) {
0e9714
+			g_autofree gchar *tmp1 = NULL;
0e9714
+			g_autofree gchar *tmp2 = NULL;
0e9714
+			tmp1 = as_markup_import (g_variant_get_string (val, NULL),
0e9714
+						 AS_MARKUP_CONVERT_FORMAT_SIMPLE,
0e9714
+						 NULL);
0e9714
+			tmp2 = as_markup_convert_simple (tmp1, error);
0e9714
+			if (tmp2 == NULL) {
0e9714
+				gs_utils_error_convert_appstream (error);
0e9714
+				return NULL;
0e9714
+			}
0e9714
+			gs_app_set_description (app, GS_APP_QUALITY_NORMAL, tmp2);
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "name") == 0) {
0e9714
+			gs_app_set_name (app, GS_APP_QUALITY_NORMAL,
0e9714
+					 g_variant_get_string (val, NULL));
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "version") == 0) {
0e9714
+			guint val_int = (guint) g_variant_get_double (val);
0e9714
+			g_autofree gchar *version = g_strdup_printf ("%u", val_int);
0e9714
+			gs_app_set_version (app, version);
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "url") == 0) {
0e9714
+			gs_app_set_url (app, AS_URL_KIND_HOMEPAGE,
0e9714
+					g_variant_get_string (val, NULL));
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "type") == 0) {
0e9714
+			guint val_int = (guint) g_variant_get_double (val);
0e9714
+			switch (val_int) {
0e9714
+			case GS_PLUGIN_SHELL_EXTENSION_KIND_SYSTEM:
0e9714
+				gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM);
0e9714
+				break;
0e9714
+			case GS_PLUGIN_SHELL_EXTENSION_KIND_PER_USER:
0e9714
+				gs_app_set_scope (app, AS_APP_SCOPE_USER);
0e9714
+				break;
0e9714
+			default:
0e9714
+				g_warning ("%s unknown type %u", uuid, val_int);
0e9714
+				break;
0e9714
+			}
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "state") == 0) {
0e9714
+			AsAppState st;
0e9714
+			guint val_int = (guint) g_variant_get_double (val);
0e9714
+			st = gs_plugin_shell_extensions_convert_state (val_int);
0e9714
+			gs_app_set_state (app, st);
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "error") == 0) {
0e9714
+			tmp = g_variant_get_string (val, NULL);
0e9714
+			if (tmp != NULL && tmp[0] != '\0') {
0e9714
+				g_warning ("unhandled shell error: %s", tmp);
0e9714
+			}
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "hasPrefs") == 0) {
0e9714
+			if (g_variant_get_boolean (val))
0e9714
+				gs_app_set_metadata (app, "shell-extensions::has-prefs", "");
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "extension-id") == 0) {
0e9714
+			tmp = g_variant_get_string (val, NULL);
0e9714
+			gs_app_set_metadata (app, "shell-extensions::extension-id", tmp);
0e9714
+			continue;
0e9714
+		}
0e9714
+		if (g_strcmp0 (str, "path") == 0) {
0e9714
+			tmp = g_variant_get_string (val, NULL);
0e9714
+			gs_app_set_metadata (app, "shell-extensions::path", tmp);
0e9714
+			continue;
0e9714
+		}
0e9714
+	}
0e9714
+
0e9714
+	/* hardcode icon */
0e9714
+	ic = as_icon_new ();
0e9714
+	as_icon_set_kind (ic, AS_ICON_KIND_STOCK);
0e9714
+	as_icon_set_name (ic, "application-x-addon-symbolic");
0e9714
+	gs_app_add_icon (app, ic);
0e9714
+
0e9714
+	/* add categories */
0e9714
+	gs_app_add_category (app, "Addon");
0e9714
+	gs_app_add_category (app, "ShellExtension");
0e9714
+
0e9714
+	return g_steal_pointer (&app);
0e9714
+}
0e9714
+
0e9714
+static void
0e9714
+gs_plugin_shell_extensions_changed_cb (GDBusProxy *proxy,
0e9714
+				       const gchar *sender_name,
0e9714
+				       const gchar *signal_name,
0e9714
+				       GVariant *parameters,
0e9714
+				       GsPlugin *plugin)
0e9714
+{
0e9714
+	if (g_strcmp0 (signal_name, "ExtensionStatusChanged") == 0) {
0e9714
+		AsAppState st;
0e9714
+		GsApp *app;
0e9714
+		const gchar *error_str;
0e9714
+		const gchar *uuid;
0e9714
+		guint state;
0e9714
+
0e9714
+		/* get what changed */
0e9714
+		g_variant_get (parameters, "(&si&s)",
0e9714
+			       &uuid, &state, &error_str);
0e9714
+
0e9714
+		/* find it in the cache; do we care? */
0e9714
+		app = gs_plugin_cache_lookup (plugin, uuid);
0e9714
+		if (app == NULL) {
0e9714
+			g_debug ("no app for changed %s", uuid);
0e9714
+			return;
0e9714
+		}
0e9714
+
0e9714
+		/* set the new state in the UI */
0e9714
+		st = gs_plugin_shell_extensions_convert_state (state);
0e9714
+		gs_app_set_state (app, st);
0e9714
+
0e9714
+		/* not sure what to do here */
0e9714
+		if (error_str != NULL && error_str[0] != '\0') {
0e9714
+			g_warning ("%s has error: %s",
0e9714
+				   gs_app_get_id (app),
0e9714
+				   error_str);
0e9714
+		}
0e9714
+	}
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_autofree gchar *name_owner = NULL;
0e9714
+	g_autoptr(GVariant) version = NULL;
0e9714
+
0e9714
+	if (priv->proxy != NULL)
0e9714
+		return TRUE;
0e9714
+	priv->proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
0e9714
+						     G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION,
0e9714
+						     NULL,
0e9714
+						     "org.gnome.Shell",
0e9714
+						     "/org/gnome/Shell",
0e9714
+						     "org.gnome.Shell.Extensions",
0e9714
+						     cancellable,
0e9714
+						     error);
0e9714
+	if (priv->proxy == NULL) {
0e9714
+		gs_utils_error_convert_gio (error);
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+
0e9714
+	/* not running under Shell */
0e9714
+	name_owner = g_dbus_proxy_get_name_owner (priv->proxy);
0e9714
+	if (name_owner == NULL) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_NOT_SUPPORTED,
0e9714
+				     "gnome-shell is not running");
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+
0e9714
+	g_signal_connect (priv->proxy, "g-signal",
0e9714
+			  G_CALLBACK (gs_plugin_shell_extensions_changed_cb), plugin);
0e9714
+
0e9714
+	/* get the GNOME Shell version */
0e9714
+	version = g_dbus_proxy_get_cached_property (priv->proxy,
0e9714
+						    "ShellVersion");
0e9714
+	if (version == NULL) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_NOT_SUPPORTED,
0e9714
+				     "unable to get shell version");
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	priv->shell_version = g_variant_dup_string (version, NULL);
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_add_installed (GsPlugin *plugin,
0e9714
+			 GsAppList *list,
0e9714
+			 GCancellable *cancellable,
0e9714
+			 GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	GVariantIter *ext_iter;
0e9714
+	gchar *ext_uuid;
0e9714
+	g_autoptr(GVariantIter) iter = NULL;
0e9714
+	g_autoptr(GVariant) retval = NULL;
0e9714
+
0e9714
+	/* installed */
0e9714
+	retval = g_dbus_proxy_call_sync (priv->proxy,
0e9714
+					 "ListExtensions",
0e9714
+					 NULL,
0e9714
+					 G_DBUS_CALL_FLAGS_NONE,
0e9714
+					 -1,
0e9714
+					 cancellable,
0e9714
+					 error);
0e9714
+	if (retval == NULL) {
0e9714
+		gs_utils_error_convert_gdbus (error);
0e9714
+		gs_utils_error_convert_gio (error);
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+
0e9714
+	/* parse each installed extension */
0e9714
+	g_variant_get (retval, "(a{sa{sv}})", &iter);
0e9714
+	while (g_variant_iter_loop (iter, "{sa{sv}}", &ext_uuid, &ext_iter)) {
0e9714
+		g_autoptr(GsApp) app = NULL;
0e9714
+
0e9714
+		/* search in the cache */
0e9714
+		app = gs_plugin_cache_lookup (plugin, ext_uuid);
0e9714
+		if (app != NULL) {
0e9714
+			gs_app_list_add (list, app);
0e9714
+			continue;
0e9714
+		}
0e9714
+
0e9714
+		/* parse the data into an GsApp */
0e9714
+		app = gs_plugin_shell_extensions_parse_installed (plugin,
0e9714
+		                                                  ext_uuid,
0e9714
+		                                                  ext_iter,
0e9714
+		                                                  error);
0e9714
+		if (app == NULL)
0e9714
+			return FALSE;
0e9714
+
0e9714
+		/* ignore system installed */
0e9714
+		if (gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM)
0e9714
+			continue;
0e9714
+
0e9714
+		/* save in the cache */
0e9714
+		gs_plugin_cache_add (plugin, ext_uuid, app);
0e9714
+
0e9714
+		/* add to results */
0e9714
+		gs_app_list_add (list, app);
0e9714
+	}
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_add_sources (GsPlugin *plugin,
0e9714
+                       GsAppList *list,
0e9714
+                       GCancellable *cancellable,
0e9714
+                       GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_autoptr(GsApp) app = NULL;
0e9714
+
0e9714
+	/* create something that we can use to enable/disable */
0e9714
+	app = gs_app_new ("org.gnome.extensions");
0e9714
+	gs_app_set_kind (app, AS_APP_KIND_SOURCE);
0e9714
+	gs_app_set_scope (app, AS_APP_SCOPE_USER);
0e9714
+	if (g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
0e9714
+		gs_app_set_state (app, AS_APP_STATE_INSTALLED);
0e9714
+	else
0e9714
+		gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
0e9714
+	gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
0e9714
+	gs_app_set_name (app, GS_APP_QUALITY_LOWEST,
0e9714
+	                 _("GNOME Shell Extensions Repository"));
0e9714
+	gs_app_set_url (app, AS_URL_KIND_HOMEPAGE,
0e9714
+	                SHELL_EXTENSIONS_API_URI);
0e9714
+	gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
0e9714
+	gs_app_list_add (list, app);
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_refine_app (GsPlugin *plugin,
0e9714
+		      GsApp *app,
0e9714
+		      GsPluginRefineFlags flags,
0e9714
+		      GCancellable *cancellable,
0e9714
+		      GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	const gchar *uuid;
0e9714
+	g_autofree gchar *xpath = NULL;
0e9714
+	g_autoptr(GError) error_local = NULL;
0e9714
+	g_autoptr(GRWLockReaderLocker) locker = NULL;
0e9714
+	g_autoptr(XbNode) component = NULL;
0e9714
+
0e9714
+	/* repo not enabled */
0e9714
+	if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
0e9714
+		return TRUE;
0e9714
+
0e9714
+	/* only process this app if was created by this plugin */
0e9714
+	if (g_strcmp0 (gs_app_get_management_plugin (app),
0e9714
+		       gs_plugin_get_name (plugin)) != 0)
0e9714
+		return TRUE;
0e9714
+
0e9714
+	/* can we get the AppStream-created app state using the cache */
0e9714
+	uuid = gs_app_get_metadata_item (app, "shell-extensions::uuid");
0e9714
+	if (uuid != NULL && gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) {
0e9714
+		GsApp *app_cache = gs_plugin_cache_lookup (plugin, uuid);
0e9714
+		if (app_cache != NULL) {
0e9714
+			g_debug ("copy cached state for %s",
0e9714
+				 gs_app_get_id (app));
0e9714
+			gs_app_set_state (app, gs_app_get_state (app_cache));
0e9714
+		}
0e9714
+	}
0e9714
+
0e9714
+	/* assume apps are available if they exist in AppStream metadata */
0e9714
+	if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
0e9714
+		gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
0e9714
+
0e9714
+	/* FIXME: assume these are small */
0e9714
+	if (gs_app_get_size_installed (app) == 0)
0e9714
+		gs_app_set_size_installed (app, 1024 * 50);
0e9714
+	if (gs_app_get_size_download (app) == 0)
0e9714
+		gs_app_set_size_download (app, GS_APP_SIZE_UNKNOWABLE);
0e9714
+
0e9714
+
0e9714
+	/* check silo is valid */
0e9714
+	if (!_check_silo (plugin, cancellable, error))
0e9714
+		return FALSE;
0e9714
+
0e9714
+	locker = g_rw_lock_reader_locker_new (&priv->silo_lock);
0e9714
+
0e9714
+	/* find the component using the UUID */
0e9714
+	if (uuid == NULL)
0e9714
+		return TRUE;
0e9714
+
0e9714
+	xpath = g_strdup_printf ("components/component/custom/"
0e9714
+				 "value[@key='shell-extensions::uuid'][text()='%s']/../..",
0e9714
+				 uuid);
0e9714
+	component = xb_silo_query_first (priv->silo, xpath, &error_local);
0e9714
+	if (component == NULL) {
0e9714
+		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
0e9714
+			return TRUE;
0e9714
+		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
0e9714
+			return TRUE;
0e9714
+		g_propagate_error (error, g_steal_pointer (&error_local));
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	return gs_appstream_refine_app (plugin, app, priv->silo, component, flags, error);
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_refine_wildcard (GsPlugin *plugin,
0e9714
+			   GsApp *app,
0e9714
+			   GsAppList *list,
0e9714
+			   GsPluginRefineFlags refine_flags,
0e9714
+			   GCancellable *cancellable,
0e9714
+			   GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	const gchar *id;
0e9714
+	g_autofree gchar *xpath = NULL;
0e9714
+	g_autoptr(GError) error_local = NULL;
0e9714
+	g_autoptr(GPtrArray) components = NULL;
0e9714
+	g_autoptr(GRWLockReaderLocker) locker = NULL;
0e9714
+
0e9714
+	/* repo not enabled */
0e9714
+	if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
0e9714
+		return TRUE;
0e9714
+
0e9714
+	/* check silo is valid */
0e9714
+	if (!_check_silo (plugin, cancellable, error))
0e9714
+		return FALSE;
0e9714
+
0e9714
+	/* not enough info to find */
0e9714
+	id = gs_app_get_id (app);
0e9714
+	if (id == NULL)
0e9714
+		return TRUE;
0e9714
+
0e9714
+	locker = g_rw_lock_reader_locker_new (&priv->silo_lock);
0e9714
+
0e9714
+	/* find all apps */
0e9714
+	xpath = g_strdup_printf ("components/component/id[text()='%s']/..", id);
0e9714
+	components = xb_silo_query (priv->silo, xpath, 0, &error_local);
0e9714
+	if (components == NULL) {
0e9714
+		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
0e9714
+			return TRUE;
0e9714
+		if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
0e9714
+			return TRUE;
0e9714
+		g_propagate_error (error, g_steal_pointer (&error_local));
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	for (guint i = 0; i < components->len; i++) {
0e9714
+		XbNode *component = g_ptr_array_index (components, i);
0e9714
+		g_autoptr(GsApp) new = NULL;
0e9714
+		new = gs_appstream_create_app (plugin, priv->silo, component, error);
0e9714
+		if (new == NULL)
0e9714
+			return FALSE;
0e9714
+		gs_app_subsume_metadata (new, app);
0e9714
+		if (!gs_appstream_refine_app (plugin, new, priv->silo, component,
0e9714
+					      refine_flags, error))
0e9714
+			return FALSE;
0e9714
+		gs_app_list_add (list, new);
0e9714
+	}
0e9714
+
0e9714
+	/* success */
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+static gboolean
0e9714
+gs_plugin_shell_extensions_parse_version (GsPlugin *plugin,
0e9714
+					  const gchar *component_id,
0e9714
+					  XbBuilderNode *app,
0e9714
+					  JsonObject *ver_map,
0e9714
+					  GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	JsonObject *json_ver = NULL;
0e9714
+	gint64 version;
0e9714
+	g_autofree gchar *shell_version = NULL;
0e9714
+
0e9714
+	/* look for version, major.minor.micro */
0e9714
+	if (json_object_has_member (ver_map, priv->shell_version)) {
0e9714
+		json_ver = json_object_get_object_member (ver_map,
0e9714
+							  priv->shell_version);
0e9714
+	}
0e9714
+
0e9714
+	/* look for version, major.minor */
0e9714
+	if (json_ver == NULL) {
0e9714
+		g_auto(GStrv) ver_majmin = NULL;
0e9714
+		ver_majmin = g_strsplit (priv->shell_version, ".", -1);
0e9714
+		if (g_strv_length (ver_majmin) >= 2) {
0e9714
+			g_autofree gchar *tmp = NULL;
0e9714
+			tmp = g_strdup_printf ("%s.%s", ver_majmin[0], ver_majmin[1]);
0e9714
+			if (json_object_has_member (ver_map, tmp))
0e9714
+				json_ver = json_object_get_object_member (ver_map, tmp);
0e9714
+		}
0e9714
+	}
0e9714
+
0e9714
+	/* FIXME: mark as incompatible? */
0e9714
+	if (json_ver == NULL)
0e9714
+		return TRUE;
0e9714
+
0e9714
+	/* parse the version */
0e9714
+	version = json_object_get_int_member (json_ver, "version");
0e9714
+	if (version == 0) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_INVALID_FORMAT,
0e9714
+				     "no version in map!");
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	shell_version = g_strdup_printf ("%" G_GINT64_FORMAT, version);
0e9714
+
0e9714
+	/* add a dummy release */
0e9714
+	xb_builder_node_insert_text (app, "release", NULL,
0e9714
+				     "version", shell_version,
0e9714
+				     NULL);
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+
0e9714
+
0e9714
+static XbBuilderNode *
0e9714
+gs_plugin_shell_extensions_parse_app (GsPlugin *plugin,
0e9714
+				      JsonObject *json_app,
0e9714
+				      GError **error)
0e9714
+{
0e9714
+	JsonObject *json_ver_map;
0e9714
+	const gchar *tmp;
0e9714
+	g_autofree gchar *component_id = NULL;
0e9714
+	g_autoptr(XbBuilderNode) app = NULL;
0e9714
+	g_autoptr(XbBuilderNode) categories = NULL;
0e9714
+	g_autoptr(XbBuilderNode) metadata = NULL;
0e9714
+
0e9714
+	app = xb_builder_node_new ("component");
0e9714
+	xb_builder_node_set_attr (app, "kind", "shell-extension");
0e9714
+	xb_builder_node_insert_text (app, "project_license", "GPL-2.0+", NULL);
0e9714
+	categories = xb_builder_node_insert (app, "categories", NULL);
0e9714
+	xb_builder_node_insert_text (categories, "category", "Addon", NULL);
0e9714
+	xb_builder_node_insert_text (categories, "category", "ShellExtension", NULL);
0e9714
+	metadata = xb_builder_node_insert (app, "custom", NULL);
0e9714
+
0e9714
+	tmp = json_object_get_string_member (json_app, "description");
0e9714
+	if (tmp != NULL) {
0e9714
+		g_auto(GStrv) paras = g_strsplit (tmp, "\n", -1);
0e9714
+		g_autoptr(XbBuilderNode) desc = xb_builder_node_insert (app, "description", NULL);
0e9714
+		for (guint i = 0; paras[i] != NULL; i++)
0e9714
+			xb_builder_node_insert_text (desc, "p", paras[i], NULL);
0e9714
+	}
0e9714
+	tmp = json_object_get_string_member (json_app, "screenshot");
0e9714
+	if (tmp != NULL) {
0e9714
+		g_autoptr(XbBuilderNode) screenshots = NULL;
0e9714
+		g_autoptr(XbBuilderNode) screenshot = NULL;
0e9714
+		g_autofree gchar *uri = NULL;
0e9714
+		screenshots = xb_builder_node_insert (app, "screenshots", NULL);
0e9714
+		screenshot = xb_builder_node_insert (screenshots, "screenshot",
0e9714
+						     "kind", "default",
0e9714
+						     NULL);
0e9714
+		uri = g_build_path ("/", SHELL_EXTENSIONS_API_URI, tmp, NULL);
0e9714
+		xb_builder_node_insert_text (screenshot, "image", uri,
0e9714
+					     "kind", "source",
0e9714
+					     NULL);
0e9714
+	}
0e9714
+	tmp = json_object_get_string_member (json_app, "name");
0e9714
+	if (tmp != NULL)
0e9714
+		xb_builder_node_insert_text (app, "name", tmp, NULL);
0e9714
+	tmp = json_object_get_string_member (json_app, "uuid");
0e9714
+	if (tmp != NULL) {
0e9714
+		component_id = as_utils_appstream_id_build (tmp);
0e9714
+		xb_builder_node_insert_text (app, "id", component_id, NULL);
0e9714
+		xb_builder_node_insert_text (metadata, "value", tmp,
0e9714
+					     "key", "shell-extensions::uuid",
0e9714
+					     NULL);
0e9714
+	}
0e9714
+	tmp = json_object_get_string_member (json_app, "link");
0e9714
+	if (tmp != NULL) {
0e9714
+		g_autofree gchar *uri = NULL;
0e9714
+		uri = g_build_filename (SHELL_EXTENSIONS_API_URI, tmp, NULL);
0e9714
+		xb_builder_node_insert_text (app, "url", uri,
0e9714
+					     "type", "homepage",
0e9714
+					     NULL);
0e9714
+	}
0e9714
+	tmp = json_object_get_string_member (json_app, "icon");
0e9714
+	if (tmp != NULL) {
0e9714
+		/* just use a stock icon as the remote icons are
0e9714
+		 * sometimes missing, poor quality and low resolution */
0e9714
+		xb_builder_node_insert_text (app, "icon",
0e9714
+					     "application-x-addon-symbolic",
0e9714
+					     "type", "stock",
0e9714
+					     NULL);
0e9714
+	}
0e9714
+
0e9714
+	/* try to get version */
0e9714
+	json_ver_map = json_object_get_object_member (json_app, "shell_version_map");
0e9714
+	if (json_ver_map != NULL) {
0e9714
+		if (!gs_plugin_shell_extensions_parse_version (plugin,
0e9714
+							       component_id,
0e9714
+							       app,
0e9714
+							       json_ver_map,
0e9714
+							       error))
0e9714
+			return NULL;
0e9714
+	}
0e9714
+
0e9714
+	return g_steal_pointer (&app);
0e9714
+}
0e9714
+
0e9714
+static GInputStream *
0e9714
+gs_plugin_appstream_load_json_cb (XbBuilderSource *self,
0e9714
+				  XbBuilderSourceCtx *ctx,
0e9714
+				  gpointer user_data,
0e9714
+				  GCancellable *cancellable,
0e9714
+				  GError **error)
0e9714
+{
0e9714
+	GsPlugin *plugin = GS_PLUGIN (user_data);
0e9714
+	JsonArray *json_extensions_array;
0e9714
+	JsonNode *json_extensions;
0e9714
+	JsonNode *json_root;
0e9714
+	JsonObject *json_item;
0e9714
+	gchar *xml;
0e9714
+	g_autoptr(JsonParser) json_parser = NULL;
0e9714
+	g_autoptr(XbBuilderNode) apps = NULL;
0e9714
+
0e9714
+	/* parse the data and find the success */
0e9714
+	json_parser = json_parser_new ();
0e9714
+	if (!json_parser_load_from_stream (json_parser,
0e9714
+					   xb_builder_source_ctx_get_stream (ctx),
0e9714
+					   cancellable, error)) {
0e9714
+		gs_utils_error_convert_json_glib (error);
0e9714
+		return NULL;
0e9714
+	}
0e9714
+	json_root = json_parser_get_root (json_parser);
0e9714
+	if (json_root == NULL) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_INVALID_FORMAT,
0e9714
+				     "no data root");
0e9714
+		return NULL;
0e9714
+	}
0e9714
+	if (json_node_get_node_type (json_root) != JSON_NODE_OBJECT) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_INVALID_FORMAT,
0e9714
+				     "no data object");
0e9714
+		return NULL;
0e9714
+	}
0e9714
+	json_item = json_node_get_object (json_root);
0e9714
+	if (json_item == NULL) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_INVALID_FORMAT,
0e9714
+				     "no data object");
0e9714
+		return NULL;
0e9714
+	}
0e9714
+
0e9714
+	/* load extensions */
0e9714
+	apps = xb_builder_node_new ("components");
0e9714
+	json_extensions = json_object_get_member (json_item, "extensions");
0e9714
+	if (json_extensions == NULL) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_INVALID_FORMAT,
0e9714
+				     "no extensions object");
0e9714
+		return NULL;
0e9714
+	}
0e9714
+	json_extensions_array = json_node_get_array (json_extensions);
0e9714
+	if (json_extensions_array == NULL) {
0e9714
+		g_set_error_literal (error,
0e9714
+				     GS_PLUGIN_ERROR,
0e9714
+				     GS_PLUGIN_ERROR_INVALID_FORMAT,
0e9714
+				     "no extensions array");
0e9714
+		return NULL;
0e9714
+	}
0e9714
+
0e9714
+	/* parse each app */
0e9714
+	for (guint i = 0; i < json_array_get_length (json_extensions_array); i++) {
0e9714
+		JsonNode *json_extension;
0e9714
+		JsonObject *json_extension_obj;
0e9714
+		g_autoptr(XbBuilderNode) component = NULL;
0e9714
+
0e9714
+		json_extension = json_array_get_element (json_extensions_array, i);
0e9714
+		json_extension_obj = json_node_get_object (json_extension);
0e9714
+		component = gs_plugin_shell_extensions_parse_app (plugin,
0e9714
+							    json_extension_obj,
0e9714
+							    error);
0e9714
+		if (component == NULL)
0e9714
+			return NULL;
0e9714
+		xb_builder_node_add_child (apps, component);
0e9714
+	}
0e9714
+
0e9714
+	/* convert back to XML */
0e9714
+	xml = xb_builder_node_export (apps, XB_NODE_EXPORT_FLAG_ADD_HEADER, error);
0e9714
+	if (xml == NULL)
0e9714
+		return NULL;
0e9714
+	return g_memory_input_stream_new_from_data (xml, -1, g_free);
0e9714
+}
0e9714
+
0e9714
+static gboolean
0e9714
+gs_plugin_shell_extensions_refresh (GsPlugin *plugin,
0e9714
+				    guint cache_age,
0e9714
+				    GCancellable *cancellable,
0e9714
+				    GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_autofree gchar *fn = NULL;
0e9714
+	g_autofree gchar *uri = NULL;
0e9714
+	g_autoptr(GFile) file = NULL;
0e9714
+	g_autoptr(GRWLockReaderLocker) locker = NULL;
0e9714
+	g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin));
0e9714
+
0e9714
+	/* get cache filename */
0e9714
+	fn = gs_utils_get_cache_filename ("shell-extensions",
0e9714
+					  "gnome.json",
0e9714
+					  GS_UTILS_CACHE_FLAG_WRITEABLE,
0e9714
+					  error);
0e9714
+	if (fn == NULL)
0e9714
+		return FALSE;
0e9714
+
0e9714
+	/* check age */
0e9714
+	file = g_file_new_for_path (fn);
0e9714
+	if (g_file_query_exists (file, NULL)) {
0e9714
+		guint age = gs_utils_get_file_age (file);
0e9714
+		if (age < cache_age) {
0e9714
+			g_debug ("%s is only %u seconds old, ignoring", fn, age);
0e9714
+			return TRUE;
0e9714
+		}
0e9714
+	}
0e9714
+
0e9714
+	/* download the file */
0e9714
+	uri = g_strdup_printf ("%s/static/extensions.json",
0e9714
+			       SHELL_EXTENSIONS_API_URI);
0e9714
+	gs_app_set_summary_missing (app_dl,
0e9714
+				    /* TRANSLATORS: status text when downloading */
0e9714
+				    _("Downloading shell extension metadata…"));
0e9714
+	if (!gs_plugin_download_file (plugin, app_dl, uri, fn, cancellable, error)) {
0e9714
+		gs_utils_error_add_origin_id (error, priv->cached_origin);
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+
0e9714
+	/* be explicit */
0e9714
+	locker = g_rw_lock_reader_locker_new (&priv->silo_lock);
0e9714
+	if (priv->silo != NULL)
0e9714
+		xb_silo_invalidate (priv->silo);
0e9714
+
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+static gboolean
0e9714
+_check_silo (GsPlugin *plugin, GCancellable *cancellable, GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_autofree gchar *blobfn = NULL;
0e9714
+	g_autofree gchar *fn = NULL;
0e9714
+	g_autoptr(GError) error_local = NULL;
0e9714
+	g_autoptr(GFile) blobfile = NULL;
0e9714
+	g_autoptr(GFile) file = NULL;
0e9714
+	g_autoptr(GRWLockReaderLocker) reader_locker = NULL;
0e9714
+	g_autoptr(GRWLockWriterLocker) writer_locker = NULL;
0e9714
+	g_autoptr(XbBuilder) builder = xb_builder_new ();
0e9714
+	g_autoptr(XbBuilderNode) info = NULL;
0e9714
+	g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
0e9714
+
0e9714
+	reader_locker = g_rw_lock_reader_locker_new (&priv->silo_lock);
0e9714
+	/* everything is okay */
0e9714
+	if (priv->silo != NULL && xb_silo_is_valid (priv->silo)) {
0e9714
+		g_debug ("silo already valid");
0e9714
+		return TRUE;
0e9714
+	}
0e9714
+	g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free);
0e9714
+
0e9714
+	/* drat! silo needs regenerating */
0e9714
+	writer_locker = g_rw_lock_writer_locker_new (&priv->silo_lock);
0e9714
+	g_clear_object (&priv->silo);
0e9714
+
0e9714
+	/* verbose profiling */
0e9714
+	if (g_getenv ("GS_XMLB_VERBOSE") != NULL) {
0e9714
+		xb_builder_set_profile_flags (builder,
0e9714
+					      XB_SILO_PROFILE_FLAG_XPATH |
0e9714
+					      XB_SILO_PROFILE_FLAG_DEBUG);
0e9714
+	}
0e9714
+
0e9714
+	/* add metadata */
0e9714
+	info = xb_builder_node_insert (NULL, "info", NULL);
0e9714
+	xb_builder_node_insert_text (info, "scope", "user", NULL);
0e9714
+	xb_builder_source_set_info (source, info);
0e9714
+
0e9714
+	/* add support for JSON files */
0e9714
+	fn = gs_utils_get_cache_filename ("shell-extensions",
0e9714
+					  "gnome.json",
0e9714
+					  GS_UTILS_CACHE_FLAG_WRITEABLE,
0e9714
+					  error);
0e9714
+	if (fn == NULL)
0e9714
+		return FALSE;
0e9714
+	xb_builder_source_add_adapter (source, "application/json",
0e9714
+				       gs_plugin_appstream_load_json_cb,
0e9714
+				       plugin, NULL);
0e9714
+	file = g_file_new_for_path (fn);
0e9714
+	if (!xb_builder_source_load_file (source, file,
0e9714
+					  XB_BUILDER_SOURCE_FLAG_WATCH_FILE,
0e9714
+					  cancellable,
0e9714
+					  error)) {
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	xb_builder_import_source (builder, source);
0e9714
+
0e9714
+	/* create binary cache */
0e9714
+	blobfn = gs_utils_get_cache_filename ("shell-extensions",
0e9714
+					      "extensions-web.xmlb",
0e9714
+					      GS_UTILS_CACHE_FLAG_WRITEABLE,
0e9714
+					      error);
0e9714
+	if (blobfn == NULL)
0e9714
+		return FALSE;
0e9714
+	blobfile = g_file_new_for_path (blobfn);
0e9714
+	g_debug ("ensuring %s", blobfn);
0e9714
+	priv->silo = xb_builder_ensure (builder, blobfile,
0e9714
+					XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID,
0e9714
+					NULL, &error_local);
0e9714
+	if (priv->silo == NULL) {
0e9714
+		g_set_error (error,
0e9714
+			     GS_PLUGIN_ERROR,
0e9714
+			     GS_PLUGIN_ERROR_FAILED,
0e9714
+			     "failed to compile %s",
0e9714
+			     error_local->message);
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+
0e9714
+	/* success */
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+static void
0e9714
+_claim_components (GsPlugin *plugin, GsAppList *list)
0e9714
+{
0e9714
+	for (guint i = 0; i < gs_app_list_length (list); i++) {
0e9714
+		GsApp *app = gs_app_list_index (list, i);
0e9714
+		gs_app_set_kind (app, AS_APP_KIND_SHELL_EXTENSION);
0e9714
+		gs_app_set_origin_hostname (app, SHELL_EXTENSIONS_API_URI);
0e9714
+		gs_app_set_origin (app, _("GNOME"));
0e9714
+		gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
0e9714
+		gs_app_set_summary (app, GS_APP_QUALITY_LOWEST,
0e9714
+				    /* TRANSLATORS: the one-line summary */
0e9714
+				    _("GNOME Shell Extension"));
0e9714
+	}
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_add_search (GsPlugin *plugin, gchar **values, GsAppList *list,
0e9714
+		      GCancellable *cancellable, GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_autoptr(GRWLockReaderLocker) locker = NULL;
0e9714
+	g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
0e9714
+	if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
0e9714
+		return TRUE;
0e9714
+	if (!_check_silo (plugin, cancellable, error))
0e9714
+		return FALSE;
0e9714
+	locker = g_rw_lock_reader_locker_new (&priv->silo_lock);
0e9714
+	if (!gs_appstream_search (plugin, priv->silo, (const gchar * const *) values, list_tmp,
0e9714
+				  cancellable, error))
0e9714
+		return FALSE;
0e9714
+	_claim_components (plugin, list_tmp);
0e9714
+	gs_app_list_add_list (list, list_tmp);
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_add_category_apps (GsPlugin *plugin, GsCategory *category, GsAppList *list,
0e9714
+			     GCancellable *cancellable, GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_autoptr(GRWLockReaderLocker) locker = NULL;
0e9714
+	if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
0e9714
+		return TRUE;
0e9714
+	if (!_check_silo (plugin, cancellable, error))
0e9714
+		return FALSE;
0e9714
+	locker = g_rw_lock_reader_locker_new (&priv->silo_lock);
0e9714
+	return gs_appstream_add_category_apps (plugin, priv->silo, category,
0e9714
+					       list, cancellable, error);
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_refresh (GsPlugin *plugin,
0e9714
+		   guint cache_age,
0e9714
+		   GCancellable *cancellable,
0e9714
+		   GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
0e9714
+		return TRUE;
0e9714
+	if (!gs_plugin_shell_extensions_refresh (plugin,
0e9714
+						 cache_age,
0e9714
+						 cancellable,
0e9714
+						 error))
0e9714
+		return FALSE;
0e9714
+	return _check_silo (plugin, cancellable, error);
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_app_remove (GsPlugin *plugin,
0e9714
+		      GsApp *app,
0e9714
+		      GCancellable *cancellable,
0e9714
+		      GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	const gchar *uuid;
0e9714
+	gboolean ret;
0e9714
+	g_autoptr(GVariant) retval = NULL;
0e9714
+
0e9714
+	/* only process this app if was created by this plugin */
0e9714
+	if (g_strcmp0 (gs_app_get_management_plugin (app),
0e9714
+		       gs_plugin_get_name (plugin)) != 0)
0e9714
+		return TRUE;
0e9714
+
0e9714
+	/* disable repository */
0e9714
+	if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) {
0e9714
+		gs_app_set_state (app, AS_APP_STATE_REMOVING);
0e9714
+		g_settings_set_boolean (priv->settings, "enable-shell-extensions-repo", FALSE);
0e9714
+		/* remove appstream data */
0e9714
+		ret = gs_plugin_shell_extensions_refresh (plugin,
0e9714
+		                                          G_MAXUINT,
0e9714
+		                                          cancellable,
0e9714
+		                                          error);
0e9714
+		gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
0e9714
+		return ret;
0e9714
+	}
0e9714
+
0e9714
+	/* remove */
0e9714
+	gs_app_set_state (app, AS_APP_STATE_REMOVING);
0e9714
+	uuid = gs_app_get_metadata_item (app, "shell-extensions::uuid");
0e9714
+	retval = g_dbus_proxy_call_sync (priv->proxy,
0e9714
+					 "UninstallExtension",
0e9714
+					 g_variant_new ("(s)", uuid),
0e9714
+					 G_DBUS_CALL_FLAGS_NONE,
0e9714
+					 -1,
0e9714
+					 cancellable,
0e9714
+					 error);
0e9714
+	if (retval == NULL) {
0e9714
+		gs_utils_error_convert_gio (error);
0e9714
+		gs_app_set_state_recover (app);
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+
0e9714
+	/* not sure why this would fail -- perhaps installed in /usr? */
0e9714
+	g_variant_get (retval, "(b)", &ret;;
0e9714
+	if (!ret) {
0e9714
+		gs_app_set_state_recover (app);
0e9714
+		g_set_error (error,
0e9714
+			     GS_PLUGIN_ERROR,
0e9714
+			     GS_PLUGIN_ERROR_NOT_SUPPORTED,
0e9714
+			     "failed to uninstall %s",
0e9714
+			     gs_app_get_id (app));
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+
0e9714
+	/* state is not known: we don't know if we can re-install this app */
0e9714
+	gs_app_set_state (app, AS_APP_STATE_UNKNOWN);
0e9714
+
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_app_install (GsPlugin *plugin,
0e9714
+		       GsApp *app,
0e9714
+		       GCancellable *cancellable,
0e9714
+		       GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	const gchar *uuid;
0e9714
+	const gchar *retstr;
0e9714
+	g_autoptr(GVariant) retval = NULL;
0e9714
+
0e9714
+	/* only process this app if was created by this plugin */
0e9714
+	if (g_strcmp0 (gs_app_get_management_plugin (app),
0e9714
+		       gs_plugin_get_name (plugin)) != 0)
0e9714
+		return TRUE;
0e9714
+
0e9714
+	/* enable repository */
0e9714
+	if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) {
0e9714
+		gboolean ret;
0e9714
+
0e9714
+		gs_app_set_state (app, AS_APP_STATE_INSTALLING);
0e9714
+		g_settings_set_boolean (priv->settings, "enable-shell-extensions-repo", TRUE);
0e9714
+		/* refresh metadata */
0e9714
+		ret = gs_plugin_shell_extensions_refresh (plugin,
0e9714
+		                                          G_MAXUINT,
0e9714
+		                                          cancellable,
0e9714
+		                                          error);
0e9714
+		gs_app_set_state (app, AS_APP_STATE_INSTALLED);
0e9714
+		return ret;
0e9714
+	}
0e9714
+
0e9714
+	/* install */
0e9714
+	uuid = gs_app_get_metadata_item (app, "shell-extensions::uuid");
0e9714
+	gs_app_set_state (app, AS_APP_STATE_INSTALLING);
0e9714
+	retval = g_dbus_proxy_call_sync (priv->proxy,
0e9714
+					 "InstallRemoteExtension",
0e9714
+					 g_variant_new ("(s)", uuid),
0e9714
+					 G_DBUS_CALL_FLAGS_NONE,
0e9714
+					 -1,
0e9714
+					 cancellable,
0e9714
+					 error);
0e9714
+	if (retval == NULL) {
0e9714
+		gs_app_set_state_recover (app);
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	g_variant_get (retval, "(&s)", &retstr);
0e9714
+
0e9714
+	/* user declined download */
0e9714
+	if (g_strcmp0 (retstr, "cancelled") == 0) {
0e9714
+		gs_app_set_state_recover (app);
0e9714
+		g_set_error (error,
0e9714
+			     GS_PLUGIN_ERROR,
0e9714
+			     GS_PLUGIN_ERROR_CANCELLED,
0e9714
+			     "extension %s download was cancelled",
0e9714
+			     gs_app_get_id (app));
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	g_debug ("shell returned: %s", retstr);
0e9714
+
0e9714
+	/* state is known */
0e9714
+	gs_app_set_state (app, AS_APP_STATE_INSTALLED);
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_launch (GsPlugin *plugin,
0e9714
+		  GsApp *app,
0e9714
+		  GCancellable *cancellable,
0e9714
+		  GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+	g_autofree gchar *uuid = NULL;
0e9714
+	g_autoptr(GVariant) retval = NULL;
0e9714
+
0e9714
+	/* launch both PackageKit-installed and user-installed */
0e9714
+	if (gs_app_get_kind (app) != AS_APP_KIND_SHELL_EXTENSION)
0e9714
+		return TRUE;
0e9714
+
0e9714
+	uuid = g_strdup (gs_app_get_metadata_item (app, "shell-extensions::uuid"));
0e9714
+	if (uuid == NULL) {
0e9714
+		const gchar *suffix = ".shell-extension";
0e9714
+		const gchar *id = gs_app_get_id (app);
0e9714
+		/* PackageKit-installed extension ID generated by appstream-builder */
0e9714
+		if (g_str_has_suffix (id, suffix))
0e9714
+			uuid = g_strndup (id, strlen (id) - strlen (suffix));
0e9714
+	}
0e9714
+	if (uuid == NULL) {
0e9714
+		g_set_error (error,
0e9714
+			     GS_PLUGIN_ERROR,
0e9714
+			     GS_PLUGIN_ERROR_FAILED,
0e9714
+			     "no uuid set for %s",
0e9714
+			     gs_app_get_id (app));
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	/* launch */
0e9714
+	retval = g_dbus_proxy_call_sync (priv->proxy,
0e9714
+					 "LaunchExtensionPrefs",
0e9714
+					 g_variant_new ("(s)", uuid),
0e9714
+					 G_DBUS_CALL_FLAGS_NONE,
0e9714
+					 -1,
0e9714
+					 cancellable,
0e9714
+					 error);
0e9714
+	if (retval == NULL) {
0e9714
+		gs_utils_error_convert_gio (error);
0e9714
+		return FALSE;
0e9714
+	}
0e9714
+	return TRUE;
0e9714
+}
0e9714
+
0e9714
+gboolean
0e9714
+gs_plugin_add_categories (GsPlugin *plugin,
0e9714
+			  GPtrArray *list,
0e9714
+			  GCancellable *cancellable,
0e9714
+			  GError **error)
0e9714
+{
0e9714
+	GsPluginData *priv = gs_plugin_get_data (plugin);
0e9714
+
0e9714
+	/* repo not enabled */
0e9714
+	if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
0e9714
+		return TRUE;
0e9714
+
0e9714
+	/* just ensure there is any data, no matter how old */
0e9714
+	return gs_plugin_shell_extensions_refresh (plugin,
0e9714
+						   G_MAXUINT,
0e9714
+						   cancellable,
0e9714
+						   error);
0e9714
+}
0e9714
diff --git a/plugins/shell-extensions/gs-self-test.c b/plugins/shell-extensions/gs-self-test.c
0e9714
new file mode 100644
0e9714
index 00000000..f96ee60d
0e9714
--- /dev/null
0e9714
+++ b/plugins/shell-extensions/gs-self-test.c
0e9714
@@ -0,0 +1,156 @@
0e9714
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
0e9714
+ *
0e9714
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
0e9714
+ *
0e9714
+ * SPDX-License-Identifier: GPL-2.0+
0e9714
+ */
0e9714
+
0e9714
+#include "config.h"
0e9714
+
0e9714
+#include <glib/gstdio.h>
0e9714
+#include <xmlb.h>
0e9714
+
0e9714
+#include "gnome-software-private.h"
0e9714
+
0e9714
+#include "gs-test.h"
0e9714
+
0e9714
+static void
0e9714
+gs_plugins_shell_extensions_installed_func (GsPluginLoader *plugin_loader)
0e9714
+{
0e9714
+	GsApp *app;
0e9714
+	g_autoptr(GError) error = NULL;
0e9714
+	g_autoptr(GsAppList) list = NULL;
0e9714
+	g_autoptr(GsPluginJob) plugin_job = NULL;
0e9714
+
0e9714
+	/* no shell-extensions, abort */
0e9714
+	if (!gs_plugin_loader_get_enabled (plugin_loader, "shell-extensions")) {
0e9714
+		g_test_skip ("not enabled");
0e9714
+		return;
0e9714
+	}
0e9714
+
0e9714
+	/* get installed packages */
0e9714
+	plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_INSTALLED,
0e9714
+					 "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES,
0e9714
+					 NULL);
0e9714
+	list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error);
0e9714
+	gs_test_flush_main_context ();
0e9714
+	g_assert_no_error (error);
0e9714
+	g_assert (list != NULL);
0e9714
+
0e9714
+	/* no shell-extensions installed, abort */
0e9714
+	if (gs_app_list_length (list) < 1) {
0e9714
+		g_test_skip ("no shell extensions installed");
0e9714
+		return;
0e9714
+	}
0e9714
+
0e9714
+	/* test properties */
0e9714
+	app = gs_app_list_lookup (list, "*/*/*/*/background-logo_fedorahosted.org/*");
0e9714
+	if (app == NULL) {
0e9714
+		g_test_skip ("not found");
0e9714
+		return;
0e9714
+	}
0e9714
+
0e9714
+	g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED);
0e9714
+	g_assert_cmpint (gs_app_get_scope (app), ==, AS_APP_SCOPE_USER);
0e9714
+	g_assert_cmpstr (gs_app_get_name (app), ==, "Background Logo");
0e9714
+	g_assert_cmpstr (gs_app_get_summary (app), ==, "GNOME Shell Extension");
0e9714
+	g_assert_cmpstr (gs_app_get_description (app), ==,
0e9714
+			 "Overlay a tasteful logo on the background to "
0e9714
+			 "enhance the user experience");
0e9714
+	g_assert_cmpstr (gs_app_get_license (app), ==, "GPL-2.0+");
0e9714
+	g_assert_cmpstr (gs_app_get_management_plugin (app), ==, "shell-extensions");
0e9714
+	g_assert (gs_app_has_category (app, "Addon"));
0e9714
+	g_assert (gs_app_has_category (app, "ShellExtension"));
0e9714
+	g_assert_cmpstr (gs_app_get_metadata_item (app, "shell-extensions::has-prefs"), ==, "");
0e9714
+	g_assert_cmpstr (gs_app_get_metadata_item (app, "shell-extensions::uuid"), ==,
0e9714
+			 "background-logo@fedorahosted.org");
0e9714
+}
0e9714
+
0e9714
+static void
0e9714
+gs_plugins_shell_extensions_remote_func (GsPluginLoader *plugin_loader)
0e9714
+{
0e9714
+	const gchar *cachedir = "/var/tmp/gs-self-test";
0e9714
+	gboolean ret;
0e9714
+	g_autofree gchar *fn = NULL;
0e9714
+	g_autoptr(GError) error = NULL;
0e9714
+	g_autoptr(GFile) file = NULL;
0e9714
+	g_autoptr(GPtrArray) components = NULL;
0e9714
+	g_autoptr(GsPluginJob) plugin_job = NULL;
0e9714
+	g_autoptr(XbSilo) silo = NULL;
0e9714
+
0e9714
+	/* no shell-extensions, abort */
0e9714
+	if (!gs_plugin_loader_get_enabled (plugin_loader, "shell-extensions")) {
0e9714
+		g_test_skip ("not enabled");
0e9714
+		return;
0e9714
+	}
0e9714
+
0e9714
+	/* ensure files are removed */
0e9714
+	g_setenv ("GS_SELF_TEST_CACHEDIR", cachedir, TRUE);
0e9714
+	gs_utils_rmtree (cachedir, NULL);
0e9714
+
0e9714
+	/* refresh the metadata */
0e9714
+	plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
0e9714
+					 "age", (guint64) G_MAXUINT,
0e9714
+					 NULL);
0e9714
+	ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
0e9714
+	g_assert_no_error (error);
0e9714
+	g_assert (ret);
0e9714
+
0e9714
+	/* ensure file was populated */
0e9714
+	silo = xb_silo_new ();
0e9714
+	fn = gs_utils_get_cache_filename ("shell-extensions",
0e9714
+					  "extensions-web.xmlb",
0e9714
+					  GS_UTILS_CACHE_FLAG_WRITEABLE,
0e9714
+					  &error);
0e9714
+	g_assert_no_error (error);
0e9714
+	g_assert_nonnull (fn);
0e9714
+	file = g_file_new_for_path (fn);
0e9714
+	ret = xb_silo_load_from_file (silo, file,
0e9714
+				      XB_SILO_LOAD_FLAG_NONE,
0e9714
+				      NULL, &error);
0e9714
+	g_assert_no_error (error);
0e9714
+	g_assert (ret);
0e9714
+	components = xb_silo_query (silo, "components/component", 0, &error);
0e9714
+	g_assert_no_error (error);
0e9714
+	g_assert_nonnull (components);
0e9714
+	g_assert_cmpint (components->len, >, 20);
0e9714
+}
0e9714
+
0e9714
+int
0e9714
+main (int argc, char **argv)
0e9714
+{
0e9714
+	gboolean ret;
0e9714
+	g_autoptr(GError) error = NULL;
0e9714
+	g_autoptr(GsPluginLoader) plugin_loader = NULL;
0e9714
+	const gchar *whitelist[] = {
0e9714
+		"shell-extensions",
0e9714
+		NULL
0e9714
+	};
0e9714
+
0e9714
+	g_test_init (&argc, &argv, NULL);
0e9714
+	g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
0e9714
+
0e9714
+	/* only critical and error are fatal */
0e9714
+	g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
0e9714
+
0e9714
+	/* we can only load this once per process */
0e9714
+	plugin_loader = gs_plugin_loader_new ();
0e9714
+	gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR);
0e9714
+	ret = gs_plugin_loader_setup (plugin_loader,
0e9714
+				      (gchar**) whitelist,
0e9714
+				      NULL,
0e9714
+				      NULL,
0e9714
+				      &error);
0e9714
+	g_assert_no_error (error);
0e9714
+	g_assert (ret);
0e9714
+
0e9714
+	/* plugin tests go here */
0e9714
+	g_test_add_data_func ("/gnome-software/plugins/shell-extensions/installed",
0e9714
+			      plugin_loader,
0e9714
+			      (GTestDataFunc) gs_plugins_shell_extensions_installed_func);
0e9714
+	g_test_add_data_func ("/gnome-software/plugins/shell-extensions/remote",
0e9714
+			      plugin_loader,
0e9714
+			      (GTestDataFunc) gs_plugins_shell_extensions_remote_func);
0e9714
+
0e9714
+	return g_test_run ();
0e9714
+}
0e9714
diff --git a/plugins/shell-extensions/meson.build b/plugins/shell-extensions/meson.build
0e9714
new file mode 100644
0e9714
index 00000000..3a008f7b
0e9714
--- /dev/null
0e9714
+++ b/plugins/shell-extensions/meson.build
0e9714
@@ -0,0 +1,47 @@
0e9714
+cargs = ['-DG_LOG_DOMAIN="GsPluginShellExtensions"']
0e9714
+cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"']
0e9714
+
0e9714
+shared_module(
0e9714
+  'gs_plugin_shell-extensions',
0e9714
+  sources : [
0e9714
+    'gs-appstream.c',
0e9714
+    'gs-plugin-shell-extensions.c'
0e9714
+  ],
0e9714
+  include_directories : [
0e9714
+    include_directories('../..'),
0e9714
+    include_directories('../../lib'),
0e9714
+  ],
0e9714
+  install : true,
0e9714
+  install_dir: plugin_dir,
0e9714
+  c_args : cargs,
0e9714
+  dependencies : [
0e9714
+    plugin_libs,
0e9714
+    libxmlb,
0e9714
+  ],
0e9714
+  link_with : [
0e9714
+    libgnomesoftware
0e9714
+  ]
0e9714
+)
0e9714
+
0e9714
+if get_option('tests')
0e9714
+  e = executable(
0e9714
+    'gs-self-test-shell-extensions',
0e9714
+    compiled_schemas,
0e9714
+    sources : [
0e9714
+      'gs-self-test.c'
0e9714
+    ],
0e9714
+    include_directories : [
0e9714
+      include_directories('../..'),
0e9714
+      include_directories('../../lib'),
0e9714
+    ],
0e9714
+    dependencies : [
0e9714
+      plugin_libs,
0e9714
+      libxmlb,
0e9714
+    ],
0e9714
+    link_with : [
0e9714
+      libgnomesoftware
0e9714
+    ],
0e9714
+    c_args : cargs,
0e9714
+  )
0e9714
+  test('gs-self-test-shell-extensions', e, suite: ['plugins', 'shell-extensions'], env: test_env)
0e9714
+endif
0e9714
diff --git a/src/gs-category-page.c b/src/gs-category-page.c
0e9714
index 10467436..a786d48d 100644
0e9714
--- a/src/gs-category-page.c
0e9714
+++ b/src/gs-category-page.c
0e9714
@@ -38,6 +38,8 @@ struct _GsCategoryPage
0e9714
 	guint		sort_name_handler_id;
0e9714
 	SubcategorySortType sort_type;
0e9714
 
0e9714
+	GtkWidget	*infobar_category_shell_extensions;
0e9714
+	GtkWidget	*button_category_shell_extensions;
0e9714
 	GtkWidget	*category_detail_box;
0e9714
 	GtkWidget	*scrolledwindow_category;
0e9714
 	GtkWidget	*subcats_filter_label;
0e9714
@@ -321,6 +323,12 @@ gs_category_page_reload (GsPage *page)
0e9714
 		gtk_widget_set_visible (self->subcats_sort_button, TRUE);
0e9714
 	}
0e9714
 
0e9714
+	/* show the shell extensions header */
0e9714
+	if (g_strcmp0 (gs_category_get_id (self->subcategory), "shell-extensions") == 0)
0e9714
+		gtk_widget_set_visible (self->infobar_category_shell_extensions, TRUE);
0e9714
+	else
0e9714
+		gtk_widget_set_visible (self->infobar_category_shell_extensions, FALSE);
0e9714
+
0e9714
 	if (self->sort_rating_handler_id > 0) {
0e9714
 		g_signal_handler_disconnect (self->sort_rating_button,
0e9714
 					     self->sort_rating_handler_id);
0e9714
@@ -524,6 +532,18 @@ gs_category_page_dispose (GObject *object)
0e9714
 	G_OBJECT_CLASS (gs_category_page_parent_class)->dispose (object);
0e9714
 }
0e9714
 
0e9714
+static void
0e9714
+button_shell_extensions_cb (GtkButton *button, GsCategoryPage *self)
0e9714
+{
0e9714
+	gboolean ret;
0e9714
+	g_autoptr(GError) error = NULL;
0e9714
+	const gchar *argv[] = { "gnome-shell-extension-prefs", NULL };
0e9714
+	ret = g_spawn_async (NULL, (gchar **) argv, NULL, G_SPAWN_SEARCH_PATH,
0e9714
+			     NULL, NULL, NULL, &error);
0e9714
+	if (!ret)
0e9714
+		g_warning ("failed to exec %s: %s", argv[0], error->message);
0e9714
+}
0e9714
+
0e9714
 static gboolean
0e9714
 gs_category_page_setup (GsPage *page,
0e9714
                         GsShell *shell,
0e9714
@@ -545,6 +565,9 @@ gs_category_page_setup (GsPage *page,
0e9714
 
0e9714
 	adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_category));
0e9714
 	gtk_container_set_focus_vadjustment (GTK_CONTAINER (self->category_detail_box), adj);
0e9714
+
0e9714
+	g_signal_connect (self->button_category_shell_extensions, "clicked",
0e9714
+			  G_CALLBACK (button_shell_extensions_cb), self);
0e9714
 	return TRUE;
0e9714
 }
0e9714
 
0e9714
@@ -563,6 +586,8 @@ gs_category_page_class_init (GsCategoryPageClass *klass)
0e9714
 	gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-category-page.ui");
0e9714
 
0e9714
 	gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, category_detail_box);
0e9714
+	gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, infobar_category_shell_extensions);
0e9714
+	gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, button_category_shell_extensions);
0e9714
 	gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, scrolledwindow_category);
0e9714
 	gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_filter_label);
0e9714
 	gtk_widget_class_bind_template_child (widget_class, GsCategoryPage, subcats_filter_button_label);
0e9714
diff --git a/src/gs-category-page.ui b/src/gs-category-page.ui
0e9714
index cbb82840..2a3c789f 100644
0e9714
--- a/src/gs-category-page.ui
0e9714
+++ b/src/gs-category-page.ui
0e9714
@@ -97,6 +97,51 @@
0e9714
                                 <property name="margin_end">24</property>
0e9714
                               </object>
0e9714
                             </child>
0e9714
+                            <child>
0e9714
+                              <object class="GtkInfoBar" id="infobar_category_shell_extensions">
0e9714
+                                <property name="visible">False</property>
0e9714
+                                <property name="app_paintable">True</property>
0e9714
+                                <property name="hexpand">True</property>
0e9714
+                                <property name="message_type">other</property>
0e9714
+                                <property name="margin_start">24</property>
0e9714
+                                <property name="margin_top">24</property>
0e9714
+                                <property name="margin_end">24</property>
0e9714
+                                <style>
0e9714
+                                  <class name="application-details-infobar"/>
0e9714
+                                </style>
0e9714
+                                <child internal-child="action_area">
0e9714
+                                  <object class="GtkButtonBox">
0e9714
+                                    <property name="spacing">6</property>
0e9714
+                                    <property name="layout_style">end</property>
0e9714
+                                    <child>
0e9714
+                                      <object class="GtkButton" id="button_category_shell_extensions">
0e9714
+                                        <property name="label" translatable="yes">Extension Settings</property>
0e9714
+                                        <property name="visible">True</property>
0e9714
+                                        <property name="can_focus">True</property>
0e9714
+                                        <property name="receives_default">True</property>
0e9714
+                                        <property name="hexpand">True</property>
0e9714
+                                      </object>
0e9714
+                                    </child>
0e9714
+                                  </object>
0e9714
+                                </child>
0e9714
+                                <child internal-child="content_area">
0e9714
+                                  <object class="GtkBox">
0e9714
+                                    <property name="halign">start</property>
0e9714
+                                    <property name="hexpand">True</property>
0e9714
+                                    <property name="spacing">16</property>
0e9714
+                                    <child>
0e9714
+                                      <object class="GtkLabel">
0e9714
+                                        <property name="visible">True</property>
0e9714
+                                        <property name="wrap">True</property>
0e9714
+                                        <property name="label" translatable="yes">Extensions are used at your own risk. If you have any system problems, it is recommended to disable them.</property>
0e9714
+                                        <property name="hexpand">True</property>
0e9714
+                                      </object>
0e9714
+                                    </child>
0e9714
+                                  </object>
0e9714
+                                </child>
0e9714
+                              </object>
0e9714
+                            </child>
0e9714
+
0e9714
                             <child>
0e9714
                               <object class="GtkBox">
0e9714
                                 <property name="visible">True</property>
0e9714
diff --git a/src/gs-details-page.c b/src/gs-details-page.c
0e9714
index 8eb07bd3..37a717bf 100644
0e9714
--- a/src/gs-details-page.c
0e9714
+++ b/src/gs-details-page.c
0e9714
@@ -923,9 +923,17 @@ gs_details_page_refresh_buttons (GsDetailsPage *self)
0e9714
 		break;
0e9714
 	}
0e9714
 
0e9714
-	gtk_button_set_label (GTK_BUTTON (self->button_details_launch),
0e9714
-			      /* TRANSLATORS: A label for a button to execute the selected application. */
0e9714
-			      _("_Launch"));
0e9714
+	if (gs_app_get_kind (self->app) == AS_APP_KIND_SHELL_EXTENSION) {
0e9714
+		gtk_button_set_label (GTK_BUTTON (self->button_details_launch),
0e9714
+		                      /* TRANSLATORS: A label for a button to show the settings for
0e9714
+		                         the selected shell extension. */
0e9714
+		                      _("Extension Settings"));
0e9714
+	} else {
0e9714
+		gtk_button_set_label (GTK_BUTTON (self->button_details_launch),
0e9714
+		                      /* TRANSLATORS: A label for a button to execute the selected
0e9714
+		                         application. */
0e9714
+		                      _("_Launch"));
0e9714
+	}
0e9714
 
0e9714
 	/* don't show the launch and shortcut buttons if the app doesn't have a desktop ID */
0e9714
 	if (gs_app_get_id (self->app) == NULL) {
0e9714
diff --git a/src/gs-repo-row.c b/src/gs-repo-row.c
0e9714
index 35b35045..7ce7e618 100644
0e9714
--- a/src/gs-repo-row.c
0e9714
+++ b/src/gs-repo-row.c
0e9714
@@ -68,7 +68,8 @@ repo_supports_removal (GsApp *repo)
0e9714
 	/* can't remove a repo, only enable/disable existing ones */
0e9714
 	if (g_strcmp0 (management_plugin, "fwupd") == 0 ||
0e9714
 	    g_strcmp0 (management_plugin, "packagekit") == 0 ||
0e9714
-	    g_strcmp0 (management_plugin, "rpm-ostree") == 0)
0e9714
+	    g_strcmp0 (management_plugin, "rpm-ostree") == 0 ||
0e9714
+	    g_strcmp0 (management_plugin, "shell-extensions") == 0)
0e9714
 		return FALSE;
0e9714
 
0e9714
 	return TRUE;
0e9714
diff --git a/src/gs-repos-dialog.c b/src/gs-repos-dialog.c
0e9714
index 93830308..7c11dc78 100644
0e9714
--- a/src/gs-repos-dialog.c
0e9714
+++ b/src/gs-repos-dialog.c
0e9714
@@ -136,7 +136,8 @@ repo_supports_removal (GsApp *repo)
0e9714
 	/* can't remove a repo, only enable/disable existing ones */
0e9714
 	if (g_strcmp0 (management_plugin, "fwupd") == 0 ||
0e9714
 	    g_strcmp0 (management_plugin, "packagekit") == 0 ||
0e9714
-	    g_strcmp0 (management_plugin, "rpm-ostree") == 0)
0e9714
+	    g_strcmp0 (management_plugin, "rpm-ostree") == 0 ||
0e9714
+	    g_strcmp0 (management_plugin, "shell-extensions") == 0)
0e9714
 		return FALSE;
0e9714
 
0e9714
 	return TRUE;
0e9714
diff --git a/src/gs-summary-tile.c b/src/gs-summary-tile.c
0e9714
index 446200de..90e810c9 100644
0e9714
--- a/src/gs-summary-tile.c
0e9714
+++ b/src/gs-summary-tile.c
0e9714
@@ -41,7 +41,7 @@ gs_summary_tile_refresh (GsAppTile *self)
0e9714
 	const GdkPixbuf *pixbuf;
0e9714
 	gboolean installed;
0e9714
 	g_autofree gchar *name = NULL;
0e9714
-	const gchar *summary;
0e9714
+	g_autofree gchar *summary = NULL;
0e9714
 	const gchar *css;
0e9714
 
0e9714
 	if (app == NULL)
0e9714
@@ -53,7 +53,18 @@ gs_summary_tile_refresh (GsAppTile *self)
0e9714
 	/* set name */
0e9714
 	gtk_label_set_label (GTK_LABEL (tile->name), gs_app_get_name (app));
0e9714
 
0e9714
-	summary = gs_app_get_summary (app);
0e9714
+	/* some kinds have boring summaries */
0e9714
+	switch (gs_app_get_kind (app)) {
0e9714
+	case AS_APP_KIND_SHELL_EXTENSION:
0e9714
+		summary = g_strdup (gs_app_get_description (app));
0e9714
+		if (summary != NULL)
0e9714
+			g_strdelimit (summary, "\n\t", ' ');
0e9714
+		break;
0e9714
+	default:
0e9714
+		summary = g_strdup (gs_app_get_summary (app));
0e9714
+		break;
0e9714
+	}
0e9714
+
0e9714
 	gtk_label_set_label (GTK_LABEL (tile->summary), summary);
0e9714
 	gtk_widget_set_visible (tile->summary, summary && summary[0]);
0e9714
 
0e9714
-- 
0e9714
2.18.2
0e9714
0e9714
0e9714
From 420396f45380ce4da2a609f139e5f7f47a6d5d5d Mon Sep 17 00:00:00 2001
0e9714
From: Kalev Lember <klember@redhat.com>
0e9714
Date: Wed, 3 Jun 2020 15:50:44 +0200
0e9714
Subject: [PATCH 2/2] Revert "Update POTFILES.in"
0e9714
0e9714
This reverts commit 9afc91a6de17117a32e4d36210230ba218e8ea7d.
0e9714
---
0e9714
 po/POTFILES.in | 1 +
0e9714
 1 file changed, 1 insertion(+)
0e9714
0e9714
diff --git a/po/POTFILES.in b/po/POTFILES.in
0e9714
index a44a6ad3..c38c56e2 100644
0e9714
--- a/po/POTFILES.in
0e9714
+++ b/po/POTFILES.in
0e9714
@@ -88,5 +88,6 @@ plugins/fwupd/gs-plugin-fwupd.c
0e9714
 plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in
0e9714
 plugins/odrs/gs-plugin-odrs.c
0e9714
 plugins/odrs/org.gnome.Software.Plugin.Odrs.metainfo.xml.in
0e9714
+plugins/shell-extensions/gs-plugin-shell-extensions.c
0e9714
 plugins/snap/gs-plugin-snap.c
0e9714
 plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in
0e9714
-- 
0e9714
2.18.2
0e9714