summaryrefslogtreecommitdiffstats
path: root/lib/gs-plugin.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 15:18:46 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 15:18:46 +0000
commit56294d30a82ec2da6f9ce399740c1ef65a9ddef4 (patch)
treebbe3823e41495d026ba8edc6eeaef166edb7e2a2 /lib/gs-plugin.c
parentInitial commit. (diff)
downloadgnome-software-upstream.tar.xz
gnome-software-upstream.zip
Adding upstream version 3.38.1.upstream/3.38.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/gs-plugin.c')
-rw-r--r--lib/gs-plugin.c2069
1 files changed, 2069 insertions, 0 deletions
diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c
new file mode 100644
index 0000000..c517de0
--- /dev/null
+++ b/lib/gs-plugin.c
@@ -0,0 +1,2069 @@
+/* -*- 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>
+
+#ifdef USE_VALGRIND
+#include <valgrind.h>
+#endif
+
+#include "gs-app-list-private.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;
+ GsPluginData *data; /* for gs-plugin-{name}.c */
+ GsPluginFlags flags;
+ SoupSession *soup_session;
+ GPtrArray *rules[GS_PLUGIN_RULE_LAST];
+ GHashTable *vfuncs; /* string:pointer */
+ GMutex vfuncs_mutex;
+ gboolean enabled;
+ guint interactive_cnt;
+ GMutex interactive_mutex;
+ gchar *locale; /* allow-none */
+ gchar *language; /* allow-none */
+ gchar *name;
+ gchar *appstream_id;
+ guint scale;
+ guint order;
+ guint priority;
+ guint timer_id;
+ GMutex timer_mutex;
+ GNetworkMonitor *network_monitor;
+} GsPluginPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsPlugin, gs_plugin, G_TYPE_OBJECT)
+
+G_DEFINE_QUARK (gs-plugin-error-quark, gs_plugin_error)
+
+enum {
+ PROP_0,
+ PROP_FLAGS,
+ PROP_LAST
+};
+
+enum {
+ SIGNAL_UPDATES_CHANGED,
+ SIGNAL_STATUS_CHANGED,
+ SIGNAL_RELOAD,
+ SIGNAL_REPORT_EVENT,
+ SIGNAL_ALLOW_UPDATES,
+ SIGNAL_BASIC_AUTH_START,
+ 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
+ * @error: a #GError, or %NULL
+ *
+ * Creates a new plugin from an external module.
+ *
+ * Returns: the #GsPlugin or %NULL
+ *
+ * Since: 3.22
+ **/
+GsPlugin *
+gs_plugin_create (const gchar *filename, GError **error)
+{
+ GsPlugin *plugin = NULL;
+ GsPluginPrivate *priv;
+ g_autofree gchar *basename = NULL;
+
+ /* 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 */
+ plugin = gs_plugin_new ();
+ priv = gs_plugin_get_instance_private (plugin);
+ priv->module = g_module_open (filename, 0);
+ if (priv->module == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "failed to open plugin %s: %s",
+ filename, g_module_error ());
+ return NULL;
+ }
+ gs_plugin_set_name (plugin, basename + 13);
+ return plugin;
+}
+
+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->data);
+ g_free (priv->locale);
+ g_free (priv->language);
+ if (priv->soup_session != NULL)
+ g_object_unref (priv->soup_session);
+ 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);
+#ifndef RUNNING_ON_VALGRIND
+ if (priv->module != NULL)
+ g_module_close (priv->module);
+#endif
+
+ G_OBJECT_CLASS (gs_plugin_parent_class)->finalize (object);
+}
+
+/**
+ * gs_plugin_get_data:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the private data for the plugin if gs_plugin_alloc_data() has
+ * been called.
+ *
+ * Returns: the #GsPluginData, or %NULL
+ *
+ * Since: 3.22
+ **/
+GsPluginData *
+gs_plugin_get_data (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_assert (priv->data != NULL);
+ return priv->data;
+}
+
+/**
+ * gs_plugin_alloc_data:
+ * @plugin: a #GsPlugin
+ * @sz: the size of data to allocate, e.g. `sizeof(FooPluginPrivate)`
+ *
+ * Allocates a private data area for the plugin which can be retrieved
+ * using gs_plugin_get_data().
+ * This is normally called in gs_plugin_initialize() and the data should
+ * not be manually freed.
+ *
+ * Returns: the #GsPluginData, cleared to NUL bytes
+ *
+ * Since: 3.22
+ **/
+GsPluginData *
+gs_plugin_alloc_data (GsPlugin *plugin, gsize sz)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_assert (priv->data == NULL);
+ priv->data = g_malloc0 (sz);
+ return priv->data;
+}
+
+/**
+ * gs_plugin_clear_data:
+ * @plugin: a #GsPlugin
+ *
+ * Clears and resets the private data. Only run this from the self tests.
+ **/
+void
+gs_plugin_clear_data (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ if (priv->data == NULL)
+ return;
+ g_clear_pointer (&priv->data, g_free);
+}
+
+/**
+ * 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 gs_plugin_initialize().
+ *
+ * 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_locale:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the user locale. This is in the form documented in `man 3 setlocale`:
+ * ```
+ * language[_territory][.codeset][@modifier]
+ * ```
+ * where `language` is an
+ * [ISO 639 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes),
+ * `territory` is an
+ * [ISO 3166 country code](https://en.wikipedia.org/wiki/ISO_3166-1), and
+ * `codeset` is a character set or encoding identifier like `ISO-8859-1` or
+ * `UTF-8`. For a list of all supported locales, run `locale -a`.
+ *
+ * Returns: the locale string, e.g. `en_GB` or `uz_UZ.utf8@cyrillic`
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_get_locale (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->locale;
+}
+
+/**
+ * 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_locale:
+ * @plugin: a #GsPlugin
+ * @locale: a locale string, e.g. "en_GB"
+ *
+ * Sets the plugin locale.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_locale (GsPlugin *plugin, const gchar *locale)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_free (priv->locale);
+ priv->locale = g_strdup (locale);
+}
+
+/**
+ * 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_get_soup_session:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the soup session that this plugin can use when downloading.
+ *
+ * Returns: the #SoupSession
+ *
+ * Since: 3.22
+ **/
+SoupSession *
+gs_plugin_get_soup_session (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->soup_session;
+}
+
+/**
+ * gs_plugin_set_soup_session:
+ * @plugin: a #GsPlugin
+ * @soup_session: a #SoupSession
+ *
+ * Sets the soup session that this plugin will use when downloading.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_soup_session (GsPlugin *plugin, SoupSession *soup_session)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_set_object (&priv->soup_session, soup_session);
+}
+
+/**
+ * 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_RUNNING_SELF
+ *
+ * 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_RUNNING_SELF
+ *
+ * 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;
+}
+
+/**
+ * gs_plugin_remove_flags:
+ * @plugin: a #GsPlugin
+ * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_RUNNING_SELF
+ *
+ * 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;
+}
+
+/**
+ * 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 {
+ GsPlugin *plugin;
+ GsApp *app;
+ GsPluginStatus status;
+ guint percentage;
+} GsPluginStatusHelper;
+
+static gboolean
+gs_plugin_status_update_cb (gpointer user_data)
+{
+ GsPluginStatusHelper *helper = (GsPluginStatusHelper *) user_data;
+ g_signal_emit (helper->plugin,
+ signals[SIGNAL_STATUS_CHANGED], 0,
+ helper->app,
+ helper->status);
+ if (helper->app != NULL)
+ g_object_unref (helper->app);
+ g_slice_free (GsPluginStatusHelper, helper);
+ return FALSE;
+}
+
+/**
+ * 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)
+{
+ GsPluginStatusHelper *helper;
+ g_autoptr(GSource) idle_source = NULL;
+
+ helper = g_slice_new0 (GsPluginStatusHelper);
+ helper->plugin = 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, helper, NULL);
+ 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 FALSE;
+}
+
+/**
+ * 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 gboolean
+gs_plugin_updates_changed_cb (gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (user_data);
+ g_signal_emit (plugin, signals[SIGNAL_UPDATES_CHANGED], 0);
+ return FALSE;
+}
+
+/**
+ * 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 (gs_plugin_updates_changed_cb, plugin);
+}
+
+static gboolean
+gs_plugin_reload_cb (gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (user_data);
+ g_signal_emit (plugin, signals[SIGNAL_RELOAD], 0);
+ return FALSE;
+}
+
+/**
+ * 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 ::reload in idle");
+ g_idle_add (gs_plugin_reload_cb, plugin);
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ GsApp *app;
+ GCancellable *cancellable;
+} GsPluginDownloadHelper;
+
+static void
+gs_plugin_download_chunk_cb (SoupMessage *msg, SoupBuffer *chunk,
+ GsPluginDownloadHelper *helper)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (helper->plugin);
+ guint percentage;
+ goffset header_size;
+ goffset body_length;
+
+ /* cancelled? */
+ if (g_cancellable_is_cancelled (helper->cancellable)) {
+ g_debug ("cancelling download of %s",
+ gs_app_get_id (helper->app));
+ soup_session_cancel_message (priv->soup_session,
+ msg,
+ SOUP_STATUS_CANCELLED);
+ return;
+ }
+
+ /* if it's returning "Found" or an error, ignore the percentage */
+ if (msg->status_code != SOUP_STATUS_OK) {
+ g_debug ("ignoring status code %u (%s)",
+ msg->status_code, msg->reason_phrase);
+ return;
+ }
+
+ /* get data */
+ body_length = msg->response_body->length;
+ header_size = soup_message_headers_get_content_length (msg->response_headers);
+
+ /* size is not known */
+ if (header_size < body_length)
+ return;
+
+ /* calculate percentage */
+ percentage = (guint) ((100 * body_length) / header_size);
+ 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);
+}
+
+/**
+ * gs_plugin_download_data:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @uri: a remote URI
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads data.
+ *
+ * Returns: the downloaded data, or %NULL
+ *
+ * Since: 3.22
+ **/
+GBytes *
+gs_plugin_download_data (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ GsPluginDownloadHelper helper;
+ guint status_code;
+ g_autoptr(SoupMessage) msg = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL);
+ g_return_val_if_fail (uri != NULL, NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ /* local */
+ if (g_str_has_prefix (uri, "file://")) {
+ gsize length = 0;
+ g_autofree gchar *contents = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_debug ("copying %s from plugin %s", uri, priv->name);
+ if (!g_file_get_contents (uri + 7, &contents, &length, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to copy %s: %s",
+ uri, error_local->message);
+ return NULL;
+ }
+ return g_bytes_new (contents, length);
+ }
+
+ /* remote */
+ g_debug ("downloading %s from plugin %s", uri, priv->name);
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ if (app != NULL) {
+ helper.plugin = plugin;
+ helper.app = app;
+ helper.cancellable = cancellable;
+ g_signal_connect (msg, "got-chunk",
+ G_CALLBACK (gs_plugin_download_chunk_cb),
+ &helper);
+ }
+ status_code = soup_session_send_message (priv->soup_session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_string_append (str, soup_status_get_phrase (status_code));
+ if (msg->response_body->data != NULL) {
+ g_string_append (str, ": ");
+ g_string_append (str, msg->response_body->data);
+ }
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to download %s: %s",
+ uri, str->str);
+ return NULL;
+ }
+ return g_bytes_new (msg->response_body->data,
+ (gsize) msg->response_body->length);
+}
+
+/**
+ * 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)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ GsPluginDownloadHelper helper;
+ guint status_code;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(SoupMessage) msg = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+ g_return_val_if_fail (filename != NULL, FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ /* local */
+ if (g_str_has_prefix (uri, "file://")) {
+ gsize length = 0;
+ g_autofree gchar *contents = NULL;
+ g_debug ("copying %s from plugin %s", uri, priv->name);
+ if (!g_file_get_contents (uri + 7, &contents, &length, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to copy %s: %s",
+ uri, error_local->message);
+ return FALSE;
+ }
+ if (!g_file_set_contents (filename, contents, length, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_WRITE_FAILED,
+ "Failed to save file: %s",
+ error_local->message);
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /* remote */
+ g_debug ("downloading %s to %s from plugin %s", uri, filename, priv->name);
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ if (msg == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to parse URI %s", uri);
+ return FALSE;
+ }
+ if (app != NULL) {
+ helper.plugin = plugin;
+ helper.app = app;
+ helper.cancellable = cancellable;
+ g_signal_connect (msg, "got-chunk",
+ G_CALLBACK (gs_plugin_download_chunk_cb),
+ &helper);
+ }
+ status_code = soup_session_send_message (priv->soup_session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_string_append (str, soup_status_get_phrase (status_code));
+ if (msg->response_body->data != NULL) {
+ g_string_append (str, ": ");
+ g_string_append (str, msg->response_body->data);
+ }
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to download %s: %s",
+ uri, str->str);
+ return FALSE;
+ }
+ if (!gs_mkdir_parent (filename, error))
+ return FALSE;
+ if (!g_file_set_contents (filename,
+ msg->response_body->data,
+ msg->response_body->length,
+ &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_WRITE_FAILED,
+ "Failed to save file: %s",
+ error_local->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,
+ 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_utils_string_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, "'%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_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_ERROR_NO_NETWORK
+ *
+ * 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_REFRESH)
+ return "gs_plugin_refresh";
+ if (action == GS_PLUGIN_ACTION_REVIEW_SUBMIT)
+ return "gs_plugin_review_submit";
+ if (action == GS_PLUGIN_ACTION_REVIEW_UPVOTE)
+ return "gs_plugin_review_upvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DOWNVOTE)
+ return "gs_plugin_review_downvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REPORT)
+ return "gs_plugin_review_report";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REMOVE)
+ return "gs_plugin_review_remove";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DISMISS)
+ return "gs_plugin_review_dismiss";
+ 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_SET_RATING)
+ return "gs_plugin_app_set_rating";
+ 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_ADD_SHORTCUT)
+ return "gs_plugin_add_shortcut";
+ if (action == GS_PLUGIN_ACTION_REMOVE_SHORTCUT)
+ return "gs_plugin_remove_shortcut";
+ if (action == GS_PLUGIN_ACTION_REFINE)
+ return "gs_plugin_refine";
+ 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_DISTRO_UPDATES)
+ return "gs_plugin_add_distro_upgrades";
+ if (action == GS_PLUGIN_ACTION_GET_SOURCES)
+ return "gs_plugin_add_sources";
+ if (action == GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS)
+ return "gs_plugin_add_unvoted_reviews";
+ if (action == GS_PLUGIN_ACTION_GET_INSTALLED)
+ return "gs_plugin_add_installed";
+ if (action == GS_PLUGIN_ACTION_GET_FEATURED)
+ return "gs_plugin_add_featured";
+ 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_POPULAR)
+ return "gs_plugin_add_popular";
+ if (action == GS_PLUGIN_ACTION_GET_RECENT)
+ return "gs_plugin_add_recent";
+ if (action == GS_PLUGIN_ACTION_SEARCH)
+ return "gs_plugin_add_search";
+ if (action == GS_PLUGIN_ACTION_SEARCH_FILES)
+ return "gs_plugin_add_search_files";
+ if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES)
+ return "gs_plugin_add_search_what_provides";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS)
+ return "gs_plugin_add_category_apps";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORIES)
+ return "gs_plugin_add_categories";
+ if (action == GS_PLUGIN_ACTION_SETUP)
+ return "gs_plugin_setup";
+ if (action == GS_PLUGIN_ACTION_INITIALIZE)
+ return "gs_plugin_initialize";
+ if (action == GS_PLUGIN_ACTION_DESTROY)
+ return "gs_plugin_destroy";
+ if (action == GS_PLUGIN_ACTION_GET_ALTERNATES)
+ return "gs_plugin_add_alternates";
+ 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_ERROR_NO_NETWORK
+ *
+ * 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_SETUP)
+ return "setup";
+ 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_SET_RATING)
+ return "set-rating";
+ 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_ADD_SHORTCUT)
+ return "add-shortcut";
+ if (action == GS_PLUGIN_ACTION_REMOVE_SHORTCUT)
+ return "remove-shortcut";
+ if (action == GS_PLUGIN_ACTION_REVIEW_SUBMIT)
+ return "review-submit";
+ if (action == GS_PLUGIN_ACTION_REVIEW_UPVOTE)
+ return "review-upvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DOWNVOTE)
+ return "review-downvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REPORT)
+ return "review-report";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REMOVE)
+ return "review-remove";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DISMISS)
+ return "review-dismiss";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES)
+ return "get-updates";
+ if (action == GS_PLUGIN_ACTION_GET_DISTRO_UPDATES)
+ return "get-distro-updates";
+ if (action == GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS)
+ return "get-unvoted-reviews";
+ if (action == GS_PLUGIN_ACTION_GET_SOURCES)
+ return "get-sources";
+ if (action == GS_PLUGIN_ACTION_GET_INSTALLED)
+ return "get-installed";
+ if (action == GS_PLUGIN_ACTION_GET_POPULAR)
+ return "get-popular";
+ if (action == GS_PLUGIN_ACTION_GET_FEATURED)
+ return "get-featured";
+ if (action == GS_PLUGIN_ACTION_SEARCH)
+ return "search";
+ if (action == GS_PLUGIN_ACTION_SEARCH_FILES)
+ return "search-files";
+ if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES)
+ return "search-provides";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORIES)
+ return "get-categories";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS)
+ return "get-category-apps";
+ if (action == GS_PLUGIN_ACTION_REFINE)
+ return "refine";
+ if (action == GS_PLUGIN_ACTION_REFRESH)
+ return "refresh";
+ 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_RECENT)
+ return "get-recent";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL)
+ return "get-updates-historical";
+ if (action == GS_PLUGIN_ACTION_INITIALIZE)
+ return "initialize";
+ if (action == GS_PLUGIN_ACTION_DESTROY)
+ return "destroy";
+ if (action == GS_PLUGIN_ACTION_GET_ALTERNATES)
+ return "get-alternates";
+ if (action == GS_PLUGIN_ACTION_GET_LANGPACKS)
+ return "get-langpacks";
+ 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, "setup") == 0)
+ return GS_PLUGIN_ACTION_SETUP;
+ 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, "set-rating") == 0)
+ return GS_PLUGIN_ACTION_SET_RATING;
+ 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, "add-shortcut") == 0)
+ return GS_PLUGIN_ACTION_ADD_SHORTCUT;
+ if (g_strcmp0 (action, "remove-shortcut") == 0)
+ return GS_PLUGIN_ACTION_REMOVE_SHORTCUT;
+ if (g_strcmp0 (action, "review-submit") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_SUBMIT;
+ if (g_strcmp0 (action, "review-upvote") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_UPVOTE;
+ if (g_strcmp0 (action, "review-downvote") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_DOWNVOTE;
+ if (g_strcmp0 (action, "review-report") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_REPORT;
+ if (g_strcmp0 (action, "review-remove") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_REMOVE;
+ if (g_strcmp0 (action, "review-dismiss") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_DISMISS;
+ if (g_strcmp0 (action, "get-updates") == 0)
+ return GS_PLUGIN_ACTION_GET_UPDATES;
+ if (g_strcmp0 (action, "get-distro-updates") == 0)
+ return GS_PLUGIN_ACTION_GET_DISTRO_UPDATES;
+ if (g_strcmp0 (action, "get-unvoted-reviews") == 0)
+ return GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS;
+ if (g_strcmp0 (action, "get-sources") == 0)
+ return GS_PLUGIN_ACTION_GET_SOURCES;
+ if (g_strcmp0 (action, "get-installed") == 0)
+ return GS_PLUGIN_ACTION_GET_INSTALLED;
+ if (g_strcmp0 (action, "get-popular") == 0)
+ return GS_PLUGIN_ACTION_GET_POPULAR;
+ if (g_strcmp0 (action, "get-featured") == 0)
+ return GS_PLUGIN_ACTION_GET_FEATURED;
+ if (g_strcmp0 (action, "search") == 0)
+ return GS_PLUGIN_ACTION_SEARCH;
+ if (g_strcmp0 (action, "search-files") == 0)
+ return GS_PLUGIN_ACTION_SEARCH_FILES;
+ if (g_strcmp0 (action, "search-provides") == 0)
+ return GS_PLUGIN_ACTION_SEARCH_PROVIDES;
+ if (g_strcmp0 (action, "get-categories") == 0)
+ return GS_PLUGIN_ACTION_GET_CATEGORIES;
+ if (g_strcmp0 (action, "get-category-apps") == 0)
+ return GS_PLUGIN_ACTION_GET_CATEGORY_APPS;
+ if (g_strcmp0 (action, "refine") == 0)
+ return GS_PLUGIN_ACTION_REFINE;
+ if (g_strcmp0 (action, "refresh") == 0)
+ return GS_PLUGIN_ACTION_REFRESH;
+ 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-recent") == 0)
+ return GS_PLUGIN_ACTION_GET_RECENT;
+ if (g_strcmp0 (action, "get-updates-historical") == 0)
+ return GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL;
+ if (g_strcmp0 (action, "initialize") == 0)
+ return GS_PLUGIN_ACTION_INITIALIZE;
+ if (g_strcmp0 (action, "destroy") == 0)
+ return GS_PLUGIN_ACTION_DESTROY;
+ if (g_strcmp0 (action, "get-alternates") == 0)
+ return GS_PLUGIN_ACTION_GET_ALTERNATES;
+ if (g_strcmp0 (action, "get-langpacks") == 0)
+ return GS_PLUGIN_ACTION_GET_LANGPACKS;
+ 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 ();
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_USE_HISTORY)
+ g_ptr_array_add (cstrs, "use-history");
+ 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_MENU_PATH)
+ g_ptr_array_add (cstrs, "require-menu-path");
+ 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_KEY_COLORS)
+ g_ptr_array_add (cstrs, "require-key-colors");
+ 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");
+ if (cstrs->len == 0)
+ return g_strdup ("none");
+ g_ptr_array_add (cstrs, NULL);
+ return g_strjoinv (",", (gchar**) cstrs->pdata);
+}
+
+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 (prop_id) {
+ case PROP_FLAGS:
+ priv->flags = g_value_get_uint64 (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 (prop_id) {
+ case PROP_FLAGS:
+ g_value_set_uint64 (value, priv->flags);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_class_init (GsPluginClass *klass)
+{
+ GParamSpec *pspec;
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = gs_plugin_set_property;
+ object_class->get_property = gs_plugin_get_property;
+ object_class->finalize = gs_plugin_finalize;
+
+ pspec = g_param_spec_uint64 ("flags", NULL, NULL,
+ 0, G_MAXUINT64, 0, G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_FLAGS, pspec);
+
+ 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);
+}
+
+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_unique_id_hash,
+ (GEqualFunc) as_utils_unique_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:
+ *
+ * Creates a new plugin.
+ *
+ * Returns: a #GsPlugin
+ *
+ * Since: 3.22
+ **/
+GsPlugin *
+gs_plugin_new (void)
+{
+ GsPlugin *plugin;
+ plugin = g_object_new (GS_TYPE_PLUGIN, NULL);
+ return plugin;
+}