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