summaryrefslogtreecommitdiffstats
path: root/lib/gs-plugin.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:57:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:57:27 +0000
commit6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 (patch)
treed423850ae901365e582137bdf2b5cbdffd7ca266 /lib/gs-plugin.c
parentInitial commit. (diff)
downloadgnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.tar.xz
gnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.zip
Adding upstream version 43.5.upstream/43.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--lib/gs-plugin.c2187
1 files changed, 2187 insertions, 0 deletions
diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c
new file mode 100644
index 0000000..51e0f7e
--- /dev/null
+++ b/lib/gs-plugin.c
@@ -0,0 +1,2187 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-plugin
+ * @title: GsPlugin Helpers
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Runtime-loaded modules providing functionality
+ *
+ * Plugins are modules that are loaded at runtime to provide information
+ * about requests and to service user actions like installing, removing
+ * and updating.
+ * This allows different distributions to pick and choose how the
+ * application installer gathers data.
+ *
+ * Plugins also have a priority system where the largest number gets
+ * run first. That means if one plugin requires some property or
+ * metadata set by another plugin then it **must** depend on the other
+ * plugin to be run in the correct order.
+ *
+ * As a general rule, try to make plugins as small and self-contained
+ * as possible and remember to cache as much data as possible for speed.
+ * Memory is cheap, time less so.
+ */
+
+#include "config.h"
+
+#include <gio/gdesktopappinfo.h>
+#include <gdk/gdk.h>
+#include <string.h>
+
+#include "gs-app-list-private.h"
+#include "gs-download-utils.h"
+#include "gs-enums.h"
+#include "gs-os-release.h"
+#include "gs-plugin-private.h"
+#include "gs-plugin.h"
+#include "gs-utils.h"
+
+typedef struct
+{
+ GHashTable *cache;
+ GMutex cache_mutex;
+ GModule *module;
+ GsPluginFlags flags;
+ GPtrArray *rules[GS_PLUGIN_RULE_LAST];
+ GHashTable *vfuncs; /* string:pointer */
+ GMutex vfuncs_mutex;
+ gboolean enabled;
+ guint interactive_cnt;
+ GMutex interactive_mutex;
+ gchar *language; /* allow-none */
+ gchar *name;
+ gchar *appstream_id;
+ guint scale;
+ guint order;
+ guint priority;
+ guint timer_id;
+ GMutex timer_mutex;
+ GNetworkMonitor *network_monitor;
+
+ GDBusConnection *session_bus_connection; /* (owned) (not nullable) */
+ GDBusConnection *system_bus_connection; /* (owned) (not nullable) */
+} GsPluginPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GsPlugin, gs_plugin, G_TYPE_OBJECT)
+
+G_DEFINE_QUARK (gs-plugin-error-quark, gs_plugin_error)
+
+typedef enum {
+ PROP_FLAGS = 1,
+ PROP_SESSION_BUS_CONNECTION,
+ PROP_SYSTEM_BUS_CONNECTION,
+} GsPluginProperty;
+
+static GParamSpec *obj_props[PROP_SYSTEM_BUS_CONNECTION + 1] = { NULL, };
+
+enum {
+ SIGNAL_UPDATES_CHANGED,
+ SIGNAL_STATUS_CHANGED,
+ SIGNAL_RELOAD,
+ SIGNAL_REPORT_EVENT,
+ SIGNAL_ALLOW_UPDATES,
+ SIGNAL_BASIC_AUTH_START,
+ SIGNAL_REPOSITORY_CHANGED,
+ SIGNAL_ASK_UNTRUSTED,
+ SIGNAL_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+typedef const gchar **(*GsPluginGetDepsFunc) (GsPlugin *plugin);
+
+/**
+ * gs_plugin_status_to_string:
+ * @status: a #GsPluginStatus, e.g. %GS_PLUGIN_STATUS_DOWNLOADING
+ *
+ * Converts the #GsPluginStatus enum to a string.
+ *
+ * Returns: the string representation, or "unknown"
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_status_to_string (GsPluginStatus status)
+{
+ if (status == GS_PLUGIN_STATUS_WAITING)
+ return "waiting";
+ if (status == GS_PLUGIN_STATUS_FINISHED)
+ return "finished";
+ if (status == GS_PLUGIN_STATUS_SETUP)
+ return "setup";
+ if (status == GS_PLUGIN_STATUS_DOWNLOADING)
+ return "downloading";
+ if (status == GS_PLUGIN_STATUS_QUERYING)
+ return "querying";
+ if (status == GS_PLUGIN_STATUS_INSTALLING)
+ return "installing";
+ if (status == GS_PLUGIN_STATUS_REMOVING)
+ return "removing";
+ return "unknown";
+}
+
+/**
+ * gs_plugin_set_name:
+ * @plugin: a #GsPlugin
+ * @name: a plugin name
+ *
+ * Sets the name of the plugin.
+ *
+ * Plugins are not required to set the plugin name as it is automatically set
+ * from the `.so` filename.
+ *
+ * Since: 3.26
+ **/
+void
+gs_plugin_set_name (GsPlugin *plugin, const gchar *name)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ if (priv->name != NULL)
+ g_free (priv->name);
+ priv->name = g_strdup (name);
+}
+
+/**
+ * gs_plugin_create:
+ * @filename: an absolute filename
+ * @session_bus_connection: (not nullable) (transfer none): a session bus
+ * connection to use
+ * @system_bus_connection: (not nullable) (transfer none): a system bus
+ * connection to use
+ * @error: a #GError, or %NULL
+ *
+ * Creates a new plugin from an external module.
+ *
+ * Returns: (transfer full): the #GsPlugin, or %NULL on error
+ *
+ * Since: 43
+ **/
+GsPlugin *
+gs_plugin_create (const gchar *filename,
+ GDBusConnection *session_bus_connection,
+ GDBusConnection *system_bus_connection,
+ GError **error)
+{
+ GsPlugin *plugin = NULL;
+ GsPluginPrivate *priv;
+ g_autofree gchar *basename = NULL;
+ GModule *module = NULL;
+ GType (*query_type_function) (void) = NULL;
+ GType plugin_type;
+
+ /* get the plugin name from the basename */
+ basename = g_path_get_basename (filename);
+ if (!g_str_has_prefix (basename, "libgs_plugin_")) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "plugin filename has wrong prefix: %s",
+ filename);
+ return NULL;
+ }
+ g_strdelimit (basename, ".", '\0');
+
+ /* create new plugin */
+ module = g_module_open (filename, 0);
+ if (module == NULL ||
+ !g_module_symbol (module, "gs_plugin_query_type", (gpointer *) &query_type_function)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "failed to open plugin %s: %s",
+ filename, g_module_error ());
+ if (module != NULL)
+ g_module_close (module);
+ return NULL;
+ }
+
+ /* Make the module resident so it can’t be unloaded: without using a
+ * full #GTypePlugin implementation for the modules, it’s not safe to
+ * re-load a module and re-register its types with GObject, as that will
+ * confuse the GType system. */
+ g_module_make_resident (module);
+
+ plugin_type = query_type_function ();
+ g_assert (g_type_is_a (plugin_type, GS_TYPE_PLUGIN));
+
+ plugin = g_object_new (plugin_type,
+ "session-bus-connection", session_bus_connection,
+ "system-bus-connection", system_bus_connection,
+ NULL);
+ priv = gs_plugin_get_instance_private (plugin);
+ priv->module = g_steal_pointer (&module);
+
+ gs_plugin_set_name (plugin, basename + 13);
+ return plugin;
+}
+
+static void
+gs_plugin_dispose (GObject *object)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+
+ g_clear_object (&priv->session_bus_connection);
+ g_clear_object (&priv->system_bus_connection);
+
+ G_OBJECT_CLASS (gs_plugin_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_finalize (GObject *object)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ guint i;
+
+ for (i = 0; i < GS_PLUGIN_RULE_LAST; i++)
+ g_ptr_array_unref (priv->rules[i]);
+
+ if (priv->timer_id > 0)
+ g_source_remove (priv->timer_id);
+ g_free (priv->name);
+ g_free (priv->appstream_id);
+ g_free (priv->language);
+ if (priv->network_monitor != NULL)
+ g_object_unref (priv->network_monitor);
+ g_hash_table_unref (priv->cache);
+ g_hash_table_unref (priv->vfuncs);
+ g_mutex_clear (&priv->cache_mutex);
+ g_mutex_clear (&priv->interactive_mutex);
+ g_mutex_clear (&priv->timer_mutex);
+ g_mutex_clear (&priv->vfuncs_mutex);
+ if (priv->module != NULL)
+ g_module_close (priv->module);
+
+ G_OBJECT_CLASS (gs_plugin_parent_class)->finalize (object);
+}
+
+/**
+ * gs_plugin_get_symbol: (skip)
+ * @plugin: a #GsPlugin
+ * @function_name: a symbol name
+ *
+ * Gets the symbol from the module that backs the plugin. If the plugin is not
+ * enabled then no symbol is returned.
+ *
+ * Returns: the pointer to the symbol, or %NULL
+ *
+ * Since: 3.22
+ **/
+gpointer
+gs_plugin_get_symbol (GsPlugin *plugin, const gchar *function_name)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ gpointer func = NULL;
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->vfuncs_mutex);
+
+ g_return_val_if_fail (function_name != NULL, NULL);
+
+ /* disabled plugins shouldn't be checked */
+ if (!priv->enabled)
+ return NULL;
+
+ /* look up the symbol from the cache */
+ if (g_hash_table_lookup_extended (priv->vfuncs, function_name, NULL, &func))
+ return func;
+
+ /* look up the symbol using the elf headers */
+ g_module_symbol (priv->module, function_name, &func);
+ g_hash_table_insert (priv->vfuncs, g_strdup (function_name), func);
+
+ return func;
+}
+
+/**
+ * gs_plugin_get_enabled:
+ * @plugin: a #GsPlugin
+ *
+ * Gets if the plugin is enabled.
+ *
+ * Returns: %TRUE if enabled
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_get_enabled (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->enabled;
+}
+
+/**
+ * gs_plugin_set_enabled:
+ * @plugin: a #GsPlugin
+ * @enabled: the enabled state
+ *
+ * Enables or disables a plugin.
+ * This is normally only called from the init function for a #GsPlugin instance.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_enabled (GsPlugin *plugin, gboolean enabled)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->enabled = enabled;
+}
+
+void
+gs_plugin_interactive_inc (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->interactive_mutex);
+ priv->interactive_cnt++;
+ gs_plugin_add_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+}
+
+void
+gs_plugin_interactive_dec (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->interactive_mutex);
+ if (priv->interactive_cnt > 0)
+ priv->interactive_cnt--;
+ if (priv->interactive_cnt == 0)
+ gs_plugin_remove_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+}
+
+/**
+ * gs_plugin_get_name:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin name.
+ *
+ * Returns: a string, e.g. "fwupd"
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_get_name (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->name;
+}
+
+/**
+ * gs_plugin_get_appstream_id:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin AppStream ID.
+ *
+ * Returns: a string, e.g. `org.gnome.Software.Plugin.Epiphany`
+ *
+ * Since: 3.24
+ **/
+const gchar *
+gs_plugin_get_appstream_id (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->appstream_id;
+}
+
+/**
+ * gs_plugin_set_appstream_id:
+ * @plugin: a #GsPlugin
+ * @appstream_id: an appstream ID, e.g. `org.gnome.Software.Plugin.Epiphany`
+ *
+ * Sets the plugin AppStream ID.
+ *
+ * Since: 3.24
+ **/
+void
+gs_plugin_set_appstream_id (GsPlugin *plugin, const gchar *appstream_id)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_free (priv->appstream_id);
+ priv->appstream_id = g_strdup (appstream_id);
+}
+
+/**
+ * gs_plugin_get_scale:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the window scale factor.
+ *
+ * Returns: the factor, usually 1 for standard screens or 2 for HiDPI
+ *
+ * Since: 3.22
+ **/
+guint
+gs_plugin_get_scale (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->scale;
+}
+
+/**
+ * gs_plugin_set_scale:
+ * @plugin: a #GsPlugin
+ * @scale: the window scale factor, usually 1 for standard screens or 2 for HiDPI
+ *
+ * Sets the window scale factor.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_scale (GsPlugin *plugin, guint scale)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->scale = scale;
+}
+
+/**
+ * gs_plugin_get_order:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin order, where higher numbers are run after lower
+ * numbers.
+ *
+ * Returns: the integer value
+ *
+ * Since: 3.22
+ **/
+guint
+gs_plugin_get_order (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->order;
+}
+
+/**
+ * gs_plugin_set_order:
+ * @plugin: a #GsPlugin
+ * @order: a integer value
+ *
+ * Sets the plugin order, where higher numbers are run after lower
+ * numbers.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_order (GsPlugin *plugin, guint order)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->order = order;
+}
+
+/**
+ * gs_plugin_get_priority:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin priority, where higher values will be chosen where
+ * multiple #GsApp's match a specific rule.
+ *
+ * Returns: the integer value
+ *
+ * Since: 3.22
+ **/
+guint
+gs_plugin_get_priority (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->priority;
+}
+
+/**
+ * gs_plugin_set_priority:
+ * @plugin: a #GsPlugin
+ * @priority: a integer value
+ *
+ * Sets the plugin priority, where higher values will be chosen where
+ * multiple #GsApp's match a specific rule.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_priority (GsPlugin *plugin, guint priority)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->priority = priority;
+}
+
+/**
+ * gs_plugin_get_language:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the user language from the locale. This is the first component of the
+ * locale.
+ *
+ * Typically you should use the full locale rather than the language, as the
+ * same language can be used quite differently in different territories.
+ *
+ * Returns: the language string, e.g. `fr`
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_get_language (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->language;
+}
+
+/**
+ * gs_plugin_set_language:
+ * @plugin: a #GsPlugin
+ * @language: a language string, e.g. "fr"
+ *
+ * Sets the plugin language.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_language (GsPlugin *plugin, const gchar *language)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_free (priv->language);
+ priv->language = g_strdup (language);
+}
+
+/**
+ * gs_plugin_set_network_monitor:
+ * @plugin: a #GsPlugin
+ * @monitor: a #GNetworkMonitor
+ *
+ * Sets the network monitor so that plugins can check the state of the network.
+ *
+ * Since: 3.28
+ **/
+void
+gs_plugin_set_network_monitor (GsPlugin *plugin, GNetworkMonitor *monitor)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_set_object (&priv->network_monitor, monitor);
+}
+
+/**
+ * gs_plugin_get_network_available:
+ * @plugin: a #GsPlugin
+ *
+ * Gets whether a network connectivity is available.
+ *
+ * Returns: %TRUE if a network is available.
+ *
+ * Since: 3.28
+ **/
+gboolean
+gs_plugin_get_network_available (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ if (priv->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-available=TRUE");
+ return TRUE;
+ }
+ return g_network_monitor_get_network_available (priv->network_monitor);
+}
+
+/**
+ * gs_plugin_has_flags:
+ * @plugin: a #GsPlugin
+ * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_INTERACTIVE
+ *
+ * Finds out if a plugin has a specific flag set.
+ *
+ * Returns: TRUE if the flag is set
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_has_flags (GsPlugin *plugin, GsPluginFlags flags)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return (priv->flags & flags) > 0;
+}
+
+/**
+ * gs_plugin_add_flags:
+ * @plugin: a #GsPlugin
+ * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_INTERACTIVE
+ *
+ * Adds specific flags to the plugin.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_add_flags (GsPlugin *plugin, GsPluginFlags flags)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->flags |= flags;
+ g_object_notify_by_pspec (G_OBJECT (plugin), obj_props[PROP_FLAGS]);
+}
+
+/**
+ * gs_plugin_remove_flags:
+ * @plugin: a #GsPlugin
+ * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_INTERACTIVE
+ *
+ * Removes specific flags from the plugin.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_remove_flags (GsPlugin *plugin, GsPluginFlags flags)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->flags &= ~flags;
+ g_object_notify_by_pspec (G_OBJECT (plugin), obj_props[PROP_FLAGS]);
+}
+
+/**
+ * gs_plugin_add_rule:
+ * @plugin: a #GsPlugin
+ * @rule: a #GsPluginRule, e.g. %GS_PLUGIN_RULE_CONFLICTS
+ * @name: a plugin name, e.g. "appstream"
+ *
+ * If the plugin name is found, the rule will be used to sort the plugin list,
+ * for example the plugin specified by @name will be ordered after this plugin
+ * when %GS_PLUGIN_RULE_RUN_AFTER is used.
+ *
+ * NOTE: The depsolver is iterative and may not solve overly-complicated rules;
+ * If depsolving fails then gnome-software will not start.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_add_rule (GsPlugin *plugin, GsPluginRule rule, const gchar *name)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_ptr_array_add (priv->rules[rule], g_strdup (name));
+}
+
+/**
+ * gs_plugin_get_rules:
+ * @plugin: a #GsPlugin
+ * @rule: a #GsPluginRule, e.g. %GS_PLUGIN_RULE_CONFLICTS
+ *
+ * Gets the plugin IDs that should be run after this plugin.
+ *
+ * Returns: (element-type utf8) (transfer none): the list of plugin names, e.g. ['appstream']
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_plugin_get_rules (GsPlugin *plugin, GsPluginRule rule)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->rules[rule];
+}
+
+/**
+ * gs_plugin_check_distro_id:
+ * @plugin: a #GsPlugin
+ * @distro_id: a distro ID, e.g. "fedora"
+ *
+ * Checks if the distro is compatible.
+ *
+ * Returns: %TRUE if compatible
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_check_distro_id (GsPlugin *plugin, const gchar *distro_id)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsOsRelease) os_release = NULL;
+ const gchar *id = NULL;
+
+ /* load /etc/os-release */
+ os_release = gs_os_release_new (&error);
+ if (os_release == NULL) {
+ g_debug ("could not parse os-release: %s", error->message);
+ return FALSE;
+ }
+
+ /* check that we are running on Fedora */
+ id = gs_os_release_get_id (os_release);
+ if (id == NULL) {
+ g_debug ("could not get distro ID");
+ return FALSE;
+ }
+ if (g_strcmp0 (id, distro_id) != 0)
+ return FALSE;
+ return TRUE;
+}
+
+typedef struct {
+ GWeakRef plugin_weak; /* (element-type GsPlugin) */
+ GsApp *app; /* (owned) */
+ GsPluginStatus status;
+ guint percentage;
+} GsPluginStatusHelper;
+
+static void
+gs_plugin_status_helper_free (GsPluginStatusHelper *helper)
+{
+ g_weak_ref_clear (&helper->plugin_weak);
+ g_clear_object (&helper->app);
+ g_slice_free (GsPluginStatusHelper, helper);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginStatusHelper, gs_plugin_status_helper_free)
+
+static gboolean
+gs_plugin_status_update_cb (gpointer user_data)
+{
+ GsPluginStatusHelper *helper = (GsPluginStatusHelper *) user_data;
+ g_autoptr(GsPlugin) plugin = NULL;
+
+ /* Does the plugin still exist? */
+ plugin = g_weak_ref_get (&helper->plugin_weak);
+
+ if (plugin != NULL)
+ g_signal_emit (plugin,
+ signals[SIGNAL_STATUS_CHANGED], 0,
+ helper->app,
+ helper->status);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * gs_plugin_status_update:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @status: a #GsPluginStatus, e.g. %GS_PLUGIN_STATUS_DOWNLOADING
+ *
+ * Update the state of the plugin so any UI can be updated.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_status_update (GsPlugin *plugin, GsApp *app, GsPluginStatus status)
+{
+ g_autoptr(GsPluginStatusHelper) helper = NULL;
+ g_autoptr(GSource) idle_source = NULL;
+
+ helper = g_slice_new0 (GsPluginStatusHelper);
+ g_weak_ref_init (&helper->plugin_weak, plugin);
+ helper->status = status;
+ if (app != NULL)
+ helper->app = g_object_ref (app);
+
+ idle_source = g_idle_source_new ();
+ g_source_set_callback (idle_source, gs_plugin_status_update_cb, g_steal_pointer (&helper), (GDestroyNotify) gs_plugin_status_helper_free);
+ g_source_attach (idle_source, NULL);
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ gchar *remote;
+ gchar *realm;
+ GCallback callback;
+ gpointer user_data;
+} GsPluginBasicAuthHelper;
+
+static gboolean
+gs_plugin_basic_auth_start_cb (gpointer user_data)
+{
+ GsPluginBasicAuthHelper *helper = user_data;
+ g_signal_emit (helper->plugin,
+ signals[SIGNAL_BASIC_AUTH_START], 0,
+ helper->remote,
+ helper->realm,
+ helper->callback,
+ helper->user_data);
+ g_free (helper->remote);
+ g_free (helper->realm);
+ g_slice_free (GsPluginBasicAuthHelper, helper);
+ return FALSE;
+}
+
+/**
+ * gs_plugin_basic_auth_start:
+ * @plugin: a #GsPlugin
+ * @remote: a string
+ * @realm: a string
+ * @callback: callback to invoke to submit the user/password
+ * @user_data: callback data to pass to the callback
+ *
+ * Emit the basic-auth-start signal in the main thread.
+ *
+ * Since: 3.38
+ **/
+void
+gs_plugin_basic_auth_start (GsPlugin *plugin,
+ const gchar *remote,
+ const gchar *realm,
+ GCallback callback,
+ gpointer user_data)
+{
+ GsPluginBasicAuthHelper *helper;
+ g_autoptr(GSource) idle_source = NULL;
+
+ helper = g_slice_new0 (GsPluginBasicAuthHelper);
+ helper->plugin = plugin;
+ helper->remote = g_strdup (remote);
+ helper->realm = g_strdup (realm);
+ helper->callback = callback;
+ helper->user_data = user_data;
+
+ idle_source = g_idle_source_new ();
+ g_source_set_callback (idle_source, gs_plugin_basic_auth_start_cb, helper, NULL);
+ g_source_attach (idle_source, NULL);
+}
+
+static gboolean
+gs_plugin_app_launch_cb (gpointer user_data)
+{
+ GAppInfo *appinfo = (GAppInfo *) user_data;
+ GdkDisplay *display;
+ g_autoptr(GAppLaunchContext) context = NULL;
+ g_autoptr(GError) error = NULL;
+
+ display = gdk_display_get_default ();
+ context = G_APP_LAUNCH_CONTEXT (gdk_display_get_app_launch_context (display));
+ if (!g_app_info_launch (appinfo, NULL, context, &error))
+ g_warning ("Failed to launch: %s", error->message);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * gs_plugin_app_launch:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @error: a #GError, or %NULL
+ *
+ * Launches the application using #GAppInfo.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_app_launch (GsPlugin *plugin, GsApp *app, GError **error)
+{
+ const gchar *desktop_id;
+ g_autoptr(GAppInfo) appinfo = NULL;
+
+ desktop_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
+ if (desktop_id == NULL)
+ desktop_id = gs_app_get_id (app);
+ if (desktop_id == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no such desktop file: %s",
+ desktop_id);
+ return FALSE;
+ }
+ appinfo = G_APP_INFO (gs_utils_get_desktop_app_info (desktop_id));
+ if (appinfo == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no such desktop file: %s",
+ desktop_id);
+ return FALSE;
+ }
+ g_idle_add_full (G_PRIORITY_DEFAULT,
+ gs_plugin_app_launch_cb,
+ g_object_ref (appinfo),
+ (GDestroyNotify) g_object_unref);
+ return TRUE;
+}
+
+static GDesktopAppInfo *
+check_directory_for_desktop_file (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginPickDesktopFileCallback cb,
+ gpointer user_data,
+ const gchar *desktop_id,
+ const gchar *data_dir)
+{
+ g_autofree gchar *filename = NULL;
+ g_autoptr(GKeyFile) key_file = NULL;
+
+ filename = g_build_filename (data_dir, "applications", desktop_id, NULL);
+ key_file = g_key_file_new ();
+
+ if (g_key_file_load_from_file (key_file, filename, G_KEY_FILE_KEEP_TRANSLATIONS, NULL) &&
+ cb (plugin, app, filename, key_file)) {
+ g_autoptr(GDesktopAppInfo) appinfo = NULL;
+ appinfo = g_desktop_app_info_new_from_keyfile (key_file);
+ if (appinfo != NULL)
+ return g_steal_pointer (&appinfo);
+ g_debug ("Failed to load '%s' as a GDesktopAppInfo", filename);
+ return NULL;
+ }
+
+ if (!g_str_has_suffix (desktop_id, ".desktop")) {
+ g_autofree gchar *desktop_filename = g_strconcat (filename, ".desktop", NULL);
+ if (g_key_file_load_from_file (key_file, desktop_filename, G_KEY_FILE_KEEP_TRANSLATIONS, NULL) &&
+ cb (plugin, app, desktop_filename, key_file)) {
+ g_autoptr(GDesktopAppInfo) appinfo = NULL;
+ appinfo = g_desktop_app_info_new_from_keyfile (key_file);
+ if (appinfo != NULL)
+ return g_steal_pointer (&appinfo);
+ g_debug ("Failed to load '%s' as a GDesktopAppInfo", desktop_filename);
+ return NULL;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * gs_plugin_app_launch_filtered:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp to launch
+ * @cb: a callback to pick the correct .desktop file
+ * @user_data: (closure cb) (scope call): user data for the @cb
+ * @error: a #GError, or %NULL
+ *
+ * Launches application @app, using the .desktop file picked by the @cb.
+ * This can help in case multiple versions of the @app are installed
+ * in the system (like a Flatpak and RPM versions).
+ *
+ * Returns: %TRUE on success
+ *
+ * Since: 43
+ **/
+gboolean
+gs_plugin_app_launch_filtered (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginPickDesktopFileCallback cb,
+ gpointer user_data,
+ GError **error)
+{
+ const gchar *desktop_id;
+ g_autoptr(GDesktopAppInfo) appinfo = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ g_return_val_if_fail (cb != NULL, FALSE);
+
+ desktop_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
+ if (desktop_id == NULL)
+ desktop_id = gs_app_get_id (app);
+ if (desktop_id == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no desktop file for app: %s",
+ gs_app_get_name (app));
+ return FALSE;
+ }
+
+ /* First, the configs. Highest priority: the user's ~/.config */
+ appinfo = check_directory_for_desktop_file (plugin, app, cb, user_data, desktop_id, g_get_user_config_dir ());
+
+ if (appinfo == NULL) {
+ /* Next, the system configs (/etc/xdg, and so on). */
+ const gchar * const *dirs;
+ dirs = g_get_system_config_dirs ();
+ for (guint i = 0; dirs[i] && appinfo == NULL; i++) {
+ appinfo = check_directory_for_desktop_file (plugin, app, cb, user_data, desktop_id, dirs[i]);
+ }
+ }
+
+ if (appinfo == NULL) {
+ /* Now the data. Highest priority: the user's ~/.local/share/applications */
+ appinfo = check_directory_for_desktop_file (plugin, app, cb, user_data, desktop_id, g_get_user_data_dir ());
+ }
+
+ if (appinfo == NULL) {
+ /* Following that, XDG_DATA_DIRS/applications, in order */
+ const gchar * const *dirs;
+ dirs = g_get_system_data_dirs ();
+ for (guint i = 0; dirs[i] && appinfo == NULL; i++) {
+ appinfo = check_directory_for_desktop_file (plugin, app, cb, user_data, desktop_id, dirs[i]);
+ }
+ }
+
+ if (appinfo == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no appropriate desktop file found: %s",
+ desktop_id);
+ return FALSE;
+ }
+
+ g_idle_add_full (G_PRIORITY_DEFAULT,
+ gs_plugin_app_launch_cb,
+ g_object_ref (appinfo),
+ (GDestroyNotify) g_object_unref);
+
+ return TRUE;
+}
+
+static void
+weak_ref_free (GWeakRef *weak)
+{
+ g_weak_ref_clear (weak);
+ g_free (weak);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GWeakRef, weak_ref_free)
+
+/* @obj is a gpointer rather than a GObject* to avoid the need for casts */
+static GWeakRef *
+weak_ref_new (gpointer obj)
+{
+ g_autoptr(GWeakRef) weak = g_new0 (GWeakRef, 1);
+ g_weak_ref_init (weak, obj);
+ return g_steal_pointer (&weak);
+}
+
+static gboolean
+gs_plugin_updates_changed_cb (gpointer user_data)
+{
+ GWeakRef *plugin_weak = user_data;
+ g_autoptr(GsPlugin) plugin = NULL;
+
+ plugin = g_weak_ref_get (plugin_weak);
+ if (plugin != NULL)
+ g_signal_emit (plugin, signals[SIGNAL_UPDATES_CHANGED], 0);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * gs_plugin_updates_changed:
+ * @plugin: a #GsPlugin
+ *
+ * Emit a signal that tells the plugin loader that the list of updates
+ * may have changed.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_updates_changed (GsPlugin *plugin)
+{
+ g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, gs_plugin_updates_changed_cb,
+ weak_ref_new (plugin), (GDestroyNotify) weak_ref_free);
+}
+
+static gboolean
+gs_plugin_reload_cb (gpointer user_data)
+{
+ GWeakRef *plugin_weak = user_data;
+ g_autoptr(GsPlugin) plugin = NULL;
+
+ plugin = g_weak_ref_get (plugin_weak);
+ if (plugin != NULL)
+ g_signal_emit (plugin, signals[SIGNAL_RELOAD], 0);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * gs_plugin_reload:
+ * @plugin: a #GsPlugin
+ *
+ * Plugins that call this function should expect that all panels will
+ * reload after a small delay, causing mush flashing, wailing and
+ * gnashing of teeth.
+ *
+ * Plugins should not call this unless absolutely required.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_reload (GsPlugin *plugin)
+{
+ g_debug ("emitting %s::reload in idle", gs_plugin_get_name (plugin));
+ g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, gs_plugin_reload_cb,
+ weak_ref_new (plugin), (GDestroyNotify) weak_ref_free);
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ GsApp *app;
+} GsPluginDownloadHelper;
+
+static void
+download_file_progress_cb (gsize total_written_bytes,
+ gsize total_download_size,
+ gpointer user_data)
+{
+ GsPluginDownloadHelper *helper = user_data;
+ guint percentage;
+
+ if (total_download_size > 0)
+ percentage = (guint) ((100 * total_written_bytes) / total_download_size);
+ else
+ percentage = 0;
+
+ g_debug ("%s progress: %u%%", gs_app_get_id (helper->app), percentage);
+ gs_app_set_progress (helper->app, percentage);
+ gs_plugin_status_update (helper->plugin,
+ helper->app,
+ GS_PLUGIN_STATUS_DOWNLOADING);
+
+}
+
+static void
+async_result_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **result_out = user_data;
+
+ g_assert (*result_out == NULL);
+ *result_out = g_object_ref (result);
+ g_main_context_wakeup (g_main_context_get_thread_default ());
+}
+
+/**
+ * gs_plugin_download_file:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @uri: a remote URI
+ * @filename: a local filename
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads data and saves it to a file.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_download_file (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(SoupSession) soup_session = NULL;
+ g_autoptr(GFile) output_file = NULL;
+ g_autoptr(GAsyncResult) result = NULL;
+ g_autoptr(GMainContext) context = g_main_context_new ();
+ g_autoptr(GMainContextPusher) context_pusher = g_main_context_pusher_new (context);
+ GsPluginDownloadHelper helper;
+ g_autoptr(GError) local_error = NULL;
+
+ helper.plugin = plugin;
+ helper.app = app;
+
+ soup_session = gs_build_soup_session ();
+
+ /* Do the download. */
+ output_file = g_file_new_for_path (filename);
+ gs_download_file_async (soup_session, uri, output_file,
+ G_PRIORITY_LOW,
+ download_file_progress_cb, &helper,
+ cancellable, async_result_cb, &result);
+
+ while (result == NULL)
+ g_main_context_iteration (context, TRUE);
+
+ if (!gs_download_file_finish (soup_session, result, &local_error) &&
+ !g_error_matches (local_error, GS_DOWNLOAD_ERROR, GS_DOWNLOAD_ERROR_NOT_MODIFIED)) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ local_error->message);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gchar *
+gs_plugin_download_rewrite_resource_uri (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *cachefn = NULL;
+
+ /* local files */
+ if (g_str_has_prefix (uri, "file://"))
+ uri += 7;
+ if (g_str_has_prefix (uri, "/")) {
+ if (!g_file_test (uri, G_FILE_TEST_EXISTS)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "Failed to find file: %s", uri);
+ return NULL;
+ }
+ return g_strdup (uri);
+ }
+
+ /* get cache location */
+ cachefn = gs_utils_get_cache_filename ("cssresource", uri,
+ GS_UTILS_CACHE_FLAG_WRITEABLE |
+ GS_UTILS_CACHE_FLAG_USE_HASH |
+ GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY,
+ error);
+ if (cachefn == NULL)
+ return NULL;
+
+ /* already exists */
+ if (g_file_test (cachefn, G_FILE_TEST_EXISTS))
+ return g_steal_pointer (&cachefn);
+
+ /* download */
+ if (!gs_plugin_download_file (plugin, app, uri, cachefn,
+ cancellable, error)) {
+ return NULL;
+ }
+ return g_steal_pointer (&cachefn);
+}
+
+/**
+ * gs_plugin_download_rewrite_resource:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @resource: the CSS resource
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads remote assets and rewrites a CSS resource to use cached local URIs.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.26
+ **/
+gchar *
+gs_plugin_download_rewrite_resource (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *resource,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint start = 0;
+ g_autoptr(GString) resource_str = g_string_new (resource);
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL);
+ g_return_val_if_fail (resource != NULL, NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ /* replace datadir */
+ as_gstring_replace (resource_str, "@datadir@", DATADIR);
+ resource = resource_str->str;
+
+ /* look in string for any url() links */
+ for (guint i = 0; resource[i] != '\0'; i++) {
+ if (i > 4 && strncmp (resource + i - 4, "url(", 4) == 0) {
+ start = i;
+ continue;
+ }
+ if (start == 0) {
+ g_string_append_c (str, resource[i]);
+ continue;
+ }
+ if (resource[i] == ')') {
+ guint len;
+ g_autofree gchar *cachefn = NULL;
+ g_autofree gchar *uri = NULL;
+
+ /* remove optional single quotes */
+ if (resource[start] == '\'' || resource[start] == '"')
+ start++;
+ len = i - start;
+ if (i > 0 && (resource[i - 1] == '\'' || resource[i - 1] == '"'))
+ len--;
+ uri = g_strndup (resource + start, len);
+
+ /* download them to per-user cache */
+ cachefn = gs_plugin_download_rewrite_resource_uri (plugin,
+ app,
+ uri,
+ cancellable,
+ error);
+ if (cachefn == NULL)
+ return NULL;
+ g_string_append_printf (str, "'file://%s'", cachefn);
+ g_string_append_c (str, resource[i]);
+ start = 0;
+ }
+ }
+ return g_strdup (str->str);
+}
+
+/**
+ * gs_plugin_cache_lookup:
+ * @plugin: a #GsPlugin
+ * @key: a string
+ *
+ * Looks up an application object from the per-plugin cache
+ *
+ * Returns: (transfer full) (nullable): the #GsApp, or %NULL
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_plugin_cache_lookup (GsPlugin *plugin, const gchar *key)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ GsApp *app;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+ app = g_hash_table_lookup (priv->cache, key);
+ if (app == NULL)
+ return NULL;
+ return g_object_ref (app);
+}
+
+/**
+ * gs_plugin_cache_lookup_by_state:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList to add applications to
+ * @state: a #GsAppState
+ *
+ * Adds each cached #GsApp with state @state into the @list.
+ * When the state is %GS_APP_STATE_UNKNOWN, then adds all
+ * cached applications.
+ *
+ * Since: 40
+ **/
+void
+gs_plugin_cache_lookup_by_state (GsPlugin *plugin,
+ GsAppList *list,
+ GsAppState state)
+{
+ GsPluginPrivate *priv;
+ GHashTableIter iter;
+ gpointer value;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (GS_IS_APP_LIST (list));
+
+ priv = gs_plugin_get_instance_private (plugin);
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+
+ g_hash_table_iter_init (&iter, priv->cache);
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ GsApp *app = value;
+
+ if (state == GS_APP_STATE_UNKNOWN ||
+ state == gs_app_get_state (app))
+ gs_app_list_add (list, app);
+ }
+}
+
+/**
+ * gs_plugin_cache_remove:
+ * @plugin: a #GsPlugin
+ * @key: a key which matches
+ *
+ * Removes an application from the per-plugin cache.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_cache_remove (GsPlugin *plugin, const gchar *key)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (key != NULL);
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+ g_hash_table_remove (priv->cache, key);
+}
+
+/**
+ * gs_plugin_cache_add:
+ * @plugin: a #GsPlugin
+ * @key: a string, or %NULL if the unique ID should be used
+ * @app: a #GsApp
+ *
+ * Adds an application to the per-plugin cache. This is optional,
+ * and the plugin can use the cache however it likes.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_cache_add (GsPlugin *plugin, const gchar *key, GsApp *app)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+
+ /* the user probably doesn't want to do this */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
+ g_warning ("adding wildcard app %s to plugin cache",
+ gs_app_get_unique_id (app));
+ }
+
+ /* default */
+ if (key == NULL)
+ key = gs_app_get_unique_id (app);
+
+ g_return_if_fail (key != NULL);
+
+ if (g_hash_table_lookup (priv->cache, key) == app)
+ return;
+ g_hash_table_insert (priv->cache, g_strdup (key), g_object_ref (app));
+}
+
+/**
+ * gs_plugin_cache_invalidate:
+ * @plugin: a #GsPlugin
+ *
+ * Invalidate the per-plugin cache by marking all entries as invalid.
+ * This is optional, and the plugin can evict the cache whenever it
+ * likes. Using this function may mean the front-end and the plugin
+ * may be operating on a different GsApp with the same cache ID.
+ *
+ * Most plugins do not need to call this function; if a suitable cache
+ * key is being used the old cache item can remain.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_cache_invalidate (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+ g_hash_table_remove_all (priv->cache);
+}
+
+/**
+ * gs_plugin_report_event:
+ * @plugin: a #GsPlugin
+ * @event: a #GsPluginEvent
+ *
+ * Report a non-fatal event to the UI. Plugins should not assume that a
+ * specific event is actually shown to the user as it may be ignored
+ * automatically.
+ *
+ * Since: 3.24
+ **/
+void
+gs_plugin_report_event (GsPlugin *plugin, GsPluginEvent *event)
+{
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+ g_signal_emit (plugin, signals[SIGNAL_REPORT_EVENT], 0, event);
+}
+
+/**
+ * gs_plugin_set_allow_updates:
+ * @plugin: a #GsPlugin
+ * @allow_updates: boolean
+ *
+ * This allows plugins to inhibit the showing of the updates panel.
+ * This will typically be used when the required permissions are not possible
+ * to obtain, or when a LiveUSB image is low on space.
+ *
+ * By default, the updates panel is shown so plugins do not need to call this
+ * function unless they called gs_plugin_set_allow_updates() with %FALSE.
+ *
+ * Since: 3.24
+ **/
+void
+gs_plugin_set_allow_updates (GsPlugin *plugin, gboolean allow_updates)
+{
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_signal_emit (plugin, signals[SIGNAL_ALLOW_UPDATES], 0, allow_updates);
+}
+
+/**
+ * gs_plugin_error_to_string:
+ * @error: a #GsPluginError, e.g. %GS_PLUGIN_ERROR_NO_NETWORK
+ *
+ * Converts the enumerated error to a string.
+ *
+ * Returns: a string, or %NULL for invalid
+ **/
+const gchar *
+gs_plugin_error_to_string (GsPluginError error)
+{
+ if (error == GS_PLUGIN_ERROR_FAILED)
+ return "failed";
+ if (error == GS_PLUGIN_ERROR_NOT_SUPPORTED)
+ return "not-supported";
+ if (error == GS_PLUGIN_ERROR_CANCELLED)
+ return "cancelled";
+ if (error == GS_PLUGIN_ERROR_NO_NETWORK)
+ return "no-network";
+ if (error == GS_PLUGIN_ERROR_NO_SECURITY)
+ return "no-security";
+ if (error == GS_PLUGIN_ERROR_NO_SPACE)
+ return "no-space";
+ if (error == GS_PLUGIN_ERROR_AUTH_REQUIRED)
+ return "auth-required";
+ if (error == GS_PLUGIN_ERROR_AUTH_INVALID)
+ return "auth-invalid";
+ if (error == GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED)
+ return "plugin-depsolve-failed";
+ if (error == GS_PLUGIN_ERROR_DOWNLOAD_FAILED)
+ return "download-failed";
+ if (error == GS_PLUGIN_ERROR_WRITE_FAILED)
+ return "write-failed";
+ if (error == GS_PLUGIN_ERROR_INVALID_FORMAT)
+ return "invalid-format";
+ if (error == GS_PLUGIN_ERROR_DELETE_FAILED)
+ return "delete-failed";
+ if (error == GS_PLUGIN_ERROR_RESTART_REQUIRED)
+ return "restart-required";
+ if (error == GS_PLUGIN_ERROR_AC_POWER_REQUIRED)
+ return "ac-power-required";
+ if (error == GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW)
+ return "battery-level-too-low";
+ if (error == GS_PLUGIN_ERROR_TIMED_OUT)
+ return "timed-out";
+ return NULL;
+}
+
+/**
+ * gs_plugin_action_to_function_name: (skip)
+ * @action: a #GsPluginAction, e.g. %GS_PLUGIN_ACTION_INSTALL
+ *
+ * Converts the enumerated action to the vfunc name.
+ *
+ * Returns: a string, or %NULL for invalid
+ **/
+const gchar *
+gs_plugin_action_to_function_name (GsPluginAction action)
+{
+ if (action == GS_PLUGIN_ACTION_INSTALL)
+ return "gs_plugin_app_install";
+ if (action == GS_PLUGIN_ACTION_REMOVE)
+ return "gs_plugin_app_remove";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD)
+ return "gs_plugin_app_upgrade_download";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER)
+ return "gs_plugin_app_upgrade_trigger";
+ if (action == GS_PLUGIN_ACTION_LAUNCH)
+ return "gs_plugin_launch";
+ if (action == GS_PLUGIN_ACTION_UPDATE_CANCEL)
+ return "gs_plugin_update_cancel";
+ if (action == GS_PLUGIN_ACTION_UPDATE)
+ return "gs_plugin_update";
+ if (action == GS_PLUGIN_ACTION_DOWNLOAD)
+ return "gs_plugin_download";
+ if (action == GS_PLUGIN_ACTION_FILE_TO_APP)
+ return "gs_plugin_file_to_app";
+ if (action == GS_PLUGIN_ACTION_URL_TO_APP)
+ return "gs_plugin_url_to_app";
+ if (action == GS_PLUGIN_ACTION_GET_SOURCES)
+ return "gs_plugin_add_sources";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL)
+ return "gs_plugin_add_updates_historical";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES)
+ return "gs_plugin_add_updates";
+ if (action == GS_PLUGIN_ACTION_GET_LANGPACKS)
+ return "gs_plugin_add_langpacks";
+ return NULL;
+}
+
+/**
+ * gs_plugin_action_to_string:
+ * @action: a #GsPluginAction, e.g. %GS_PLUGIN_ACTION_INSTALL
+ *
+ * Converts the enumerated action to a string.
+ *
+ * Returns: a string, or %NULL for invalid
+ **/
+const gchar *
+gs_plugin_action_to_string (GsPluginAction action)
+{
+ if (action == GS_PLUGIN_ACTION_UNKNOWN)
+ return "unknown";
+ if (action == GS_PLUGIN_ACTION_INSTALL)
+ return "install";
+ if (action == GS_PLUGIN_ACTION_DOWNLOAD)
+ return "download";
+ if (action == GS_PLUGIN_ACTION_REMOVE)
+ return "remove";
+ if (action == GS_PLUGIN_ACTION_UPDATE)
+ return "update";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD)
+ return "upgrade-download";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER)
+ return "upgrade-trigger";
+ if (action == GS_PLUGIN_ACTION_LAUNCH)
+ return "launch";
+ if (action == GS_PLUGIN_ACTION_UPDATE_CANCEL)
+ return "update-cancel";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES)
+ return "get-updates";
+ if (action == GS_PLUGIN_ACTION_GET_SOURCES)
+ return "get-sources";
+ if (action == GS_PLUGIN_ACTION_FILE_TO_APP)
+ return "file-to-app";
+ if (action == GS_PLUGIN_ACTION_URL_TO_APP)
+ return "url-to-app";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL)
+ return "get-updates-historical";
+ if (action == GS_PLUGIN_ACTION_GET_LANGPACKS)
+ return "get-langpacks";
+ if (action == GS_PLUGIN_ACTION_INSTALL_REPO)
+ return "repo-install";
+ if (action == GS_PLUGIN_ACTION_REMOVE_REPO)
+ return "repo-remove";
+ if (action == GS_PLUGIN_ACTION_ENABLE_REPO)
+ return "repo-enable";
+ if (action == GS_PLUGIN_ACTION_DISABLE_REPO)
+ return "repo-disable";
+ return NULL;
+}
+
+/**
+ * gs_plugin_action_from_string:
+ * @action: a #GsPluginAction, e.g. "install"
+ *
+ * Converts the string to an enumerated action.
+ *
+ * Returns: a GsPluginAction, e.g. %GS_PLUGIN_ACTION_INSTALL
+ *
+ * Since: 3.26
+ **/
+GsPluginAction
+gs_plugin_action_from_string (const gchar *action)
+{
+ if (g_strcmp0 (action, "install") == 0)
+ return GS_PLUGIN_ACTION_INSTALL;
+ if (g_strcmp0 (action, "download") == 0)
+ return GS_PLUGIN_ACTION_DOWNLOAD;
+ if (g_strcmp0 (action, "remove") == 0)
+ return GS_PLUGIN_ACTION_REMOVE;
+ if (g_strcmp0 (action, "update") == 0)
+ return GS_PLUGIN_ACTION_UPDATE;
+ if (g_strcmp0 (action, "upgrade-download") == 0)
+ return GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD;
+ if (g_strcmp0 (action, "upgrade-trigger") == 0)
+ return GS_PLUGIN_ACTION_UPGRADE_TRIGGER;
+ if (g_strcmp0 (action, "launch") == 0)
+ return GS_PLUGIN_ACTION_LAUNCH;
+ if (g_strcmp0 (action, "update-cancel") == 0)
+ return GS_PLUGIN_ACTION_UPDATE_CANCEL;
+ if (g_strcmp0 (action, "get-updates") == 0)
+ return GS_PLUGIN_ACTION_GET_UPDATES;
+ if (g_strcmp0 (action, "get-sources") == 0)
+ return GS_PLUGIN_ACTION_GET_SOURCES;
+ if (g_strcmp0 (action, "file-to-app") == 0)
+ return GS_PLUGIN_ACTION_FILE_TO_APP;
+ if (g_strcmp0 (action, "url-to-app") == 0)
+ return GS_PLUGIN_ACTION_URL_TO_APP;
+ if (g_strcmp0 (action, "get-updates-historical") == 0)
+ return GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL;
+ if (g_strcmp0 (action, "get-langpacks") == 0)
+ return GS_PLUGIN_ACTION_GET_LANGPACKS;
+ if (g_strcmp0 (action, "repo-install") == 0)
+ return GS_PLUGIN_ACTION_INSTALL_REPO;
+ if (g_strcmp0 (action, "repo-remove") == 0)
+ return GS_PLUGIN_ACTION_REMOVE_REPO;
+ if (g_strcmp0 (action, "repo-enable") == 0)
+ return GS_PLUGIN_ACTION_ENABLE_REPO;
+ if (g_strcmp0 (action, "repo-disable") == 0)
+ return GS_PLUGIN_ACTION_DISABLE_REPO;
+ return GS_PLUGIN_ACTION_UNKNOWN;
+}
+
+/**
+ * gs_plugin_refine_flags_to_string:
+ * @refine_flags: some #GsPluginRefineFlags, e.g. %GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE
+ *
+ * Converts the flags to a string.
+ *
+ * Returns: a string
+ **/
+gchar *
+gs_plugin_refine_flags_to_string (GsPluginRefineFlags refine_flags)
+{
+ g_autoptr(GPtrArray) cstrs = g_ptr_array_new ();
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers"
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID)
+ g_ptr_array_add (cstrs, "require-id");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE)
+ g_ptr_array_add (cstrs, "require-license");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL)
+ g_ptr_array_add (cstrs, "require-url");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION)
+ g_ptr_array_add (cstrs, "require-description");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE)
+ g_ptr_array_add (cstrs, "require-size");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING)
+ g_ptr_array_add (cstrs, "require-rating");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION)
+ g_ptr_array_add (cstrs, "require-version");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY)
+ g_ptr_array_add (cstrs, "require-history");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION)
+ g_ptr_array_add (cstrs, "require-setup-action");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS)
+ g_ptr_array_add (cstrs, "require-update-details");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN)
+ g_ptr_array_add (cstrs, "require-origin");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED)
+ g_ptr_array_add (cstrs, "require-related");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS)
+ g_ptr_array_add (cstrs, "require-addons");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES)
+ g_ptr_array_add (cstrs, "require-allow-packages");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY)
+ g_ptr_array_add (cstrs, "require-update-severity");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED)
+ g_ptr_array_add (cstrs, "require-upgrade-removed");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE)
+ g_ptr_array_add (cstrs, "require-provenance");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS)
+ g_ptr_array_add (cstrs, "require-reviews");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS)
+ g_ptr_array_add (cstrs, "require-review-ratings");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON)
+ g_ptr_array_add (cstrs, "require-icon");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS)
+ g_ptr_array_add (cstrs, "require-permissions");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME)
+ g_ptr_array_add (cstrs, "require-origin-hostname");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI)
+ g_ptr_array_add (cstrs, "require-origin-ui");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME)
+ g_ptr_array_add (cstrs, "require-runtime");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS)
+ g_ptr_array_add (cstrs, "require-screenshots");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES)
+ g_ptr_array_add (cstrs, "require-categories");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP)
+ g_ptr_array_add (cstrs, "require-project-group");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME)
+ g_ptr_array_add (cstrs, "require-developer-name");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS)
+ g_ptr_array_add (cstrs, "require-kudos");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING)
+ g_ptr_array_add (cstrs, "content-rating");
+#pragma GCC diagnostic pop
+ if (cstrs->len == 0)
+ return g_strdup ("none");
+ g_ptr_array_add (cstrs, NULL);
+ return g_strjoinv (",", (gchar**) cstrs->pdata);
+}
+
+static void
+gs_plugin_constructed (GObject *object)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+
+ G_OBJECT_CLASS (gs_plugin_parent_class)->constructed (object);
+
+ /* Check all required properties have been set. */
+ g_assert (priv->session_bus_connection != NULL);
+ g_assert (priv->system_bus_connection != NULL);
+}
+
+static void
+gs_plugin_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+
+ switch ((GsPluginProperty) prop_id) {
+ case PROP_FLAGS:
+ priv->flags = g_value_get_flags (value);
+ g_object_notify_by_pspec (G_OBJECT (plugin), obj_props[PROP_FLAGS]);
+ break;
+ case PROP_SESSION_BUS_CONNECTION:
+ /* Construct only */
+ g_assert (priv->session_bus_connection == NULL);
+ priv->session_bus_connection = g_value_dup_object (value);
+ break;
+ case PROP_SYSTEM_BUS_CONNECTION:
+ /* Construct only */
+ g_assert (priv->system_bus_connection == NULL);
+ priv->system_bus_connection = g_value_dup_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+
+ switch ((GsPluginProperty) prop_id) {
+ case PROP_FLAGS:
+ g_value_set_flags (value, priv->flags);
+ break;
+ case PROP_SESSION_BUS_CONNECTION:
+ g_value_set_object (value, priv->session_bus_connection);
+ break;
+ case PROP_SYSTEM_BUS_CONNECTION:
+ g_value_set_object (value, priv->system_bus_connection);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_class_init (GsPluginClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = gs_plugin_constructed;
+ object_class->set_property = gs_plugin_set_property;
+ object_class->get_property = gs_plugin_get_property;
+ object_class->dispose = gs_plugin_dispose;
+ object_class->finalize = gs_plugin_finalize;
+
+ /**
+ * GsPlugin:flags:
+ *
+ * Flags which indicate various boolean properties of the plugin.
+ *
+ * These may change during the plugin’s lifetime.
+ */
+ obj_props[PROP_FLAGS] =
+ g_param_spec_flags ("flags", NULL, NULL,
+ GS_TYPE_PLUGIN_FLAGS, GS_PLUGIN_FLAGS_NONE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsPlugin:session-bus-connection: (not nullable)
+ *
+ * A connection to the D-Bus session bus.
+ *
+ * This must be set at construction time and will not be %NULL
+ * afterwards.
+ *
+ * Since: 43
+ */
+ obj_props[PROP_SESSION_BUS_CONNECTION] =
+ g_param_spec_object ("session-bus-connection", NULL, NULL,
+ G_TYPE_DBUS_CONNECTION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsPlugin:system-bus-connection: (not nullable)
+ *
+ * A connection to the D-Bus system bus.
+ *
+ * This must be set at construction time and will not be %NULL
+ * afterwards.
+ *
+ * Since: 43
+ */
+ obj_props[PROP_SYSTEM_BUS_CONNECTION] =
+ g_param_spec_object ("system-bus-connection", NULL, NULL,
+ G_TYPE_DBUS_CONNECTION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);
+
+ signals [SIGNAL_UPDATES_CHANGED] =
+ g_signal_new ("updates-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, updates_changed),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ signals [SIGNAL_STATUS_CHANGED] =
+ g_signal_new ("status-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, status_changed),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 2, GS_TYPE_APP, G_TYPE_UINT);
+
+ signals [SIGNAL_RELOAD] =
+ g_signal_new ("reload",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, reload),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ signals [SIGNAL_REPORT_EVENT] =
+ g_signal_new ("report-event",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, report_event),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, GS_TYPE_PLUGIN_EVENT);
+
+ signals [SIGNAL_ALLOW_UPDATES] =
+ g_signal_new ("allow-updates",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, allow_updates),
+ NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN,
+ G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+
+ signals [SIGNAL_BASIC_AUTH_START] =
+ g_signal_new ("basic-auth-start",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, basic_auth_start),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER);
+
+ signals [SIGNAL_REPOSITORY_CHANGED] =
+ g_signal_new ("repository-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, repository_changed),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, GS_TYPE_APP);
+
+ signals [SIGNAL_ASK_UNTRUSTED] =
+ g_signal_new ("ask-untrusted",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, ask_untrusted),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+}
+
+static void
+gs_plugin_init (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ guint i;
+
+ for (i = 0; i < GS_PLUGIN_RULE_LAST; i++)
+ priv->rules[i] = g_ptr_array_new_with_free_func (g_free);
+
+ priv->enabled = TRUE;
+ priv->scale = 1;
+ priv->cache = g_hash_table_new_full ((GHashFunc) as_utils_data_id_hash,
+ (GEqualFunc) as_utils_data_id_equal,
+ g_free,
+ (GDestroyNotify) g_object_unref);
+ priv->vfuncs = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, NULL);
+ g_mutex_init (&priv->cache_mutex);
+ g_mutex_init (&priv->interactive_mutex);
+ g_mutex_init (&priv->timer_mutex);
+ g_mutex_init (&priv->vfuncs_mutex);
+}
+
+/**
+ * gs_plugin_new:
+ * @session_bus_connection: (not nullable) (transfer none): a session bus
+ * connection to use
+ * @system_bus_connection: (not nullable) (transfer none): a system bus
+ * connection to use
+ *
+ * Creates a new plugin.
+ *
+ * Returns: a #GsPlugin
+ *
+ * Since: 43
+ **/
+GsPlugin *
+gs_plugin_new (GDBusConnection *session_bus_connection,
+ GDBusConnection *system_bus_connection)
+{
+ g_return_val_if_fail (G_IS_DBUS_CONNECTION (session_bus_connection), NULL);
+ g_return_val_if_fail (G_IS_DBUS_CONNECTION (system_bus_connection), NULL);
+
+ return g_object_new (GS_TYPE_PLUGIN,
+ "session-bus-connection", session_bus_connection,
+ "system-bus-connection", system_bus_connection,
+ NULL);
+}
+
+typedef struct {
+ GWeakRef plugin_weak; /* (owned) (element-type GsPlugin) */
+ GsApp *repository; /* (owned) */
+} GsPluginRepositoryChangedHelper;
+
+static void
+gs_plugin_repository_changed_helper_free (GsPluginRepositoryChangedHelper *helper)
+{
+ g_clear_object (&helper->repository);
+ g_weak_ref_clear (&helper->plugin_weak);
+ g_slice_free (GsPluginRepositoryChangedHelper, helper);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginRepositoryChangedHelper, gs_plugin_repository_changed_helper_free)
+
+static gboolean
+gs_plugin_repository_changed_cb (gpointer user_data)
+{
+ GsPluginRepositoryChangedHelper *helper = user_data;
+ g_autoptr(GsPlugin) plugin = NULL;
+
+ plugin = g_weak_ref_get (&helper->plugin_weak);
+ if (plugin != NULL)
+ g_signal_emit (plugin,
+ signals[SIGNAL_REPOSITORY_CHANGED], 0,
+ helper->repository);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * gs_plugin_repository_changed:
+ * @plugin: a #GsPlugin
+ * @repository: a #GsApp representing the repository
+ *
+ * Emit the "repository-changed" signal in the main thread.
+ *
+ * Since: 40
+ **/
+void
+gs_plugin_repository_changed (GsPlugin *plugin,
+ GsApp *repository)
+{
+ g_autoptr(GsPluginRepositoryChangedHelper) helper = NULL;
+ g_autoptr(GSource) idle_source = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (GS_IS_APP (repository));
+
+ helper = g_slice_new0 (GsPluginRepositoryChangedHelper);
+ g_weak_ref_init (&helper->plugin_weak, plugin);
+ helper->repository = g_object_ref (repository);
+
+ idle_source = g_idle_source_new ();
+ g_source_set_callback (idle_source, gs_plugin_repository_changed_cb, g_steal_pointer (&helper), (GDestroyNotify) gs_plugin_repository_changed_helper_free);
+ g_source_attach (idle_source, NULL);
+}
+
+/**
+ * gs_plugin_update_cache_state_for_repository:
+ * @plugin: a #GsPlugin
+ * @repository: a #GsApp representing a repository, which changed
+ *
+ * Update state of the all cached #GsApp instances related
+ * to the @repository.
+ *
+ * Since: 40
+ **/
+void
+gs_plugin_update_cache_state_for_repository (GsPlugin *plugin,
+ GsApp *repository)
+{
+ GsPluginPrivate *priv;
+ GHashTableIter iter;
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_autoptr(GsPlugin) repo_plugin = NULL;
+ gpointer value;
+ const gchar *repo_id;
+ GsAppState repo_state;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (GS_IS_APP (repository));
+
+ priv = gs_plugin_get_instance_private (plugin);
+ repo_id = gs_app_get_id (repository);
+ repo_state = gs_app_get_state (repository);
+ repo_plugin = gs_app_dup_management_plugin (repository);
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+
+ g_hash_table_iter_init (&iter, priv->cache);
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ GsApp *app = value;
+ GsAppState app_state = gs_app_get_state (app);
+ g_autoptr(GsPlugin) app_plugin = gs_app_dup_management_plugin (app);
+
+ if (app_plugin != repo_plugin ||
+ gs_app_get_scope (app) != gs_app_get_scope (repository) ||
+ gs_app_get_bundle_kind (app) != gs_app_get_bundle_kind (repository))
+ continue;
+
+ if (((app_state == GS_APP_STATE_AVAILABLE &&
+ repo_state != GS_APP_STATE_INSTALLED) ||
+ (app_state == GS_APP_STATE_UNAVAILABLE &&
+ repo_state == GS_APP_STATE_INSTALLED)) &&
+ g_strcmp0 (gs_app_get_origin (app), repo_id) == 0) {
+ /* First reset the state, because move from 'available' to 'unavailable' is not correct */
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ gs_app_set_state (app, repo_state == GS_APP_STATE_INSTALLED ? GS_APP_STATE_AVAILABLE : GS_APP_STATE_UNAVAILABLE);
+ }
+ }
+}
+
+/**
+ * gs_plugin_ask_untrusted:
+ * @plugin: a #GsPlugin
+ * @title: the title for the question
+ * @msg: the message for the question
+ * @details: (nullable): the detailed error message, or %NULL for none
+ * @accept_label: (nullable): a label of the 'accept' button, or %NULL to use 'Accept'
+ *
+ * Asks the user whether he/she accepts an untrusted package install/download/update,
+ * as described by @title and @msg, eventually with the @details.
+ *
+ * Note: This is a blocking call and can be called only from the main/GUI thread.
+ *
+ * Returns: whether the user accepted the question
+ *
+ * Since: 42
+ **/
+gboolean
+gs_plugin_ask_untrusted (GsPlugin *plugin,
+ const gchar *title,
+ const gchar *msg,
+ const gchar *details,
+ const gchar *accept_label)
+{
+ gboolean accepts = FALSE;
+ g_signal_emit (plugin,
+ signals[SIGNAL_ASK_UNTRUSTED], 0,
+ title,
+ msg,
+ details,
+ accept_label,
+ &accepts);
+ return accepts;
+}
+
+/**
+ * gs_plugin_get_session_bus_connection:
+ * @self: a #GsPlugin
+ *
+ * Get the D-Bus session bus connection in use by the plugin.
+ *
+ * Returns: (transfer none) (not nullable): a D-Bus connection
+ * Since: 43
+ */
+GDBusConnection *
+gs_plugin_get_session_bus_connection (GsPlugin *self)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (self);
+
+ g_return_val_if_fail (GS_IS_PLUGIN (self), NULL);
+
+ return priv->session_bus_connection;
+}
+
+/**
+ * gs_plugin_get_system_bus_connection:
+ * @self: a #GsPlugin
+ *
+ * Get the D-Bus system bus connection in use by the plugin.
+ *
+ * Returns: (transfer none) (not nullable): a D-Bus connection
+ * Since: 43
+ */
+GDBusConnection *
+gs_plugin_get_system_bus_connection (GsPlugin *self)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (self);
+
+ g_return_val_if_fail (GS_IS_PLUGIN (self), NULL);
+
+ return priv->system_bus_connection;
+}