diff options
Diffstat (limited to 'lib/gs-plugin.c')
-rw-r--r-- | lib/gs-plugin.c | 2069 |
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; +} |