diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:57:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:57:27 +0000 |
commit | 6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 (patch) | |
tree | d423850ae901365e582137bdf2b5cbdffd7ca266 /lib | |
parent | Initial commit. (diff) | |
download | gnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.tar.xz gnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.zip |
Adding upstream version 43.5.upstream/43.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
85 files changed, 37898 insertions, 0 deletions
diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..628c58e --- /dev/null +++ b/lib/README.md @@ -0,0 +1,7 @@ +libgnomesoftware +================ + +This is a static library, and is not all API stable. + +Only the plugin headers installed into /usr/include/gnome-software should be +considered API. diff --git a/lib/gnome-software-private.h b/lib/gnome-software-private.h new file mode 100644 index 0000000..1e3addf --- /dev/null +++ b/lib/gnome-software-private.h @@ -0,0 +1,24 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#ifndef I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#define I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#endif + +#include <gnome-software.h> + +#include <gs-app-list-private.h> +#include <gs-app-private.h> +#include <gs-category-private.h> +#include <gs-fedora-third-party.h> +#include <gs-os-release.h> +#include <gs-plugin-loader.h> +#include <gs-plugin-loader-sync.h> +#include <gs-plugin-private.h> diff --git a/lib/gnome-software.h b/lib/gnome-software.h new file mode 100644 index 0000000..b57e257 --- /dev/null +++ b/lib/gnome-software.h @@ -0,0 +1,41 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#ifndef I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#error You have to define I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#endif + +#include <gs-app.h> +#include <gs-app-list.h> +#include <gs-app-collation.h> +#include <gs-app-permissions.h> +#include <gs-app-query.h> +#include <gs-category.h> +#include <gs-category-manager.h> +#include <gs-desktop-data.h> +#include <gs-download-utils.h> +#include <gs-enums.h> +#include <gs-icon.h> +#include <gs-metered.h> +#include <gs-odrs-provider.h> +#include <gs-os-release.h> +#include <gs-plugin.h> +#include <gs-plugin-helpers.h> +#include <gs-plugin-job.h> +#include <gs-plugin-job-list-apps.h> +#include <gs-plugin-job-list-categories.h> +#include <gs-plugin-job-list-distro-upgrades.h> +#include <gs-plugin-job-manage-repository.h> +#include <gs-plugin-job-refine.h> +#include <gs-plugin-job-refresh-metadata.h> +#include <gs-plugin-vfuncs.h> +#include <gs-remote-icon.h> +#include <gs-utils.h> +#include <gs-worker-thread.h> diff --git a/lib/gs-app-collation.h b/lib/gs-app-collation.h new file mode 100644 index 0000000..876ae54 --- /dev/null +++ b/lib/gs-app-collation.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app-list.h" + +G_BEGIN_DECLS + +GsAppList *gs_app_get_related (GsApp *app); +GsAppList *gs_app_dup_addons (GsApp *app); +GsAppList *gs_app_get_history (GsApp *app); + +G_END_DECLS diff --git a/lib/gs-app-list-private.h b/lib/gs-app-list-private.h new file mode 100644 index 0000000..5335173 --- /dev/null +++ b/lib/gs-app-list-private.h @@ -0,0 +1,51 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app-list.h" + +G_BEGIN_DECLS + +/** + * GsAppListFlags: + * @GS_APP_LIST_FLAG_NONE: No flags set + * @GS_APP_LIST_FLAG_IS_TRUNCATED: List has been truncated + * @GS_APP_LIST_FLAG_WATCH_APPS: Applications will be monitored + * @GS_APP_LIST_FLAG_WATCH_APPS_RELATED: Applications related apps will be monitored + * @GS_APP_LIST_FLAG_WATCH_APPS_ADDONS: Applications addon apps will be monitored + * + * Flags used to describe the list. + **/ +typedef enum { + GS_APP_LIST_FLAG_NONE = 0, + /* empty slot */ + GS_APP_LIST_FLAG_IS_TRUNCATED = 1 << 1, + GS_APP_LIST_FLAG_WATCH_APPS = 1 << 2, + GS_APP_LIST_FLAG_WATCH_APPS_RELATED = 1 << 3, + GS_APP_LIST_FLAG_WATCH_APPS_ADDONS = 1 << 4, + GS_APP_LIST_FLAG_LAST /*< skip >*/ +} GsAppListFlags; + +guint gs_app_list_get_size_peak (GsAppList *list); +void gs_app_list_set_size_peak (GsAppList *list, + guint size_peak); +void gs_app_list_filter_duplicates (GsAppList *list, + GsAppListFilterFlags flags); +void gs_app_list_randomize (GsAppList *list); +void gs_app_list_remove_all (GsAppList *list); +void gs_app_list_truncate (GsAppList *list, + guint length); +gboolean gs_app_list_has_flag (GsAppList *list, + GsAppListFlags flag); +void gs_app_list_add_flag (GsAppList *list, + GsAppListFlags flag); +GsAppState gs_app_list_get_state (GsAppList *list); +guint gs_app_list_get_progress (GsAppList *list); + +G_END_DECLS diff --git a/lib/gs-app-list.c b/lib/gs-app-list.c new file mode 100644 index 0000000..a5f6785 --- /dev/null +++ b/lib/gs-app-list.c @@ -0,0 +1,1025 @@ +/* -*- 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) 2017-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-app-list + * @title: GsAppList + * @include: gnome-software.h + * @stability: Unstable + * @short_description: An application list + * + * These functions provide a refcounted list of #GsApp objects. + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-app-private.h" +#include "gs-app-list-private.h" +#include "gs-app-collation.h" +#include "gs-enums.h" + +struct _GsAppList +{ + GObject parent_instance; + GPtrArray *array; + GMutex mutex; + guint size_peak; + GsAppListFlags flags; + GsAppState state; + guint progress; /* 0–100 inclusive, or %GS_APP_PROGRESS_UNKNOWN */ + guint custom_progress; /* overrides the 'progress', if not %GS_APP_PROGRESS_UNKNOWN */ +}; + +G_DEFINE_TYPE (GsAppList, gs_app_list, G_TYPE_OBJECT) + +enum { + PROP_STATE = 1, + PROP_PROGRESS, + PROP_LAST +}; + +enum { + SIGNAL_APP_STATE_CHANGED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +/** + * gs_app_list_get_state: + * @list: A #GsAppList + * + * Gets the state of the list. + * + * This method will only return a valid result if gs_app_list_add_flag() has + * been called with %GS_APP_LIST_FLAG_WATCH_APPS. + * + * Returns: the #GsAppState, e.g. %GS_APP_STATE_INSTALLED + * + * Since: 3.30 + **/ +GsAppState +gs_app_list_get_state (GsAppList *list) +{ + g_return_val_if_fail (GS_IS_APP_LIST (list), GS_APP_STATE_UNKNOWN); + return list->state; +} + +/** + * gs_app_list_get_progress: + * @list: A #GsAppList + * + * Gets the average percentage completion of all apps in the list. If any of the + * apps in the list has progress %GS_APP_PROGRESS_UNKNOWN, or if the app list + * is empty, %GS_APP_PROGRESS_UNKNOWN will be returned. + * + * This method will only return a valid result if gs_app_list_add_flag() has + * been called with %GS_APP_LIST_FLAG_WATCH_APPS. + * + * Returns: the percentage completion (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN for unknown + * + * Since: 3.30 + **/ +guint +gs_app_list_get_progress (GsAppList *list) +{ + g_return_val_if_fail (GS_IS_APP_LIST (list), GS_APP_PROGRESS_UNKNOWN); + if (list->custom_progress != GS_APP_PROGRESS_UNKNOWN) + return list->custom_progress; + return list->progress; +} + +static gboolean +app_list_notify_progress_idle_cb (gpointer user_data) +{ + GsAppList *list = user_data; + + g_object_notify (G_OBJECT (list), "progress"); + g_object_unref (list); + + return G_SOURCE_REMOVE; +} + +/** + * gs_app_list_override_progress: + * @list: a #GsAppList + * @progress: a progress to set, between 0 and 100 inclusive, or %GS_APP_PROGRESS_UNKNOWN + * + * Override the progress property to be this value, or use %GS_APP_PROGRESS_UNKNOWN, + * to unset the override. This can be used when only the overall progress is known, + * instead of a per-application progress. + * + * Since: 42 + **/ +void +gs_app_list_override_progress (GsAppList *list, + guint progress) +{ + g_return_if_fail (GS_IS_APP_LIST (list)); + + if (list->custom_progress != progress) { + list->custom_progress = progress; + g_idle_add (app_list_notify_progress_idle_cb, g_object_ref (list)); + } +} + +static void +gs_app_list_add_watched_for_app (GsAppList *list, GPtrArray *apps, GsApp *app) +{ + if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS) + g_ptr_array_add (apps, app); + if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_ADDONS) { + g_autoptr(GsAppList) list2 = gs_app_dup_addons (app); + + for (guint i = 0; list2 != NULL && i < gs_app_list_length (list2); i++) { + GsApp *app2 = gs_app_list_index (list2, i); + g_ptr_array_add (apps, app2); + } + } + if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_RELATED) { + GsAppList *list2 = gs_app_get_related (app); + for (guint i = 0; i < gs_app_list_length (list2); i++) { + GsApp *app2 = gs_app_list_index (list2, i); + g_ptr_array_add (apps, app2); + } + } +} + +static GPtrArray * +gs_app_list_get_watched_for_app (GsAppList *list, GsApp *app) +{ + GPtrArray *apps = g_ptr_array_new (); + gs_app_list_add_watched_for_app (list, apps, app); + return apps; +} + +static GPtrArray * +gs_app_list_get_watched (GsAppList *list) +{ + GPtrArray *apps = g_ptr_array_new (); + for (guint i = 0; i < list->array->len; i++) { + GsApp *app_tmp = g_ptr_array_index (list->array, i); + gs_app_list_add_watched_for_app (list, apps, app_tmp); + } + return apps; +} + +static void +gs_app_list_invalidate_progress (GsAppList *self) +{ + guint progress = 0; + g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self); + + /* find the average percentage complete of the list */ + if (apps->len > 0) { + guint64 pc_cnt = 0; + gboolean unknown_seen = FALSE; + + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + guint app_progress = gs_app_get_progress (app_tmp); + + if (app_progress == GS_APP_PROGRESS_UNKNOWN) { + unknown_seen = TRUE; + break; + } + pc_cnt += gs_app_get_progress (app_tmp); + } + + progress = (!unknown_seen) ? pc_cnt / apps->len : GS_APP_PROGRESS_UNKNOWN; + } else { + progress = GS_APP_PROGRESS_UNKNOWN; + } + + if (self->progress != progress) { + self->progress = progress; + g_object_notify (G_OBJECT (self), "progress"); + } +} + +static void +gs_app_list_invalidate_state (GsAppList *self) +{ + GsAppState state = GS_APP_STATE_UNKNOWN; + g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self); + + /* find any action state of the list */ + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + GsAppState state_tmp = gs_app_get_state (app_tmp); + if (state_tmp == GS_APP_STATE_INSTALLING || + state_tmp == GS_APP_STATE_REMOVING) { + state = state_tmp; + break; + } + } + if (self->state != state) { + self->state = state; + g_object_notify (G_OBJECT (self), "state"); + } +} + +static void +gs_app_list_progress_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self) +{ + gs_app_list_invalidate_progress (self); +} + +static void +gs_app_list_state_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self) +{ + gs_app_list_invalidate_state (self); + + g_signal_emit (self, signals[SIGNAL_APP_STATE_CHANGED], 0, app); +} + +static void +gs_app_list_maybe_watch_app (GsAppList *list, GsApp *app) +{ + g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app); + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + g_signal_connect_object (app_tmp, "notify::progress", + G_CALLBACK (gs_app_list_progress_notify_cb), + list, 0); + g_signal_connect_object (app_tmp, "notify::state", + G_CALLBACK (gs_app_list_state_notify_cb), + list, 0); + } +} + +static void +gs_app_list_maybe_unwatch_app (GsAppList *list, GsApp *app) +{ + g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app); + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + g_signal_handlers_disconnect_by_data (app_tmp, list); + } +} + +/** + * gs_app_list_get_size_peak: + * @list: A #GsAppList + * + * Returns the largest size the list has ever been. + * + * Returns: integer + * + * Since: 3.24 + **/ +guint +gs_app_list_get_size_peak (GsAppList *list) +{ + return list->size_peak; +} + +/** + * gs_app_list_set_size_peak: + * @list: A #GsAppList + * @size_peak: A value to set + * + * Sets the largest size the list has ever been. + * + * Since: 43 + **/ +void +gs_app_list_set_size_peak (GsAppList *list, + guint size_peak) +{ + g_return_if_fail (GS_IS_APP_LIST (list)); + list->size_peak = size_peak; +} + +static GsApp * +gs_app_list_lookup_safe (GsAppList *list, const gchar *unique_id) +{ + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = g_ptr_array_index (list->array, i); + if (as_utils_data_id_equal (gs_app_get_unique_id (app), unique_id)) + return app; + } + return NULL; +} + +/** + * gs_app_list_lookup: + * @list: A #GsAppList + * @unique_id: A unique_id + * + * Finds the first matching application in the list using the usual wildcard + * rules allowed in unique_ids. + * + * Returns: (transfer none): a #GsApp, or %NULL if not found + * + * Since: 3.22 + **/ +GsApp * +gs_app_list_lookup (GsAppList *list, const gchar *unique_id) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&list->mutex); + return gs_app_list_lookup_safe (list, unique_id); +} + +/** + * gs_app_list_has_flag: + * @list: A #GsAppList + * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED + * + * Gets if a specific flag is set. + * + * Returns: %TRUE if the flag is set + * + * Since: 3.24 + **/ +gboolean +gs_app_list_has_flag (GsAppList *list, GsAppListFlags flag) +{ + return (list->flags & flag) > 0; +} + +/** + * gs_app_list_add_flag: + * @list: A #GsAppList + * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED + * + * Gets if a specific flag is set. + * + * Returns: %TRUE if the flag is set + * + * Since: 3.30 + **/ +void +gs_app_list_add_flag (GsAppList *list, GsAppListFlags flag) +{ + if (list->flags & flag) + return; + list->flags |= flag; + + /* turn this on for existing apps */ + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = g_ptr_array_index (list->array, i); + gs_app_list_maybe_watch_app (list, app); + } +} + +static gboolean +gs_app_list_check_for_duplicate (GsAppList *list, GsApp *app) +{ + GsApp *app_old; + const gchar *id; + + /* adding a wildcard */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + for (guint i = 0; i < list->array->len; i++) { + GsApp *app_tmp = g_ptr_array_index (list->array, i); + if (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD)) + continue; + /* not adding exactly the same wildcard */ + if (g_strcmp0 (gs_app_get_unique_id (app_tmp), + gs_app_get_unique_id (app)) == 0) + return FALSE; + } + return TRUE; + } + + for (guint i = 0; i < list->array->len; i++) { + GsApp *app_tmp = g_ptr_array_index (list->array, i); + if (app_tmp == app) + return FALSE; + } + + /* does not exist */ + id = gs_app_get_unique_id (app); + if (id == NULL) { + /* not much else we can do... */ + return TRUE; + } + + /* existing app is a wildcard */ + app_old = gs_app_list_lookup_safe (list, id); + if (app_old == NULL) + return TRUE; + if (gs_app_has_quirk (app_old, GS_APP_QUIRK_IS_WILDCARD)) + return TRUE; + + /* already exists */ + return FALSE; +} + +typedef enum { + GS_APP_LIST_ADD_FLAG_NONE = 0, + GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE = 1 << 0, + GS_APP_LIST_ADD_FLAG_LAST +} GsAppListAddFlag; + +static void +gs_app_list_add_safe (GsAppList *list, GsApp *app, GsAppListAddFlag flag) +{ + /* check for duplicate */ + if ((flag & GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE) > 0 && + !gs_app_list_check_for_duplicate (list, app)) + return; + + /* just use the ref */ + gs_app_list_maybe_watch_app (list, app); + g_ptr_array_add (list->array, g_object_ref (app)); + + /* update the historical max */ + if (list->array->len > list->size_peak) + list->size_peak = list->array->len; +} + +/** + * gs_app_list_add: + * @list: A #GsAppList + * @app: A #GsApp + * + * If the application does not already exist in the list then it is added, + * incrementing the reference count. + * If the application already exists then a warning is printed to the console. + * + * Applications that have the application ID lazy-loaded will always be added + * to the list, and to clean these up the plugin loader will also call the + * gs_app_list_filter_duplicates() method when all plugins have run. + * + * Since: 3.22 + **/ +void +gs_app_list_add (GsAppList *list, GsApp *app) +{ + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&list->mutex); + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE); + + /* recalculate global state */ + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); +} + +/** + * gs_app_list_remove: + * @list: A #GsAppList + * @app: A #GsApp + * + * Removes an application from the list. If the application does not exist the + * request is ignored. + * + * Returns: %TRUE if the app was removed, %FALSE if it did not exist in the @list + * Since: 43 + **/ +gboolean +gs_app_list_remove (GsAppList *list, GsApp *app) +{ + g_autoptr(GMutexLocker) locker = NULL; + gboolean removed; + + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + locker = g_mutex_locker_new (&list->mutex); + removed = g_ptr_array_remove (list->array, app); + if (removed) { + gs_app_list_maybe_unwatch_app (list, app); + + /* recalculate global state */ + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); + } + + return removed; +} + +/** + * gs_app_list_add_list: + * @list: A #GsAppList + * @donor: Another #GsAppList + * + * Adds all the applications in @donor to @list. + * + * Since: 3.22 + **/ +void +gs_app_list_add_list (GsAppList *list, GsAppList *donor) +{ + guint i; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (GS_IS_APP_LIST (donor)); + g_return_if_fail (list != donor); + + locker = g_mutex_locker_new (&list->mutex); + + /* add each app */ + for (i = 0; i < donor->array->len; i++) { + GsApp *app = gs_app_list_index (donor, i); + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE); + } + + /* recalculate global state */ + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); +} + +/** + * gs_app_list_index: + * @list: A #GsAppList + * @idx: An index into the list + * + * Gets an application at a specific position in the list. + * + * Returns: (transfer none): a #GsApp, or %NULL if invalid + * + * Since: 3.22 + **/ +GsApp * +gs_app_list_index (GsAppList *list, guint idx) +{ + return GS_APP (g_ptr_array_index (list->array, idx)); +} + +/** + * gs_app_list_length: + * @list: A #GsAppList + * + * Gets the length of the application list. + * + * Returns: Integer + * + * Since: 3.22 + **/ +guint +gs_app_list_length (GsAppList *list) +{ + g_return_val_if_fail (GS_IS_APP_LIST (list), 0); + return list->array->len; +} + +static void +gs_app_list_remove_all_safe (GsAppList *list) +{ + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = g_ptr_array_index (list->array, i); + gs_app_list_maybe_unwatch_app (list, app); + } + g_ptr_array_set_size (list->array, 0); + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); +} + +/** + * gs_app_list_remove_all: + * @list: A #GsAppList + * + * Removes all applications from the list. + * + * Since: 3.22 + **/ +void +gs_app_list_remove_all (GsAppList *list) +{ + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP_LIST (list)); + locker = g_mutex_locker_new (&list->mutex); + gs_app_list_remove_all_safe (list); +} + +/** + * gs_app_list_filter: + * @list: A #GsAppList + * @func: A #GsAppListFilterFunc + * @user_data: the user pointer to pass to @func + * + * If func() returns TRUE for the GsApp, then the app is kept. + * + * Since: 3.22 + **/ +void +gs_app_list_filter (GsAppList *list, GsAppListFilterFunc func, gpointer user_data) +{ + guint i; + GsApp *app; + g_autoptr(GsAppList) old = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (func != NULL); + + locker = g_mutex_locker_new (&list->mutex); + + /* deep copy to a temp list and clear the current one */ + old = gs_app_list_copy (list); + gs_app_list_remove_all_safe (list); + + /* see if any of the apps need filtering */ + for (i = 0; i < old->array->len; i++) { + app = gs_app_list_index (old, i); + if (func (app, user_data)) + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE); + } +} + +typedef struct { + GsAppListSortFunc func; + gpointer user_data; +} GsAppListSortHelper; + +static gint +gs_app_list_sort_cb (gconstpointer a, gconstpointer b, gpointer user_data) +{ + GsApp *app1 = GS_APP (*(GsApp **) a); + GsApp *app2 = GS_APP (*(GsApp **) b); + const GsAppListSortHelper *helper = (GsAppListSortHelper *) user_data; + return helper->func (app1, app2, helper->user_data); +} + +/** + * gs_app_list_sort: + * @list: A #GsAppList + * @func: A #GsAppListSortFunc + * @user_data: user data to pass to @func + * + * Sorts the application list. + * + * Since: 3.22 + **/ +void +gs_app_list_sort (GsAppList *list, GsAppListSortFunc func, gpointer user_data) +{ + g_autoptr(GMutexLocker) locker = NULL; + GsAppListSortHelper helper; + g_return_if_fail (GS_IS_APP_LIST (list)); + locker = g_mutex_locker_new (&list->mutex); + helper.func = func; + helper.user_data = user_data; + g_ptr_array_sort_with_data (list->array, gs_app_list_sort_cb, &helper); +} + +/** + * gs_app_list_truncate: + * @list: A #GsAppList + * @length: the new length + * + * Truncates the application list. It is an error if @length is larger than the + * size of the list. + * + * Since: 3.24 + **/ +void +gs_app_list_truncate (GsAppList *list, guint length) +{ + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (length <= list->array->len); + + /* mark this list as unworthy */ + list->flags |= GS_APP_LIST_FLAG_IS_TRUNCATED; + + /* everything */ + if (length == 0) { + gs_app_list_remove_all (list); + return; + } + + /* remove the apps in the positions larger than the length */ + locker = g_mutex_locker_new (&list->mutex); + g_ptr_array_set_size (list->array, length); +} + +/** + * gs_app_list_randomize: + * @list: A #GsAppList + * + * Randomize the order of the list, but don't change the order until + * the next day. + * + * Since: 3.22 + **/ +void +gs_app_list_randomize (GsAppList *list) +{ + GRand *rand; + g_autoptr(GDateTime) date = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + + locker = g_mutex_locker_new (&list->mutex); + + if (!gs_app_list_length (list)) + return; + + rand = g_rand_new (); + date = g_date_time_new_now_utc (); + g_rand_set_seed (rand, (guint32) g_date_time_get_day_of_year (date)); + + /* Fisher–Yates shuffle of the array. + * See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle */ + for (guint i = gs_app_list_length (list) - 1; i >= 1; i--) { + gpointer tmp; + guint j = g_rand_int_range (rand, 0, i + 1); + + tmp = list->array->pdata[i]; + list->array->pdata[i] = list->array->pdata[j]; + list->array->pdata[j] = tmp; + } + + g_rand_free (rand); +} + +static gboolean +gs_app_list_filter_app_is_better (GsApp *app, GsApp *found, GsAppListFilterFlags flags) +{ + /* optional 1st layer sort */ + if ((flags & GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED) > 0) { + if (gs_app_is_installed (app) && !gs_app_is_installed (found)) + return TRUE; + if (!gs_app_is_installed (app) && gs_app_is_installed (found)) + return FALSE; + } + + /* 2nd layer, priority and bundle kind */ + if (gs_app_compare_priority (app, found) < 0) + return TRUE; + + /* assume is worse */ + return FALSE; +} + +static GPtrArray * +gs_app_list_filter_app_get_keys (GsApp *app, GsAppListFilterFlags flags) +{ + GPtrArray *keys = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GString) key = NULL; + + /* just use the unique ID */ + if (flags == GS_APP_LIST_FILTER_FLAG_NONE) { + if (gs_app_get_unique_id (app) != NULL) + g_ptr_array_add (keys, g_strdup (gs_app_get_unique_id (app))); + return keys; + } + + /* use the ID and any provided items */ + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES) { + GPtrArray *provided = gs_app_get_provided (app); + g_ptr_array_add (keys, g_strdup (gs_app_get_id (app))); + for (guint i = 0; i < provided->len; i++) { + AsProvided *prov = g_ptr_array_index (provided, i); + GPtrArray *items; + if (as_provided_get_kind (prov) != AS_PROVIDED_KIND_ID) + continue; + items = as_provided_get_items (prov); + for (guint j = 0; j < items->len; j++) + g_ptr_array_add (keys, g_strdup (g_ptr_array_index (items, j))); + } + return keys; + } + + /* specific compound type */ + key = g_string_new (NULL); + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID) { + const gchar *tmp = gs_app_get_id (app); + if (tmp != NULL) + g_string_append (key, gs_app_get_id (app)); + } + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_SOURCE) { + const gchar *tmp = gs_app_get_source_default (app); + if (tmp != NULL) + g_string_append_printf (key, ":%s", tmp); + } + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_VERSION) { + const gchar *tmp = gs_app_get_version (app); + if (tmp != NULL) + g_string_append_printf (key, ":%s", tmp); + } + if (key->len == 0) + return keys; + g_ptr_array_add (keys, g_string_free (g_steal_pointer (&key), FALSE)); + return keys; +} + +/** + * gs_app_list_filter_duplicates: + * @list: A #GsAppList + * @flags: a #GsAppListFilterFlags, e.g. GS_APP_LIST_FILTER_KEY_ID + * + * Filter any duplicate applications from the list. + * + * Since: 3.22 + **/ +void +gs_app_list_filter_duplicates (GsAppList *list, GsAppListFilterFlags flags) +{ + g_autoptr(GHashTable) hash = NULL; + g_autoptr(GHashTable) kept_apps = NULL; + g_autoptr(GsAppList) old = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + + locker = g_mutex_locker_new (&list->mutex); + + /* a hash table to hold apps with unique app ids */ + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + /* a hash table containing apps we want to keep */ + kept_apps = g_hash_table_new (g_direct_hash, g_direct_equal); + + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = gs_app_list_index (list, i); + GsApp *found = NULL; + g_autoptr(GPtrArray) keys = NULL; + + /* get all the keys used to identify this app */ + keys = gs_app_list_filter_app_get_keys (app, flags); + for (guint j = 0; j < keys->len; j++) { + const gchar *key = g_ptr_array_index (keys, j); + found = g_hash_table_lookup (hash, key); + if (found != NULL) + break; + } + + /* new app */ + if (found == NULL) { + for (guint j = 0; j < keys->len; j++) { + const gchar *key = g_ptr_array_index (keys, j); + g_hash_table_insert (hash, g_strdup (key), app); + } + g_hash_table_add (kept_apps, app); + continue; + } + + /* better? */ + if (flags != GS_APP_LIST_FILTER_FLAG_NONE) { + if (gs_app_list_filter_app_is_better (app, found, flags)) { + for (guint j = 0; j < keys->len; j++) { + const gchar *key = g_ptr_array_index (keys, j); + g_hash_table_insert (hash, g_strdup (key), app); + } + g_hash_table_remove (kept_apps, found); + g_hash_table_add (kept_apps, app); + continue; + } + continue; + } + continue; + } + + /* deep copy to a temp list and clear the current one */ + old = gs_app_list_copy (list); + gs_app_list_remove_all_safe (list); + + /* add back the apps we want to keep */ + for (guint i = 0; i < old->array->len; i++) { + GsApp *app = gs_app_list_index (old, i); + if (g_hash_table_contains (kept_apps, app)) { + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE); + /* In case the same instance is in the 'list' multiple times */ + g_hash_table_remove (kept_apps, app); + } + } +} + +/** + * gs_app_list_copy: + * @list: A #GsAppList + * + * Returns a deep copy of the application list. + * + * Returns: A newly allocated #GsAppList + * + * Since: 3.22 + **/ +GsAppList * +gs_app_list_copy (GsAppList *list) +{ + GsAppList *new; + guint i; + + g_return_val_if_fail (GS_IS_APP_LIST (list), NULL); + + new = gs_app_list_new (); + for (i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gs_app_list_add_safe (new, app, GS_APP_LIST_ADD_FLAG_NONE); + } + return new; +} + +static void +gs_app_list_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsAppList *self = GS_APP_LIST (object); + switch (prop_id) { + case PROP_STATE: + g_value_set_enum (value, self->state); + break; + case PROP_PROGRESS: + g_value_set_uint (value, self->progress); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_list_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_list_finalize (GObject *object) +{ + GsAppList *list = GS_APP_LIST (object); + g_ptr_array_unref (list->array); + g_mutex_clear (&list->mutex); + G_OBJECT_CLASS (gs_app_list_parent_class)->finalize (object); +} + +static void +gs_app_list_class_init (GsAppListClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->get_property = gs_app_list_get_property; + object_class->set_property = gs_app_list_set_property; + object_class->finalize = gs_app_list_finalize; + + /** + * GsAppList:state: + */ + pspec = g_param_spec_enum ("state", NULL, NULL, + GS_TYPE_APP_STATE, + GS_APP_STATE_UNKNOWN, + G_PARAM_READABLE); + g_object_class_install_property (object_class, PROP_STATE, pspec); + + /** + * GsAppList:progress: + * + * A percentage (0–100, inclusive) indicating the progress through the + * current task on this app list. The value may otherwise be + * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide + * confidence interval on any app, or if the app list is empty. + */ + pspec = g_param_spec_uint ("progress", NULL, NULL, + 0, GS_APP_PROGRESS_UNKNOWN, GS_APP_PROGRESS_UNKNOWN, + G_PARAM_READABLE); + g_object_class_install_property (object_class, PROP_PROGRESS, pspec); + + /** + * GsAppList:app-state-changed: + * @app: a #GsApp + * + * Emitted when any of the internal #GsApp instances changes its state. + * + * Since: 3.40 + */ + signals [SIGNAL_APP_STATE_CHANGED] = + g_signal_new ("app-state-changed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 1, GS_TYPE_APP); +} + +static void +gs_app_list_init (GsAppList *list) +{ + g_mutex_init (&list->mutex); + list->array = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + list->custom_progress = GS_APP_PROGRESS_UNKNOWN; +} + +/** + * gs_app_list_new: + * + * Creates a new list. + * + * Returns: A newly allocated #GsAppList + * + * Since: 3.22 + **/ +GsAppList * +gs_app_list_new (void) +{ + GsAppList *list; + list = g_object_new (GS_TYPE_APP_LIST, NULL); + return GS_APP_LIST (list); +} diff --git a/lib/gs-app-list.h b/lib/gs-app-list.h new file mode 100644 index 0000000..adf85f7 --- /dev/null +++ b/lib/gs-app-list.h @@ -0,0 +1,89 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" + +G_BEGIN_DECLS + +/** + * GsAppListFilterFlags: + * @GS_APP_LIST_FILTER_FLAG_NONE: No flags set + * @GS_APP_LIST_FILTER_FLAG_KEY_ID: Filter by ID + * @GS_APP_LIST_FILTER_FLAG_KEY_SOURCE: Filter by default source + * @GS_APP_LIST_FILTER_FLAG_KEY_VERSION: Filter by version + * @GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED: Prefer installed applications + * @GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES: Filter using the provides ID + * + * Flags to use when filtering. The priority of each #GsApp is used to choose + * which application object to keep. + * + * Since: 40 + **/ +typedef enum { + GS_APP_LIST_FILTER_FLAG_NONE = 0, + GS_APP_LIST_FILTER_FLAG_KEY_ID = 1 << 0, + GS_APP_LIST_FILTER_FLAG_KEY_SOURCE = 1 << 1, + GS_APP_LIST_FILTER_FLAG_KEY_VERSION = 1 << 2, + GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED= 1 << 3, + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES = 1 << 4, + GS_APP_LIST_FILTER_FLAG_LAST, /*< skip >*/ + GS_APP_LIST_FILTER_FLAG_MASK = G_MAXUINT64 +} GsAppListFilterFlags; + +/* All the properties which use #GsAppListFilterFlags are guint64s. */ +G_STATIC_ASSERT (sizeof (GsAppListFilterFlags) == sizeof (guint64)); + +#define GS_TYPE_APP_LIST (gs_app_list_get_type ()) + +G_DECLARE_FINAL_TYPE (GsAppList, gs_app_list, GS, APP_LIST, GObject) + +/** + * GsAppListSortFunc: + * @app1: + * @app2: + * @user_data: user data passed into the sort function + * + * A version of #GCompareFunc which is specific to #GsApps. + * + * Returns: zero if @app1 and @app2 are equal, a negative value if @app1 comes + * before @app2, or a positive value if @app1 comes after @app2 + * Since: 41 + */ +typedef gint (*GsAppListSortFunc) (GsApp *app1, + GsApp *app2, + gpointer user_data); +typedef gboolean (*GsAppListFilterFunc) (GsApp *app, + gpointer user_data); + +GsAppList *gs_app_list_new (void); +GsAppList *gs_app_list_copy (GsAppList *list); +void gs_app_list_add (GsAppList *list, + GsApp *app); +void gs_app_list_add_list (GsAppList *list, + GsAppList *donor); +gboolean gs_app_list_remove (GsAppList *list, + GsApp *app); +GsApp *gs_app_list_index (GsAppList *list, + guint idx); +GsApp *gs_app_list_lookup (GsAppList *list, + const gchar *unique_id); +guint gs_app_list_length (GsAppList *list); +void gs_app_list_sort (GsAppList *list, + GsAppListSortFunc func, + gpointer user_data); +void gs_app_list_filter (GsAppList *list, + GsAppListFilterFunc func, + gpointer user_data); +void gs_app_list_override_progress (GsAppList *list, + guint progress); + +G_END_DECLS diff --git a/lib/gs-app-permissions.c b/lib/gs-app-permissions.c new file mode 100644 index 0000000..bbae07c --- /dev/null +++ b/lib/gs-app-permissions.c @@ -0,0 +1,430 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Red Hat <www.redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-app-permissions + * @short_description: A representation of the permissions requested by an app + * + * #GsAppPermissions is an object to represent the permissions requested by an app. + * + * While some common permissions are handled with the #GsAppPermissionsFlags, + * the object allows more detailed permissions to be represented, such as + * specific file system path access. + * + * Since: 43 + */ + +#include "config.h" + +#include <stdlib.h> + +#include <glib.h> +#include <glib-object.h> + +#include "gs-app-permissions.h" + +#define DOES_NOT_CONTAIN ((guint) ~0) + +struct _GsAppPermissions +{ + GObject parent; + + gboolean is_sealed; + GsAppPermissionsFlags flags; + GPtrArray *filesystem_read; /* (owner) (nullable) (element-type utf-8) */ + GPtrArray *filesystem_full; /* (owner) (nullable) (element-type utf-8) */ +}; + +G_DEFINE_TYPE (GsAppPermissions, gs_app_permissions, G_TYPE_OBJECT) + +static gint +cmp_filename_qsort (gconstpointer item1, + gconstpointer item2) +{ + const gchar * const *pitem1 = item1; + const gchar * const *pitem2 = item2; + return strcmp (*pitem1, *pitem2); +} + +static gint +cmp_filename_bsearch (gconstpointer item1, + gconstpointer item2) +{ + return strcmp (item1, item2); +} + +static void +gs_app_permissions_finalize (GObject *object) +{ + GsAppPermissions *self = GS_APP_PERMISSIONS (object); + + g_clear_pointer (&self->filesystem_read, g_ptr_array_unref); + g_clear_pointer (&self->filesystem_full, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_app_permissions_parent_class)->finalize (object); +} + +static void +gs_app_permissions_class_init (GsAppPermissionsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gs_app_permissions_finalize; +} + +static void +gs_app_permissions_init (GsAppPermissions *self) +{ +} + +/** + * gs_app_permissions_new: + * + * Create a new #GsAppPermissions containing the app permissions. + * + * Returns: (transfer full): a new #GsAppPermissions + * Since: 43 + */ +GsAppPermissions * +gs_app_permissions_new (void) +{ + return g_object_new (GS_TYPE_APP_PERMISSIONS, NULL); +} + +/** + * gs_app_permissions_seal: + * @self: a #GsAppPermissions + * + * Seal the @self. After being called, no modifications can be + * done on the @self. + * + * Since: 43 + **/ +void +gs_app_permissions_seal (GsAppPermissions *self) +{ + g_return_if_fail (GS_IS_APP_PERMISSIONS (self)); + + if (self->is_sealed) + return; + + self->is_sealed = TRUE; + + /* Sort the arrays, which will help with searching */ + if (self->filesystem_read) + qsort (self->filesystem_read->pdata, self->filesystem_read->len, sizeof (gpointer), cmp_filename_qsort); + + if (self->filesystem_full) + qsort (self->filesystem_full->pdata, self->filesystem_full->len, sizeof (gpointer), cmp_filename_qsort); +} + +/** + * gs_app_permissions_is_sealed: + * @self: a #GsAppPermissions + * + * Checks whether the @self had been sealed. Once the @self is sealed, + * no modifications can be made to it. + * + * Returns: whether the @self had been sealed + * + * Since: 43 + **/ +gboolean +gs_app_permissions_is_sealed (GsAppPermissions *self) +{ + g_return_val_if_fail (GS_IS_APP_PERMISSIONS (self), TRUE); + + return self->is_sealed; +} + +/** + * gs_app_permissions_set_flags: + * @self: a #GsAppPermissions + * @flags: a #GsAppPermissionsFlags to set + * + * Set the permission flags, overwriting any previously set flags. + * Compare to gs_app_permissions_add_flag() and + * gs_app_permissions_remove_flag(). + * + * Since: 43 + */ +void +gs_app_permissions_set_flags (GsAppPermissions *self, + GsAppPermissionsFlags flags) +{ + g_return_if_fail (GS_IS_APP_PERMISSIONS (self)); + + g_assert (!self->is_sealed); + + self->flags = flags; +} + +/** + * gs_app_permissions_get_flags: + * @self: a #GsAppPermissions + * + * Get the permission flags. + * + * Returns: the permission flags + * Since: 43 + */ +GsAppPermissionsFlags +gs_app_permissions_get_flags (GsAppPermissions *self) +{ + g_return_val_if_fail (GS_IS_APP_PERMISSIONS (self), GS_APP_PERMISSIONS_FLAGS_UNKNOWN); + + return self->flags; +} + +/** + * gs_app_permissions_add_flag: + * @self: a #GsAppPermissions + * @flags: a #GsAppPermissionsFlags to add + * + * Add the @flags into the already set flags. The @flags cannot contain + * #GS_APP_PERMISSIONS_FLAGS_NONE, neither cannot be #GS_APP_PERMISSIONS_FLAGS_UNKNOWN. + * To set these two use gs_app_permissions_set_flags() instead. + * + * In case the current flags contain #GS_APP_PERMISSIONS_FLAGS_NONE, it's + * automatically unset. + * + * Since: 43 + */ +void +gs_app_permissions_add_flag (GsAppPermissions *self, + GsAppPermissionsFlags flags) +{ + g_return_if_fail (GS_IS_APP_PERMISSIONS (self)); + g_return_if_fail (flags != GS_APP_PERMISSIONS_FLAGS_UNKNOWN); + g_return_if_fail ((flags & GS_APP_PERMISSIONS_FLAGS_NONE) == 0); + + g_assert (!self->is_sealed); + + self->flags = (self->flags & (~GS_APP_PERMISSIONS_FLAGS_NONE)) | flags; +} + +/** + * gs_app_permissions_remove_flag: + * @self: a #GsAppPermissions + * @flags: a #GsAppPermissionsFlags to remove + * + * Remove the @flags from the already set flags. The @flags cannot contain + * #GS_APP_PERMISSIONS_FLAGS_NONE, neither cannot be #GS_APP_PERMISSIONS_FLAGS_UNKNOWN. + * To set these two use gs_app_permissions_set_flags() instead. + * + * In case the result of the removal would lead to no flag set the #GS_APP_PERMISSIONS_FLAGS_NONE + * is set automatically. + * + * Since: 43 + */ +void +gs_app_permissions_remove_flag (GsAppPermissions *self, + GsAppPermissionsFlags flags) +{ + g_return_if_fail (GS_IS_APP_PERMISSIONS (self)); + g_return_if_fail (flags != GS_APP_PERMISSIONS_FLAGS_UNKNOWN); + g_return_if_fail ((flags & GS_APP_PERMISSIONS_FLAGS_NONE) == 0); + + g_assert (!self->is_sealed); + + self->flags = (self->flags & (~flags)); + + if (!self->flags) + self->flags = GS_APP_PERMISSIONS_FLAGS_NONE; +} + +static guint +app_permissions_get_array_index (GPtrArray *array, + const gchar *filename) +{ + g_return_val_if_fail (filename != NULL, DOES_NOT_CONTAIN); + + if (array == NULL) + return DOES_NOT_CONTAIN; + + for (guint i = 0; i < array->len; i++) { + const gchar *item = g_ptr_array_index (array, i); + if (g_strcmp0 (item, filename) == 0) + return 0; + } + + return DOES_NOT_CONTAIN; +} + +/** + * gs_app_permissions_add_filesystem_read: + * @self: a #GsAppPermissions + * @filename: a filename to access + * + * Add @filename as a file to access for read. The @filename + * can be either a path or a localized pretty name of it, like "Documents". + * The addition is ignored in case the same @filename is part of + * the read or full access file names. + * + * Since: 43 + */ +void +gs_app_permissions_add_filesystem_read (GsAppPermissions *self, + const gchar *filename) +{ + g_return_if_fail (GS_IS_APP_PERMISSIONS (self)); + g_return_if_fail (filename != NULL); + + g_assert (!self->is_sealed); + + /* Already known */ + if (app_permissions_get_array_index (self->filesystem_read, filename) != DOES_NOT_CONTAIN || + app_permissions_get_array_index (self->filesystem_full, filename) != DOES_NOT_CONTAIN) + return; + + if (self->filesystem_read == NULL) + self->filesystem_read = g_ptr_array_new_with_free_func (g_free); + + g_ptr_array_add (self->filesystem_read, g_strdup (filename)); +} + +/** + * gs_app_permissions_get_filesystem_read: + * @self: a #GsAppPermissions + * + * Get the list of filesystem file names requested for read access using + * gs_app_permissions_add_filesystem_read(). + * The array is owned by the @self and should not be modified by any way. + * It can be %NULL, when no file access was set. + * + * Returns: (nullable) (transfer none) (element-type utf-8): an array of + * file names requesting read access or %NULL, when none was set. + * + * Since: 43 + */ +const GPtrArray * +gs_app_permissions_get_filesystem_read (GsAppPermissions *self) +{ + g_return_val_if_fail (GS_IS_APP_PERMISSIONS (self), NULL); + + return self->filesystem_read; +} + +static gboolean +array_contains_filename (GPtrArray *array, + const gchar *filename) +{ + if (array == NULL) + return FALSE; + + return bsearch (filename, array->pdata, array->len, sizeof (gpointer), cmp_filename_bsearch) != NULL; +} + +/** + * gs_app_permissions_contains_filesystem_read: + * @self: a #GsAppPermissions + * @filename: a file name to search for + * + * Checks whether the @filename is included in the filesystem read permissions. + * This can be called only after the @self is sealed. + * + * Returns: whether the @filename is part of the filesystem read permissions + * + * Since: 43 + **/ +gboolean +gs_app_permissions_contains_filesystem_read (GsAppPermissions *self, + const gchar *filename) +{ + g_return_val_if_fail (GS_IS_APP_PERMISSIONS (self), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + g_return_val_if_fail (self->is_sealed, FALSE); + + return array_contains_filename (self->filesystem_read, filename); +} + +/** + * gs_app_permissions_add_filesystem_full: + * @self: a #GsAppPermissions + * @filename: a filename to access + * + * Add @filename as a file to access for read and write. The @filename + * can be either a path or a localized pretty name of it, like "Documents". + * The addition is ignored in case the same @filename is include in the list + * already. The @filename is removed from the read list, if it's part of it. + * + * Since: 43 + */ +void +gs_app_permissions_add_filesystem_full (GsAppPermissions *self, + const gchar *filename) +{ + guint read_index; + + g_return_if_fail (GS_IS_APP_PERMISSIONS (self)); + g_return_if_fail (filename != NULL); + + g_assert (!self->is_sealed); + + /* Already known */ + if (app_permissions_get_array_index (self->filesystem_full, filename) != DOES_NOT_CONTAIN) + return; + + if (self->filesystem_full == NULL) + self->filesystem_full = g_ptr_array_new_with_free_func (g_free); + + g_ptr_array_add (self->filesystem_full, g_strdup (filename)); + + /* Remove from the read list and free the read list if becomes empty */ + read_index = app_permissions_get_array_index (self->filesystem_read, filename); + if (read_index != DOES_NOT_CONTAIN) { + g_ptr_array_remove_index (self->filesystem_read, read_index); + if (self->filesystem_read->len == 0) + g_clear_pointer (&self->filesystem_read, g_ptr_array_unref); + } +} + +/** + * gs_app_permissions_get_filesystem_full: + * @self: a #GsAppPermissions + * + * Get the list of filesystem file names requested for read and write access using + * gs_app_permissions_add_filesystem_full(). + * The array is owned by the @self and should not be modified by any way. + * It can be %NULL, when no file access was set. + * + * Returns: (nullable) (transfer none) (element-type utf-8): an array of + * file names requesting read and write access or %NULL, when none was set. + * + * Since: 43 + */ +const GPtrArray * +gs_app_permissions_get_filesystem_full (GsAppPermissions *self) +{ + g_return_val_if_fail (GS_IS_APP_PERMISSIONS (self), NULL); + + return self->filesystem_full; +} + +/** + * gs_app_permissions_contains_filesystem_full: + * @self: a #GsAppPermissions + * @filename: a file name to search for + * + * Checks whether the @filename is included in the filesystem full permissions. + * This can be called only after the @self is sealed. + * + * Returns: whether the @filename is part of the filesystem full permissions + * + * Since: 43 + **/ +gboolean +gs_app_permissions_contains_filesystem_full (GsAppPermissions *self, + const gchar *filename) +{ + g_return_val_if_fail (GS_IS_APP_PERMISSIONS (self), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + g_return_val_if_fail (self->is_sealed, FALSE); + + return array_contains_filename (self->filesystem_full, filename); +} diff --git a/lib/gs-app-permissions.h b/lib/gs-app-permissions.h new file mode 100644 index 0000000..96c482b --- /dev/null +++ b/lib/gs-app-permissions.h @@ -0,0 +1,74 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Red Hat <www.redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef enum { + GS_APP_PERMISSIONS_FLAGS_UNKNOWN = 0, + GS_APP_PERMISSIONS_FLAGS_NONE = 1 << 0, + GS_APP_PERMISSIONS_FLAGS_NETWORK = 1 << 1, + GS_APP_PERMISSIONS_FLAGS_SYSTEM_BUS = 1 << 2, + GS_APP_PERMISSIONS_FLAGS_SESSION_BUS = 1 << 3, + GS_APP_PERMISSIONS_FLAGS_DEVICES = 1 << 4, + GS_APP_PERMISSIONS_FLAGS_HOME_FULL = 1 << 5, + GS_APP_PERMISSIONS_FLAGS_HOME_READ = 1 << 6, + GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL = 1 << 7, + GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_READ = 1 << 8, + GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL = 1 << 9, + GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_READ = 1 << 10, + GS_APP_PERMISSIONS_FLAGS_SETTINGS = 1 << 11, + GS_APP_PERMISSIONS_FLAGS_X11 = 1 << 12, + GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX = 1 << 13, + GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_OTHER = 1 << 14, + GS_APP_PERMISSIONS_FLAGS_LAST /*< skip >*/ +} GsAppPermissionsFlags; + +#define LIMITED_PERMISSIONS (GS_APP_PERMISSIONS_FLAGS_SETTINGS | \ + GS_APP_PERMISSIONS_FLAGS_NETWORK | \ + GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_READ | \ + GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL) +#define MEDIUM_PERMISSIONS (LIMITED_PERMISSIONS | \ + GS_APP_PERMISSIONS_FLAGS_X11) + +#define GS_TYPE_APP_PERMISSIONS (gs_app_permissions_get_type ()) + +G_DECLARE_FINAL_TYPE (GsAppPermissions, gs_app_permissions, GS, APP_PERMISSIONS, GObject) + +GsAppPermissions *gs_app_permissions_new (void); +void gs_app_permissions_seal (GsAppPermissions *self); +gboolean gs_app_permissions_is_sealed (GsAppPermissions *self); +void gs_app_permissions_set_flags (GsAppPermissions *self, + GsAppPermissionsFlags flags); +GsAppPermissionsFlags gs_app_permissions_get_flags (GsAppPermissions *self); +void gs_app_permissions_add_flag (GsAppPermissions *self, + GsAppPermissionsFlags flags); +void gs_app_permissions_remove_flag (GsAppPermissions *self, + GsAppPermissionsFlags flags); +void gs_app_permissions_add_filesystem_read + (GsAppPermissions *self, + const gchar *filename); +const GPtrArray *gs_app_permissions_get_filesystem_read + (GsAppPermissions *self); +gboolean gs_app_permissions_contains_filesystem_read + (GsAppPermissions *self, + const gchar *filename); +void gs_app_permissions_add_filesystem_full + (GsAppPermissions *self, + const gchar *filename); +const GPtrArray *gs_app_permissions_get_filesystem_full + (GsAppPermissions *self); +gboolean gs_app_permissions_contains_filesystem_full + (GsAppPermissions *self, + const gchar *filename); + +G_END_DECLS diff --git a/lib/gs-app-private.h b/lib/gs-app-private.h new file mode 100644 index 0000000..1163bf3 --- /dev/null +++ b/lib/gs-app-private.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +void gs_app_set_priority (GsApp *app, + guint priority); +guint gs_app_get_priority (GsApp *app); +void gs_app_set_unique_id (GsApp *app, + const gchar *unique_id); +void gs_app_remove_addon (GsApp *app, + GsApp *addon); +GCancellable *gs_app_get_cancellable (GsApp *app); +GsPluginAction gs_app_get_pending_action (GsApp *app); +void gs_app_set_pending_action (GsApp *app, + GsPluginAction action); +gint gs_app_compare_priority (GsApp *app1, + GsApp *app2); + +G_END_DECLS diff --git a/lib/gs-app-query.c b/lib/gs-app-query.c new file mode 100644 index 0000000..cd2fc93 --- /dev/null +++ b/lib/gs-app-query.c @@ -0,0 +1,1173 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-app-query + * @short_description: Immutable representation of a query for apps + * + * #GsAppQuery is an object to represent a query for apps. + * + * It will typically be used with #GsPluginJobListApps, which searches for + * matching apps, but it may have multiple consumers. #GsAppQuery only + * represents the query and does not provide an implementation for executing + * that query. + * + * It is immutable after construction, and hence threadsafe. It may be extended + * in future by adding more query properties. The existing query properties are + * conjunctive: results should only be returned which match *all* properties + * which are set, not _any_ properties which are set. + * + * The set of apps returned for the query can be controlled with the + * #GsAppQuery:refine-flags, + * #GsAppQuery:max-results and + * #GsAppQuery:dedupe-flags properties. If `refine-flags` is + * set, all results must be refined using the given set of refine flags (see + * #GsPluginJobRefine). `max-results` and `dedupe-flags` are used to limit the + * set of results. + * + * Results must always be processed in this order: + * - Filtering using #GsAppQuery:filter-func (and any other custom filter + * functions the query executor provides). + * - Deduplication using #GsAppQuery:dedupe-flags. + * - Sorting using #GsAppQuery:sort-func. + * - Truncating result list length to #GsAppQuery:max-results. + * + * Since: 43 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> + +#include "gs-app.h" +#include "gs-app-list.h" +#include "gs-app-query.h" +#include "gs-enums.h" +#include "gs-plugin-types.h" +#include "gs-utils.h" + +struct _GsAppQuery +{ + GObject parent; + + GsPluginRefineFlags refine_flags; + guint max_results; + GsAppListFilterFlags dedupe_flags; + + GsAppListSortFunc sort_func; + gpointer sort_user_data; + GDestroyNotify sort_user_data_notify; + + GsAppListFilterFunc filter_func; + gpointer filter_user_data; + GDestroyNotify filter_user_data_notify; + + /* This is guaranteed to either be %NULL, or a non-empty array */ + gchar **provides_files; /* (owned) (nullable) (array zero-terminated=1) */ + GDateTime *released_since; /* (owned) (nullable) */ + GsAppQueryTristate is_curated; + GsAppQueryTristate is_featured; + GsCategory *category; /* (nullable) (owned) */ + GsAppQueryTristate is_installed; + + /* This is guaranteed to either be %NULL, or a non-empty array */ + gchar **deployment_featured; /* (owned) (nullable) (array zero-terminated=1) */ + /* This is guaranteed to either be %NULL, or a non-empty array */ + gchar **developers; /* (owned) (nullable) (array zero-terminated=1) */ + + gchar **keywords; /* (owned) (nullable) (array zero-terminated=1) */ + GsApp *alternate_of; /* (nullable) (owned) */ + gchar *provides_tag; /* (owned) (nullable) */ + GsAppQueryProvidesType provides_type; +}; + +G_DEFINE_TYPE (GsAppQuery, gs_app_query, G_TYPE_OBJECT) + +typedef enum { + PROP_REFINE_FLAGS = 1, + PROP_MAX_RESULTS, + PROP_DEDUPE_FLAGS, + PROP_SORT_FUNC, + PROP_SORT_USER_DATA, + PROP_SORT_USER_DATA_NOTIFY, + PROP_FILTER_FUNC, + PROP_FILTER_USER_DATA, + PROP_FILTER_USER_DATA_NOTIFY, + PROP_DEPLOYMENT_FEATURED, + PROP_DEVELOPERS, + PROP_PROVIDES_FILES, + PROP_RELEASED_SINCE, + PROP_IS_CURATED, + PROP_IS_FEATURED, + PROP_CATEGORY, + PROP_IS_INSTALLED, + PROP_KEYWORDS, + PROP_ALTERNATE_OF, + PROP_PROVIDES_TAG, + PROP_PROVIDES_TYPE, +} GsAppQueryProperty; + +static GParamSpec *props[PROP_PROVIDES_TYPE + 1] = { NULL, }; + +static void +gs_app_query_constructed (GObject *object) +{ + GsAppQuery *self = GS_APP_QUERY (object); + + G_OBJECT_CLASS (gs_app_query_parent_class)->constructed (object); + + g_assert ((self->provides_tag != NULL) == (self->provides_type != GS_APP_QUERY_PROVIDES_UNKNOWN)); +} + +static void +gs_app_query_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsAppQuery *self = GS_APP_QUERY (object); + + switch ((GsAppQueryProperty) prop_id) { + case PROP_REFINE_FLAGS: + g_value_set_flags (value, self->refine_flags); + break; + case PROP_MAX_RESULTS: + g_value_set_uint (value, self->max_results); + break; + case PROP_DEDUPE_FLAGS: + g_value_set_flags (value, self->dedupe_flags); + break; + case PROP_SORT_FUNC: + g_value_set_pointer (value, self->sort_func); + break; + case PROP_SORT_USER_DATA: + g_value_set_pointer (value, self->sort_user_data); + break; + case PROP_SORT_USER_DATA_NOTIFY: + g_value_set_pointer (value, self->sort_user_data_notify); + break; + case PROP_FILTER_FUNC: + g_value_set_pointer (value, self->filter_func); + break; + case PROP_FILTER_USER_DATA: + g_value_set_pointer (value, self->filter_user_data); + break; + case PROP_FILTER_USER_DATA_NOTIFY: + g_value_set_pointer (value, self->filter_user_data_notify); + break; + case PROP_DEPLOYMENT_FEATURED: + g_value_set_boxed (value, self->deployment_featured); + break; + case PROP_DEVELOPERS: + g_value_set_boxed (value, self->developers); + break; + case PROP_PROVIDES_FILES: + g_value_set_boxed (value, self->provides_files); + break; + case PROP_RELEASED_SINCE: + g_value_set_boxed (value, self->released_since); + break; + case PROP_IS_CURATED: + g_value_set_enum (value, self->is_curated); + break; + case PROP_IS_FEATURED: + g_value_set_enum (value, self->is_featured); + break; + case PROP_CATEGORY: + g_value_set_object (value, self->category); + break; + case PROP_IS_INSTALLED: + g_value_set_enum (value, self->is_installed); + break; + case PROP_KEYWORDS: + g_value_set_boxed (value, self->keywords); + break; + case PROP_ALTERNATE_OF: + g_value_set_object (value, self->alternate_of); + break; + case PROP_PROVIDES_TAG: + g_value_set_string (value, self->provides_tag); + break; + case PROP_PROVIDES_TYPE: + g_value_set_enum (value, self->provides_type); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_query_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsAppQuery *self = GS_APP_QUERY (object); + + switch ((GsAppQueryProperty) prop_id) { + case PROP_REFINE_FLAGS: + /* Construct only. */ + g_assert (self->refine_flags == 0); + self->refine_flags = g_value_get_flags (value); + break; + case PROP_MAX_RESULTS: + /* Construct only. */ + g_assert (self->max_results == 0); + self->max_results = g_value_get_uint (value); + break; + case PROP_DEDUPE_FLAGS: + /* Construct only. */ + g_assert (self->dedupe_flags == 0); + self->dedupe_flags = g_value_get_flags (value); + break; + case PROP_SORT_FUNC: + /* Construct only. */ + g_assert (self->sort_func == NULL); + self->sort_func = g_value_get_pointer (value); + break; + case PROP_SORT_USER_DATA: + /* Construct only. */ + g_assert (self->sort_user_data == NULL); + self->sort_user_data = g_value_get_pointer (value); + break; + case PROP_SORT_USER_DATA_NOTIFY: + /* Construct only. */ + g_assert (self->sort_user_data_notify == NULL); + self->sort_user_data_notify = g_value_get_pointer (value); + break; + case PROP_FILTER_FUNC: + /* Construct only. */ + g_assert (self->filter_func == NULL); + self->filter_func = g_value_get_pointer (value); + break; + case PROP_FILTER_USER_DATA: + /* Construct only. */ + g_assert (self->filter_user_data == NULL); + self->filter_user_data = g_value_get_pointer (value); + break; + case PROP_FILTER_USER_DATA_NOTIFY: + /* Construct only. */ + g_assert (self->filter_user_data_notify == NULL); + self->filter_user_data_notify = g_value_get_pointer (value); + break; + case PROP_DEPLOYMENT_FEATURED: + /* Construct only. */ + g_assert (self->deployment_featured == NULL); + self->deployment_featured = g_value_dup_boxed (value); + + /* Squash empty arrays to %NULL. */ + if (self->deployment_featured != NULL && self->deployment_featured[0] == NULL) + g_clear_pointer (&self->deployment_featured, g_strfreev); + + break; + case PROP_DEVELOPERS: + /* Construct only. */ + g_assert (self->developers == NULL); + self->developers = g_value_dup_boxed (value); + + /* Squash empty arrays to %NULL. */ + if (self->developers != NULL && self->developers[0] == NULL) + g_clear_pointer (&self->developers, g_strfreev); + + break; + case PROP_PROVIDES_FILES: + /* Construct only. */ + g_assert (self->provides_files == NULL); + self->provides_files = g_value_dup_boxed (value); + + /* Squash empty arrays to %NULL. */ + if (self->provides_files != NULL && self->provides_files[0] == NULL) + g_clear_pointer (&self->provides_files, g_strfreev); + + break; + case PROP_RELEASED_SINCE: + /* Construct only. */ + g_assert (self->released_since == NULL); + self->released_since = g_value_dup_boxed (value); + break; + case PROP_IS_CURATED: + /* Construct only. */ + g_assert (self->is_curated == GS_APP_QUERY_TRISTATE_UNSET); + self->is_curated = g_value_get_enum (value); + break; + case PROP_IS_FEATURED: + /* Construct only. */ + g_assert (self->is_featured == GS_APP_QUERY_TRISTATE_UNSET); + self->is_featured = g_value_get_enum (value); + break; + case PROP_CATEGORY: + /* Construct only. */ + g_assert (self->category == NULL); + self->category = g_value_dup_object (value); + break; + case PROP_IS_INSTALLED: + /* Construct only. */ + g_assert (self->is_installed == GS_APP_QUERY_TRISTATE_UNSET); + self->is_installed = g_value_get_enum (value); + break; + case PROP_KEYWORDS: + /* Construct only. */ + g_assert (self->keywords == NULL); + self->keywords = g_value_dup_boxed (value); + + /* Squash empty arrays to %NULL. */ + if (self->keywords != NULL && self->keywords[0] == NULL) + g_clear_pointer (&self->keywords, g_strfreev); + + break; + case PROP_ALTERNATE_OF: + /* Construct only. */ + g_assert (self->alternate_of == NULL); + self->alternate_of = g_value_dup_object (value); + break; + case PROP_PROVIDES_TAG: + /* Construct only. */ + g_assert (self->provides_tag == NULL); + self->provides_tag = g_value_dup_string (value); + break; + case PROP_PROVIDES_TYPE: + /* Construct only. */ + g_assert (self->provides_type == GS_APP_QUERY_PROVIDES_UNKNOWN); + self->provides_type = g_value_get_enum (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_query_dispose (GObject *object) +{ + GsAppQuery *self = GS_APP_QUERY (object); + + if (self->sort_user_data_notify != NULL && self->sort_user_data != NULL) { + self->sort_user_data_notify (g_steal_pointer (&self->sort_user_data)); + self->sort_user_data_notify = NULL; + } + + if (self->filter_user_data_notify != NULL && self->filter_user_data != NULL) { + self->filter_user_data_notify (g_steal_pointer (&self->filter_user_data)); + self->filter_user_data_notify = NULL; + } + + g_clear_object (&self->category); + g_clear_object (&self->alternate_of); + + G_OBJECT_CLASS (gs_app_query_parent_class)->dispose (object); +} + +static void +gs_app_query_finalize (GObject *object) +{ + GsAppQuery *self = GS_APP_QUERY (object); + + g_clear_pointer (&self->deployment_featured, g_strfreev); + g_clear_pointer (&self->developers, g_strfreev); + g_clear_pointer (&self->provides_files, g_strfreev); + g_clear_pointer (&self->released_since, g_date_time_unref); + g_clear_pointer (&self->keywords, g_strfreev); + g_clear_pointer (&self->provides_tag, g_free); + + G_OBJECT_CLASS (gs_app_query_parent_class)->finalize (object); +} + +static void +gs_app_query_class_init (GsAppQueryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = gs_app_query_constructed; + object_class->get_property = gs_app_query_get_property; + object_class->set_property = gs_app_query_set_property; + object_class->dispose = gs_app_query_dispose; + object_class->finalize = gs_app_query_finalize; + + /** + * GsAppQuery:refine-flags: + * + * Flags to specify how the returned apps must be refined, if at all. + * + * Since: 43 + */ + props[PROP_REFINE_FLAGS] = + g_param_spec_flags ("refine-flags", "Refine Flags", + "Flags to specify how the returned apps must be refined, if at all.", + GS_TYPE_PLUGIN_REFINE_FLAGS, GS_PLUGIN_REFINE_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:max-results: + * + * Maximum number of results to return, or 0 for no limit. + * + * Since: 43 + */ + props[PROP_MAX_RESULTS] = + g_param_spec_uint ("max-results", "Max Results", + "Maximum number of results to return, or 0 for no limit.", + 0, G_MAXUINT, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:dedupe-flags: + * + * Flags to specify how to deduplicate the returned apps, if at all. + * + * Since: 43 + */ + props[PROP_DEDUPE_FLAGS] = + g_param_spec_flags ("dedupe-flags", "Dedupe Flags", + "Flags to specify how to deduplicate the returned apps, if at all.", + GS_TYPE_APP_LIST_FILTER_FLAGS, GS_APP_LIST_FILTER_FLAG_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:sort-func: (nullable) + * + * A sort function to sort the returned apps. + * + * This must be of type #GsAppListSortFunc. + * + * Since: 43 + */ + props[PROP_SORT_FUNC] = + g_param_spec_pointer ("sort-func", "Sort Function", + "A sort function to sort the returned apps.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:sort-user-data: (nullable) + * + * User data to pass to #GsAppQuery:sort-func. + * + * Since: 43 + */ + props[PROP_SORT_USER_DATA] = + g_param_spec_pointer ("sort-user-data", "Sort User Data", + "User data to pass to #GsAppQuery:sort-func.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:sort-user-data-notify: (nullable) + * + * A function to free #GsAppQuery:sort-user-data once it is no longer + * needed. + * + * This must be of type #GDestroyNotify. + * + * This will be called exactly once between being set and when the + * #GsAppQuery is finalized. + * + * Since: 43 + */ + props[PROP_SORT_USER_DATA_NOTIFY] = + g_param_spec_pointer ("sort-user-data-notify", "Sort User Data Notify", + "A function to free #GsAppQuery:sort-user-data once it is no longer needed.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:filter-func: (nullable) + * + * A filter function to filter the returned apps. + * + * This must be of type #GsAppListFilterFunc. + * + * Since: 43 + */ + props[PROP_FILTER_FUNC] = + g_param_spec_pointer ("filter-func", "Filter Function", + "A filter function to filter the returned apps.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:filter-user-data: (nullable) + * + * User data to pass to #GsAppQuery:filter-func. + * + * Since: 43 + */ + props[PROP_FILTER_USER_DATA] = + g_param_spec_pointer ("filter-user-data", "Filter User Data", + "User data to pass to #GsAppQuery:filter-func.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:filter-user-data-notify: (nullable) + * + * A function to free #GsAppQuery:filter-user-data once it is no longer + * needed. + * + * This must be of type #GDestroyNotify. + * + * This will be called exactly once between being set and when the + * #GsAppQuery is finalized. + * + * Since: 43 + */ + props[PROP_FILTER_USER_DATA_NOTIFY] = + g_param_spec_pointer ("filter-user-data-notify", "Filter User Data Notify", + "A function to free #GsAppQuery:filter-user-data once it is no longer needed.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:deployment-featured: (nullable) + * + * A list of `GnomeSoftware::DeploymentFeatured` app keys. + * + * Search for applications that should be featured in a deployment-specific + * section on the overview page. + * This is expected to be a curated list of applications that are high quality + * and feature-complete. Only apps matching at least one of the keys in this + * list are returned. + * + * This may be %NULL to not filter on it. An empty array is + * considered equivalent to %NULL. + * + * Since: 43 + */ + props[PROP_DEPLOYMENT_FEATURED] = + g_param_spec_boxed ("deployment-featured", "Deployment Featured", + "A list of `GnomeSoftware::DeploymentFeatured` app keys.", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:developers: (nullable) + * + * A list of developers to search the apps for. + * + * Used to search for apps which are provided by given developer(s). + * + * This may be %NULL to not filter on by them. An empty array is + * considered equivalent to %NULL. + * + * Since: 43 + */ + props[PROP_DEVELOPERS] = + g_param_spec_boxed ("developers", "Developers", + "A list of developers who provide the apps.", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:provides-files: (nullable) + * + * A list of file paths which the apps must provide. + * + * Used to search for apps which provide specific files on the local + * file system. + * + * This may be %NULL to not filter on file paths. An empty array is + * considered equivalent to %NULL. + * + * Since: 43 + */ + props[PROP_PROVIDES_FILES] = + g_param_spec_boxed ("provides-files", "Provides Files", + "A list of file paths which the apps must provide.", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:released-since: (nullable) + * + * A date/time which apps must have been released since (exclusive). + * + * Used to search for apps which have been updated recently. + * + * This may be %NULL to not filter on release date. + * + * Since: 43 + */ + props[PROP_RELEASED_SINCE] = + g_param_spec_boxed ("released-since", "Released Since", + "A date/time which apps must have been released since (exclusive).", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:is-curated: + * + * Whether apps must be curated (%GS_APP_QUERY_TRISTATE_TRUE), or not + * curated (%GS_APP_QUERY_TRISTATE_FALSE). + * + * If this is %GS_APP_QUERY_TRISTATE_UNSET, apps are not filtered by + * their curation state. + * + * ‘Curated’ apps have been reviewed and picked by an editor to be + * promoted to users in some way. They should be high quality and + * feature complete. + * + * Since: 43 + */ + props[PROP_IS_CURATED] = + g_param_spec_enum ("is-curated", "Is Curated", + "Whether apps must be curated, or not curated.", + GS_TYPE_APP_QUERY_TRISTATE, + GS_APP_QUERY_TRISTATE_UNSET, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:is-featured: + * + * Whether apps must be featured (%GS_APP_QUERY_TRISTATE_TRUE), or not + * featured (%GS_APP_QUERY_TRISTATE_FALSE). + * + * If this is %GS_APP_QUERY_TRISTATE_UNSET, apps are not filtered by + * their featured state. + * + * ‘Featured’ apps have been selected by the distribution or software + * source to be highlighted or promoted to users in some way. They + * should be high quality and feature complete. + * + * Since: 43 + */ + props[PROP_IS_FEATURED] = + g_param_spec_enum ("is-featured", "Is Featured", + "Whether apps must be featured, or not featured.", + GS_TYPE_APP_QUERY_TRISTATE, + GS_APP_QUERY_TRISTATE_UNSET, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:category: (nullable) + * + * A category which apps must be in. + * + * If this is %NULL, apps are not filtered by category. + * + * Since: 43 + */ + props[PROP_CATEGORY] = + g_param_spec_object ("category", "Category", + "A category which apps must be in.", + GS_TYPE_CATEGORY, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:is-installed: + * + * Whether apps must be installed (%GS_APP_QUERY_TRISTATE_TRUE), or not + * installed (%GS_APP_QUERY_TRISTATE_FALSE). + * + * If this is %GS_APP_QUERY_TRISTATE_UNSET, apps are not filtered by + * their installed state. + * + * Since: 43 + */ + props[PROP_IS_INSTALLED] = + g_param_spec_enum ("is-installed", "Is Installed", + "Whether apps must be installed, or not installed.", + GS_TYPE_APP_QUERY_TRISTATE, + GS_APP_QUERY_TRISTATE_UNSET, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:keywords: + * + * A set of search keywords which apps must match. + * + * Search matches may be done against multiple properties of the app, + * such as its name, description, supported content types, defined + * keywords, etc. The keywords in this property may be stemmed in an + * undefined way after being retrieved from #GsAppQuery. + * + * If this is %NULL, apps are not filtered by matches to this set of + * keywords. An empty array is considered equivalent to %NULL. + * + * Since: 43 + */ + props[PROP_KEYWORDS] = + g_param_spec_boxed ("keywords", "Keywords", + "A set of search keywords which apps must match.", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:alternate-of: (nullable) + * + * An app which apps must be related to. + * + * The definition of ‘related to’ depends on the code consuming + * #GsAppQuery, but it will typically be other applications which + * implement the same feature, or other applications which are packaged + * together with this one. + * + * If this is %NULL, apps are not filtered by alternatives. + * + * Since: 43 + */ + props[PROP_ALTERNATE_OF] = + g_param_spec_object ("alternate-of", "Alternate Of", + "An app which apps must be related to.", + GS_TYPE_APP, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:provides-tag: (nullable) + * + * A tag which apps must provide. + * + * The interpretation of the tag depends on #GsAppQuery:provides-type, + * which must not be %GS_APP_QUERY_PROVIDES_UNKNOWN if this is + * non-%NULL. Typically a tag will be a content type which the app + * implements, or the name of a printer which the app provides the + * driver for, etc. + * + * If this is %NULL, apps are not filtered by what they provide. + * + * Since: 43 + */ + props[PROP_PROVIDES_TAG] = + g_param_spec_string ("provides-tag", "Provides Tag", + "A tag which apps must provide.", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsAppQuery:provides-type: + * + * The type of #GsAppQuery:provides-tag. + * + * If this is %GS_APP_QUERY_PROVIDES_UNKNOWN, apps are not filtered by + * what they provide. + * + * Since: 43 + */ + props[PROP_PROVIDES_TYPE] = + g_param_spec_enum ("provides-type", "Provides Type", + "The type of #GsAppQuery:provides-tag.", + GS_TYPE_APP_QUERY_PROVIDES_TYPE, GS_APP_QUERY_PROVIDES_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static void +gs_app_query_init (GsAppQuery *self) +{ + self->is_curated = GS_APP_QUERY_TRISTATE_UNSET; + self->is_featured = GS_APP_QUERY_TRISTATE_UNSET; + self->is_installed = GS_APP_QUERY_TRISTATE_UNSET; + self->provides_type = GS_APP_QUERY_PROVIDES_UNKNOWN; +} + +/** + * gs_app_query_new: + * @first_property_name: name of the first #GObject property + * @...: value for the first property, followed by additional property/value + * pairs, then a terminating %NULL + * + * Create a new #GsAppQuery containing the given query properties. + * + * Returns: (transfer full): a new #GsAppQuery + * Since: 43 + */ +GsAppQuery * +gs_app_query_new (const gchar *first_property_name, + ...) +{ + va_list args; + g_autoptr(GsAppQuery) query = NULL; + + va_start (args, first_property_name); + query = GS_APP_QUERY (g_object_new_valist (GS_TYPE_APP_QUERY, first_property_name, args)); + va_end (args); + + return g_steal_pointer (&query); +} + +/** + * gs_app_query_get_refine_flags: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:refine-flags. + * + * Returns: the refine flags for the query + * Since: 43 + */ +GsPluginRefineFlags +gs_app_query_get_refine_flags (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), GS_PLUGIN_REFINE_FLAGS_NONE); + + return self->refine_flags; +} + +/** + * gs_app_query_get_max_results: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:max-results. + * + * Returns: the maximum number of results to return for the query, or `0` to + * indicate no limit + * Since: 43 + */ +guint +gs_app_query_get_max_results (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), 0); + + return self->max_results; +} + +/** + * gs_app_query_get_dedupe_flags: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:dedupe-flags. + * + * Returns: the dedupe flags for the query + * Since: 43 + */ +GsAppListFilterFlags +gs_app_query_get_dedupe_flags (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), GS_APP_LIST_FILTER_FLAG_NONE); + + return self->dedupe_flags; +} + +/** + * gs_app_query_get_sort_func: + * @self: a #GsAppQuery + * @user_data_out: (out) (transfer none) (optional) (nullable): return location + * for the #GsAppQuery:sort-user-data, or %NULL to ignore + * + * Get the value of #GsAppQuery:sort-func. + * + * Returns: (nullable): the sort function for the query + * Since: 43 + */ +GsAppListSortFunc +gs_app_query_get_sort_func (GsAppQuery *self, + gpointer *user_data_out) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + if (user_data_out != NULL) + *user_data_out = self->sort_user_data; + + return self->sort_func; +} + +/** + * gs_app_query_get_filter_func: + * @self: a #GsAppQuery + * @user_data_out: (out) (transfer none) (optional) (nullable): return location + * for the #GsAppQuery:filter-user-data, or %NULL to ignore + * + * Get the value of #GsAppQuery:filter-func. + * + * Returns: (nullable): the filter function for the query + * Since: 43 + */ +GsAppListFilterFunc +gs_app_query_get_filter_func (GsAppQuery *self, + gpointer *user_data_out) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + if (user_data_out != NULL) + *user_data_out = self->filter_user_data; + + return self->filter_func; +} + +/** + * gs_app_query_get_n_properties_set: + * @self: a #GsAppQuery + * + * Get the number of query properties which have been set. + * + * These are the properties which determine the query results, rather than ones + * which control refining the results (#GsAppQuery:refine-flags, + * #GsAppQuery:max-results, #GsAppQuery:dedupe-flags, #GsAppQuery:sort-func and + * its user data, #GsAppQuery:filter-func and its user data). + * + * Returns: number of properties set so they will affect query results + * Since: 43 + */ +guint +gs_app_query_get_n_properties_set (GsAppQuery *self) +{ + guint n = 0; + + g_return_val_if_fail (GS_IS_APP_QUERY (self), 0); + + if (self->provides_files != NULL) + n++; + if (self->released_since != NULL) + n++; + if (self->is_curated != GS_APP_QUERY_TRISTATE_UNSET) + n++; + if (self->is_featured != GS_APP_QUERY_TRISTATE_UNSET) + n++; + if (self->category != NULL) + n++; + if (self->is_installed != GS_APP_QUERY_TRISTATE_UNSET) + n++; + if (self->deployment_featured != NULL) + n++; + if (self->developers != NULL) + n++; + if (self->keywords != NULL) + n++; + if (self->alternate_of != NULL) + n++; + if (self->provides_tag != NULL) + n++; + + return n; +} + +/** + * gs_app_query_get_provides_files: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:provides-files. + * + * Returns: (nullable): a list of file paths which the apps must provide, + * or %NULL to not filter on file paths + * Since: 43 + */ +const gchar * const * +gs_app_query_get_provides_files (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + /* Always return %NULL or a non-empty array */ + g_assert (self->provides_files == NULL || self->provides_files[0] != NULL); + + return (const gchar * const *) self->provides_files; +} + +/** + * gs_app_query_get_released_since: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:released-since. + * + * Returns: (nullable): a date/time which apps must have been released since, + * or %NULL to not filter on release date + * Since: 43 + */ +GDateTime * +gs_app_query_get_released_since (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + return self->released_since; +} + +/** + * gs_app_query_get_is_curated: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:is-curated. + * + * Returns: %GS_APP_QUERY_TRISTATE_TRUE if apps must be curated, + * %GS_APP_QUERY_TRISTATE_FALSE if they must be not curated, or + * %GS_APP_QUERY_TRISTATE_UNSET if it doesn’t matter + * Since: 43 + */ +GsAppQueryTristate +gs_app_query_get_is_curated (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), GS_APP_QUERY_TRISTATE_UNSET); + + return self->is_curated; +} + +/** + * gs_app_query_get_is_featured: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:is-featured. + * + * Returns: %GS_APP_QUERY_TRISTATE_TRUE if apps must be featured, + * %GS_APP_QUERY_TRISTATE_FALSE if they must be not featured, or + * %GS_APP_QUERY_TRISTATE_UNSET if it doesn’t matter + * Since: 43 + */ +GsAppQueryTristate +gs_app_query_get_is_featured (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), GS_APP_QUERY_TRISTATE_UNSET); + + return self->is_featured; +} + +/** + * gs_app_query_get_category: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:category. + * + * Returns: (nullable) (transfer none): a category which apps must be part of, + * or %NULL to not filter on category + * Since: 43 + */ +GsCategory * +gs_app_query_get_category (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + return self->category; +} + +/** + * gs_app_query_get_is_installed: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:is-installed. + * + * Returns: %GS_APP_QUERY_TRISTATE_TRUE if apps must be installed, + * %GS_APP_QUERY_TRISTATE_FALSE if they must be not installed, or + * %GS_APP_QUERY_TRISTATE_UNSET if it doesn’t matter + * Since: 43 + */ +GsAppQueryTristate +gs_app_query_get_is_installed (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), GS_APP_QUERY_TRISTATE_UNSET); + + return self->is_installed; +} + +/** + * gs_app_query_get_deployment_featured: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:deployment-featured. + * + * Returns: (nullable): a list of `GnomeSoftware::DeploymentFeatured` app keys, + * which the apps have set in a custom key, or %NULL to not filter on this + * Since: 43 + */ +const gchar * const * +gs_app_query_get_deployment_featured (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + /* Always return %NULL or a non-empty array */ + g_assert (self->deployment_featured == NULL || self->deployment_featured[0] != NULL); + + return (const gchar * const *) self->deployment_featured; +} + +/** + * gs_app_query_get_developers: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:developers. + * + * Returns: (nullable): a list of developers who provide the apps, + * or %NULL to not filter by it + * Since: 43 + */ +const gchar * const * +gs_app_query_get_developers (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + /* Always return %NULL or a non-empty array */ + g_assert (self->developers == NULL || self->developers[0] != NULL); + + return (const gchar * const *) self->developers; +} + +/** + * gs_app_query_get_keywords: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:keywords. + * + * Returns: a set of search keywords which apps must match, or %NULL to not + * filter by it + * Since: 43 + */ +const gchar * const * +gs_app_query_get_keywords (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + /* Always return %NULL or a non-empty array */ + g_assert (self->keywords == NULL || self->keywords[0] != NULL); + + return (const gchar * const *) self->keywords; +} + +/** + * gs_app_query_get_alternate_of: + * @self: a #GsAppQuery + * + * Get the value of #GsAppQuery:alternate-of. + * + * Returns: (nullable) (transfer none): an app which apps must be related to, + * or %NULL to not filter on alternates + * Since: 43 + */ +GsApp * +gs_app_query_get_alternate_of (GsAppQuery *self) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), NULL); + + return self->alternate_of; +} + +/** + * gs_app_query_get_provides: + * @self: a #GsAppQuery + * @out_provides_tag: (transfer none) (optional) (nullable) (out): return + * location for the value of #GsAppQuery:provides-tag, or %NULL to ignore + * + * Get the value of #GsAppQuery:provides-type and #GsAppQuery:provides-tag. + * + * Returns: the type of tag to filter on, or %GS_APP_QUERY_PROVIDES_UNKNOWN to + * not filter on provides + * Since: 43 + */ +GsAppQueryProvidesType +gs_app_query_get_provides (GsAppQuery *self, + const gchar **out_provides_tag) +{ + g_return_val_if_fail (GS_IS_APP_QUERY (self), GS_APP_QUERY_PROVIDES_UNKNOWN); + + if (out_provides_tag != NULL) + *out_provides_tag = self->provides_tag; + + return self->provides_type; +} diff --git a/lib/gs-app-query.h b/lib/gs-app-query.h new file mode 100644 index 0000000..b66c56e --- /dev/null +++ b/lib/gs-app-query.h @@ -0,0 +1,108 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-app-list.h" +#include "gs-category.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +/** + * GsAppQueryTristate: + * @GS_APP_QUERY_TRISTATE_UNSET: Value is unset. + * @GS_APP_QUERY_TRISTATE_FALSE: False. Equal in value to %FALSE. + * @GS_APP_QUERY_TRISTATE_TRUE: True. Equal in value to %TRUE. + * + * A type for storing a boolean value which can also have an ‘unknown’ or + * ‘unset’ state. + * + * Within #GsAppQuery this is used for boolean query properties which are unset + * by default so that they don’t affect the query. + * + * Since: 43 + */ +typedef enum +{ + GS_APP_QUERY_TRISTATE_UNSET = -1, + GS_APP_QUERY_TRISTATE_FALSE = 0, + GS_APP_QUERY_TRISTATE_TRUE = 1, +} GsAppQueryTristate; + +/** + * GsAppQueryProvidesType: + * @GS_APP_QUERY_PROVIDES_UNKNOWN: Format is unknown and value is unset. + * @GS_APP_QUERY_PROVIDES_PACKAGE_NAME: A package name in whatever ID format is + * used natively by the current distro. + * @GS_APP_QUERY_PROVIDES_GSTREAMER: A GStreamer plugin name which the app must + * provide. + * @GS_APP_QUERY_PROVIDES_FONT: A font name which the app must provide. + * @GS_APP_QUERY_PROVIDES_MIME_HANDLER: A MIME type/content type which the app + * must support. + * @GS_APP_QUERY_PROVIDES_PS_DRIVER: A printer/PostScript driver which the app + * must provide. + * @GS_APP_QUERY_PROVIDES_PLASMA: A Plasma ID which the app must provide. + * (FIXME: It’s not really clear what this means, but it’s historically been + * supported.) + * + * A type for identifying the format or meaning of #GsAppQuery:provides-tag. + * + * This allows querying for apps which provide various types of functionality, + * such as printer drivers or fonts. + * + * Since: 43 + */ +typedef enum { + GS_APP_QUERY_PROVIDES_UNKNOWN = 0, + GS_APP_QUERY_PROVIDES_PACKAGE_NAME, + GS_APP_QUERY_PROVIDES_GSTREAMER, + GS_APP_QUERY_PROVIDES_FONT, + GS_APP_QUERY_PROVIDES_MIME_HANDLER, + GS_APP_QUERY_PROVIDES_PS_DRIVER, + GS_APP_QUERY_PROVIDES_PLASMA, +} GsAppQueryProvidesType; + +#define GS_TYPE_APP_QUERY (gs_app_query_get_type ()) + +G_DECLARE_FINAL_TYPE (GsAppQuery, gs_app_query, GS, APP_QUERY, GObject) + +GsAppQuery *gs_app_query_new (const gchar *first_property_name, + ...) G_GNUC_NULL_TERMINATED; + +GsPluginRefineFlags gs_app_query_get_refine_flags (GsAppQuery *self); +guint gs_app_query_get_max_results (GsAppQuery *self); +GsAppListFilterFlags gs_app_query_get_dedupe_flags (GsAppQuery *self); +GsAppListSortFunc gs_app_query_get_sort_func (GsAppQuery *self, + gpointer *user_data_out); +GsAppListFilterFunc gs_app_query_get_filter_func (GsAppQuery *self, + gpointer *user_data_out); + +guint gs_app_query_get_n_properties_set (GsAppQuery *self); + +const gchar * const *gs_app_query_get_provides_files (GsAppQuery *self); +GDateTime *gs_app_query_get_released_since (GsAppQuery *self); +GsAppQueryTristate gs_app_query_get_is_curated (GsAppQuery *self); +GsAppQueryTristate gs_app_query_get_is_featured (GsAppQuery *self); +GsCategory *gs_app_query_get_category (GsAppQuery *self); +GsAppQueryTristate gs_app_query_get_is_installed (GsAppQuery *self); +const gchar * const *gs_app_query_get_deployment_featured + (GsAppQuery *self); +const gchar * const *gs_app_query_get_developers (GsAppQuery *self); +const gchar * const *gs_app_query_get_keywords (GsAppQuery *self); +GsApp *gs_app_query_get_alternate_of (GsAppQuery *self); +GsAppQueryProvidesType gs_app_query_get_provides (GsAppQuery *self, + const gchar **out_provides_tag); + +G_END_DECLS diff --git a/lib/gs-app.c b/lib/gs-app.c new file mode 100644 index 0000000..51215c7 --- /dev/null +++ b/lib/gs-app.c @@ -0,0 +1,6714 @@ +/* -*- 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) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-app + * @title: GsApp + * @include: gnome-software.h + * @stability: Unstable + * @short_description: An application that is either installed or that can be installed + * + * For GsApps of kind %AS_COMPONENT_KIND_DESKTOP_APP, this object represents a 1:1 mapping + * to a .desktop file. The design is such so you can't have different GsApp's for different + * versions or architectures of a package. For other AppStream component types, GsApp maps + * their properties and %AS_COMPONENT_KIND_GENERIC is used if their type is a generic software + * component. For GNOME Software specific app-like entries, which don't correspond to desktop + * files or distinct software components, but e.g. represent a system update and its individual + * components, use the separate #GsAppSpecialKind enum and %gs_app_set_special_kind while setting + * the AppStream component-kind to generic. + * + * The #GsPluginLoader de-duplicates the GsApp instances that are produced by + * plugins to ensure that there is a single instance of GsApp for each id, making + * the id the primary key for this object. This ensures that actions triggered on + * a #GsApp in different parts of gnome-software can be observed by connecting to + * signals on the #GsApp. + * + * Information about other #GsApp objects can be stored in this object, for + * instance in the gs_app_add_related() method or gs_app_get_history(). + */ + +#include "config.h" + +#include <string.h> +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "gs-app-collation.h" +#include "gs-app-private.h" +#include "gs-desktop-data.h" +#include "gs-enums.h" +#include "gs-icon.h" +#include "gs-key-colors.h" +#include "gs-os-release.h" +#include "gs-plugin.h" +#include "gs-plugin-private.h" +#include "gs-remote-icon.h" +#include "gs-utils.h" + +typedef struct +{ + GMutex mutex; + gchar *id; + gchar *unique_id; + gboolean unique_id_valid; + gchar *branch; + gchar *name; + gchar *renamed_from; + GsAppQuality name_quality; + GPtrArray *icons; /* (nullable) (owned) (element-type AsIcon), sorted by pixel size, smallest first */ + GPtrArray *sources; + GPtrArray *source_ids; + gchar *project_group; + gchar *developer_name; + gchar *agreement; + gchar *version; + gchar *version_ui; + gchar *summary; + GsAppQuality summary_quality; + gchar *summary_missing; + gchar *description; + GsAppQuality description_quality; + GPtrArray *screenshots; + GPtrArray *categories; + GArray *key_colors; /* (nullable) (element-type GdkRGBA) */ + gboolean user_key_colors; + GHashTable *urls; /* (element-type AsUrlKind utf8) (owned) (nullable) */ + GHashTable *launchables; + gchar *url_missing; + gchar *license; + GsAppQuality license_quality; + gchar **menu_path; + gchar *origin; + gchar *origin_ui; + gchar *origin_appstream; + gchar *origin_hostname; + gchar *update_version; + gchar *update_version_ui; + gchar *update_details_markup; + AsUrgencyKind update_urgency; + GsAppPermissions *update_permissions; + GWeakRef management_plugin_weak; /* (element-type GsPlugin) */ + guint match_value; + guint priority; + gint rating; + GArray *review_ratings; + GPtrArray *reviews; /* of AsReview */ + GPtrArray *provided; /* of AsProvided */ + + GsSizeType size_installed_type; + guint64 size_installed; + GsSizeType size_download_type; + guint64 size_download; + GsSizeType size_user_data_type; + guint64 size_user_data; + GsSizeType size_cache_data_type; + guint64 size_cache_data; + + AsComponentKind kind; + GsAppSpecialKind special_kind; + GsAppState state; + GsAppState state_recover; + AsComponentScope scope; + AsBundleKind bundle_kind; + guint progress; /* integer 0–100 (inclusive), or %GS_APP_PROGRESS_UNKNOWN */ + gboolean allow_cancel; + GHashTable *metadata; + GsAppList *addons; + GsAppList *related; + GsAppList *history; + guint64 install_date; + guint64 release_date; + guint64 kudos; + gboolean to_be_installed; + GsAppQuirk quirk; + gboolean license_is_free; + GsApp *runtime; + GFile *local_file; + AsContentRating *content_rating; + AsScreenshot *action_screenshot; /* (nullable) (owned) */ + GCancellable *cancellable; + GsPluginAction pending_action; + GsAppPermissions *permissions; + gboolean is_update_downloaded; + GPtrArray *version_history; /* (element-type AsRelease) (nullable) (owned) */ + GPtrArray *relations; /* (nullable) (element-type AsRelation) (owned) */ + gboolean has_translations; +} GsAppPrivate; + +typedef enum { + PROP_ID = 1, + PROP_NAME, + PROP_VERSION, + PROP_SUMMARY, + PROP_DESCRIPTION, + PROP_RATING, + PROP_KIND, + PROP_SPECIAL_KIND, + PROP_STATE, + PROP_PROGRESS, + PROP_CAN_CANCEL_INSTALLATION, + PROP_INSTALL_DATE, + PROP_RELEASE_DATE, + PROP_QUIRK, + PROP_PENDING_ACTION, + PROP_KEY_COLORS, + PROP_IS_UPDATE_DOWNLOADED, + PROP_URLS, + PROP_URL_MISSING, + PROP_CONTENT_RATING, + PROP_LICENSE, + PROP_SIZE_CACHE_DATA_TYPE, + PROP_SIZE_CACHE_DATA, + PROP_SIZE_DOWNLOAD_TYPE, + PROP_SIZE_DOWNLOAD, + PROP_SIZE_DOWNLOAD_DEPENDENCIES_TYPE, + PROP_SIZE_DOWNLOAD_DEPENDENCIES, + PROP_SIZE_INSTALLED_TYPE, + PROP_SIZE_INSTALLED, + PROP_SIZE_INSTALLED_DEPENDENCIES_TYPE, + PROP_SIZE_INSTALLED_DEPENDENCIES, + PROP_SIZE_USER_DATA_TYPE, + PROP_SIZE_USER_DATA, + PROP_PERMISSIONS, + PROP_RELATIONS, + PROP_ORIGIN_UI, + PROP_HAS_TRANSLATIONS, +} GsAppProperty; + +static GParamSpec *obj_props[PROP_HAS_TRANSLATIONS + 1] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (GsApp, gs_app, G_TYPE_OBJECT) + +static gboolean +_g_set_str (gchar **str_ptr, const gchar *new_str) +{ + if (*str_ptr == new_str || g_strcmp0 (*str_ptr, new_str) == 0) + return FALSE; + g_free (*str_ptr); + *str_ptr = g_strdup (new_str); + return TRUE; +} + +static gboolean +_g_set_strv (gchar ***strv_ptr, gchar **new_strv) +{ + if (*strv_ptr == new_strv) + return FALSE; + g_strfreev (*strv_ptr); + *strv_ptr = g_strdupv (new_strv); + return TRUE; +} + +static gboolean +_g_set_ptr_array (GPtrArray **array_ptr, GPtrArray *new_array) +{ + if (*array_ptr == new_array) + return FALSE; + if (new_array != NULL) + g_ptr_array_ref (new_array); + if (*array_ptr != NULL) + g_ptr_array_unref (*array_ptr); + *array_ptr = new_array; + return TRUE; +} + +static gboolean +_g_set_array (GArray **array_ptr, GArray *new_array) +{ + if (*array_ptr == new_array) + return FALSE; + if (new_array != NULL) + g_array_ref (new_array); + if (*array_ptr != NULL) + g_array_unref (*array_ptr); + *array_ptr = new_array; + return TRUE; +} + +/** + * gs_app_state_to_string: + * @state: the #GsAppState. + * + * Converts the enumerated value to an text representation. + * + * Returns: string version of @state, or %NULL for unknown + **/ +const gchar * +gs_app_state_to_string (GsAppState state) +{ + if (state == GS_APP_STATE_UNKNOWN) + return "unknown"; + if (state == GS_APP_STATE_INSTALLED) + return "installed"; + if (state == GS_APP_STATE_AVAILABLE) + return "available"; + if (state == GS_APP_STATE_PURCHASABLE) + return "purchasable"; + if (state == GS_APP_STATE_PURCHASING) + return "purchasing"; + if (state == GS_APP_STATE_AVAILABLE_LOCAL) + return "local"; + if (state == GS_APP_STATE_QUEUED_FOR_INSTALL) + return "queued"; + if (state == GS_APP_STATE_INSTALLING) + return "installing"; + if (state == GS_APP_STATE_REMOVING) + return "removing"; + if (state == GS_APP_STATE_UPDATABLE) + return "updatable"; + if (state == GS_APP_STATE_UPDATABLE_LIVE) + return "updatable-live"; + if (state == GS_APP_STATE_UNAVAILABLE) + return "unavailable"; + if (state == GS_APP_STATE_PENDING_INSTALL) + return "pending-install"; + if (state == GS_APP_STATE_PENDING_REMOVE) + return "pending-remove"; + return NULL; +} + +static void +gs_app_kv_lpad (GString *str, const gchar *key, const gchar *value) +{ + gs_utils_append_key_value (str, 20, key, value); +} + +static void +gs_app_kv_size (GString *str, + const gchar *key, + GsSizeType size_type, + guint64 value) +{ + g_autofree gchar *tmp = NULL; + + switch (size_type) { + case GS_SIZE_TYPE_UNKNOWN: + gs_app_kv_lpad (str, key, "unknown"); + break; + case GS_SIZE_TYPE_UNKNOWABLE: + gs_app_kv_lpad (str, key, "unknowable"); + break; + case GS_SIZE_TYPE_VALID: + tmp = g_format_size (value); + gs_app_kv_lpad (str, key, tmp); + break; + default: + g_assert_not_reached (); + } +} + +G_GNUC_PRINTF (3, 4) +static void +gs_app_kv_printf (GString *str, const gchar *key, const gchar *fmt, ...) +{ + va_list args; + g_autofree gchar *tmp = NULL; + va_start (args, fmt); + tmp = g_strdup_vprintf (fmt, args); + va_end (args); + gs_app_kv_lpad (str, key, tmp); +} + +static const gchar * +_as_component_quirk_flag_to_string (GsAppQuirk quirk) +{ + switch (quirk) { + case GS_APP_QUIRK_PROVENANCE: + return "provenance"; + case GS_APP_QUIRK_COMPULSORY: + return "compulsory"; + case GS_APP_QUIRK_HAS_SOURCE: + return "has-source"; + case GS_APP_QUIRK_IS_WILDCARD: + return "is-wildcard"; + case GS_APP_QUIRK_NEEDS_REBOOT: + return "needs-reboot"; + case GS_APP_QUIRK_NOT_REVIEWABLE: + return "not-reviewable"; + case GS_APP_QUIRK_NOT_LAUNCHABLE: + return "not-launchable"; + case GS_APP_QUIRK_NEEDS_USER_ACTION: + return "needs-user-action"; + case GS_APP_QUIRK_IS_PROXY: + return "is-proxy"; + case GS_APP_QUIRK_REMOVABLE_HARDWARE: + return "removable-hardware"; + case GS_APP_QUIRK_DEVELOPER_VERIFIED: + return "developer-verified"; + case GS_APP_QUIRK_PARENTAL_FILTER: + return "parental-filter"; + case GS_APP_QUIRK_NEW_PERMISSIONS: + return "new-permissions"; + case GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE: + return "parental-not-launchable"; + case GS_APP_QUIRK_HIDE_FROM_SEARCH: + return "hide-from-search"; + case GS_APP_QUIRK_HIDE_EVERYWHERE: + return "hide-everywhere"; + case GS_APP_QUIRK_DO_NOT_AUTO_UPDATE: + return "do-not-auto-update"; + default: + return NULL; + } +} + +/* mutex must be held */ +static const gchar * +gs_app_get_unique_id_unlocked (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + /* invalid */ + if (priv->id == NULL) + return NULL; + + /* hmm, do what we can */ + if (priv->unique_id == NULL || !priv->unique_id_valid) { + g_free (priv->unique_id); + priv->unique_id = gs_utils_build_unique_id (priv->scope, + priv->bundle_kind, + priv->origin, + priv->id, + priv->branch); + priv->unique_id_valid = TRUE; + } + return priv->unique_id; +} + +/** + * gs_app_compare_priority: + * @app1: a #GsApp + * @app2: a #GsApp + * + * Compares two applications using their priority. + * + * Use `gs_plugin_add_rule(plugin,GS_PLUGIN_RULE_BETTER_THAN,"plugin-name")` + * to set the application priority values. + * + * Returns: a negative value if @app1 is less than @app2, a positive value if + * @app1 is greater than @app2, and zero if @app1 is equal to @app2 + **/ +gint +gs_app_compare_priority (GsApp *app1, GsApp *app2) +{ + GsAppPrivate *priv1 = gs_app_get_instance_private (app1); + GsAppPrivate *priv2 = gs_app_get_instance_private (app2); + guint prio1, prio2; + + g_return_val_if_fail (GS_IS_APP (app1), 0); + g_return_val_if_fail (GS_IS_APP (app2), 0); + + /* prefer prio */ + prio1 = gs_app_get_priority (app1); + prio2 = gs_app_get_priority (app2); + if (prio1 > prio2) + return -1; + if (prio1 < prio2) + return 1; + + /* fall back to bundle kind */ + if (priv1->bundle_kind < priv2->bundle_kind) + return -1; + if (priv1->bundle_kind > priv2->bundle_kind) + return 1; + return 0; +} + +/** + * gs_app_quirk_to_string: + * @quirk: a #GsAppQuirk + * + * Returns the quirk bitfield as a string. + * + * Returns: (transfer full): a string + **/ +static gchar * +gs_app_quirk_to_string (GsAppQuirk quirk) +{ + GString *str = g_string_new (""); + guint64 i; + + /* nothing set */ + if (quirk == GS_APP_QUIRK_NONE) { + g_string_append (str, "none"); + return g_string_free (str, FALSE); + } + + /* get flags */ + for (i = 1; i < GS_APP_QUIRK_LAST; i *= 2) { + if ((quirk & i) == 0) + continue; + g_string_append_printf (str, "%s,", + _as_component_quirk_flag_to_string (i)); + } + + /* nothing recognised */ + if (str->len == 0) { + g_string_append (str, "unknown"); + return g_string_free (str, FALSE); + } + + /* remove trailing comma */ + g_string_truncate (str, str->len - 1); + return g_string_free (str, FALSE); +} + +static gchar * +gs_app_kudos_to_string (guint64 kudos) +{ + g_autoptr(GPtrArray) array = g_ptr_array_new (); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers" + if ((kudos & GS_APP_KUDO_MY_LANGUAGE) > 0) + g_ptr_array_add (array, "my-language"); + if ((kudos & GS_APP_KUDO_RECENT_RELEASE) > 0) + g_ptr_array_add (array, "recent-release"); + if ((kudos & GS_APP_KUDO_FEATURED_RECOMMENDED) > 0) + g_ptr_array_add (array, "featured-recommended"); + if ((kudos & GS_APP_KUDO_MODERN_TOOLKIT) > 0) + g_ptr_array_add (array, "modern-toolkit"); + if ((kudos & GS_APP_KUDO_SEARCH_PROVIDER) > 0) + g_ptr_array_add (array, "search-provider"); + if ((kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0) + g_ptr_array_add (array, "installs-user-docs"); + if ((kudos & GS_APP_KUDO_USES_NOTIFICATIONS) > 0) + g_ptr_array_add (array, "uses-notifications"); + if ((kudos & GS_APP_KUDO_HAS_KEYWORDS) > 0) + g_ptr_array_add (array, "has-keywords"); + if ((kudos & GS_APP_KUDO_HAS_SCREENSHOTS) > 0) + g_ptr_array_add (array, "has-screenshots"); + if ((kudos & GS_APP_KUDO_HIGH_CONTRAST) > 0) + g_ptr_array_add (array, "high-contrast"); + if ((kudos & GS_APP_KUDO_HI_DPI_ICON) > 0) + g_ptr_array_add (array, "hi-dpi-icon"); + if ((kudos & GS_APP_KUDO_SANDBOXED) > 0) + g_ptr_array_add (array, "sandboxed"); + if ((kudos & GS_APP_KUDO_SANDBOXED_SECURE) > 0) + g_ptr_array_add (array, "sandboxed-secure"); +#pragma GCC diagnostic pop + g_ptr_array_add (array, NULL); + return g_strjoinv ("|", (gchar **) array->pdata); +} + +/** + * gs_app_to_string: + * @app: a #GsApp + * + * Converts the application to a string. + * This is not designed to serialize the object but to produce a string suitable + * for debugging. + * + * Returns: A multi-line string + * + * Since: 3.22 + **/ +gchar * +gs_app_to_string (GsApp *app) +{ + GString *str; + + g_return_val_if_fail (GS_IS_APP (app), NULL); + + str = g_string_new ("GsApp:"); + gs_app_to_string_append (app, str); + if (str->len > 0) + g_string_truncate (str, str->len - 1); + return g_string_free (str, FALSE); +} + +/** + * gs_app_to_string_append: + * @app: a #GsApp + * @str: a #GString + * + * Appends the application to an existing string. + * + * Since: 3.26 + **/ +void +gs_app_to_string_append (GsApp *app, GString *str) +{ + GsAppClass *klass; + GsAppPrivate *priv = gs_app_get_instance_private (app); + AsImage *im; + GList *keys; + const gchar *tmp; + guint i; + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GsPlugin) management_plugin = NULL; + GsSizeType size_download_dependencies_type, size_installed_dependencies_type; + guint64 size_download_dependencies_bytes, size_installed_dependencies_bytes; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (str != NULL); + + klass = GS_APP_GET_CLASS (app); + + locker = g_mutex_locker_new (&priv->mutex); + + g_string_append_printf (str, " [%p]\n", app); + gs_app_kv_lpad (str, "kind", as_component_kind_to_string (priv->kind)); + gs_app_kv_lpad (str, "state", gs_app_state_to_string (priv->state)); + if (priv->quirk > 0) { + g_autofree gchar *qstr = gs_app_quirk_to_string (priv->quirk); + gs_app_kv_lpad (str, "quirk", qstr); + } + if (priv->progress == GS_APP_PROGRESS_UNKNOWN) + gs_app_kv_printf (str, "progress", "unknown"); + else + gs_app_kv_printf (str, "progress", "%u%%", priv->progress); + if (priv->id != NULL) + gs_app_kv_lpad (str, "id", priv->id); + if (priv->unique_id != NULL) + gs_app_kv_lpad (str, "unique-id", priv->unique_id); + if (priv->scope != AS_COMPONENT_SCOPE_UNKNOWN) + gs_app_kv_lpad (str, "scope", as_component_scope_to_string (priv->scope)); + if (priv->bundle_kind != AS_BUNDLE_KIND_UNKNOWN) { + gs_app_kv_lpad (str, "bundle-kind", + as_bundle_kind_to_string (priv->bundle_kind)); + } + if (priv->kudos > 0) { + g_autofree gchar *kudo_str = NULL; + kudo_str = gs_app_kudos_to_string (priv->kudos); + gs_app_kv_lpad (str, "kudos", kudo_str); + } + gs_app_kv_printf (str, "kudo-percentage", "%u", + gs_app_get_kudos_percentage (app)); + if (priv->name != NULL) + gs_app_kv_lpad (str, "name", priv->name); + if (priv->action_screenshot != NULL) + gs_app_kv_printf (str, "action-screenshot", "%p", priv->action_screenshot); + for (i = 0; priv->icons != NULL && i < priv->icons->len; i++) { + GIcon *icon = g_ptr_array_index (priv->icons, i); + g_autofree gchar *icon_str = g_icon_to_string (icon); + gs_app_kv_lpad (str, "icon", icon_str); + } + if (priv->match_value != 0) + gs_app_kv_printf (str, "match-value", "%05x", priv->match_value); + if (gs_app_get_priority (app) != 0) + gs_app_kv_printf (str, "priority", "%u", gs_app_get_priority (app)); + if (priv->version != NULL) + gs_app_kv_lpad (str, "version", priv->version); + if (priv->version_ui != NULL) + gs_app_kv_lpad (str, "version-ui", priv->version_ui); + if (priv->update_version != NULL) + gs_app_kv_lpad (str, "update-version", priv->update_version); + if (priv->update_version_ui != NULL) + gs_app_kv_lpad (str, "update-version-ui", priv->update_version_ui); + if (priv->update_details_markup != NULL) + gs_app_kv_lpad (str, "update-details-markup", priv->update_details_markup); + if (priv->update_urgency != AS_URGENCY_KIND_UNKNOWN) { + gs_app_kv_printf (str, "update-urgency", "%u", + priv->update_urgency); + } + if (priv->summary != NULL) + gs_app_kv_lpad (str, "summary", priv->summary); + if (priv->description != NULL) + gs_app_kv_lpad (str, "description", priv->description); + for (i = 0; i < priv->screenshots->len; i++) { + AsScreenshot *ss = g_ptr_array_index (priv->screenshots, i); + g_autofree gchar *key = NULL; + tmp = as_screenshot_get_caption (ss); + im = as_screenshot_get_image (ss, 0, 0); + if (im == NULL) + continue; + key = g_strdup_printf ("screenshot-%02u", i); + gs_app_kv_printf (str, key, "%s [%s]", + as_image_get_url (im), + tmp != NULL ? tmp : "<none>"); + } + for (i = 0; i < priv->sources->len; i++) { + g_autofree gchar *key = NULL; + tmp = g_ptr_array_index (priv->sources, i); + key = g_strdup_printf ("source-%02u", i); + gs_app_kv_lpad (str, key, tmp); + } + for (i = 0; i < priv->source_ids->len; i++) { + g_autofree gchar *key = NULL; + tmp = g_ptr_array_index (priv->source_ids, i); + key = g_strdup_printf ("source-id-%02u", i); + gs_app_kv_lpad (str, key, tmp); + } + if (priv->local_file != NULL) { + g_autofree gchar *fn = g_file_get_path (priv->local_file); + gs_app_kv_lpad (str, "local-filename", fn); + } + if (priv->content_rating != NULL) { + guint age = as_content_rating_get_minimum_age (priv->content_rating); + if (age != G_MAXUINT) { + g_autofree gchar *value = g_strdup_printf ("%u", age); + gs_app_kv_lpad (str, "content-age", value); + } + gs_app_kv_lpad (str, "content-rating", + as_content_rating_get_kind (priv->content_rating)); + } + if (priv->urls != NULL) { + tmp = g_hash_table_lookup (priv->urls, GINT_TO_POINTER (AS_URL_KIND_HOMEPAGE)); + if (tmp != NULL) + gs_app_kv_lpad (str, "url{homepage}", tmp); + } + keys = g_hash_table_get_keys (priv->launchables); + for (GList *l = keys; l != NULL; l = l->next) { + g_autofree gchar *key = NULL; + key = g_strdup_printf ("launchable{%s}", (const gchar *) l->data); + tmp = g_hash_table_lookup (priv->launchables, l->data); + gs_app_kv_lpad (str, key, tmp); + } + g_list_free (keys); + if (priv->license != NULL) { + gs_app_kv_lpad (str, "license", priv->license); + gs_app_kv_lpad (str, "license-is-free", + gs_app_get_license_is_free (app) ? "yes" : "no"); + } + management_plugin = g_weak_ref_get (&priv->management_plugin_weak); + if (management_plugin != NULL) + gs_app_kv_lpad (str, "management-plugin", gs_plugin_get_name (management_plugin)); + if (priv->summary_missing != NULL) + gs_app_kv_lpad (str, "summary-missing", priv->summary_missing); + if (priv->menu_path != NULL && + priv->menu_path[0] != NULL && + priv->menu_path[0][0] != '\0') { + g_autofree gchar *path = g_strjoinv (" → ", priv->menu_path); + gs_app_kv_lpad (str, "menu-path", path); + } + if (priv->branch != NULL) + gs_app_kv_lpad (str, "branch", priv->branch); + if (priv->origin != NULL && priv->origin[0] != '\0') + gs_app_kv_lpad (str, "origin", priv->origin); + if (priv->origin_ui != NULL && priv->origin_ui[0] != '\0') + gs_app_kv_lpad (str, "origin-ui", priv->origin_ui); + if (priv->origin_appstream != NULL && priv->origin_appstream[0] != '\0') + gs_app_kv_lpad (str, "origin-appstream", priv->origin_appstream); + if (priv->origin_hostname != NULL && priv->origin_hostname[0] != '\0') + gs_app_kv_lpad (str, "origin-hostname", priv->origin_hostname); + if (priv->rating != -1) + gs_app_kv_printf (str, "rating", "%i", priv->rating); + if (priv->review_ratings != NULL) { + for (i = 0; i < priv->review_ratings->len; i++) { + guint32 rat = g_array_index (priv->review_ratings, guint32, i); + gs_app_kv_printf (str, "review-rating", "[%u:%u]", + i, rat); + } + } + if (priv->reviews != NULL) + gs_app_kv_printf (str, "reviews", "%u", priv->reviews->len); + if (priv->provided != NULL) { + guint total = 0; + for (i = 0; i < priv->provided->len; i++) + total += as_provided_get_items (AS_PROVIDED (g_ptr_array_index (priv->provided, i)))->len; + gs_app_kv_printf (str, "provided", "%u", total); + } + if (priv->install_date != 0) { + gs_app_kv_printf (str, "install-date", "%" + G_GUINT64_FORMAT "", + priv->install_date); + } + if (priv->release_date != 0) { + gs_app_kv_printf (str, "release-date", "%" + G_GUINT64_FORMAT "", + priv->release_date); + } + + gs_app_kv_size (str, "size-installed", priv->size_installed_type, priv->size_installed); + size_installed_dependencies_type = gs_app_get_size_installed_dependencies (app, &size_installed_dependencies_bytes); + gs_app_kv_size (str, "size-installed-dependencies", size_installed_dependencies_type, size_installed_dependencies_bytes); + gs_app_kv_size (str, "size-download", priv->size_download_type, priv->size_download); + size_download_dependencies_type = gs_app_get_size_download_dependencies (app, &size_download_dependencies_bytes); + gs_app_kv_size (str, "size-download-dependencies", size_download_dependencies_type, size_download_dependencies_bytes); + gs_app_kv_size (str, "size-cache-data", priv->size_cache_data_type, priv->size_cache_data); + gs_app_kv_size (str, "size-user-data", priv->size_user_data_type, priv->size_user_data); + + for (i = 0; i < gs_app_list_length (priv->related); i++) { + GsApp *app_tmp = gs_app_list_index (priv->related, i); + const gchar *id = gs_app_get_unique_id (app_tmp); + if (id == NULL) + id = gs_app_get_source_default (app_tmp); + /* For example PackageKit can create apps without id */ + if (id != NULL) + gs_app_kv_lpad (str, "related", id); + } + for (i = 0; i < gs_app_list_length (priv->history); i++) { + GsApp *app_tmp = gs_app_list_index (priv->history, i); + const gchar *id = gs_app_get_unique_id (app_tmp); + if (id == NULL) + id = gs_app_get_source_default (app_tmp); + /* For example PackageKit can create apps without id */ + if (id != NULL) + gs_app_kv_lpad (str, "history", id); + } + for (i = 0; i < priv->categories->len; i++) { + tmp = g_ptr_array_index (priv->categories, i); + gs_app_kv_lpad (str, "category", tmp); + } + if (priv->user_key_colors) + gs_app_kv_lpad (str, "user-key-colors", "yes"); + for (i = 0; priv->key_colors != NULL && i < priv->key_colors->len; i++) { + GdkRGBA *color = &g_array_index (priv->key_colors, GdkRGBA, i); + g_autofree gchar *key = NULL; + key = g_strdup_printf ("key-color-%02u", i); + gs_app_kv_printf (str, key, "%.0f,%.0f,%.0f", + color->red * 255.f, + color->green * 255.f, + color->blue * 255.f); + } + keys = g_hash_table_get_keys (priv->metadata); + for (GList *l = keys; l != NULL; l = l->next) { + GVariant *val; + const GVariantType *val_type; + g_autofree gchar *key = NULL; + g_autofree gchar *val_str = NULL; + + key = g_strdup_printf ("{%s}", (const gchar *) l->data); + val = g_hash_table_lookup (priv->metadata, l->data); + val_type = g_variant_get_type (val); + if (g_variant_type_equal (val_type, G_VARIANT_TYPE_STRING)) { + val_str = g_variant_dup_string (val, NULL); + } else if (g_variant_type_equal (val_type, G_VARIANT_TYPE_BOOLEAN)) { + val_str = g_strdup (g_variant_get_boolean (val) ? "True" : "False"); + } else if (g_variant_type_equal (val_type, G_VARIANT_TYPE_UINT32)) { + val_str = g_strdup_printf ("%" G_GUINT32_FORMAT, + g_variant_get_uint32 (val)); + } else { + val_str = g_strdup_printf ("unknown type of %s", + g_variant_get_type_string (val)); + } + gs_app_kv_lpad (str, key, val_str); + } + g_list_free (keys); + + for (i = 0; priv->relations != NULL && i < priv->relations->len; i++) { + AsRelation *relation = g_ptr_array_index (priv->relations, i); + gs_app_kv_printf (str, "relation", "%s, %s", + as_relation_kind_to_string (as_relation_get_kind (relation)), + as_relation_item_kind_to_string (as_relation_get_item_kind (relation))); + } + + /* add subclassed info */ + if (klass->to_string != NULL) + klass->to_string (app, str); + + /* print runtime data too */ + if (priv->runtime != NULL) { + g_string_append (str, "\n\tRuntime:\n\t"); + gs_app_to_string_append (priv->runtime, str); + } + g_string_append_printf (str, "\n"); +} + +typedef struct { + GsApp *app; + GParamSpec *pspec; +} AppNotifyData; + +static gboolean +notify_idle_cb (gpointer data) +{ + AppNotifyData *notify_data = data; + + g_object_notify_by_pspec (G_OBJECT (notify_data->app), notify_data->pspec); + + g_object_unref (notify_data->app); + g_free (notify_data); + + return G_SOURCE_REMOVE; +} + +static void +gs_app_queue_notify (GsApp *app, GParamSpec *pspec) +{ + AppNotifyData *notify_data; + + notify_data = g_new (AppNotifyData, 1); + notify_data->app = g_object_ref (app); + notify_data->pspec = pspec; + + g_idle_add (notify_idle_cb, notify_data); +} + +/** + * gs_app_get_id: + * @app: a #GsApp + * + * Gets the application ID. + * + * Returns: The whole ID, e.g. "gimp.desktop" + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_id (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->id; +} + +/** + * gs_app_set_id: + * @app: a #GsApp + * @id: a application ID, e.g. "gimp.desktop" + * + * Sets the application ID. + */ +void +gs_app_set_id (GsApp *app, const gchar *id) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (_g_set_str (&priv->id, id)) + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_scope: + * @app: a #GsApp + * + * Gets the scope of the application. + * + * Returns: the #AsComponentScope, e.g. %AS_COMPONENT_SCOPE_USER + * + * Since: 40 + **/ +AsComponentScope +gs_app_get_scope (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_COMPONENT_SCOPE_UNKNOWN); + return priv->scope; +} + +/** + * gs_app_set_scope: + * @app: a #GsApp + * @scope: a #AsComponentScope, e.g. %AS_COMPONENT_SCOPE_SYSTEM + * + * This sets the scope of the application. + * + * Since: 40 + **/ +void +gs_app_set_scope (GsApp *app, AsComponentScope scope) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if (scope == priv->scope) + return; + + priv->scope = scope; + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_bundle_kind: + * @app: a #GsApp + * + * Gets the bundle kind of the application. + * + * Returns: the #AsComponentScope, e.g. %AS_BUNDLE_KIND_FLATPAK + * + * Since: 40 + **/ +AsBundleKind +gs_app_get_bundle_kind (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_BUNDLE_KIND_UNKNOWN); + return priv->bundle_kind; +} + +/** + * gs_app_set_bundle_kind: + * @app: a #GsApp + * @bundle_kind: a #AsComponentScope, e.g. AS_BUNDLE_KIND_FLATPAK + * + * This sets the bundle kind of the application. + * + * Since: 40 + **/ +void +gs_app_set_bundle_kind (GsApp *app, AsBundleKind bundle_kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if (bundle_kind == priv->bundle_kind) + return; + + priv->bundle_kind = bundle_kind; + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_special_kind: + * @app: a #GsApp + * + * Gets the special occupation of the application. + * + * Returns: the #GsAppSpecialKind, e.g. %GS_APP_SPECIAL_KIND_OS_UPDATE + * + * Since: 40 + **/ +GsAppSpecialKind +gs_app_get_special_kind (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), GS_APP_SPECIAL_KIND_NONE); + return priv->special_kind; +} + +/** + * gs_app_set_special_kind: + * @app: a #GsApp + * @kind: a #GsAppSpecialKind, e.g. %GS_APP_SPECIAL_KIND_OS_UPDATE + * + * This sets the special occupation of the application (making + * the #AsComponentKind of this application %AS_COMPONENT_KIND_GENERIC + * per definition). + * + * Since: 40 + **/ +void +gs_app_set_special_kind (GsApp *app, GsAppSpecialKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + + if (priv->special_kind == kind) + return; + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + priv->special_kind = kind; + gs_app_queue_notify (app, obj_props[PROP_SPECIAL_KIND]); +} + +/** + * gs_app_get_state: + * @app: a #GsApp + * + * Gets the state of the application. + * + * Returns: the #GsAppState, e.g. %GS_APP_STATE_INSTALLED + * + * Since: 40 + **/ +GsAppState +gs_app_get_state (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), GS_APP_STATE_UNKNOWN); + return priv->state; +} + +/** + * gs_app_get_progress: + * @app: a #GsApp + * + * Gets the percentage completion. + * + * Returns: the percentage completion (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN for unknown + * + * Since: 3.22 + **/ +guint +gs_app_get_progress (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), GS_APP_PROGRESS_UNKNOWN); + return priv->progress; +} + +/** + * gs_app_get_allow_cancel: + * @app: a #GsApp + * + * Gets whether the app's installation or upgrade can be cancelled. + * + * Returns: TRUE if cancellation is possible, FALSE otherwise. + * + * Since: 3.26 + **/ +gboolean +gs_app_get_allow_cancel (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return priv->allow_cancel; +} + +/** + * gs_app_set_state_recover: + * @app: a #GsApp + * + * Sets the application state to the last status value that was not + * transient. + * + * Since: 3.22 + **/ +void +gs_app_set_state_recover (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + if (priv->state_recover == GS_APP_STATE_UNKNOWN) + return; + if (priv->state_recover == priv->state) + return; + + g_debug ("recovering state on %s from %s to %s", + priv->id, + gs_app_state_to_string (priv->state), + gs_app_state_to_string (priv->state_recover)); + + /* make sure progress gets reset when recovering state, to prevent + * confusing initial states when going through more than one attempt */ + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + + priv->state = priv->state_recover; + gs_app_queue_notify (app, obj_props[PROP_STATE]); +} + +/* mutex must be held */ +static gboolean +gs_app_set_state_internal (GsApp *app, GsAppState state) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + gboolean state_change_ok = FALSE; + + /* same */ + if (priv->state == state) + return FALSE; + + /* check the state change is allowed */ + switch (priv->state) { + case GS_APP_STATE_UNKNOWN: + /* unknown has to go into one of the stable states */ + if (state == GS_APP_STATE_INSTALLED || + state == GS_APP_STATE_QUEUED_FOR_INSTALL || + state == GS_APP_STATE_AVAILABLE || + state == GS_APP_STATE_AVAILABLE_LOCAL || + state == GS_APP_STATE_UPDATABLE || + state == GS_APP_STATE_UPDATABLE_LIVE || + state == GS_APP_STATE_UNAVAILABLE || + state == GS_APP_STATE_PENDING_INSTALL || + state == GS_APP_STATE_PENDING_REMOVE) + state_change_ok = TRUE; + break; + case GS_APP_STATE_INSTALLED: + /* installed has to go into an action state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_REMOVING || + state == GS_APP_STATE_UNAVAILABLE || + state == GS_APP_STATE_UPDATABLE || + state == GS_APP_STATE_UPDATABLE_LIVE) + state_change_ok = TRUE; + break; + case GS_APP_STATE_QUEUED_FOR_INSTALL: + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_INSTALLING || + state == GS_APP_STATE_AVAILABLE) + state_change_ok = TRUE; + break; + case GS_APP_STATE_AVAILABLE: + /* available has to go into an action state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_QUEUED_FOR_INSTALL || + state == GS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + case GS_APP_STATE_INSTALLING: + /* installing has to go into an stable state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_INSTALLED || + state == GS_APP_STATE_UPDATABLE || + state == GS_APP_STATE_UPDATABLE_LIVE || + state == GS_APP_STATE_AVAILABLE || + state == GS_APP_STATE_PENDING_INSTALL) + state_change_ok = TRUE; + break; + case GS_APP_STATE_REMOVING: + /* removing has to go into an stable state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_UNAVAILABLE || + state == GS_APP_STATE_AVAILABLE || + state == GS_APP_STATE_INSTALLED || + state == GS_APP_STATE_PENDING_REMOVE) + state_change_ok = TRUE; + break; + case GS_APP_STATE_UPDATABLE: + /* updatable has to go into an action state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_REMOVING || + state == GS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + case GS_APP_STATE_UPDATABLE_LIVE: + /* updatable-live has to go into an action state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_REMOVING || + state == GS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + case GS_APP_STATE_UNAVAILABLE: + /* updatable has to go into an action state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_AVAILABLE) + state_change_ok = TRUE; + break; + case GS_APP_STATE_AVAILABLE_LOCAL: + /* local has to go into an action state */ + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_QUEUED_FOR_INSTALL || + state == GS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + case GS_APP_STATE_PENDING_INSTALL: + case GS_APP_STATE_PENDING_REMOVE: + state_change_ok = TRUE; + break; + default: + g_warning ("state %s unhandled", + gs_app_state_to_string (priv->state)); + g_assert_not_reached (); + } + + /* this state change was unexpected */ + if (!state_change_ok) { + g_warning ("State change on %s (%s) from %s to %s is not OK", + gs_app_get_unique_id_unlocked (app), + priv->name, + gs_app_state_to_string (priv->state), + gs_app_state_to_string (state)); + } + + priv->state = state; + + if (state == GS_APP_STATE_UNKNOWN || + state == GS_APP_STATE_AVAILABLE_LOCAL || + state == GS_APP_STATE_AVAILABLE) + priv->install_date = 0; + + /* save this to simplify error handling in the plugins */ + switch (state) { + case GS_APP_STATE_INSTALLING: + case GS_APP_STATE_REMOVING: + case GS_APP_STATE_QUEUED_FOR_INSTALL: + /* transient, so ignore */ + break; + default: + if (priv->state_recover != state) + priv->state_recover = state; + break; + } + + return TRUE; +} + +/** + * gs_app_set_progress: + * @app: a #GsApp + * @percentage: a percentage progress (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN + * + * This sets the progress completion of the application. Use + * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide confidence + * interval. + * + * If called more than once with the same value then subsequent calls + * will be ignored. + * + * Since: 3.22 + **/ +void +gs_app_set_progress (GsApp *app, guint percentage) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (priv->progress == percentage) + return; + if (percentage != GS_APP_PROGRESS_UNKNOWN && percentage > 100) { + g_warning ("cannot set %u%% for %s, setting instead: 100%%", + percentage, gs_app_get_unique_id_unlocked (app)); + percentage = 100; + } + priv->progress = percentage; + gs_app_queue_notify (app, obj_props[PROP_PROGRESS]); +} + +/** + * gs_app_set_allow_cancel: + * @app: a #GsApp + * @allow_cancel: if the installation or upgrade can be cancelled or not + * + * This sets a flag indicating whether the operation can be cancelled or not. + * This is used by the UI to set the "Cancel" button insensitive as + * appropriate. + * + * Since: 3.26 + **/ +void +gs_app_set_allow_cancel (GsApp *app, gboolean allow_cancel) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (priv->allow_cancel == allow_cancel) + return; + priv->allow_cancel = allow_cancel; + gs_app_queue_notify (app, obj_props[PROP_CAN_CANCEL_INSTALLATION]); +} + +static void +gs_app_set_pending_action_internal (GsApp *app, + GsPluginAction action) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + if (priv->pending_action == action) + return; + + priv->pending_action = action; + gs_app_queue_notify (app, obj_props[PROP_PENDING_ACTION]); +} + +/** + * gs_app_set_state: + * @app: a #GsApp + * @state: a #GsAppState, e.g. GS_APP_STATE_UPDATABLE_LIVE + * + * This sets the state of the application. + * The following state diagram explains the typical states. + * All applications start in state %GS_APP_STATE_UNKNOWN, + * but the frontend is not supposed to see GsApps with this state. + * + * Plugins are responsible for changing the state to one of the other + * states before the GsApp is passed to the frontend. + * + * |[ + * UPDATABLE --> INSTALLING --> INSTALLED + * UPDATABLE --> REMOVING --> AVAILABLE + * INSTALLED --> REMOVING --> AVAILABLE + * AVAILABLE --> INSTALLING --> INSTALLED + * AVAILABLE <--> QUEUED --> INSTALLING --> INSTALLED + * UNKNOWN --> UNAVAILABLE + * ]| + * + * Since: 3.22 + **/ +void +gs_app_set_state (GsApp *app, GsAppState state) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (gs_app_set_state_internal (app, state)) { + /* since the state changed, and the pending-action refers to + * actions that usually change the state, we assign it to the + * appropriate action here */ + GsPluginAction action = GS_PLUGIN_ACTION_UNKNOWN; + if (priv->state == GS_APP_STATE_QUEUED_FOR_INSTALL) { + if (priv->kind == AS_COMPONENT_KIND_REPOSITORY) + action = GS_PLUGIN_ACTION_INSTALL_REPO; + else + action = GS_PLUGIN_ACTION_INSTALL; + } + gs_app_set_pending_action_internal (app, action); + + gs_app_queue_notify (app, obj_props[PROP_STATE]); + } +} + +/** + * gs_app_get_kind: + * @app: a #GsApp + * + * Gets the kind of the application. + * + * Returns: the #AsComponentKind, e.g. %AS_COMPONENT_KIND_UNKNOWN + * + * Since: 40 + **/ +AsComponentKind +gs_app_get_kind (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_COMPONENT_KIND_UNKNOWN); + return priv->kind; +} + +/** + * gs_app_set_kind: + * @app: a #GsApp + * @kind: a #AsComponentKind, e.g. #AS_COMPONENT_KIND_DESKTOP_APP + * + * This sets the kind of the application. + * The following state diagram explains the typical states. + * All applications start with kind %AS_COMPONENT_KIND_UNKNOWN. + * + * |[ + * PACKAGE --> NORMAL + * PACKAGE --> SYSTEM + * NORMAL --> SYSTEM + * ]| + * + * Since: 40 + **/ +void +gs_app_set_kind (GsApp *app, AsComponentKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + gboolean state_change_ok = FALSE; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (priv->kind == kind) + return; + + /* trying to change */ + if (priv->kind != AS_COMPONENT_KIND_UNKNOWN && + kind == AS_COMPONENT_KIND_UNKNOWN) { + g_warning ("automatically prevented from changing " + "kind on %s from %s to %s!", + gs_app_get_unique_id_unlocked (app), + as_component_kind_to_string (priv->kind), + as_component_kind_to_string (kind)); + return; + } + + /* check the state change is allowed */ + switch (priv->kind) { + case AS_COMPONENT_KIND_UNKNOWN: + case AS_COMPONENT_KIND_GENERIC: + /* all others derive from generic */ + state_change_ok = TRUE; + break; + case AS_COMPONENT_KIND_DESKTOP_APP: + /* desktop has to be reset to override */ + if (kind == AS_COMPONENT_KIND_UNKNOWN) + state_change_ok = TRUE; + break; + default: + /* this can never change state */ + break; + } + + /* this state change was unexpected */ + if (!state_change_ok) { + g_warning ("Kind change on %s from %s to %s is not OK", + priv->id, + as_component_kind_to_string (priv->kind), + as_component_kind_to_string (kind)); + return; + } + + priv->kind = kind; + gs_app_queue_notify (app, obj_props[PROP_KIND]); + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_unique_id: + * @app: a #GsApp + * + * Gets the unique application ID used for de-duplication. + * + * The format is "<scope>/<kind>/<origin>/<id>/<branch>". Any unset fields will + * appear as "*". This string can be used with libappstream's functions for + * handling data IDs, e.g. + * https://www.freedesktop.org/software/appstream/docs/api/appstream-as-utils.html#as-utils-data-id-valid + * + * Returns: The unique ID, e.g. `system/flatpak/flathub/org.gnome.Notes/stable`, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_unique_id (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return gs_app_get_unique_id_unlocked (app); +} + +/** + * gs_app_set_unique_id: + * @app: a #GsApp + * @unique_id: a unique application ID, e.g. `user/fedora/\*\/gimp.desktop/\*` + * + * Sets the unique application ID used for de-duplication. See + * gs_app_get_unique_id() for information about the format. Normally you should + * not have to use this function since the unique ID can be constructed from + * other fields, but it can be useful for unit tests. + */ +void +gs_app_set_unique_id (GsApp *app, const gchar *unique_id) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* check for sanity */ + if (!as_utils_data_id_valid (unique_id)) + g_warning ("unique_id %s not valid", unique_id); + + g_free (priv->unique_id); + priv->unique_id = g_strdup (unique_id); + priv->unique_id_valid = TRUE; +} + +/** + * gs_app_get_name: + * @app: a #GsApp + * + * Gets the application name. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_name (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->name; +} + +/** + * gs_app_set_name: + * @app: a #GsApp + * @quality: A #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST + * @name: The short localized name, e.g. "Calculator" + * + * Sets the application name. + * + * Since: 3.22 + **/ +void +gs_app_set_name (GsApp *app, GsAppQuality quality, const gchar *name) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality < priv->name_quality) + return; + priv->name_quality = quality; + if (_g_set_str (&priv->name, name)) + gs_app_queue_notify (app, obj_props[PROP_NAME]); +} + +/** + * gs_app_get_renamed_from: + * @app: a #GsApp + * + * Gets the old human-readable name of an application that's being renamed, the + * same name that was returned by gs_app_get_name() before the rename. + * + * Returns: (nullable): a string, or %NULL for unset + * + * Since: 40 + **/ +const gchar * +gs_app_get_renamed_from (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->renamed_from; +} + +/** + * gs_app_set_renamed_from: + * @app: a #GsApp + * @renamed_from: (nullable): The old name, e.g. "Iagno" + * + * Sets the old name of an application that's being renamed + * + * Since: 40 + **/ +void +gs_app_set_renamed_from (GsApp *app, const gchar *renamed_from) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->renamed_from, renamed_from); +} + +/** + * gs_app_get_branch: + * @app: a #GsApp + * + * Gets the application branch. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_branch (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->branch; +} + +/** + * gs_app_set_branch: + * @app: a #GsApp + * @branch: The branch, e.g. "master" + * + * Sets the application branch. + * + * Since: 3.22 + **/ +void +gs_app_set_branch (GsApp *app, const gchar *branch) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (_g_set_str (&priv->branch, branch)) + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_source_default: + * @app: a #GsApp + * + * Gets the default source. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_source_default (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + if (priv->sources->len == 0) + return NULL; + return g_ptr_array_index (priv->sources, 0); +} + +/** + * gs_app_add_source: + * @app: a #GsApp + * @source: a source name + * + * Adds a source name for the application. + * + * Since: 3.22 + **/ +void +gs_app_add_source (GsApp *app, const gchar *source) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (source != NULL); + + locker = g_mutex_locker_new (&priv->mutex); + + /* check source doesn't already exist */ + for (i = 0; i < priv->sources->len; i++) { + tmp = g_ptr_array_index (priv->sources, i); + if (g_strcmp0 (tmp, source) == 0) + return; + } + g_ptr_array_add (priv->sources, g_strdup (source)); +} + +/** + * gs_app_get_sources: + * @app: a #GsApp + * + * Gets the list of sources for the application. + * + * Returns: (element-type utf8) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_sources (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->sources; +} + +/** + * gs_app_set_sources: + * @app: a #GsApp + * @sources: The non-localized short names, e.g. ["gnome-calculator"] + * + * This name is used for the update page if the application is collected into + * the 'OS Updates' group. + * It is typically the package names, although this should not be relied upon. + * + * Since: 3.22 + **/ +void +gs_app_set_sources (GsApp *app, GPtrArray *sources) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->sources, sources); +} + +/** + * gs_app_get_source_id_default: + * @app: a #GsApp + * + * Gets the default source ID. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_source_id_default (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + if (priv->source_ids->len == 0) + return NULL; + return g_ptr_array_index (priv->source_ids, 0); +} + +/** + * gs_app_get_source_ids: + * @app: a #GsApp + * + * Gets the list of source IDs. + * + * Returns: (element-type utf8) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_source_ids (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->source_ids; +} + +/** + * gs_app_clear_source_ids: + * @app: a #GsApp + * + * Clear the list of source IDs. + * + * Since: 3.22 + **/ +void +gs_app_clear_source_ids (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_set_size (priv->source_ids, 0); +} + +/** + * gs_app_set_source_ids: + * @app: a #GsApp + * @source_ids: The source-id, e.g. ["gnome-calculator;0.134;fedora"] + * or ["/home/hughsie/.local/share/applications/0ad.desktop"] + * + * This ID is used internally to the controlling plugin. + * + * Since: 3.22 + **/ +void +gs_app_set_source_ids (GsApp *app, GPtrArray *source_ids) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->source_ids, source_ids); +} + +/** + * gs_app_add_source_id: + * @app: a #GsApp + * @source_id: a source ID, e.g. "gnome-calculator;0.134;fedora" + * + * Adds a source ID to the application. + * + * Since: 3.22 + **/ +void +gs_app_add_source_id (GsApp *app, const gchar *source_id) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (source_id != NULL); + + /* only add if not already present */ + for (i = 0; i < priv->source_ids->len; i++) { + tmp = g_ptr_array_index (priv->source_ids, i); + if (g_strcmp0 (tmp, source_id) == 0) + return; + } + g_ptr_array_add (priv->source_ids, g_strdup (source_id)); +} + +/** + * gs_app_get_project_group: + * @app: a #GsApp + * + * Gets a project group for the application. + * Applications belonging to other project groups may not be shown in + * this software center. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_project_group (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->project_group; +} + +/** + * gs_app_get_developer_name: + * @app: a #GsApp + * + * Gets the developer name for the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_developer_name (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->developer_name; +} + +/** + * gs_app_set_project_group: + * @app: a #GsApp + * @project_group: The non-localized project group, e.g. "GNOME" or "KDE" + * + * Sets a project group for the application. + * + * Since: 3.22 + **/ +void +gs_app_set_project_group (GsApp *app, const gchar *project_group) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->project_group, project_group); +} + +/** + * gs_app_set_developer_name: + * @app: a #GsApp + * @developer_name: The developer name, e.g. "Richard Hughes" + * + * Sets a developer name for the application. + * + * Since: 3.22 + **/ +void +gs_app_set_developer_name (GsApp *app, const gchar *developer_name) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->developer_name, developer_name); +} + +/** + * gs_app_get_icon_for_size: + * @app: a #GsApp + * @size: size (width or height, square) of the icon to fetch, in device pixels + * @scale: scale of the icon to fetch, typically from gtk_widget_get_scale_factor() + * @fallback_icon_name: (nullable): name of an icon to load as a fallback if + * no other suitable one is found, or %NULL for no fallback + * + * Finds the most appropriate icon in the @app’s set of icons to be loaded at + * the given @size×@scale to represent the application. This might be provided + * by the backend at the given @size, or downsized from a larger icon provided + * by the backend. The return value is guaranteed to be suitable for loading as + * a pixbuf at @size, if it’s not %NULL. + * + * If an image at least @size pixels in width isn’t available, and + * @fallback_icon_name has not been provided, %NULL will be returned. If + * @fallback_icon_name has been provided, a #GIcon representing that will be + * returned, and %NULL is guaranteed not to be returned. + * + * Icons which come from a remote server (over HTTP or HTTPS) will be returned + * as a pointer into a local cache, which may not have been populated. You must + * call gs_remote_icon_ensure_cached() on icons of type #GsRemoteIcon to + * download them; this function will not do that for you. + * + * This function may do disk I/O or image resizing, but it will not do network + * I/O to load a pixbuf. It should be acceptable to call this from a UI thread. + * + * Returns: (transfer full) (nullable): a #GIcon, or %NULL + * + * Since: 40 + */ +GIcon * +gs_app_get_icon_for_size (GsApp *app, + guint size, + guint scale, + const gchar *fallback_icon_name) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_val_if_fail (GS_IS_APP (app), NULL); + g_return_val_if_fail (size > 0, NULL); + g_return_val_if_fail (scale >= 1, NULL); + + g_debug ("Looking for icon for %s, at size %u×%u, with fallback %s", + gs_app_get_id (app), size, scale, fallback_icon_name); + + /* See if there’s an icon the right size, or the first one which is too + * big which could be scaled down. Note that the icons array may be + * lazily created. */ + for (guint i = 0; priv->icons != NULL && i < priv->icons->len; i++) { + GIcon *icon = priv->icons->pdata[i]; + g_autofree gchar *icon_str = g_icon_to_string (icon); + guint icon_width = gs_icon_get_width (icon); + guint icon_height = gs_icon_get_height (icon); + guint icon_scale = gs_icon_get_scale (icon); + + g_debug ("\tConsidering icon of type %s (%s), width %u×%u", + G_OBJECT_TYPE_NAME (icon), icon_str, icon_width, icon_scale); + + /* Appstream only guarantees the 64x64@1 cached icon is present, ignore other icons that aren't installed. */ + if (G_IS_FILE_ICON (icon) && !(icon_width == 64 && icon_height == 64 && icon_scale == 1)) { + GFile *file = g_file_icon_get_file (G_FILE_ICON (icon)); + if (!g_file_query_exists (file, NULL)) { + continue; + } + } + + /* Ignore icons with unknown width and skip over ones which + * are too small. */ + if (icon_width == 0 || icon_width * icon_scale < size * scale) + continue; + + if (icon_width * icon_scale >= size * scale) + return g_object_ref (icon); + } + + g_debug ("Found no icons of the right size; checking themed icons"); + + /* If there’s a themed icon with no width set, use that, as typically + * themed icons are available in all the right sizes. */ + for (guint i = 0; priv->icons != NULL && i < priv->icons->len; i++) { + GIcon *icon = priv->icons->pdata[i]; + guint icon_width = gs_icon_get_width (icon); + + if (icon_width == 0 && G_IS_THEMED_ICON (icon)) + return g_object_ref (icon); + } + + if (scale > 1) { + g_debug ("Retrying at scale 1"); + return gs_app_get_icon_for_size (app, size, 1, fallback_icon_name); + } else if (fallback_icon_name != NULL) { + g_debug ("Using fallback icon %s", fallback_icon_name); + return g_themed_icon_new (fallback_icon_name); + } else { + g_debug ("No icon found"); + return NULL; + } +} + +/** + * gs_app_get_action_screenshot: + * @app: a #GsApp + * + * Gets a screenshot for the pending user action. + * + * Returns: (transfer none) (nullable): a #AsScreenshot, or %NULL + * + * Since: 40 + **/ +AsScreenshot * +gs_app_get_action_screenshot (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->action_screenshot; +} + +/** + * gs_app_get_icons: + * @app: a #GsApp + * + * Gets the icons for the application. + * + * This will never return an empty array; it will always return either %NULL or + * a non-empty array. + * + * Returns: (transfer none) (element-type GIcon) (nullable): an array of icons, + * or %NULL if there are no icons + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_icons (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + if (priv->icons != NULL && priv->icons->len == 0) + return NULL; + + return priv->icons; +} + +static gint +icon_sort_width_cb (gconstpointer a, + gconstpointer b) +{ + GIcon *icon_a = *((GIcon **) a); + GIcon *icon_b = *((GIcon **) b); + guint width_a = gs_icon_get_width (icon_a); + guint width_b = gs_icon_get_width (icon_b); + + /* Sort unknown widths (0 value) to the end. */ + if (width_a == 0 && width_b == 0) + return 0; + else if (width_a == 0) + return 1; + else if (width_b == 0) + return -1; + else + return width_a - width_b; +} + +/** + * gs_app_add_icon: + * @app: a #GsApp + * @icon: a #GIcon + * + * Adds an icon to use for the application. + * If the first icon added cannot be loaded then the next one is tried. + * + * Since: 40 + **/ +void +gs_app_add_icon (GsApp *app, GIcon *icon) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (G_IS_ICON (icon)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->icons == NULL) + priv->icons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + g_ptr_array_add (priv->icons, g_object_ref (icon)); + + /* Ensure the array is sorted by increasing width. */ + g_ptr_array_sort (priv->icons, icon_sort_width_cb); +} + +/** + * gs_app_remove_all_icons: + * @app: a #GsApp + * + * Remove all icons from @app. + * + * Since: 40 + */ +void +gs_app_remove_all_icons (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->icons != NULL) + g_ptr_array_set_size (priv->icons, 0); +} + +/** + * gs_app_get_agreement: + * @app: a #GsApp + * + * Gets the agreement text for the application. + * + * Returns: a string in AppStream description format, or %NULL for unset + * + * Since: 3.28 + **/ +const gchar * +gs_app_get_agreement (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->agreement; +} + +/** + * gs_app_set_agreement: + * @app: a #GsApp + * @agreement: The agreement text, e.g. "<p>Foobar</p>" + * + * Sets the application end-user agreement (e.g. a EULA) in AppStream + * description format. + * + * Since: 3.28 + **/ +void +gs_app_set_agreement (GsApp *app, const gchar *agreement) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->agreement, agreement); +} + +/** + * gs_app_get_local_file: + * @app: a #GsApp + * + * Gets the file that backs this application, for instance this might + * be a local file in ~/Downloads that we are installing. + * + * Returns: (transfer none): a #GFile, or %NULL + * + * Since: 3.22 + **/ +GFile * +gs_app_get_local_file (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->local_file; +} + +/** + * gs_app_set_local_file: + * @app: a #GsApp + * @local_file: a #GFile, or %NULL + * + * Sets the file that backs this application, for instance this might + * be a local file in ~/Downloads that we are installing. + * + * Since: 3.22 + **/ +void +gs_app_set_local_file (GsApp *app, GFile *local_file) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->local_file, local_file); +} + +/** + * gs_app_dup_content_rating: + * @app: a #GsApp + * + * Gets the content rating for this application. + * + * Returns: (transfer full) (nullable): a #AsContentRating, or %NULL + * + * Since: 41 + **/ +AsContentRating * +gs_app_dup_content_rating (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return (priv->content_rating != NULL) ? g_object_ref (priv->content_rating) : NULL; +} + +/** + * gs_app_set_content_rating: + * @app: a #GsApp + * @content_rating: a #AsContentRating, or %NULL + * + * Sets the content rating for this application. + * + * Since: 40 + **/ +void +gs_app_set_content_rating (GsApp *app, AsContentRating *content_rating) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (g_set_object (&priv->content_rating, content_rating)) + gs_app_queue_notify (app, obj_props[PROP_CONTENT_RATING]); +} + +/** + * gs_app_get_runtime: + * @app: a #GsApp + * + * Gets the runtime for the installed application. + * + * Returns: (transfer none): a #GsApp, or %NULL for unset + * + * Since: 3.22 + **/ +GsApp * +gs_app_get_runtime (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->runtime; +} + +/** + * gs_app_set_runtime: + * @app: a #GsApp + * @runtime: a #GsApp + * + * Sets the runtime that the installed application requires. + * + * Since: 3.22 + **/ +void +gs_app_set_runtime (GsApp *app, GsApp *runtime) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (runtime)); + g_return_if_fail (app != runtime); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->runtime, runtime); + + /* The runtime adds to the main app’s sizes. */ + gs_app_queue_notify (app, obj_props[PROP_SIZE_DOWNLOAD_DEPENDENCIES_TYPE]); + gs_app_queue_notify (app, obj_props[PROP_SIZE_DOWNLOAD_DEPENDENCIES]); +} + +/** + * gs_app_set_action_screenshot: + * @app: a #GsApp + * @action_screenshot: (transfer none) (nullable): a #AsScreenshot, or %NULL + * + * Sets a screenshot used to represent the action. + * + * Since: 40 + **/ +void +gs_app_set_action_screenshot (GsApp *app, AsScreenshot *action_screenshot) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->action_screenshot, action_screenshot); +} + +typedef enum { + GS_APP_VERSION_FIXUP_RELEASE = 1, + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX = 2, + GS_APP_VERSION_FIXUP_GIT_SUFFIX = 4, + GS_APP_VERSION_FIXUP_LAST, +} GsAppVersionFixup; + +/** + * gs_app_get_ui_version: + * + * convert 1:1.6.2-7.fc17 into "Version 1.6.2" + **/ +static gchar * +gs_app_get_ui_version (const gchar *version, guint64 flags) +{ + guint i; + gchar *new; + gchar *f; + + /* nothing set */ + if (version == NULL) + return NULL; + + /* first remove any epoch */ + for (i = 0; version[i] != '\0'; i++) { + if (version[i] == ':') { + version = &version[i+1]; + break; + } + if (!g_ascii_isdigit (version[i])) + break; + } + + /* then remove any distro suffix */ + new = g_strdup (version); + if ((flags & GS_APP_VERSION_FIXUP_DISTRO_SUFFIX) > 0) { + f = g_strstr_len (new, -1, ".fc"); + if (f != NULL) + *f= '\0'; + f = g_strstr_len (new, -1, ".el"); + if (f != NULL) + *f= '\0'; + } + + /* then remove any release */ + if ((flags & GS_APP_VERSION_FIXUP_RELEASE) > 0) { + f = g_strrstr_len (new, -1, "-"); + if (f != NULL) + *f= '\0'; + } + + /* then remove any git suffix */ + if ((flags & GS_APP_VERSION_FIXUP_GIT_SUFFIX) > 0) { + f = g_strrstr_len (new, -1, ".2012"); + if (f != NULL) + *f= '\0'; + f = g_strrstr_len (new, -1, ".2013"); + if (f != NULL) + *f= '\0'; + } + + return new; +} + +static void +gs_app_ui_versions_invalidate (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_free (priv->version_ui); + g_free (priv->update_version_ui); + priv->version_ui = NULL; + priv->update_version_ui = NULL; +} + +static void +gs_app_ui_versions_populate (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + guint i; + guint64 flags[] = { GS_APP_VERSION_FIXUP_RELEASE | + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX | + GS_APP_VERSION_FIXUP_GIT_SUFFIX, + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX | + GS_APP_VERSION_FIXUP_GIT_SUFFIX, + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX, + 0 }; + + /* try each set of bitfields in order */ + for (i = 0; flags[i] != 0; i++) { + priv->version_ui = gs_app_get_ui_version (priv->version, flags[i]); + priv->update_version_ui = gs_app_get_ui_version (priv->update_version, flags[i]); + if (g_strcmp0 (priv->version_ui, priv->update_version_ui) != 0) { + gs_app_queue_notify (app, obj_props[PROP_VERSION]); + return; + } + gs_app_ui_versions_invalidate (app); + } + + /* we tried, but failed */ + priv->version_ui = g_strdup (priv->version); + priv->update_version_ui = g_strdup (priv->update_version); +} + +/** + * gs_app_get_version: + * @app: a #GsApp + * + * Gets the exact version for the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_version (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->version; +} + +/** + * gs_app_get_version_ui: + * @app: a #GsApp + * + * Gets a version string that can be displayed in a UI. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_version_ui (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + /* work out the two version numbers */ + if (priv->version != NULL && + priv->version_ui == NULL) { + gs_app_ui_versions_populate (app); + } + + return priv->version_ui; +} + +/** + * gs_app_set_version: + * @app: a #GsApp + * @version: The version, e.g. "2:1.2.3.fc19" + * + * This saves the version after stripping out any non-friendly parts, such as + * distro tags, git revisions and that kind of thing. + * + * Since: 3.22 + **/ +void +gs_app_set_version (GsApp *app, const gchar *version) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (_g_set_str (&priv->version, version)) { + gs_app_ui_versions_invalidate (app); + gs_app_queue_notify (app, obj_props[PROP_VERSION]); + } +} + +/** + * gs_app_get_summary: + * @app: a #GsApp + * + * Gets the single-line description of the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_summary (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->summary; +} + +/** + * gs_app_set_summary: + * @app: a #GsApp + * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST + * @summary: a string, e.g. "A graphical calculator for GNOME" + * + * The medium length one-line localized name. + * + * Since: 3.22 + **/ +void +gs_app_set_summary (GsApp *app, GsAppQuality quality, const gchar *summary) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality < priv->summary_quality) + return; + priv->summary_quality = quality; + if (_g_set_str (&priv->summary, summary)) + gs_app_queue_notify (app, obj_props[PROP_SUMMARY]); +} + +/** + * gs_app_get_description: + * @app: a #GsApp + * + * Gets the long multi-line description of the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_description (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->description; +} + +/** + * gs_app_set_description: + * @app: a #GsApp + * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST + * @description: a string, e.g. "GNOME Calculator is a graphical calculator for GNOME..." + * + * Sets the long multi-line description of the application. + * + * Since: 3.22 + **/ +void +gs_app_set_description (GsApp *app, GsAppQuality quality, const gchar *description) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality < priv->description_quality) + return; + priv->description_quality = quality; + _g_set_str (&priv->description, description); +} + +/** + * gs_app_get_url: + * @app: a #GsApp + * @kind: a #AsUrlKind, e.g. %AS_URL_KIND_HOMEPAGE + * + * Gets a web address of a specific type. + * + * Returns: (nullable): a string, or %NULL for unset + * + * Since: 40 + **/ +const gchar * +gs_app_get_url (GsApp *app, AsUrlKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->urls == NULL) + return NULL; + return g_hash_table_lookup (priv->urls, GINT_TO_POINTER (kind)); +} + +/** + * gs_app_set_url: + * @app: a #GsApp + * @kind: a #AsUrlKind, e.g. %AS_URL_KIND_HOMEPAGE + * @url: a web URL, e.g. "http://www.hughsie.com/" + * + * Sets a web address of a specific type. + * + * Since: 40 + **/ +void +gs_app_set_url (GsApp *app, AsUrlKind kind, const gchar *url) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->urls == NULL) + priv->urls = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, g_free); + + g_hash_table_insert (priv->urls, + GINT_TO_POINTER (kind), + g_strdup (url)); + + gs_app_queue_notify (app, obj_props[PROP_URLS]); +} + +/** + * gs_app_get_url_missing: + * @app: a #GsApp + * + * Gets a web address for the application with explanations + * why it does not have an installation candidate. + * + * Returns: (nullable): a string, or %NULL for unset + * + * Since: 40 + **/ +const gchar * +gs_app_get_url_missing (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return priv->url_missing; +} + +/** + * gs_app_set_url_missing: + * @app: a #GsApp + * @url: (nullable): a web URL, e.g. `http://www.packagekit.org/pk-package-not-found.html`, or %NULL + * + * Sets a web address containing explanations why this app + * does not have an installation candidate. + * + * Since: 40 + **/ +void +gs_app_set_url_missing (GsApp *app, const gchar *url) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + + if (g_strcmp0 (priv->url_missing, url) == 0) + return; + g_free (priv->url_missing); + priv->url_missing = g_strdup (url); + gs_app_queue_notify (app, obj_props[PROP_URL_MISSING]); +} + +/** + * gs_app_get_launchable: + * @app: a #GsApp + * @kind: a #AsLaunchableKind, e.g. %AS_LAUNCHABLE_KIND_DESKTOP_ID + * + * Gets a launchable of a specific type. + * + * Returns: a string, or %NULL for unset + * + * Since: 40 + **/ +const gchar * +gs_app_get_launchable (GsApp *app, AsLaunchableKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return g_hash_table_lookup (priv->launchables, + as_launchable_kind_to_string (kind)); +} + +/** + * gs_app_set_launchable: + * @app: a #GsApp + * @kind: a #AsLaunchableKind, e.g. %AS_LAUNCHABLE_KIND_DESKTOP_ID + * @launchable: a way to launch, e.g. "org.gnome.Sysprof2.desktop" + * + * Sets a launchable of a specific type. + * + * Since: 40 + **/ +void +gs_app_set_launchable (GsApp *app, AsLaunchableKind kind, const gchar *launchable) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + gpointer current_value = NULL; + const gchar *key; + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + key = as_launchable_kind_to_string (kind); + if (g_hash_table_lookup_extended (priv->launchables, key, NULL, ¤t_value)) { + if (g_strcmp0 ((const gchar *) current_value, launchable) != 0) + g_debug ("Preventing app '%s' replace of %s's launchable '%s' with '%s'", + priv->name, key, (const gchar *) current_value, launchable); + } else { + g_hash_table_insert (priv->launchables, + (gpointer) as_launchable_kind_to_string (kind), + g_strdup (launchable)); + } +} + +/** + * gs_app_get_license: + * @app: a #GsApp + * + * Gets the project license of the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_license (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->license; +} + +/** + * gs_app_get_license_is_free: + * @app: a #GsApp + * + * Returns if the application is free software. + * + * Returns: %TRUE if the application is free software + * + * Since: 3.22 + **/ +gboolean +gs_app_get_license_is_free (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return priv->license_is_free; +} + +/** + * gs_app_set_license: + * @app: a #GsApp + * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_NORMAL + * @license: a SPDX license string, e.g. "GPL-3.0 AND LGPL-2.0+" + * + * Sets the project licenses used in the application. + * + * Since: 3.22 + **/ +void +gs_app_set_license (GsApp *app, GsAppQuality quality, const gchar *license) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality <= priv->license_quality) + return; + if (license == NULL) + return; + priv->license_quality = quality; + + priv->license_is_free = as_license_is_free_license (license); + + if (_g_set_str (&priv->license, license)) + gs_app_queue_notify (app, obj_props[PROP_LICENSE]); +} + +/** + * gs_app_get_summary_missing: + * @app: a #GsApp + * + * Gets the one-line summary to use when this application is missing. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_summary_missing (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->summary_missing; +} + +/** + * gs_app_set_summary_missing: + * @app: a #GsApp + * @summary_missing: a string, or %NULL + * + * Sets the one-line summary to use when this application is missing. + * + * Since: 3.22 + **/ +void +gs_app_set_summary_missing (GsApp *app, const gchar *summary_missing) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->summary_missing, summary_missing); +} + +static gboolean +_gs_app_has_desktop_group (GsApp *app, const gchar *desktop_group) +{ + guint i; + g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1); + for (i = 0; split[i] != NULL; i++) { + if (!gs_app_has_category (app, split[i])) + return FALSE; + } + return TRUE; +} + +/** + * gs_app_get_menu_path: + * @app: a #GsApp + * + * Returns the menu path which is an array of path elements. + * The resulting array is an internal structure and must not be + * modified or freed. + * + * Returns: a %NULL-terminated array of strings + * + * Since: 3.22 + **/ +gchar ** +gs_app_get_menu_path (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + /* Lazy load. */ + if (priv->menu_path == NULL) { + const gchar *strv[] = { "", NULL, NULL }; + const GsDesktopData *msdata; + gboolean found = FALSE; + + /* find a top level category the app has */ + msdata = gs_desktop_get_data (); + for (gsize i = 0; !found && msdata[i].id != NULL; i++) { + const GsDesktopData *data = &msdata[i]; + for (gsize j = 0; !found && data->mapping[j].id != NULL; j++) { + const GsDesktopMap *map = &data->mapping[j]; + g_autofree gchar *msgctxt = NULL; + + if (g_strcmp0 (map->id, "all") == 0) + continue; + if (g_strcmp0 (map->id, "featured") == 0) + continue; + msgctxt = g_strdup_printf ("Menu of %s", data->name); + for (gsize k = 0; !found && map->fdo_cats[k] != NULL; k++) { + const gchar *tmp = msdata[i].mapping[j].fdo_cats[k]; + if (_gs_app_has_desktop_group (app, tmp)) { + strv[0] = g_dgettext (GETTEXT_PACKAGE, msdata[i].name); + strv[1] = g_dpgettext2 (GETTEXT_PACKAGE, msgctxt, + msdata[i].mapping[j].name); + found = TRUE; + break; + } + } + } + } + + /* always set something to avoid keep searching for this */ + gs_app_set_menu_path (app, (gchar **) strv); + } + + return priv->menu_path; +} + +/** + * gs_app_set_menu_path: + * @app: a #GsApp + * @menu_path: a %NULL-terminated array of strings + * + * Sets the new menu path. The menu path is an array of path elements. + * This function creates a deep copy of the path. + * + * Since: 3.22 + **/ +void +gs_app_set_menu_path (GsApp *app, gchar **menu_path) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_strv (&priv->menu_path, menu_path); +} + +/** + * gs_app_get_origin: + * @app: a #GsApp + * + * Gets the origin for the application, e.g. "fedora". + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_origin (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->origin; +} + +/** + * gs_app_set_origin: + * @app: a #GsApp + * @origin: a string, or %NULL + * + * The origin is the original source of the application e.g. "fedora-updates" + * + * Since: 3.22 + **/ +void +gs_app_set_origin (GsApp *app, const gchar *origin) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (g_strcmp0 (origin, priv->origin) == 0) + return; + + /* trying to change */ + if (priv->origin != NULL && origin != NULL) { + g_warning ("automatically prevented from changing " + "origin on %s from %s to %s!", + gs_app_get_unique_id_unlocked (app), + priv->origin, origin); + return; + } + + g_free (priv->origin); + priv->origin = g_strdup (origin); + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_origin_appstream: + * @app: a #GsApp + * + * Gets the appstream origin for the application, e.g. "fedora". + * + * Returns: a string, or %NULL for unset + * + * Since: 3.28 + **/ +const gchar * +gs_app_get_origin_appstream (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->origin_appstream; +} + +/** + * gs_app_set_origin_appstream: + * @app: a #GsApp + * @origin_appstream: a string, or %NULL + * + * The appstream origin is the appstream source of the application e.g. "fedora" + * + * Since: 3.28 + **/ +void +gs_app_set_origin_appstream (GsApp *app, const gchar *origin_appstream) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (g_strcmp0 (origin_appstream, priv->origin_appstream) == 0) + return; + + g_free (priv->origin_appstream); + priv->origin_appstream = g_strdup (origin_appstream); +} + +/** + * gs_app_get_origin_hostname: + * @app: a #GsApp + * + * Gets the hostname of the origin used to install the application, e.g. + * "fedoraproject.org" or "sdk.gnome.org". + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_origin_hostname (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->origin_hostname; +} + +/** + * gs_app_set_origin_hostname: + * @app: a #GsApp + * @origin_hostname: a string, or %NULL + * + * The origin is the hostname of the source used to install the application + * e.g. "fedoraproject.org" + * + * You can also use a full URL as @origin_hostname and this will be parsed and + * the hostname extracted. This process will also remove any unnecessary DNS + * prefixes like "download" or "mirrors". + * + * Since: 3.22 + **/ +void +gs_app_set_origin_hostname (GsApp *app, const gchar *origin_hostname) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GUri) uri = NULL; + guint i; + const gchar *prefixes[] = { "download.", "mirrors.", NULL }; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (g_strcmp0 (origin_hostname, priv->origin_hostname) == 0) + return; + g_free (priv->origin_hostname); + + /* convert a URL */ + uri = g_uri_parse (origin_hostname, SOUP_HTTP_URI_FLAGS, NULL); + if (uri != NULL) + origin_hostname = g_uri_get_host (uri); + + /* remove some common prefixes */ + for (i = 0; prefixes[i] != NULL; i++) { + if (g_str_has_prefix (origin_hostname, prefixes[i])) + origin_hostname += strlen (prefixes[i]); + } + + /* fallback for localhost */ + if (g_strcmp0 (origin_hostname, "") == 0) + origin_hostname = "localhost"; + + /* success */ + priv->origin_hostname = g_strdup (origin_hostname); +} + +/** + * gs_app_add_screenshot: + * @app: a #GsApp + * @screenshot: a #AsScreenshot + * + * Adds a screenshot to the application. + * + * Since: 40 + **/ +void +gs_app_add_screenshot (GsApp *app, AsScreenshot *screenshot) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (AS_IS_SCREENSHOT (screenshot)); + + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_add (priv->screenshots, g_object_ref (screenshot)); +} + +/** + * gs_app_get_screenshots: + * @app: a #GsApp + * + * Gets the list of screenshots. + * + * Returns: (element-type AsScreenshot) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_screenshots (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->screenshots; +} + +/** + * gs_app_get_update_version: + * @app: a #GsApp + * + * Gets the newest update version. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_update_version (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->update_version; +} + +/** + * gs_app_get_update_version_ui: + * @app: a #GsApp + * + * Gets the update version for the UI. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_update_version_ui (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + /* work out the two version numbers */ + if (priv->update_version != NULL && + priv->update_version_ui == NULL) { + gs_app_ui_versions_populate (app); + } + + return priv->update_version_ui; +} + +static void +gs_app_set_update_version_internal (GsApp *app, const gchar *update_version) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + if (_g_set_str (&priv->update_version, update_version)) + gs_app_ui_versions_invalidate (app); +} + +/** + * gs_app_set_update_version: + * @app: a #GsApp + * @update_version: a string, e.g. "0.1.2.3" + * + * Sets the new version number of the update. + * + * Since: 3.22 + **/ +void +gs_app_set_update_version (GsApp *app, const gchar *update_version) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + gs_app_set_update_version_internal (app, update_version); + gs_app_queue_notify (app, obj_props[PROP_VERSION]); +} + +/** + * gs_app_get_update_details_markup: + * @app: a #GsApp + * + * Gets the multi-line description for the update as a Pango markup. + * + * Returns: a string, or %NULL for unset + * + * Since: 42.0 + **/ +const gchar * +gs_app_get_update_details_markup (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->update_details_markup; +} + +/** + * gs_app_set_update_details_markup: + * @app: a #GsApp + * @markup: a Pango markup + * + * Sets the multi-line description for the update as markup. + * + * See: gs_app_set_update_details_text() + * + * Since: 42.0 + **/ +void +gs_app_set_update_details_markup (GsApp *app, + const gchar *markup) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->update_details_markup, markup); +} + +/** + * gs_app_set_update_details_text: + * @app: a #GsApp + * @text: a text without Pango markup + * + * Sets the multi-line description for the update as text, + * escaping the @text to be safe for a Pango markup. + * + * See: gs_app_set_update_details_markup() + * + * Since: 42.0 + **/ +void +gs_app_set_update_details_text (GsApp *app, + const gchar *text) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (text == NULL) { + _g_set_str (&priv->update_details_markup, NULL); + } else { + gchar *markup = g_markup_escape_text (text, -1); + g_free (priv->update_details_markup); + priv->update_details_markup = markup; + } +} + +/** + * gs_app_get_update_urgency: + * @app: a #GsApp + * + * Gets the update urgency. + * + * Returns: a #AsUrgencyKind, or %AS_URGENCY_KIND_UNKNOWN for unset + * + * Since: 40 + **/ +AsUrgencyKind +gs_app_get_update_urgency (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_URGENCY_KIND_UNKNOWN); + return priv->update_urgency; +} + +/** + * gs_app_set_update_urgency: + * @app: a #GsApp + * @update_urgency: a #AsUrgencyKind + * + * Sets the update urgency. + * + * Since: 40 + **/ +void +gs_app_set_update_urgency (GsApp *app, AsUrgencyKind update_urgency) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (update_urgency == priv->update_urgency) + return; + priv->update_urgency = update_urgency; +} + +/** + * gs_app_dup_management_plugin: + * @app: a #GsApp + * + * Gets the management plugin. + * + * This is some metadata about the application which gives which plugin should + * handle the install, remove or upgrade actions. + * + * Returns: (nullable) (transfer full): the management plugin, or %NULL for unset + * + * Since: 42 + **/ +GsPlugin * +gs_app_dup_management_plugin (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return g_weak_ref_get (&priv->management_plugin_weak); +} + +/** + * gs_app_has_management_plugin: + * @app: a #GsApp + * @plugin: (nullable) (transfer none): a #GsPlugin to check against, or %NULL + * + * Check whether the management plugin for @app is set to @plugin. + * + * If @plugin is %NULL, %TRUE is returned only if the @app has no management + * plugin set. + * + * Returns: %TRUE if @plugin is the management plugin for @app, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_app_has_management_plugin (GsApp *app, + GsPlugin *plugin) +{ + g_autoptr(GsPlugin) app_plugin = gs_app_dup_management_plugin (app); + return (app_plugin == plugin); +} + +/** + * gs_app_set_management_plugin: + * @app: a #GsApp + * @management_plugin: (nullable) (transfer none): a plugin, or %NULL + * + * The management plugin is the plugin that can handle doing install and remove + * operations on the #GsApp. + * + * It is an error to attempt to change the management plugin once it has been + * previously set or to try to use this function on a wildcard application. + * + * Since: 42 + **/ +void +gs_app_set_management_plugin (GsApp *app, + GsPlugin *management_plugin) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GsPlugin) old_plugin = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (management_plugin == NULL || GS_IS_PLUGIN (management_plugin)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* plugins cannot adopt wildcard packages */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + g_warning ("plugins should not set the management plugin on " + "%s to %s -- create a new GsApp in refine()!", + gs_app_get_unique_id_unlocked (app), + (management_plugin != NULL) ? gs_plugin_get_name (management_plugin) : "(null)"); + return; + } + + /* same */ + old_plugin = g_weak_ref_get (&priv->management_plugin_weak); + + if (old_plugin == management_plugin) + return; + + /* trying to change */ + if (old_plugin != NULL && management_plugin != NULL) { + g_warning ("automatically prevented from changing " + "management plugin on %s from %s to %s!", + gs_app_get_unique_id_unlocked (app), + gs_plugin_get_name (old_plugin), + gs_plugin_get_name (management_plugin)); + return; + } + + g_weak_ref_set (&priv->management_plugin_weak, management_plugin); +} + +/** + * gs_app_get_rating: + * @app: a #GsApp + * + * Gets the percentage rating of the application, where 100 is 5 stars. + * + * Returns: a percentage, or -1 for unset + * + * Since: 3.22 + **/ +gint +gs_app_get_rating (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), -1); + return priv->rating; +} + +/** + * gs_app_set_rating: + * @app: a #GsApp + * @rating: a percentage, or -1 for invalid + * + * Gets the percentage rating of the application. + * + * Since: 3.22 + **/ +void +gs_app_set_rating (GsApp *app, gint rating) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (rating == priv->rating) + return; + priv->rating = rating; + gs_app_queue_notify (app, obj_props[PROP_RATING]); +} + +/** + * gs_app_get_review_ratings: + * @app: a #GsApp + * + * Gets the review ratings. + * + * Returns: (element-type guint32) (transfer none): a list + * + * Since: 3.22 + **/ +GArray * +gs_app_get_review_ratings (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->review_ratings; +} + +/** + * gs_app_set_review_ratings: + * @app: a #GsApp + * @review_ratings: (element-type guint32): a list + * + * Sets the review ratings. + * + * Since: 3.22 + **/ +void +gs_app_set_review_ratings (GsApp *app, GArray *review_ratings) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_array (&priv->review_ratings, review_ratings); +} + +/** + * gs_app_get_reviews: + * @app: a #GsApp + * + * Gets all the user-submitted reviews for the application. + * + * Returns: (element-type AsReview) (transfer none): the list of reviews + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_reviews (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->reviews; +} + +/** + * gs_app_add_review: + * @app: a #GsApp + * @review: a #AsReview + * + * Adds a user-submitted review to the application. + * + * Since: 40 + **/ +void +gs_app_add_review (GsApp *app, AsReview *review) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (AS_IS_REVIEW (review)); + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_add (priv->reviews, g_object_ref (review)); +} + +/** + * gs_app_remove_review: + * @app: a #GsApp + * @review: a #AsReview + * + * Removes a user-submitted review to the application. + * + * Since: 40 + **/ +void +gs_app_remove_review (GsApp *app, AsReview *review) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_remove (priv->reviews, review); +} + +/** + * gs_app_get_provided: + * @app: a #GsApp + * + * Gets all the provided item sets for the application. + * + * Returns: (element-type AsProvided) (transfer none): the list of provided items + * + * Since: 40 + **/ +GPtrArray* +gs_app_get_provided (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->provided; +} + +/** + * gs_app_get_provided_for_kind: + * @cpt: a #AsComponent instance. + * @kind: kind of the provided item, e.g. %AS_PROVIDED_KIND_MIMETYPE + * + * Get an #AsProvided object for the given interface type, or %NULL if + * none was found. + * + * Returns: (nullable) (transfer none): the #AsProvided + * + * Since: 40 + */ +AsProvided* +gs_app_get_provided_for_kind (GsApp *app, AsProvidedKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + for (guint i = 0; i < priv->provided->len; i++) { + AsProvided *prov = AS_PROVIDED (g_ptr_array_index (priv->provided, i)); + if (as_provided_get_kind (prov) == kind) + return prov; + } + return NULL; +} + +/** + * gs_app_add_provided: + * @app: a #GsApp + * @kind: the kind of the provided item, e.g. %AS_PROVIDED_KIND_MEDIATYPE + * @item: the item to add. + * + * Adds a provided items of the given kind to the application. + * + * Since: 40 + **/ +void +gs_app_add_provided_item (GsApp *app, AsProvidedKind kind, const gchar *item) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + AsProvided *prov; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (item != NULL); + g_return_if_fail (kind != AS_PROVIDED_KIND_UNKNOWN && kind < AS_PROVIDED_KIND_LAST); + + locker = g_mutex_locker_new (&priv->mutex); + prov = gs_app_get_provided_for_kind (app, kind); + if (prov == NULL) { + prov = as_provided_new (); + as_provided_set_kind (prov, kind); + g_ptr_array_add (priv->provided, prov); + } + as_provided_add_item (prov, item); +} + +/** + * gs_app_get_size_download: + * @app: A #GsApp + * @size_bytes_out: (optional) (out caller-allocates): return location for + * the download size, in bytes, or %NULL to ignore + * + * Get the values of #GsApp:size-download-type and #GsApp:size-download. + * + * If this returns %GS_SIZE_TYPE_VALID, @size_bytes_out (if non-%NULL) will be + * set to the download size. Otherwise, its value will be undefined. + * + * Returns: type of the download size + * Since: 43 + **/ +GsSizeType +gs_app_get_size_download (GsApp *app, + guint64 *size_bytes_out) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + if (size_bytes_out != NULL) + *size_bytes_out = (priv->size_download_type == GS_SIZE_TYPE_VALID) ? priv->size_download : 0; + + return priv->size_download_type; +} + +/** + * gs_app_set_size_download: + * @app: a #GsApp + * @size_type: type of the download size + * @size_bytes: size in bytes + * + * Sets the download size of the application, not including any + * required runtime. + * + * @size_bytes will be ignored unless @size_type is %GS_SIZE_TYPE_VALID. + * + * Since: 43 + **/ +void +gs_app_set_size_download (GsApp *app, + GsSizeType size_type, + guint64 size_bytes) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + if (size_type != GS_SIZE_TYPE_VALID) + size_bytes = 0; + + if (priv->size_download_type != size_type) { + priv->size_download_type = size_type; + gs_app_queue_notify (app, obj_props[PROP_SIZE_DOWNLOAD_TYPE]); + } + + if (priv->size_download != size_bytes) { + priv->size_download = size_bytes; + gs_app_queue_notify (app, obj_props[PROP_SIZE_DOWNLOAD]); + } +} + +/* Add two sizes, accounting for their validity, and checking for overflow. This + * is essentially `out_bytes = a_bytes + b_bytes` with additional checking. + * + * If either of @a_type or @b_type is %GS_SIZE_TYPE_UNKNOWN or + * %GS_SIZE_TYPE_UNKNOWABLE, that type will be propagated to @out_type. + * + * If the sum of @a_bytes and @b_bytes exceeds %G_MAXUINT64, the result in + * @out_bytes will silently be clamped to %G_MAXUINT64. + * + * The lifetime of @app must be at least as long as the lifetime of + * @covered_uids, which allows us to avoid some string copies. + */ +static gboolean +add_sizes (GsApp *app, + GHashTable *covered_uids, + GsSizeType a_type, + guint64 a_bytes, + GsSizeType b_type, + guint64 b_bytes, + GsSizeType *out_type, + guint64 *out_bytes) +{ + g_return_val_if_fail (out_type != NULL, FALSE); + g_return_val_if_fail (out_bytes != NULL, FALSE); + + if (app != NULL && covered_uids != NULL) { + const gchar *id = gs_app_get_unique_id (app); + if (id != NULL && + !g_hash_table_add (covered_uids, (gpointer) id)) + return TRUE; + } + + if (a_type == GS_SIZE_TYPE_VALID && b_type == GS_SIZE_TYPE_VALID) { + *out_type = GS_SIZE_TYPE_VALID; + if (!g_uint64_checked_add (out_bytes, a_bytes, b_bytes)) + *out_bytes = G_MAXUINT64; + return TRUE; + } + + *out_type = (a_type == GS_SIZE_TYPE_UNKNOWABLE || b_type == GS_SIZE_TYPE_UNKNOWABLE) ? GS_SIZE_TYPE_UNKNOWABLE : GS_SIZE_TYPE_UNKNOWN; + *out_bytes = 0; + + return FALSE; +} + +static GsSizeType +get_size_download_dependencies (GsApp *app, + guint64 *size_bytes_out, + GHashTable *covered_uids) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + GsSizeType size_type = GS_SIZE_TYPE_VALID; + guint64 size_bytes = 0; + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + /* add the runtime if this is not installed */ + if (priv->runtime != NULL && + gs_app_get_state (priv->runtime) == GS_APP_STATE_AVAILABLE) { + GsSizeType runtime_size_download_type, runtime_size_download_dependencies_type; + guint64 runtime_size_download_bytes, runtime_size_download_dependencies_bytes; + + runtime_size_download_type = gs_app_get_size_download (priv->runtime, &runtime_size_download_bytes); + + if (add_sizes (priv->runtime, covered_uids, + size_type, size_bytes, + runtime_size_download_type, runtime_size_download_bytes, + &size_type, &size_bytes)) { + runtime_size_download_dependencies_type = get_size_download_dependencies (priv->runtime, + &runtime_size_download_dependencies_bytes, + covered_uids); + + add_sizes (NULL, NULL, + size_type, size_bytes, + runtime_size_download_dependencies_type, runtime_size_download_dependencies_bytes, + &size_type, &size_bytes); + } + } + + /* add related apps */ + for (guint i = 0; i < gs_app_list_length (priv->related); i++) { + GsApp *app_related = gs_app_list_index (priv->related, i); + GsSizeType related_size_download_type, related_size_download_dependencies_type; + guint64 related_size_download_bytes, related_size_download_dependencies_bytes; + + related_size_download_type = gs_app_get_size_download (app_related, &related_size_download_bytes); + + if (!add_sizes (app_related, covered_uids, + size_type, size_bytes, + related_size_download_type, related_size_download_bytes, + &size_type, &size_bytes)) + break; + + related_size_download_dependencies_type = get_size_download_dependencies (app_related, + &related_size_download_dependencies_bytes, + covered_uids); + + if (!add_sizes (NULL, NULL, + size_type, size_bytes, + related_size_download_dependencies_type, related_size_download_dependencies_bytes, + &size_type, &size_bytes)) + break; + } + + if (size_bytes_out != NULL) + *size_bytes_out = (size_type == GS_SIZE_TYPE_VALID) ? size_bytes : 0; + + return size_type; +} + +/** + * gs_app_get_size_download_dependencies: + * @app: A #GsApp + * @size_bytes_out: (optional) (out caller-allocates): return location for + * the download size of dependencies, in bytes, or %NULL to ignore + * + * Get the value of #GsApp:size-download-dependencies-type and + * #GsApp:size-download-dependencies. + * + * If this returns %GS_SIZE_TYPE_VALID, @size_bytes_out (if non-%NULL) will be + * set to the download size of dependencies. Otherwise, its value will be + * undefined. + * + * Returns: type of the download size of dependencies + * Since: 43 + **/ +GsSizeType +gs_app_get_size_download_dependencies (GsApp *app, + guint64 *size_bytes_out) +{ + g_autoptr(GHashTable) covered_uids = NULL; + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + covered_uids = g_hash_table_new_full ((GHashFunc) as_utils_data_id_hash, (GEqualFunc) as_utils_data_id_equal, NULL, NULL); + + return get_size_download_dependencies (app, size_bytes_out, covered_uids); +} + +/** + * gs_app_get_size_installed: + * @app: a #GsApp + * @size_bytes_out: (optional) (out caller-allocates): return location for + * the installed size, in bytes, or %NULL to ignore + * + * Get the values of #GsApp:size-installed-type and #GsApp:size-installed. + * + * If this returns %GS_SIZE_TYPE_VALID, @size_bytes_out (if non-%NULL) will be + * set to the installed size. Otherwise, its value will be undefined. + * + * Returns: type of the installed size + * Since: 43 + **/ +GsSizeType +gs_app_get_size_installed (GsApp *app, + guint64 *size_bytes_out) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + if (size_bytes_out != NULL) + *size_bytes_out = (priv->size_installed_type == GS_SIZE_TYPE_VALID) ? priv->size_installed : 0; + + return priv->size_installed_type; +} + +/** + * gs_app_set_size_installed: + * @app: a #GsApp + * @size_type: type of the installed size + * @size_bytes: size in bytes + * + * Sets the installed size of the application. + * + * @size_bytes will be ignored unless @size_type is %GS_SIZE_TYPE_VALID. + * + * Since: 43 + **/ +void +gs_app_set_size_installed (GsApp *app, + GsSizeType size_type, + guint64 size_bytes) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + if (size_type != GS_SIZE_TYPE_VALID) + size_bytes = 0; + + if (priv->size_installed_type != size_type) { + priv->size_installed_type = size_type; + gs_app_queue_notify (app, obj_props[PROP_SIZE_INSTALLED_TYPE]); + } + + if (priv->size_installed != size_bytes) { + priv->size_installed = size_bytes; + gs_app_queue_notify (app, obj_props[PROP_SIZE_INSTALLED]); + } +} + +static GsSizeType +get_size_installed_dependencies (GsApp *app, + guint64 *size_bytes_out, + GHashTable *covered_uids) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + GsSizeType size_type = GS_SIZE_TYPE_VALID; + guint64 size_bytes = 0; + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + /* add related apps */ + for (guint i = 0; i < gs_app_list_length (priv->related); i++) { + GsApp *app_related = gs_app_list_index (priv->related, i); + GsSizeType related_size_installed_type, related_size_installed_dependencies_type; + guint64 related_size_installed_bytes, related_size_installed_dependencies_bytes; + + related_size_installed_type = gs_app_get_size_installed (app_related, &related_size_installed_bytes); + + if (!add_sizes (app_related, covered_uids, + size_type, size_bytes, + related_size_installed_type, related_size_installed_bytes, + &size_type, &size_bytes)) + break; + + related_size_installed_dependencies_type = get_size_installed_dependencies (app_related, + &related_size_installed_dependencies_bytes, + covered_uids); + + if (!add_sizes (NULL, NULL, + size_type, size_bytes, + related_size_installed_dependencies_type, related_size_installed_dependencies_bytes, + &size_type, &size_bytes)) + break; + } + + if (size_bytes_out != NULL) + *size_bytes_out = (size_type == GS_SIZE_TYPE_VALID) ? size_bytes : 0; + + return size_type; +} + +/** + * gs_app_get_size_installed_dependencies: + * @app: a #GsApp + * @size_bytes_out: (optional) (out caller-allocates): return location for + * the installed size of dependencies, in bytes, or %NULL to ignore + * + * Get the values of #GsApp:size-installed-dependencies-type and + * #GsApp:size-installed-dependencies. + * + * If this returns %GS_SIZE_TYPE_VALID, @size_bytes_out (if non-%NULL) will be + * set to the installed size of dependencies. Otherwise, its value will be + * undefined. + * + * Returns: type of the installed size of dependencies + * Since: 43 + **/ +GsSizeType +gs_app_get_size_installed_dependencies (GsApp *app, + guint64 *size_bytes_out) +{ + g_autoptr(GHashTable) covered_uids = NULL; + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + covered_uids = g_hash_table_new_full ((GHashFunc) as_utils_data_id_hash, (GEqualFunc) as_utils_data_id_equal, NULL, NULL); + + return get_size_installed_dependencies (app, size_bytes_out, covered_uids); +} + +/** + * gs_app_get_size_user_data: + * @app: A #GsApp + * @size_bytes_out: (optional) (out caller-allocates): return location for + * the user data size, in bytes, or %NULL to ignore + * + * Get the values of #GsApp:size-user-data-type and #GsApp:size-user-data. + * + * If this returns %GS_SIZE_TYPE_VALID, @size_bytes_out (if non-%NULL) will be + * set to the user data size. Otherwise, its value will be undefined. + * + * Returns: type of the user data size + * Since: 43 + **/ +GsSizeType +gs_app_get_size_user_data (GsApp *app, + guint64 *size_bytes_out) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + if (size_bytes_out != NULL) + *size_bytes_out = (priv->size_user_data_type == GS_SIZE_TYPE_VALID) ? priv->size_user_data : 0; + + return priv->size_user_data_type; +} + +/** + * gs_app_set_size_user_data: + * @app: a #GsApp + * @size_type: type of the user data size + * @size_bytes: size in bytes + * + * Sets the user data size of the @app. + * + * @size_bytes will be ignored unless @size_type is %GS_SIZE_TYPE_VALID. + * + * Since: 43 + **/ +void +gs_app_set_size_user_data (GsApp *app, + GsSizeType size_type, + guint64 size_bytes) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + if (size_type != GS_SIZE_TYPE_VALID) + size_bytes = 0; + + if (priv->size_user_data_type != size_type) { + priv->size_user_data_type = size_type; + gs_app_queue_notify (app, obj_props[PROP_SIZE_USER_DATA_TYPE]); + } + + if (priv->size_user_data != size_bytes) { + priv->size_user_data = size_bytes; + gs_app_queue_notify (app, obj_props[PROP_SIZE_USER_DATA]); + } +} + +/** + * gs_app_get_size_cache_data: + * @app: A #GsApp + * @size_bytes_out: (optional) (out caller-allocates): return location for + * the cache data size, in bytes, or %NULL to ignore + * + * Get the values of #GsApp:size-cache-data-type and #GsApp:size-cache-data. + * + * If this returns %GS_SIZE_TYPE_VALID, @size_bytes_out (if non-%NULL) will be + * set to the cache data size. Otherwise, its value will be undefined. + * + * Returns: type of the cache data size + * Since: 43 + **/ +GsSizeType +gs_app_get_size_cache_data (GsApp *app, + guint64 *size_bytes_out) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_val_if_fail (GS_IS_APP (app), GS_SIZE_TYPE_UNKNOWN); + + if (size_bytes_out != NULL) + *size_bytes_out = (priv->size_cache_data_type == GS_SIZE_TYPE_VALID) ? priv->size_cache_data : 0; + + return priv->size_cache_data_type; +} + +/** + * gs_app_set_size_cache_data: + * @app: a #GsApp + * @size_type: type of the cache data size + * @size_bytes: size in bytes + * + * Sets the cache data size of the @app. + * + * @size_bytes will be ignored unless @size_type is %GS_SIZE_TYPE_VALID. + * + * Since: 43 + **/ +void +gs_app_set_size_cache_data (GsApp *app, + GsSizeType size_type, + guint64 size_bytes) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + if (size_type != GS_SIZE_TYPE_VALID) + size_bytes = 0; + + if (priv->size_cache_data_type != size_type) { + priv->size_cache_data_type = size_type; + gs_app_queue_notify (app, obj_props[PROP_SIZE_CACHE_DATA_TYPE]); + } + + if (priv->size_cache_data != size_bytes) { + priv->size_cache_data = size_bytes; + gs_app_queue_notify (app, obj_props[PROP_SIZE_CACHE_DATA]); + } +} + +/** + * gs_app_get_metadata_item: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::device-id" + * + * Gets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata, + * for example `fwupd::device-id`. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_metadata_item (GsApp *app, const gchar *key) +{ + GVariant *tmp; + g_return_val_if_fail (GS_IS_APP (app), NULL); + g_return_val_if_fail (key != NULL, NULL); + tmp = gs_app_get_metadata_variant (app, key); + if (tmp == NULL) + return NULL; + return g_variant_get_string (tmp, NULL); +} + +/** + * gs_app_set_metadata: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::DeviceID" + * @value: a string, e.g. "fubar" + * + * Sets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata. + * + * Since: 3.22 + **/ +void +gs_app_set_metadata (GsApp *app, const gchar *key, const gchar *value) +{ + g_autoptr(GVariant) tmp = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (key != NULL); + if (value != NULL) + tmp = g_variant_new_string (value); + gs_app_set_metadata_variant (app, key, tmp); +} + +/** + * gs_app_get_metadata_variant: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::device-id" + * + * Gets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata. + * + * Returns: (transfer none) (nullable): a variant, or %NULL for unset + * + * Since: 3.26 + **/ +GVariant * +gs_app_get_metadata_variant (GsApp *app, const gchar *key) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + g_return_val_if_fail (key != NULL, NULL); + return g_hash_table_lookup (priv->metadata, key); +} + +/** + * gs_app_set_metadata_variant: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::DeviceID" + * @value: a #GVariant + * + * Sets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata, + * for example `fwupd::device-id`. + * + * Since: 3.26 + **/ +void +gs_app_set_metadata_variant (GsApp *app, const gchar *key, GVariant *value) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + GVariant *found; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* if no value, then remove the key */ + if (value == NULL) { + g_hash_table_remove (priv->metadata, key); + return; + } + + /* check we're not overwriting */ + found = g_hash_table_lookup (priv->metadata, key); + if (found != NULL) { + if (g_variant_equal (found, value)) + return; + if (g_variant_type_equal (g_variant_get_type (value), G_VARIANT_TYPE_STRING) && + g_variant_type_equal (g_variant_get_type (found), G_VARIANT_TYPE_STRING)) { + g_debug ("tried overwriting %s key %s from %s to %s", + priv->id, key, + g_variant_get_string (found, NULL), + g_variant_get_string (value, NULL)); + } else { + g_debug ("tried overwriting %s key %s (%s->%s)", + priv->id, key, + g_variant_get_type_string (found), + g_variant_get_type_string (value)); + } + return; + } + g_hash_table_insert (priv->metadata, g_strdup (key), g_variant_ref (value)); +} + +/** + * gs_app_dup_addons: + * @app: a #GsApp + * + * Gets the list of addons for the application. + * + * Returns: (transfer full) (nullable): a list of addons, or %NULL if there are none + * + * Since: 43 + */ +GsAppList * +gs_app_dup_addons (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return (priv->addons != NULL) ? g_object_ref (priv->addons) : NULL; +} + +/** + * gs_app_add_addons: + * @app: a #GsApp + * @addons: (transfer none) (not nullable): a list of #GsApps + * + * Adds zero or more addons to the list of application addons. + * + * Since: 43 + **/ +void +gs_app_add_addons (GsApp *app, + GsAppList *addons) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GsAppList) new_addons = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP_LIST (addons)); + + if (gs_app_list_length (addons) == 0) + return; + + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->addons != NULL) + new_addons = gs_app_list_copy (priv->addons); + else + new_addons = gs_app_list_new (); + gs_app_list_add_list (new_addons, addons); + + g_set_object (&priv->addons, new_addons); +} + +/** + * gs_app_remove_addon: + * @app: a #GsApp + * @addon: a #GsApp + * + * Removes an addon from the list of application addons. + * + * Since: 3.22 + **/ +void +gs_app_remove_addon (GsApp *app, GsApp *addon) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (addon)); + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->addons != NULL) + gs_app_list_remove (priv->addons, addon); +} + +/** + * gs_app_get_related: + * @app: a #GsApp + * + * Gets any related applications. + * + * Returns: (transfer none): a list of applications + * + * Since: 3.22 + **/ +GsAppList * +gs_app_get_related (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->related; +} + +/** + * gs_app_add_related: + * @app: a #GsApp + * @app2: a #GsApp + * + * Adds a related application. + * + * Since: 3.22 + **/ +void +gs_app_add_related (GsApp *app, GsApp *app2) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + GsAppPrivate *priv2 = gs_app_get_instance_private (app2); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (app2)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* if the app is updatable-live and any related app is not then + * degrade to the offline state */ + if (priv->state == GS_APP_STATE_UPDATABLE_LIVE && + priv2->state == GS_APP_STATE_UPDATABLE) + priv->state = priv2->state; + + gs_app_list_add (priv->related, app2); + + /* The related apps add to the main app’s sizes. */ + gs_app_queue_notify (app, obj_props[PROP_SIZE_DOWNLOAD_DEPENDENCIES_TYPE]); + gs_app_queue_notify (app, obj_props[PROP_SIZE_DOWNLOAD_DEPENDENCIES]); + gs_app_queue_notify (app, obj_props[PROP_SIZE_INSTALLED_DEPENDENCIES_TYPE]); + gs_app_queue_notify (app, obj_props[PROP_SIZE_INSTALLED_DEPENDENCIES]); +} + +/** + * gs_app_get_history: + * @app: a #GsApp + * + * Gets the history of this application. + * + * Returns: (transfer none): a list + * + * Since: 3.22 + **/ +GsAppList * +gs_app_get_history (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->history; +} + +/** + * gs_app_add_history: + * @app: a #GsApp + * @app2: a #GsApp + * + * Adds a history item for this package. + * + * Since: 3.22 + **/ +void +gs_app_add_history (GsApp *app, GsApp *app2) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (app2)); + locker = g_mutex_locker_new (&priv->mutex); + gs_app_list_add (priv->history, app2); +} + +/** + * gs_app_get_install_date: + * @app: a #GsApp + * + * Gets the date that an application was installed. + * + * Returns: A UNIX epoch, or 0 for unset + * + * Since: 3.22 + **/ +guint64 +gs_app_get_install_date (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->install_date; +} + +/** + * gs_app_set_install_date: + * @app: a #GsApp + * @install_date: an epoch, or %GS_APP_INSTALL_DATE_UNKNOWN + * + * Sets the date that an application was installed. + * + * Since: 3.22 + **/ +void +gs_app_set_install_date (GsApp *app, guint64 install_date) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (install_date == priv->install_date) + return; + priv->install_date = install_date; +} + +/** + * gs_app_get_release_date: + * @app: a #GsApp + * + * Gets the date that an application was released. + * + * Returns: A UNIX epoch, or 0 for unset + * + * Since: 3.40 + **/ +guint64 +gs_app_get_release_date (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->release_date; +} + +/** + * gs_app_set_release_date: + * @app: a #GsApp + * @release_date: an epoch, or 0 + * + * Sets the date that an application was released. + * + * Since: 3.40 + **/ +void +gs_app_set_release_date (GsApp *app, guint64 release_date) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (release_date == priv->release_date) + return; + priv->release_date = release_date; + + gs_app_queue_notify (app, obj_props[PROP_RELEASE_DATE]); +} + +/** + * gs_app_is_installed: + * @app: a #GsApp + * + * Gets whether the app is installed or not. + * + * Returns: %TRUE if the app is installed, %FALSE otherwise. + * + * Since: 3.22 + **/ +gboolean +gs_app_is_installed (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return (priv->state == GS_APP_STATE_INSTALLED) || + (priv->state == GS_APP_STATE_UPDATABLE) || + (priv->state == GS_APP_STATE_UPDATABLE_LIVE) || + (priv->state == GS_APP_STATE_REMOVING); +} + +/** + * gs_app_is_updatable: + * @app: a #GsApp + * + * Gets whether the app is updatable or not. + * + * Returns: %TRUE if the app is updatable, %FALSE otherwise. + * + * Since: 3.22 + **/ +gboolean +gs_app_is_updatable (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + if (priv->kind == AS_COMPONENT_KIND_OPERATING_SYSTEM) + return TRUE; + return (priv->state == GS_APP_STATE_UPDATABLE) || + (priv->state == GS_APP_STATE_UPDATABLE_LIVE); +} + +/** + * gs_app_get_categories: + * @app: a #GsApp + * + * Gets the list of categories for an application. + * + * Returns: (element-type utf8) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_categories (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->categories; +} + +/** + * gs_app_has_category: + * @app: a #GsApp + * @category: a category ID, e.g. "AudioVideo" + * + * Checks if the application is in a specific category. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_app_has_category (GsApp *app, const gchar *category) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + /* find the category */ + for (i = 0; i < priv->categories->len; i++) { + tmp = g_ptr_array_index (priv->categories, i); + if (g_strcmp0 (tmp, category) == 0) + return TRUE; + } + return FALSE; +} + +/** + * gs_app_set_categories: + * @app: a #GsApp + * @categories: a set of categories + * + * Set the list of categories for an application. + * + * Since: 3.22 + **/ +void +gs_app_set_categories (GsApp *app, GPtrArray *categories) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (categories != NULL); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->categories, categories); +} + +/** + * gs_app_add_category: + * @app: a #GsApp + * @category: a category ID, e.g. "AudioVideo" + * + * Adds a category ID to an application. + * + * Since: 3.22 + **/ +void +gs_app_add_category (GsApp *app, const gchar *category) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (category != NULL); + locker = g_mutex_locker_new (&priv->mutex); + if (gs_app_has_category (app, category)) + return; + g_ptr_array_add (priv->categories, g_strdup (category)); +} + +/** + * gs_app_remove_category: + * @app: a #GsApp + * @category: a category ID, e.g. "AudioVideo" + * + * Removes an category ID from an application, it exists. + * + * Returns: %TRUE for success + * + * Since: 3.24 + **/ +gboolean +gs_app_remove_category (GsApp *app, const gchar *category) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + locker = g_mutex_locker_new (&priv->mutex); + + for (i = 0; i < priv->categories->len; i++) { + tmp = g_ptr_array_index (priv->categories, i); + if (g_strcmp0 (tmp, category) != 0) + continue; + g_ptr_array_remove_index_fast (priv->categories, i); + return TRUE; + } + return FALSE; +} + +/** + * gs_app_set_is_update_downloaded: + * @app: a #GsApp + * @is_update_downloaded: Whether a new update is already downloaded locally + * + * Sets if the new update is already downloaded for the app. + * + * Since: 3.36 + **/ +void +gs_app_set_is_update_downloaded (GsApp *app, gboolean is_update_downloaded) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->is_update_downloaded = is_update_downloaded; +} + +/** + * gs_app_get_is_update_downloaded: + * @app: a #GsApp + * + * Gets if the new update is already downloaded for the app and + * is locally available. + * + * Returns: (element-type gboolean): Whether a new update for the #GsApp is already downloaded. + * + * Since: 3.36 + **/ +gboolean +gs_app_get_is_update_downloaded (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return priv->is_update_downloaded; +} + +static void +calculate_key_colors (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GIcon) icon_small = NULL; + g_autoptr(GdkPixbuf) pb_small = NULL; + const gchar *overrides_str; + + /* Lazily create the array */ + if (priv->key_colors == NULL) + priv->key_colors = g_array_new (FALSE, FALSE, sizeof (GdkRGBA)); + priv->user_key_colors = FALSE; + + /* Look for an override first. Parse and use it if possible. This is + * typically specified in the appdata for an app as: + * |[ + * <component> + * <custom> + * <value key="GnomeSoftware::key-colors">[(124, 53, 77), (99, 16, 0)]</value> + * </custom> + * </component> + * ]| + */ + overrides_str = gs_app_get_metadata_item (app, "GnomeSoftware::key-colors"); + if (overrides_str != NULL) { + g_autoptr(GVariant) overrides = NULL; + g_autoptr(GError) local_error = NULL; + + overrides = g_variant_parse (G_VARIANT_TYPE ("a(yyy)"), + overrides_str, + NULL, + NULL, + &local_error); + + if (overrides != NULL && g_variant_n_children (overrides) > 0) { + GVariantIter iter; + guint8 red, green, blue; + + g_variant_iter_init (&iter, overrides); + while (g_variant_iter_loop (&iter, "(yyy)", &red, &green, &blue)) { + GdkRGBA rgba; + rgba.red = (gdouble) red / 255.0; + rgba.green = (gdouble) green / 255.0; + rgba.blue = (gdouble) blue / 255.0; + rgba.alpha = 1.0; + g_array_append_val (priv->key_colors, rgba); + } + + priv->user_key_colors = TRUE; + + return; + } else { + g_warning ("Invalid value for GnomeSoftware::key-colors for %s: %s", + gs_app_get_id (app), local_error->message); + /* fall through */ + } + } + + /* Try and load the pixbuf. */ + icon_small = gs_app_get_icon_for_size (app, 32, 1, NULL); + + if (icon_small == NULL) { + g_debug ("no pixbuf, so no key colors"); + return; + } else if (G_IS_LOADABLE_ICON (icon_small)) { + g_autoptr(GInputStream) icon_stream = g_loadable_icon_load (G_LOADABLE_ICON (icon_small), 32, NULL, NULL, NULL); + if (icon_stream) + pb_small = gdk_pixbuf_new_from_stream_at_scale (icon_stream, 32, 32, TRUE, NULL, NULL); + } else if (G_IS_THEMED_ICON (icon_small)) { + g_autoptr(GtkIconPaintable) icon_paintable = NULL; + g_autoptr(GtkIconTheme) theme = NULL; + GdkDisplay *display; + + display = gdk_display_get_default (); + if (display != NULL) { + theme = g_object_ref (gtk_icon_theme_get_for_display (display)); + } else { + const gchar *test_search_path; + + /* This fallback path is needed for the unit tests, + * which run without a screen, and in an environment + * where the XDG dir variables don’t point to the system + * datadir which contains the system icon theme. */ + theme = gtk_icon_theme_new (); + + test_search_path = g_getenv ("GS_SELF_TEST_ICON_THEME_PATH"); + if (test_search_path != NULL) { + g_auto(GStrv) dirs = g_strsplit (test_search_path, ":", -1); + gtk_icon_theme_set_search_path (theme, (const char * const *)dirs); + + } + } + + icon_paintable = gtk_icon_theme_lookup_by_gicon (theme, icon_small, + 32, 1, + gtk_get_locale_direction (), + 0); + if (icon_paintable != NULL) { + g_autoptr(GFile) file = NULL; + g_autofree gchar *path = NULL; + + file = gtk_icon_paintable_get_file (icon_paintable); + if (file != NULL) + path = g_file_get_path (file); + + if (path != NULL) { + pb_small = gdk_pixbuf_new_from_file_at_size (path, 32, 32, NULL); + } else { + g_autoptr(GskRenderNode) render_node = NULL; + g_autoptr(GtkSnapshot) snapshot = NULL; + cairo_surface_t *surface; + cairo_t *cr; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 32, 32); + cr = cairo_create (surface); + + /* TODO: this can be done entirely on the GPU using shaders */ + snapshot = gtk_snapshot_new (); + gdk_paintable_snapshot (GDK_PAINTABLE (icon_paintable), + GDK_SNAPSHOT (snapshot), + 32.0, + 32.0); + + render_node = gtk_snapshot_free_to_node (g_steal_pointer (&snapshot)); + gsk_render_node_draw (render_node, cr); + + pb_small = gdk_pixbuf_get_from_surface (surface, 0, 0, 32, 32); + + cairo_surface_destroy (surface); + cairo_destroy (cr); + } + } + + } else { + g_debug ("unsupported pixbuf, so no key colors"); + return; + } + + if (pb_small == NULL) { + g_debug ("pixbuf couldn’t be loaded, so no key colors"); + return; + } + + /* get a list of key colors */ + g_clear_pointer (&priv->key_colors, g_array_unref); + priv->key_colors = gs_calculate_key_colors (pb_small); +} + +/** + * gs_app_get_key_colors: + * @app: a #GsApp + * + * Gets the key colors used in the application icon. + * + * Returns: (element-type GdkRGBA) (transfer none): a list + * + * Since: 40 + **/ +GArray * +gs_app_get_key_colors (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + if (priv->key_colors == NULL) + calculate_key_colors (app); + + return priv->key_colors; +} + +/** + * gs_app_set_key_colors: + * @app: a #GsApp + * @key_colors: (element-type GdkRGBA): a set of key colors + * + * Sets the key colors used in the application icon. + * + * Since: 40 + **/ +void +gs_app_set_key_colors (GsApp *app, GArray *key_colors) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (key_colors != NULL); + locker = g_mutex_locker_new (&priv->mutex); + priv->user_key_colors = FALSE; + if (_g_set_array (&priv->key_colors, key_colors)) + gs_app_queue_notify (app, obj_props[PROP_KEY_COLORS]); +} + +/** + * gs_app_add_key_color: + * @app: a #GsApp + * @key_color: a #GdkRGBA + * + * Adds a key color used in the application icon. + * + * Since: 3.22 + **/ +void +gs_app_add_key_color (GsApp *app, GdkRGBA *key_color) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (key_color != NULL); + + /* Lazily create the array */ + if (priv->key_colors == NULL) + priv->key_colors = g_array_new (FALSE, FALSE, sizeof (GdkRGBA)); + + priv->user_key_colors = FALSE; + g_array_append_val (priv->key_colors, *key_color); + gs_app_queue_notify (app, obj_props[PROP_KEY_COLORS]); +} + +/** + * gs_app_get_user_key_colors: + * @app: a #GsApp + * + * Returns whether the key colors provided by gs_app_get_key_colors() + * are set by the user (using `GnomeSoftware::key-colors`). %FALSE + * means the colors have been calculated from the @app icon. + * + * Returns: whether the key colors have been provided by the user. + * + * Since: 42 + **/ +gboolean +gs_app_get_user_key_colors (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return priv->user_key_colors; +} + +/** + * gs_app_add_kudo: + * @app: a #GsApp + * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE + * + * Adds a kudo to the application. + * + * Since: 3.22 + **/ +void +gs_app_add_kudo (GsApp *app, GsAppKudo kudo) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (kudo & GS_APP_KUDO_SANDBOXED_SECURE) + kudo |= GS_APP_KUDO_SANDBOXED; + priv->kudos |= kudo; +} + +/** + * gs_app_remove_kudo: + * @app: a #GsApp + * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE + * + * Removes a kudo from the application. + * + * Since: 3.30 + **/ +void +gs_app_remove_kudo (GsApp *app, GsAppKudo kudo) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->kudos &= ~kudo; +} + +/** + * gs_app_has_kudo: + * @app: a #GsApp + * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE + * + * Finds out if a kudo has been awarded by the application. + * + * Returns: %TRUE if the app has the specified kudo + * + * Since: 3.22 + **/ +gboolean +gs_app_has_kudo (GsApp *app, GsAppKudo kudo) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return (priv->kudos & kudo) > 0; +} + +/** + * gs_app_get_kudos: + * @app: a #GsApp + * + * Gets all the kudos the application has been awarded. + * + * Returns: the kudos, as a bitfield + * + * Since: 3.22 + **/ +guint64 +gs_app_get_kudos (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->kudos; +} + +/** + * gs_app_get_kudos_percentage: + * @app: a #GsApp + * + * Gets the kudos, as a percentage value. + * + * Returns: a percentage, with 0 for no kudos and a maximum of 100. + * + * Since: 3.22 + **/ +guint +gs_app_get_kudos_percentage (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + guint percentage = 0; + + g_return_val_if_fail (GS_IS_APP (app), 0); + + if ((priv->kudos & GS_APP_KUDO_MY_LANGUAGE) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_RECENT_RELEASE) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_FEATURED_RECOMMENDED) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_MODERN_TOOLKIT) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_SEARCH_PROVIDER) > 0) + percentage += 10; + if ((priv->kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0) + percentage += 10; + if ((priv->kudos & GS_APP_KUDO_USES_NOTIFICATIONS) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_HAS_KEYWORDS) > 0) + percentage += 5; + if ((priv->kudos & GS_APP_KUDO_HAS_SCREENSHOTS) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_HIGH_CONTRAST) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_HI_DPI_ICON) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_SANDBOXED) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_SANDBOXED_SECURE) > 0) + percentage += 20; + + return MIN (percentage, 100); +} + +/** + * gs_app_get_to_be_installed: + * @app: a #GsApp + * + * Gets if the application is queued for installation. + * + * This is only set for addons when the user has selected some addons to be + * installed before installing the main application. + * Plugins should check all the addons for this property when installing + * main applications so that the chosen set of addons is also installed at the + * same time. This is never set when applications do not have addons. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_app_get_to_be_installed (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + return priv->to_be_installed; +} + +/** + * gs_app_set_to_be_installed: + * @app: a #GsApp + * @to_be_installed: if the app is due to be installed + * + * Sets if the application is queued for installation. + * + * Since: 3.22 + **/ +void +gs_app_set_to_be_installed (GsApp *app, gboolean to_be_installed) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + + priv->to_be_installed = to_be_installed; +} + +/** + * gs_app_has_quirk: + * @app: a #GsApp + * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY + * + * Finds out if an application has a specific quirk. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_app_has_quirk (GsApp *app, GsAppQuirk quirk) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + return (priv->quirk & quirk) > 0; +} + +/** + * gs_app_add_quirk: + * @app: a #GsApp + * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY + * + * Adds a quirk to an application. + * + * Since: 3.22 + **/ +void +gs_app_add_quirk (GsApp *app, GsAppQuirk quirk) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if ((priv->quirk & quirk) > 0) + return; + + locker = g_mutex_locker_new (&priv->mutex); + priv->quirk |= quirk; + gs_app_queue_notify (app, obj_props[PROP_QUIRK]); +} + +/** + * gs_app_remove_quirk: + * @app: a #GsApp + * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY + * + * Removes a quirk from an application. + * + * Since: 3.22 + **/ +void +gs_app_remove_quirk (GsApp *app, GsAppQuirk quirk) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if ((priv->quirk & quirk) == 0) + return; + + locker = g_mutex_locker_new (&priv->mutex); + priv->quirk &= ~quirk; + gs_app_queue_notify (app, obj_props[PROP_QUIRK]); +} + +/** + * gs_app_set_match_value: + * @app: a #GsApp + * @match_value: a value + * + * Set a match quality value, where higher values correspond to a + * "better" search match, and should be shown above lower results. + * + * Since: 3.22 + **/ +void +gs_app_set_match_value (GsApp *app, guint match_value) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->match_value = match_value; +} + +/** + * gs_app_get_match_value: + * @app: a #GsApp + * + * Get a match quality value, where higher values correspond to a + * "better" search match, and should be shown above lower results. + * + * Note: This value is only valid when processing the result set + * and may be overwritten on subsequent searches if the plugin is using + * a cache. + * + * Returns: a value, where higher is better + * + * Since: 3.22 + **/ +guint +gs_app_get_match_value (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->match_value; +} + +/** + * gs_app_set_priority: + * @app: a #GsApp + * @priority: a value + * + * Set a priority value. + * + * Since: 3.22 + **/ +void +gs_app_set_priority (GsApp *app, guint priority) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->priority = priority; +} + +/** + * gs_app_get_priority: + * @app: a #GsApp + * + * Get a priority value, where higher values will be chosen where + * multiple #GsApp's match a specific rule. + * + * Returns: a value, where higher is better + * + * Since: 3.22 + **/ +guint +gs_app_get_priority (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + + /* If the priority hasn’t been explicitly set, fetch it from the app’s + * management plugin. */ + if (priv->priority == 0) { + g_autoptr(GsPlugin) plugin = gs_app_dup_management_plugin (app); + if (plugin != NULL) + priv->priority = gs_plugin_get_priority (plugin); + } + + return priv->priority; +} + +/** + * gs_app_get_cancellable: + * @app: a #GsApp + * + * Get a cancellable to be used with operations related to the #GsApp. This is a + * way for views to be able to cancel an on-going operation. If the #GCancellable + * is canceled, it will be unreferenced and renewed before returning it, i.e. the + * cancellable object will always be ready to use for new operations. So be sure + * to keep a reference to it if you do more than just passing the cancellable to + * a process. + * + * Returns: a #GCancellable + * + * Since: 3.28 + **/ +GCancellable * +gs_app_get_cancellable (GsApp *app) +{ + g_autoptr(GCancellable) cancellable = NULL; + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_val_if_fail (GS_IS_APP (app), NULL); + + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->cancellable == NULL || g_cancellable_is_cancelled (priv->cancellable)) { + cancellable = g_cancellable_new (); + g_set_object (&priv->cancellable, cancellable); + } + return priv->cancellable; +} + +/** + * gs_app_get_pending_action: + * @app: a #GsApp + * + * Get the pending action for this #GsApp, or %NULL if no action is pending. + * + * Returns: the #GsAppAction of the @app. + **/ +GsPluginAction +gs_app_get_pending_action (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), GS_PLUGIN_ACTION_UNKNOWN); + locker = g_mutex_locker_new (&priv->mutex); + return priv->pending_action; +} + +/** + * gs_app_set_pending_action: + * @app: a #GsApp + * @action: a #GsPluginAction + * + * Set an action that is pending on this #GsApp. + **/ +void +gs_app_set_pending_action (GsApp *app, + GsPluginAction action) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + gs_app_set_pending_action_internal (app, action); +} + +static void +gs_app_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + switch ((GsAppProperty) prop_id) { + case PROP_ID: + g_value_set_string (value, priv->id); + break; + case PROP_NAME: + g_value_set_string (value, priv->name); + break; + case PROP_VERSION: + g_value_set_string (value, priv->version); + break; + case PROP_SUMMARY: + g_value_set_string (value, priv->summary); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, priv->description); + break; + case PROP_RATING: + g_value_set_int (value, priv->rating); + break; + case PROP_KIND: + g_value_set_uint (value, priv->kind); + break; + case PROP_SPECIAL_KIND: + g_value_set_enum (value, priv->special_kind); + break; + case PROP_STATE: + g_value_set_enum (value, priv->state); + break; + case PROP_PROGRESS: + g_value_set_uint (value, priv->progress); + break; + case PROP_CAN_CANCEL_INSTALLATION: + g_value_set_boolean (value, priv->allow_cancel); + break; + case PROP_INSTALL_DATE: + g_value_set_uint64 (value, priv->install_date); + break; + case PROP_RELEASE_DATE: + g_value_set_uint64 (value, priv->release_date); + break; + case PROP_QUIRK: + g_value_set_flags (value, priv->quirk); + break; + case PROP_PENDING_ACTION: + g_value_set_enum (value, priv->pending_action); + break; + case PROP_KEY_COLORS: + g_value_set_boxed (value, gs_app_get_key_colors (app)); + break; + case PROP_IS_UPDATE_DOWNLOADED: + g_value_set_boolean (value, priv->is_update_downloaded); + break; + case PROP_URLS: + g_value_set_boxed (value, priv->urls); + break; + case PROP_URL_MISSING: + g_value_set_string (value, priv->url_missing); + break; + case PROP_CONTENT_RATING: + g_value_set_object (value, priv->content_rating); + break; + case PROP_LICENSE: + g_value_set_string (value, priv->license); + break; + case PROP_SIZE_CACHE_DATA_TYPE: + g_value_set_enum (value, gs_app_get_size_cache_data (app, NULL)); + break; + case PROP_SIZE_CACHE_DATA: { + guint64 size_bytes; + gs_app_get_size_cache_data (app, &size_bytes); + g_value_set_uint64 (value, size_bytes); + break; + } + case PROP_SIZE_DOWNLOAD_TYPE: + g_value_set_enum (value, gs_app_get_size_download (app, NULL)); + break; + case PROP_SIZE_DOWNLOAD: { + guint64 size_bytes; + gs_app_get_size_download (app, &size_bytes); + g_value_set_uint64 (value, size_bytes); + break; + } + case PROP_SIZE_DOWNLOAD_DEPENDENCIES_TYPE: + g_value_set_enum (value, gs_app_get_size_download_dependencies (app, NULL)); + break; + case PROP_SIZE_DOWNLOAD_DEPENDENCIES: { + guint64 size_bytes; + gs_app_get_size_download_dependencies (app, &size_bytes); + g_value_set_uint64 (value, size_bytes); + break; + } + case PROP_SIZE_INSTALLED_TYPE: + g_value_set_enum (value, gs_app_get_size_installed (app, NULL)); + break; + case PROP_SIZE_INSTALLED: { + guint64 size_bytes; + gs_app_get_size_installed (app, &size_bytes); + g_value_set_uint64 (value, size_bytes); + break; + } + case PROP_SIZE_INSTALLED_DEPENDENCIES_TYPE: + g_value_set_enum (value, gs_app_get_size_installed_dependencies (app, NULL)); + break; + case PROP_SIZE_INSTALLED_DEPENDENCIES: { + guint64 size_bytes; + gs_app_get_size_installed_dependencies (app, &size_bytes); + g_value_set_uint64 (value, size_bytes); + break; + } + case PROP_SIZE_USER_DATA_TYPE: + g_value_set_enum (value, gs_app_get_size_user_data (app, NULL)); + break; + case PROP_SIZE_USER_DATA: { + guint64 size_bytes; + gs_app_get_size_user_data (app, &size_bytes); + g_value_set_uint64 (value, size_bytes); + break; + } + case PROP_PERMISSIONS: + g_value_take_object (value, gs_app_dup_permissions (app)); + break; + case PROP_RELATIONS: + g_value_take_boxed (value, gs_app_get_relations (app)); + break; + case PROP_ORIGIN_UI: + g_value_take_string (value, gs_app_dup_origin_ui (app, TRUE)); + break; + case PROP_HAS_TRANSLATIONS: + g_value_set_boolean (value, gs_app_get_has_translations (app)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + switch ((GsAppProperty) prop_id) { + case PROP_ID: + gs_app_set_id (app, g_value_get_string (value)); + break; + case PROP_NAME: + gs_app_set_name (app, + GS_APP_QUALITY_UNKNOWN, + g_value_get_string (value)); + break; + case PROP_VERSION: + gs_app_set_version (app, g_value_get_string (value)); + break; + case PROP_SUMMARY: + gs_app_set_summary (app, + GS_APP_QUALITY_UNKNOWN, + g_value_get_string (value)); + break; + case PROP_DESCRIPTION: + gs_app_set_description (app, + GS_APP_QUALITY_UNKNOWN, + g_value_get_string (value)); + break; + case PROP_RATING: + gs_app_set_rating (app, g_value_get_int (value)); + break; + case PROP_KIND: + gs_app_set_kind (app, g_value_get_uint (value)); + break; + case PROP_SPECIAL_KIND: + gs_app_set_special_kind (app, g_value_get_enum (value)); + break; + case PROP_STATE: + gs_app_set_state_internal (app, g_value_get_enum (value)); + break; + case PROP_PROGRESS: + gs_app_set_progress (app, g_value_get_uint (value)); + break; + case PROP_CAN_CANCEL_INSTALLATION: + priv->allow_cancel = g_value_get_boolean (value); + break; + case PROP_INSTALL_DATE: + gs_app_set_install_date (app, g_value_get_uint64 (value)); + break; + case PROP_RELEASE_DATE: + gs_app_set_release_date (app, g_value_get_uint64 (value)); + break; + case PROP_QUIRK: + priv->quirk = g_value_get_flags (value); + break; + case PROP_PENDING_ACTION: + /* Read only */ + g_assert_not_reached (); + break; + case PROP_KEY_COLORS: + gs_app_set_key_colors (app, g_value_get_boxed (value)); + break; + case PROP_IS_UPDATE_DOWNLOADED: + gs_app_set_is_update_downloaded (app, g_value_get_boolean (value)); + break; + case PROP_URLS: + /* Read only */ + g_assert_not_reached (); + break; + case PROP_URL_MISSING: + gs_app_set_url_missing (app, g_value_get_string (value)); + break; + case PROP_CONTENT_RATING: + gs_app_set_content_rating (app, g_value_get_object (value)); + break; + case PROP_LICENSE: + /* Read-only */ + g_assert_not_reached (); + case PROP_SIZE_CACHE_DATA_TYPE: + gs_app_set_size_cache_data (app, g_value_get_enum (value), priv->size_cache_data); + break; + case PROP_SIZE_CACHE_DATA: + gs_app_set_size_cache_data (app, priv->size_cache_data_type, g_value_get_uint64 (value)); + break; + case PROP_SIZE_DOWNLOAD_TYPE: + gs_app_set_size_download (app, g_value_get_enum (value), priv->size_download); + break; + case PROP_SIZE_DOWNLOAD: + gs_app_set_size_download (app, priv->size_download_type, g_value_get_uint64 (value)); + break; + case PROP_SIZE_DOWNLOAD_DEPENDENCIES_TYPE: + case PROP_SIZE_DOWNLOAD_DEPENDENCIES: + /* Read-only */ + g_assert_not_reached (); + case PROP_SIZE_INSTALLED_TYPE: + gs_app_set_size_installed (app, g_value_get_enum (value), priv->size_installed); + break; + case PROP_SIZE_INSTALLED: + gs_app_set_size_installed (app, priv->size_installed_type, g_value_get_uint64 (value)); + break; + case PROP_SIZE_INSTALLED_DEPENDENCIES_TYPE: + case PROP_SIZE_INSTALLED_DEPENDENCIES: + /* Read-only */ + g_assert_not_reached (); + case PROP_SIZE_USER_DATA_TYPE: + gs_app_set_size_user_data (app, g_value_get_enum (value), priv->size_user_data); + break; + case PROP_SIZE_USER_DATA: + gs_app_set_size_user_data (app, priv->size_user_data_type, g_value_get_uint64 (value)); + break; + case PROP_PERMISSIONS: + gs_app_set_permissions (app, g_value_get_object (value)); + break; + case PROP_RELATIONS: + gs_app_set_relations (app, g_value_get_boxed (value)); + break; + case PROP_ORIGIN_UI: + gs_app_set_origin_ui (app, g_value_get_string (value)); + break; + case PROP_HAS_TRANSLATIONS: + gs_app_set_has_translations (app, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_dispose (GObject *object) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_clear_object (&priv->runtime); + + g_clear_pointer (&priv->addons, g_object_unref); + g_clear_pointer (&priv->history, g_object_unref); + g_clear_pointer (&priv->related, g_object_unref); + g_clear_pointer (&priv->screenshots, g_ptr_array_unref); + g_clear_pointer (&priv->review_ratings, g_array_unref); + g_clear_pointer (&priv->reviews, g_ptr_array_unref); + g_clear_pointer (&priv->provided, g_ptr_array_unref); + g_clear_pointer (&priv->icons, g_ptr_array_unref); + g_clear_pointer (&priv->version_history, g_ptr_array_unref); + g_clear_pointer (&priv->relations, g_ptr_array_unref); + g_weak_ref_clear (&priv->management_plugin_weak); + + G_OBJECT_CLASS (gs_app_parent_class)->dispose (object); +} + +static void +gs_app_finalize (GObject *object) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_mutex_clear (&priv->mutex); + g_free (priv->id); + g_free (priv->unique_id); + g_free (priv->branch); + g_free (priv->name); + g_free (priv->renamed_from); + g_free (priv->url_missing); + g_clear_pointer (&priv->urls, g_hash_table_unref); + g_hash_table_unref (priv->launchables); + g_free (priv->license); + g_strfreev (priv->menu_path); + g_free (priv->origin); + g_free (priv->origin_ui); + g_free (priv->origin_appstream); + g_free (priv->origin_hostname); + g_ptr_array_unref (priv->sources); + g_ptr_array_unref (priv->source_ids); + g_free (priv->project_group); + g_free (priv->developer_name); + g_free (priv->agreement); + g_free (priv->version); + g_free (priv->version_ui); + g_free (priv->summary); + g_free (priv->summary_missing); + g_free (priv->description); + g_free (priv->update_version); + g_free (priv->update_version_ui); + g_free (priv->update_details_markup); + g_hash_table_unref (priv->metadata); + g_ptr_array_unref (priv->categories); + g_clear_pointer (&priv->key_colors, g_array_unref); + g_clear_object (&priv->cancellable); + g_clear_object (&priv->local_file); + g_clear_object (&priv->content_rating); + g_clear_object (&priv->action_screenshot); + g_clear_object (&priv->update_permissions); + g_clear_object (&priv->permissions); + + G_OBJECT_CLASS (gs_app_parent_class)->finalize (object); +} + +static void +gs_app_class_init (GsAppClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = gs_app_dispose; + object_class->finalize = gs_app_finalize; + object_class->get_property = gs_app_get_property; + object_class->set_property = gs_app_set_property; + + /** + * GsApp:id: + */ + obj_props[PROP_ID] = g_param_spec_string ("id", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:name: + */ + obj_props[PROP_NAME] = g_param_spec_string ("name", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:version: + */ + obj_props[PROP_VERSION] = g_param_spec_string ("version", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:summary: + */ + obj_props[PROP_SUMMARY] = g_param_spec_string ("summary", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:description: + */ + obj_props[PROP_DESCRIPTION] = g_param_spec_string ("description", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:rating: + */ + obj_props[PROP_RATING] = g_param_spec_int ("rating", NULL, NULL, + -1, 100, -1, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:kind: + */ + /* FIXME: Should use AS_TYPE_APP_KIND when it’s available */ + obj_props[PROP_KIND] = g_param_spec_uint ("kind", NULL, NULL, + AS_COMPONENT_KIND_UNKNOWN, + AS_COMPONENT_KIND_LAST, + AS_COMPONENT_KIND_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:special-kind: + * + * GNOME Software specific occupation of the #GsApp entity + * that does not reflect a software type defined by AppStream. + * + * Since: 40 + */ + obj_props[PROP_SPECIAL_KIND] = g_param_spec_enum ("special-kind", NULL, NULL, + GS_TYPE_APP_SPECIAL_KIND, + GS_APP_SPECIAL_KIND_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:state: + */ + obj_props[PROP_STATE] = g_param_spec_enum ("state", NULL, NULL, + GS_TYPE_APP_STATE, + GS_APP_STATE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:progress: + * + * A percentage (0–100, inclusive) indicating the progress through the + * current task on this app. The value may otherwise be + * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide + * confidence interval. + */ + obj_props[PROP_PROGRESS] = g_param_spec_uint ("progress", NULL, NULL, + 0, GS_APP_PROGRESS_UNKNOWN, GS_APP_PROGRESS_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:allow-cancel: + */ + obj_props[PROP_CAN_CANCEL_INSTALLATION] = + g_param_spec_boolean ("allow-cancel", NULL, NULL, TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:install-date: + */ + obj_props[PROP_INSTALL_DATE] = g_param_spec_uint64 ("install-date", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:release-date: + * + * Set to the release date of the application on the server. Can be 0, + * which means the release date is unknown. + * + * Since: 3.40 + */ + obj_props[PROP_RELEASE_DATE] = g_param_spec_uint64 ("release-date", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:quirk: + */ + obj_props[PROP_QUIRK] = g_param_spec_flags ("quirk", NULL, NULL, + GS_TYPE_APP_QUIRK, GS_APP_QUIRK_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:pending-action: + */ + obj_props[PROP_PENDING_ACTION] = g_param_spec_enum ("pending-action", NULL, NULL, + GS_TYPE_PLUGIN_ACTION, GS_PLUGIN_ACTION_UNKNOWN, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:key-colors: + */ + obj_props[PROP_KEY_COLORS] = g_param_spec_boxed ("key-colors", NULL, NULL, + G_TYPE_ARRAY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:is-update-downloaded: + */ + obj_props[PROP_IS_UPDATE_DOWNLOADED] = g_param_spec_boolean ("is-update-downloaded", NULL, NULL, + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:urls: (nullable) (element-type AsUrlKind utf8) + * + * The URLs associated with the app. + * + * This is %NULL if no URLs are available. If provided, it is a mapping + * from #AsUrlKind to the URLs. + * + * This property is read-only: use gs_app_set_url() to set URLs. + * + * Since: 41 + */ + obj_props[PROP_URLS] = + g_param_spec_boxed ("urls", NULL, NULL, + G_TYPE_HASH_TABLE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:url-missing: + * + * A web URL pointing to explanations why this app + * does not have an installation candidate. + * + * Since: 40 + */ + obj_props[PROP_URL_MISSING] = g_param_spec_string ("url-missing", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:content-rating: (nullable) + * + * The content rating for the app, which gives information on how + * suitable it is for different age ranges of user. + * + * This is %NULL if no content rating information is available. + * + * Since: 41 + */ + obj_props[PROP_CONTENT_RATING] = + g_param_spec_object ("content-rating", NULL, NULL, + /* FIXME: Use the get_type() function directly here to work + * around https://github.com/ximion/appstream/pull/318 */ + as_content_rating_get_type (), + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:license: (nullable) + * + * The license for the app, which is typically its source code license. + * + * Use gs_app_set_license() to set this. + * + * This is %NULL if no licensing information is available. + * + * Since: 41 + */ + obj_props[PROP_LICENSE] = + g_param_spec_string ("license", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-cache-data-type + * + * The type of #GsApp:size-cache-data. + * + * Since: 43 + */ + obj_props[PROP_SIZE_CACHE_DATA_TYPE] = + g_param_spec_enum ("size-cache-data-type", NULL, NULL, + GS_TYPE_SIZE_TYPE, GS_SIZE_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-cache-data + * + * The size on the disk for the cache data of the application. + * + * This is undefined if #GsApp:size-cache-data-type is not + * %GS_SIZE_TYPE_VALID. + * + * Since: 41 + */ + obj_props[PROP_SIZE_CACHE_DATA] = + g_param_spec_uint64 ("size-cache-data", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-download-type + * + * The type of #GsApp:size-download. + * + * Since: 43 + */ + obj_props[PROP_SIZE_DOWNLOAD_TYPE] = + g_param_spec_enum ("size-download-type", NULL, NULL, + GS_TYPE_SIZE_TYPE, GS_SIZE_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-download + * + * The size of the total download needed to either install or update + * this application, in bytes. If the app is partially downloaded, this + * is the number of bytes remaining to download. + * + * This is undefined if #GsApp:size-download-type is not + * %GS_SIZE_TYPE_VALID. + * + * To get the runtime or other dependencies download size, + * use #GsApp:size-download-dependencies. + * + * Since: 41 + */ + obj_props[PROP_SIZE_DOWNLOAD] = + g_param_spec_uint64 ("size-download", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-download-dependencies-type + * + * The type of #GsApp:size-download-dependencies. + * + * Since: 43 + */ + obj_props[PROP_SIZE_DOWNLOAD_DEPENDENCIES_TYPE] = + g_param_spec_enum ("size-download-dependencies-type", NULL, NULL, + GS_TYPE_SIZE_TYPE, GS_SIZE_TYPE_UNKNOWN, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-download-dependencies + * + * The size of the total download needed to either install or update + * this application's dependencies, in bytes. If the dependencies are partially + * downloaded, this is the number of bytes remaining to download. + * + * This is undefined if #GsApp:size-download-dependencies-type is not + * %GS_SIZE_TYPE_VALID. + * + * Since: 41 + */ + obj_props[PROP_SIZE_DOWNLOAD_DEPENDENCIES] = + g_param_spec_uint64 ("size-download-dependencies", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-installed-type + * + * The type of #GsApp:size-installed. + * + * Since: 43 + */ + obj_props[PROP_SIZE_INSTALLED_TYPE] = + g_param_spec_enum ("size-installed-type", NULL, NULL, + GS_TYPE_SIZE_TYPE, GS_SIZE_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-installed + * + * The size of the application on disk, in bytes. If the application is + * not yet installed, this is the size it would need, once installed. + * + * This is undefined if #GsApp:size-installed-type is not + * %GS_SIZE_TYPE_VALID. + * + * To get the application runtime or extensions installed sizes, + * use #GsApp:size-installed-dependencies. + * + * Since: 41 + */ + obj_props[PROP_SIZE_INSTALLED] = + g_param_spec_uint64 ("size-installed", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-installed-dependencies-type + * + * The type of #GsApp:size-installed-dependencies. + * + * Since: 43 + */ + obj_props[PROP_SIZE_INSTALLED_DEPENDENCIES_TYPE] = + g_param_spec_enum ("size-installed-dependencies-type", NULL, NULL, + GS_TYPE_SIZE_TYPE, GS_SIZE_TYPE_UNKNOWN, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-installed-dependencies + * + * The size of the application's dependencies on disk, in bytes. If the dependencies are + * not yet installed, this is the size it would need, once installed. + * + * This is undefined if #GsApp:size-installed-dependencies-type is not + * %GS_SIZE_TYPE_VALID. + * + * Since: 41 + */ + obj_props[PROP_SIZE_INSTALLED_DEPENDENCIES] = + g_param_spec_uint64 ("size-installed-dependencies", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-user-data-type + * + * The type of #GsApp:size-user-data. + * + * Since: 43 + */ + obj_props[PROP_SIZE_USER_DATA_TYPE] = + g_param_spec_enum ("size-user-data-type", NULL, NULL, + GS_TYPE_SIZE_TYPE, GS_SIZE_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:size-user-data + * + * The size on the disk for the user data of the application. + * + * This is undefined if #GsApp:size-user-data-type is not + * %GS_SIZE_TYPE_VALID. + * + * Since: 41 + */ + obj_props[PROP_SIZE_USER_DATA] = + g_param_spec_uint64 ("size-user-data", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:permissions + * + * The permissions the app requires to run, as a #GsAppPermissions object. + * + * This is %NULL, if the permissions are unknown. + * + * Since: 43 + */ + obj_props[PROP_PERMISSIONS] = + g_param_spec_object ("permissions", NULL, NULL, + GS_TYPE_APP_PERMISSIONS, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:relations: (nullable) (element-type AsRelation) + * + * Relations between this app and other things. For example, + * requirements or recommendations that the computer have certain input + * devices to use the app (the app requires a touchscreen or gamepad), + * or that the screen is a certain size. + * + * %NULL is equivalent to an empty array. Relations of kind + * %AS_RELATION_KIND_REQUIRES are conjunctive, so each additional + * relation further restricts the set of computers which can run the + * app. Relations of kind %AS_RELATION_KIND_RECOMMENDS and + * %AS_RELATION_KIND_SUPPORTS are disjunctive. + * + * Since: 41 + */ + obj_props[PROP_RELATIONS] = + g_param_spec_boxed ("relations", NULL, NULL, + G_TYPE_PTR_ARRAY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:origin-ui: (not nullable) + * + * The package origin, in a human readable format suitable for use in + * the UI. For example ‘Local file (RPM)’ or ‘Flathub (Flatpak)’. + * + * Since: 41 + */ + obj_props[PROP_ORIGIN_UI] = + g_param_spec_string ("origin-ui", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GsApp:has-translations + * + * Whether the app has any information about provided translations. If + * this is %TRUE, the app provides information about the translations + * it ships. If %FALSE, the app does not provide any information (but + * might ship translations which aren’t mentioned). + * + * Since: 41 + */ + obj_props[PROP_HAS_TRANSLATIONS] = + g_param_spec_boolean ("has-translations", NULL, NULL, + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); +} + +static void +gs_app_init (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + priv->rating = -1; + priv->sources = g_ptr_array_new_with_free_func (g_free); + priv->source_ids = g_ptr_array_new_with_free_func (g_free); + priv->categories = g_ptr_array_new_with_free_func (g_free); + priv->related = gs_app_list_new (); + priv->history = gs_app_list_new (); + priv->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->provided = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->metadata = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_variant_unref); + priv->launchables = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + g_free); + priv->allow_cancel = TRUE; + priv->size_download_type = GS_SIZE_TYPE_UNKNOWN; + priv->size_installed_type = GS_SIZE_TYPE_UNKNOWN; + priv->size_cache_data_type = GS_SIZE_TYPE_UNKNOWN; + priv->size_user_data_type = GS_SIZE_TYPE_UNKNOWN; + g_mutex_init (&priv->mutex); +} + +/** + * gs_app_new: + * @id: an application ID, or %NULL, e.g. "org.gnome.Software.desktop" + * + * Creates a new application object. + * + * The ID should only be set when the application ID (with optional prefix) is + * known; it is perfectly valid to use gs_app_new() with an @id of %NULL, and + * then relying on another plugin to set the @id using gs_app_set_id() based on + * some other information. + * + * For instance, a #GsApp is created with no ID when returning results from the + * packagekit plugin, but with the default source name set as the package name. + * The source name is read by the appstream plugin, and if matched in the + * AppStream XML the correct ID is set, along with other higher quality data + * like the application icon and long description. + * + * Returns: a new #GsApp + * + * Since: 3.22 + **/ +GsApp * +gs_app_new (const gchar *id) +{ + GsApp *app; + app = g_object_new (GS_TYPE_APP, + "id", id, + NULL); + return GS_APP (app); +} + +/** + * gs_app_set_from_unique_id: + * @app: a #GsApp + * @unique_id: an application unique ID, e.g. + * `system/flatpak/gnome/desktop/org.gnome.Software.desktop/master` + * + * Sets details on an application object. + * + * The unique ID will be parsed to set some information in the application such + * as the scope, bundle kind, id, etc. + * + * Since: 3.26 + **/ +void +gs_app_set_from_unique_id (GsApp *app, const gchar *unique_id, AsComponentKind kind) +{ + g_auto(GStrv) split = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (unique_id != NULL); + + if (kind != AS_COMPONENT_KIND_UNKNOWN) + gs_app_set_kind (app, kind); + + split = g_strsplit (unique_id, "/", -1); + if (g_strv_length (split) != 5) + return; + if (g_strcmp0 (split[0], "*") != 0) + gs_app_set_scope (app, as_component_scope_from_string (split[0])); + if (g_strcmp0 (split[1], "*") != 0) + gs_app_set_bundle_kind (app, as_bundle_kind_from_string (split[1])); + if (g_strcmp0 (split[2], "*") != 0) + gs_app_set_origin (app, split[2]); + if (g_strcmp0 (split[3], "*") != 0) + gs_app_set_id (app, split[3]); + if (g_strcmp0 (split[4], "*") != 0) + gs_app_set_branch (app, split[4]); +} + +/** + * gs_app_new_from_unique_id: + * @unique_id: an application unique ID, e.g. + * `system/flatpak/gnome/desktop/org.gnome.Software.desktop/master` + * + * Creates a new application object. + * + * The unique ID will be parsed to set some information in the application such + * as the scope, bundle kind, id, etc. Unlike gs_app_new(), it cannot take a + * %NULL argument. + * + * Returns: a new #GsApp + * + * Since: 3.22 + **/ +GsApp * +gs_app_new_from_unique_id (const gchar *unique_id) +{ + GsApp *app; + g_return_val_if_fail (unique_id != NULL, NULL); + app = gs_app_new (NULL); + gs_app_set_from_unique_id (app, unique_id, AS_COMPONENT_KIND_UNKNOWN); + return app; +} + +/** + * gs_app_dup_origin_ui: + * @app: a #GsApp + * @with_packaging_format: %TRUE, to include also packaging format + * + * Gets the package origin that's suitable for UI use, i.e. the value of + * #GsApp:origin-ui. + * + * Returns: (not nullable) (transfer full): The package origin for UI use + * + * Since: 43 + **/ +gchar * +gs_app_dup_origin_ui (GsApp *app, + gboolean with_packaging_format) +{ + GsAppPrivate *priv; + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + const gchar *origin_str = NULL; + + g_return_val_if_fail (GS_IS_APP (app), NULL); + + /* use the distro name for official packages */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE) && + gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY) { + os_release = gs_os_release_new (NULL); + if (os_release != NULL) + origin_str = gs_os_release_get_name (os_release); + } + + priv = gs_app_get_instance_private (app); + locker = g_mutex_locker_new (&priv->mutex); + + if (!origin_str) { + origin_str = priv->origin_ui; + + if (origin_str == NULL || origin_str[0] == '\0') { + /* use "Local file" rather than the filename for local files */ + if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE_LOCAL || + gs_app_get_local_file (app) != NULL) + origin_str = _("Local file"); + else if (g_strcmp0 (gs_app_get_origin (app), "flathub") == 0) + origin_str = "Flathub"; + else if (g_strcmp0 (gs_app_get_origin (app), "flathub-beta") == 0) + origin_str = "Flathub Beta"; + else + origin_str = gs_app_get_origin (app); + } + } + + if (with_packaging_format) { + g_autofree gchar *packaging_format = NULL; + + packaging_format = gs_app_get_packaging_format (app); + + if (packaging_format) { + /* TRANSLATORS: the first %s is replaced with an origin name; + the second %s is replaced with the packaging format. + Example string: "Local file (RPM)" */ + return g_strdup_printf (_("%s (%s)"), origin_str, packaging_format); + } + } + + return g_strdup (origin_str); +} + +/** + * gs_app_set_origin_ui: + * @app: a #GsApp + * @origin_ui: (not nullable): the new origin UI + * + * Set the value of #GsApp:origin-ui. + */ +void +gs_app_set_origin_ui (GsApp *app, + const gchar *origin_ui) +{ + GsAppPrivate *priv; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + + priv = gs_app_get_instance_private (app); + locker = g_mutex_locker_new (&priv->mutex); + + if (origin_ui && !*origin_ui) + origin_ui = NULL; + + if (g_strcmp0 (priv->origin_ui, origin_ui) == 0) + return; + + g_free (priv->origin_ui); + priv->origin_ui = g_strdup (origin_ui); + gs_app_queue_notify (app, obj_props[PROP_ORIGIN_UI]); +} + +/** + * gs_app_get_packaging_format: + * @app: a #GsApp + * + * Gets the packaging format, e.g. 'RPM' or 'Flatpak'. + * + * Returns: The packaging format + * + * Since: 3.32 + **/ +gchar * +gs_app_get_packaging_format (GsApp *app) +{ + AsBundleKind bundle_kind; + const gchar *bundle_kind_ui; + const gchar *packaging_format; + + g_return_val_if_fail (GS_IS_APP (app), NULL); + + /* does the app have packaging format set? */ + packaging_format = gs_app_get_metadata_item (app, "GnomeSoftware::PackagingFormat"); + if (packaging_format != NULL) + return g_strdup (packaging_format); + + /* fall back to bundle kind */ + bundle_kind = gs_app_get_bundle_kind (app); + switch (bundle_kind) { + case AS_BUNDLE_KIND_UNKNOWN: + bundle_kind_ui = NULL; + break; + case AS_BUNDLE_KIND_LIMBA: + bundle_kind_ui = "Limba"; + break; + case AS_BUNDLE_KIND_FLATPAK: + bundle_kind_ui = "Flatpak"; + break; + case AS_BUNDLE_KIND_SNAP: + bundle_kind_ui = "Snap"; + break; + case AS_BUNDLE_KIND_PACKAGE: + bundle_kind_ui = _("Package"); + break; + case AS_BUNDLE_KIND_CABINET: + bundle_kind_ui = "Cabinet"; + break; + case AS_BUNDLE_KIND_APPIMAGE: + bundle_kind_ui = "AppImage"; + break; + default: + g_warning ("unhandled bundle kind %s", as_bundle_kind_to_string (bundle_kind)); + bundle_kind_ui = as_bundle_kind_to_string (bundle_kind); + } + + return g_strdup (bundle_kind_ui); +} + +/** + * gs_app_get_packaging_format_raw: + * @app: a #GsApp + * + * Similar to gs_app_get_packaging_format(), but it does not return a newly + * allocated string and the value is not suitable for the UI. Depending on + * the plugin, it can be "deb", "flatpak", "package", "RPM", "snap", .... + * + * Returns: The raw value of the packaging format + * + * Since: 41 + **/ +const gchar * +gs_app_get_packaging_format_raw (GsApp *app) +{ + const gchar *packaging_format; + + g_return_val_if_fail (GS_IS_APP (app), NULL); + + packaging_format = gs_app_get_metadata_item (app, "GnomeSoftware::PackagingFormat"); + if (packaging_format != NULL) + return packaging_format; + + return as_bundle_kind_to_string (gs_app_get_bundle_kind (app)); +} + +/** + * gs_app_subsume_metadata: + * @app: a #GsApp + * @donor: another #GsApp + * + * Copies any metadata from @donor to @app. + * + * Since: 3.32 + **/ +void +gs_app_subsume_metadata (GsApp *app, GsApp *donor) +{ + GsAppPrivate *priv = gs_app_get_instance_private (donor); + g_autoptr(GList) keys = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (donor)); + + keys = g_hash_table_get_keys (priv->metadata); + for (GList *l = keys; l != NULL; l = l->next) { + const gchar *key = l->data; + GVariant *tmp = gs_app_get_metadata_variant (donor, key); + if (gs_app_get_metadata_variant (app, key) != NULL) + continue; + gs_app_set_metadata_variant (app, key, tmp); + } +} + +/** + * gs_app_dup_permissions: + * @app: a #GsApp + * + * Get a reference to the @app permissions. The returned value can + * be %NULL, when the app's permissions are unknown. Free the returned pointer, + * if not %NULL, with g_object_unref(), when no longer needed. + * + * Returns: (nullable) (transfer full): referenced #GsAppPermissions, + * or %NULL + * + * Since: 43 + **/ +GsAppPermissions * +gs_app_dup_permissions (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return priv->permissions ? g_object_ref (priv->permissions) : NULL; +} + +/** + * gs_app_set_permissions: + * @app: a #GsApp + * @permissions: (nullable) (transfer none): a #GsAppPermissions, or %NULL + * + * Set permissions for the @app. The @permissions is referenced, + * if not %NULL. + * + * Note the @permissions need to be sealed. + * + * Since: 43 + **/ +void +gs_app_set_permissions (GsApp *app, + GsAppPermissions *permissions) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (permissions == NULL || gs_app_permissions_is_sealed (permissions)); + + locker = g_mutex_locker_new (&priv->mutex); + if (priv->permissions == permissions) + return; + g_clear_object (&priv->permissions); + if (permissions != NULL) + priv->permissions = g_object_ref (permissions); + gs_app_queue_notify (app, obj_props[PROP_PERMISSIONS]); +} + +/** + * gs_app_dup_update_permissions: + * @app: a #GsApp + * + * Get a reference to the update permissions. The returned value can + * be %NULL, when no update permissions had been set. Free + * the returned pointer, if not %NULL, with g_object_unref(), when + * no longer needed. + * + * Returns: (nullable) (transfer full): referenced #GsAppPermissions, + * or %NULL + * + * Since: 43 + **/ +GsAppPermissions * +gs_app_dup_update_permissions (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return priv->update_permissions ? g_object_ref (priv->update_permissions) : NULL; +} + +/** + * gs_app_set_update_permissions: + * @app: a #GsApp + * @update_permissions: (nullable) (transfer none): a #GsAppPermissions, or %NULL + * + * Set update permissions for the @app, that is, the permissions, which change + * in an update or similar reasons. The @update_permissions is referenced, + * if not %NULL. + * + * Note the @update_permissions need to be sealed. + * + * Since: 43 + **/ +void +gs_app_set_update_permissions (GsApp *app, + GsAppPermissions *update_permissions) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (update_permissions == NULL || gs_app_permissions_is_sealed (update_permissions)); + locker = g_mutex_locker_new (&priv->mutex); + if (priv->update_permissions != update_permissions) { + g_clear_object (&priv->update_permissions); + if (update_permissions != NULL) + priv->update_permissions = g_object_ref (update_permissions); + } +} + +/** + * gs_app_get_version_history: + * @app: a #GsApp + * + * Gets the list of past releases for an application (including the latest + * one). + * + * Returns: (element-type AsRelease) (transfer container) (nullable): a list, or + * %NULL if the version history is not known + * + * Since: 41 + **/ +GPtrArray * +gs_app_get_version_history (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + + locker = g_mutex_locker_new (&priv->mutex); + if (priv->version_history == NULL) + return NULL; + return g_ptr_array_ref (priv->version_history); +} + +/** + * gs_app_set_version_history: + * @app: a #GsApp + * @version_history: (element-type AsRelease) (nullable): a set of entries + * representing the version history, or %NULL if none are known + * + * Set the list of past releases for an application (including the latest one). + * + * Since: 40 + **/ +void +gs_app_set_version_history (GsApp *app, GPtrArray *version_history) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + if (version_history != NULL && version_history->len == 0) + version_history = NULL; + + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->version_history, version_history); +} + +/** + * gs_app_ensure_icons_downloaded: + * @app: a #GsApp + * @soup_session: a #SoupSession + * @maximum_icon_size: maximum icon size + * @cancellable: (nullable): optional #GCancellable object + * + * Ensure all remote icons in the @app's icons are locally cached. + * + * Since: 41 + **/ +void +gs_app_ensure_icons_downloaded (GsApp *app, + SoupSession *soup_session, + guint maximum_icon_size, + GCancellable *cancellable) +{ + GsAppPrivate *priv; + g_autoptr(GMutexLocker) locker = NULL; + GPtrArray *icons; + guint i; + + g_return_if_fail (GS_IS_APP (app)); + + priv = gs_app_get_instance_private (app); + locker = g_mutex_locker_new (&priv->mutex); + + /* process all icons */ + icons = priv->icons; + + for (i = 0; icons != NULL && i < icons->len; i++) { + GIcon *icon = g_ptr_array_index (icons, i); + g_autoptr(GError) error_local = NULL; + + /* Only remote icons need to be cached. */ + if (!GS_IS_REMOTE_ICON (icon)) + continue; + + if (!gs_remote_icon_ensure_cached (GS_REMOTE_ICON (icon), + soup_session, + maximum_icon_size, + cancellable, + &error_local)) { + /* we failed, but keep going */ + g_debug ("failed to cache icon for %s: %s", + gs_app_get_id (app), + error_local->message); + } + } +} + +/** + * gs_app_get_relations: + * @app: a #GsApp + * + * Gets the value of #GsApp:relations. %NULL is equivalent to an empty array. + * + * The returned array should not be modified. + * + * Returns: (transfer container) (element-type AsRelation) (nullable): the value of + * #GsApp:relations, or %NULL + * Since: 41 + */ +GPtrArray * +gs_app_get_relations (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_val_if_fail (GS_IS_APP (app), NULL); + + locker = g_mutex_locker_new (&priv->mutex); + return (priv->relations != NULL) ? g_ptr_array_ref (priv->relations) : NULL; +} + +/** + * gs_app_add_relation: + * @app: a #GsApp + * @relation: (transfer none) (not nullable): a new #AsRelation to add to the app + * + * Adds @relation to #GsApp:relations. @relation must have all its properties + * set already. + * + * Since: 41 + */ +void +gs_app_add_relation (GsApp *app, + AsRelation *relation) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (AS_IS_RELATION (relation)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->relations == NULL) + priv->relations = g_ptr_array_new_with_free_func (g_object_unref); + g_ptr_array_add (priv->relations, g_object_ref (relation)); + + gs_app_queue_notify (app, obj_props[PROP_RELATIONS]); +} + +/** + * gs_app_set_relations: + * @app: a #GsApp + * @relations: (element-type AsRelation) (nullable) (transfer none): a new set + * of relations for #GsApp:relations; %NULL represents an empty array + * + * Set #GsApp:relations to @relations, replacing its previous value. %NULL is + * equivalent to an empty array. + * + * Since: 41 + */ +void +gs_app_set_relations (GsApp *app, + GPtrArray *relations) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GPtrArray) old_relations = NULL; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (relations == NULL && priv->relations == NULL) + return; + + if (priv->relations != NULL) + old_relations = g_steal_pointer (&priv->relations); + + if (relations != NULL) + priv->relations = g_ptr_array_ref (relations); + + gs_app_queue_notify (app, obj_props[PROP_RELATIONS]); +} + +/** + * gs_app_get_has_translations: + * @app: a #GsApp + * + * Get the value of #GsApp:has-translations. + * + * Returns: %TRUE if the app has translation metadata, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_app_get_has_translations (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + return priv->has_translations; +} + +/** + * gs_app_set_has_translations: + * @app: a #GsApp + * @has_translations: %TRUE if the app has translation metadata, %FALSE otherwise + * + * Set the value of #GsApp:has-translations. + * + * Since: 41 + */ +void +gs_app_set_has_translations (GsApp *app, + gboolean has_translations) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (priv->has_translations == has_translations) + return; + + priv->has_translations = has_translations; + gs_app_queue_notify (app, obj_props[PROP_HAS_TRANSLATIONS]); +} + +/** + * gs_app_is_downloaded: + * @app: a #GsApp + * + * Returns whether the @app is downloaded for updates or not, + * considering also its dependencies. + * + * Returns: %TRUE, when the @app is downloaded, %FALSE otherwise + * + * Since: 43 + **/ +gboolean +gs_app_is_downloaded (GsApp *app) +{ + GsSizeType size_type; + guint64 size_bytes = 0; + + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)) { + size_type = gs_app_get_size_download (app, &size_bytes); + if (size_type != GS_SIZE_TYPE_VALID || size_bytes != 0) + return FALSE; + } + + size_type = gs_app_get_size_download_dependencies (app, &size_bytes); + if (size_type != GS_SIZE_TYPE_VALID || size_bytes != 0) + return FALSE; + + return TRUE; +} diff --git a/lib/gs-app.h b/lib/gs-app.h new file mode 100644 index 0000000..5240b2e --- /dev/null +++ b/lib/gs-app.h @@ -0,0 +1,524 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gdk/gdk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <libsoup/soup.h> +#include <appstream.h> + +#include <gs-app-permissions.h> + +G_BEGIN_DECLS + +/* Dependency loop means we can’t include the header. */ +typedef struct _GsPlugin GsPlugin; +typedef struct _GsAppList GsAppList; + +#define GS_TYPE_APP (gs_app_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsApp, gs_app, GS, APP, GObject) + +struct _GsAppClass +{ + GObjectClass parent_class; + void (*to_string) (GsApp *app, + GString *str); + gpointer padding[30]; +}; + +/** + * GsAppState: + * @GS_APP_STATE_UNKNOWN: Unknown state + * @GS_APP_STATE_INSTALLED: Application is installed + * @GS_APP_STATE_AVAILABLE: Application is available + * @GS_APP_STATE_AVAILABLE_LOCAL: Application is locally available as a file + * @GS_APP_STATE_UPDATABLE: Application is installed and updatable + * @GS_APP_STATE_UNAVAILABLE: Application is referenced, but not available + * @GS_APP_STATE_QUEUED_FOR_INSTALL: Application is queued for install + * @GS_APP_STATE_INSTALLING: Application is being installed + * @GS_APP_STATE_REMOVING: Application is being removed + * @GS_APP_STATE_UPDATABLE_LIVE: Application is installed and updatable live + * @GS_APP_STATE_PURCHASABLE: Application is available for purchasing + * @GS_APP_STATE_PURCHASING: Application is being purchased + * @GS_APP_STATE_PENDING_INSTALL: Application is installed, but may have pending some actions, + * like restart, to finish it + * @GS_APP_STATE_PENDING_REMOVE: Application is removed, but may have pending some actions, + * like restart, to finish it + * + * The application state. + **/ +typedef enum { + GS_APP_STATE_UNKNOWN, /* Since: 0.2.2 */ + GS_APP_STATE_INSTALLED, /* Since: 0.2.2 */ + GS_APP_STATE_AVAILABLE, /* Since: 0.2.2 */ + GS_APP_STATE_AVAILABLE_LOCAL, /* Since: 0.2.2 */ + GS_APP_STATE_UPDATABLE, /* Since: 0.2.2 */ + GS_APP_STATE_UNAVAILABLE, /* Since: 0.2.2 */ + GS_APP_STATE_QUEUED_FOR_INSTALL, /* Since: 0.2.2 */ + GS_APP_STATE_INSTALLING, /* Since: 0.2.2 */ + GS_APP_STATE_REMOVING, /* Since: 0.2.2 */ + GS_APP_STATE_UPDATABLE_LIVE, /* Since: 0.5.4 */ + GS_APP_STATE_PURCHASABLE, /* Since: 0.5.17 */ + GS_APP_STATE_PURCHASING, /* Since: 0.5.17 */ + GS_APP_STATE_PENDING_INSTALL, /* Since: 41 */ + GS_APP_STATE_PENDING_REMOVE, /* Since: 41 */ + GS_APP_STATE_LAST /*< skip >*/ +} GsAppState; + +/** + * GsAppSpecialKind: + * @GS_APP_SPECIAL_KIND_NONE: No special occupation + * @GS_APP_SPECIAL_KIND_OS_UPDATE: Application represents an OS update + * + * A special occupation for #GsApp. #AsComponentKind can not represent certain + * GNOME Software specific features, like representing a #GsApp as OS updates + * which have no associated AppStream entry. + * They are represented by a #GsApp of kind %AS_COMPONENT_KIND_GENERIC and a value + * from #GsAppSpecialKind. which does not match any AppStream component type. + **/ +typedef enum { + GS_APP_SPECIAL_KIND_NONE, /* Since: 40 */ + GS_APP_SPECIAL_KIND_OS_UPDATE, /* Since: 40 */ +} GsAppSpecialKind; + +/** + * GsAppKudo: + * @GS_APP_KUDO_MY_LANGUAGE: Localised in my language + * @GS_APP_KUDO_RECENT_RELEASE: Released recently + * @GS_APP_KUDO_FEATURED_RECOMMENDED: Chosen for the front page + * @GS_APP_KUDO_MODERN_TOOLKIT: Uses a modern toolkit + * @GS_APP_KUDO_SEARCH_PROVIDER: Provides a search provider + * @GS_APP_KUDO_INSTALLS_USER_DOCS: Installs user docs + * @GS_APP_KUDO_USES_NOTIFICATIONS: Registers notifications + * @GS_APP_KUDO_HAS_KEYWORDS: Has at least 1 keyword + * @GS_APP_KUDO_HAS_SCREENSHOTS: Supplies screenshots + * @GS_APP_KUDO_HIGH_CONTRAST: Installs a high contrast icon + * @GS_APP_KUDO_HI_DPI_ICON: Installs a HiDPI icon + * @GS_APP_KUDO_SANDBOXED: Application is sandboxed + * @GS_APP_KUDO_SANDBOXED_SECURE: Application is sandboxed securely + * + * Any awards given to the application. + **/ +typedef enum { + GS_APP_KUDO_MY_LANGUAGE = 1 << 0, + GS_APP_KUDO_RECENT_RELEASE = 1 << 1, + GS_APP_KUDO_FEATURED_RECOMMENDED = 1 << 2, + GS_APP_KUDO_MODERN_TOOLKIT = 1 << 3, + GS_APP_KUDO_SEARCH_PROVIDER = 1 << 4, + GS_APP_KUDO_INSTALLS_USER_DOCS = 1 << 5, + GS_APP_KUDO_USES_NOTIFICATIONS = 1 << 6, + GS_APP_KUDO_HAS_KEYWORDS = 1 << 7, + GS_APP_KUDO_HAS_SCREENSHOTS = 1 << 9, + GS_APP_KUDO_HIGH_CONTRAST = 1 << 13, + GS_APP_KUDO_HI_DPI_ICON = 1 << 14, + GS_APP_KUDO_SANDBOXED = 1 << 15, + GS_APP_KUDO_SANDBOXED_SECURE = 1 << 16, + GS_APP_KUDO_LAST /*< skip >*/ +} GsAppKudo; + +/** + * GsAppQuirk: + * @GS_APP_QUIRK_NONE: No special attributes + * @GS_APP_QUIRK_PROVENANCE: Installed by OS vendor + * @GS_APP_QUIRK_COMPULSORY: Cannot be removed + * @GS_APP_QUIRK_HAS_SOURCE: Has a source to allow staying up-to-date + * @GS_APP_QUIRK_IS_WILDCARD: Matches applications from any plugin + * @GS_APP_QUIRK_NEEDS_REBOOT: A reboot is required after the action + * @GS_APP_QUIRK_NOT_REVIEWABLE: The app is not reviewable + * @GS_APP_QUIRK_NOT_LAUNCHABLE: The app is not launchable (run-able) + * @GS_APP_QUIRK_NEEDS_USER_ACTION: The component requires some kind of user action + * @GS_APP_QUIRK_IS_PROXY: Is a proxy app that operates on other applications + * @GS_APP_QUIRK_REMOVABLE_HARDWARE: The device is unusable whilst the action is performed + * @GS_APP_QUIRK_DEVELOPER_VERIFIED: The app developer has been verified + * @GS_APP_QUIRK_PARENTAL_FILTER: The app has been filtered by parental controls, and should be hidden + * @GS_APP_QUIRK_NEW_PERMISSIONS: The update requires new permissions + * @GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE: The app cannot be run by the current user due to parental controls, and should not be launchable + * @GS_APP_QUIRK_HIDE_FROM_SEARCH: The app should not be shown in search results + * @GS_APP_QUIRK_HIDE_EVERYWHERE: The app should not be shown anywhere (it’s blocklisted) + * @GS_APP_QUIRK_DO_NOT_AUTO_UPDATE: The app should not be automatically updated + * @GS_APP_QUIRK_DEVELOPMENT_SOURCE: The app is from a development source (Since: 43) + * + * The application attributes. + **/ +typedef enum { + GS_APP_QUIRK_NONE = 0, /* Since: 3.32 */ + GS_APP_QUIRK_PROVENANCE = 1 << 0, /* Since: 3.32 */ + GS_APP_QUIRK_COMPULSORY = 1 << 1, /* Since: 3.32 */ + GS_APP_QUIRK_HAS_SOURCE = 1 << 2, /* Since: 3.32 */ + GS_APP_QUIRK_IS_WILDCARD = 1 << 3, /* Since: 3.32 */ + GS_APP_QUIRK_NEEDS_REBOOT = 1 << 4, /* Since: 3.32 */ + GS_APP_QUIRK_NOT_REVIEWABLE = 1 << 5, /* Since: 3.32 */ + /* there’s a hole here where GS_APP_QUIRK_HAS_SHORTCUT used to be */ + GS_APP_QUIRK_NOT_LAUNCHABLE = 1 << 7, /* Since: 3.32 */ + GS_APP_QUIRK_NEEDS_USER_ACTION = 1 << 8, /* Since: 3.32 */ + GS_APP_QUIRK_IS_PROXY = 1 << 9, /* Since: 3.32 */ + GS_APP_QUIRK_REMOVABLE_HARDWARE = 1 << 10, /* Since: 3.32 */ + GS_APP_QUIRK_DEVELOPER_VERIFIED = 1 << 11, /* Since: 3.32 */ + GS_APP_QUIRK_PARENTAL_FILTER = 1 << 12, /* Since: 3.32 */ + GS_APP_QUIRK_NEW_PERMISSIONS = 1 << 13, /* Since: 3.32 */ + GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE = 1 << 14, /* Since: 3.32 */ + GS_APP_QUIRK_HIDE_FROM_SEARCH = 1 << 15, /* Since: 3.32 */ + GS_APP_QUIRK_HIDE_EVERYWHERE = 1 << 16, /* Since: 3.36 */ + GS_APP_QUIRK_DO_NOT_AUTO_UPDATE = 1 << 17, /* Since: 3.36 */ + GS_APP_QUIRK_DEVELOPMENT_SOURCE = 1 << 18, /* Since: 43 */ + GS_APP_QUIRK_LAST /*< skip >*/ +} GsAppQuirk; + +#define GS_APP_INSTALL_DATE_UNSET 0 +#define GS_APP_INSTALL_DATE_UNKNOWN 1 /* 1s past the epoch */ + +/** + * GsSizeType: + * @GS_SIZE_TYPE_UNKNOWN: Size is unknown + * @GS_SIZE_TYPE_UNKNOWABLE: Size is unknown and is impossible to calculate + * @GS_SIZE_TYPE_VALID: Size is known and valid + * + * Types of download or file size for applications. + * + * These are used to represent the validity of properties like + * #GsApp:size-download. + * + * Since: 43 + */ +typedef enum { + GS_SIZE_TYPE_UNKNOWN, + GS_SIZE_TYPE_UNKNOWABLE, + GS_SIZE_TYPE_VALID, +} GsSizeType; + +/** + * GsAppQuality: + * @GS_APP_QUALITY_UNKNOWN: The quality value is unknown + * @GS_APP_QUALITY_LOWEST: Lowest quality + * @GS_APP_QUALITY_NORMAL: Normal quality + * @GS_APP_QUALITY_HIGHEST: Highest quality + * + * Any awards given to the application. + **/ +typedef enum { + GS_APP_QUALITY_UNKNOWN, + GS_APP_QUALITY_LOWEST, + GS_APP_QUALITY_NORMAL, + GS_APP_QUALITY_HIGHEST, + GS_APP_QUALITY_LAST /*< skip >*/ +} GsAppQuality; + +/** + * GS_APP_PROGRESS_UNKNOWN: + * + * A value returned by gs_app_get_progress() if the app’s progress is unknown + * or has a wide confidence interval. Typically this would be represented in the + * UI using a pulsing progress bar or spinner. + * + * Since: 3.38 + */ +#define GS_APP_PROGRESS_UNKNOWN G_MAXUINT + +const gchar *gs_app_state_to_string (GsAppState state); + +GsApp *gs_app_new (const gchar *id); +G_DEPRECATED_FOR(gs_app_set_from_unique_id) +GsApp *gs_app_new_from_unique_id (const gchar *unique_id); +void gs_app_set_from_unique_id (GsApp *app, + const gchar *unique_id, + AsComponentKind kind); +gchar *gs_app_to_string (GsApp *app); +void gs_app_to_string_append (GsApp *app, + GString *str); + +const gchar *gs_app_get_id (GsApp *app); +void gs_app_set_id (GsApp *app, + const gchar *id); +AsComponentKind gs_app_get_kind (GsApp *app); +void gs_app_set_kind (GsApp *app, + AsComponentKind kind); +GsAppState gs_app_get_state (GsApp *app); +void gs_app_set_state (GsApp *app, + GsAppState state); +AsComponentScope gs_app_get_scope (GsApp *app); +void gs_app_set_scope (GsApp *app, + AsComponentScope scope); +AsBundleKind gs_app_get_bundle_kind (GsApp *app); +void gs_app_set_bundle_kind (GsApp *app, + AsBundleKind bundle_kind); +GsAppSpecialKind gs_app_get_special_kind (GsApp *app); +void gs_app_set_special_kind (GsApp *app, + GsAppSpecialKind kind); +void gs_app_set_state_recover (GsApp *app); +guint gs_app_get_progress (GsApp *app); +void gs_app_set_progress (GsApp *app, + guint percentage); +gboolean gs_app_get_allow_cancel (GsApp *app); +void gs_app_set_allow_cancel (GsApp *app, + gboolean allow_cancel); +const gchar *gs_app_get_unique_id (GsApp *app); +const gchar *gs_app_get_branch (GsApp *app); +void gs_app_set_branch (GsApp *app, + const gchar *branch); +const gchar *gs_app_get_name (GsApp *app); +void gs_app_set_name (GsApp *app, + GsAppQuality quality, + const gchar *name); +const gchar *gs_app_get_renamed_from (GsApp *app); +void gs_app_set_renamed_from (GsApp *app, + const gchar *renamed_from); +const gchar *gs_app_get_source_default (GsApp *app); +void gs_app_add_source (GsApp *app, + const gchar *source); +GPtrArray *gs_app_get_sources (GsApp *app); +void gs_app_set_sources (GsApp *app, + GPtrArray *sources); +const gchar *gs_app_get_source_id_default (GsApp *app); +void gs_app_add_source_id (GsApp *app, + const gchar *source_id); +GPtrArray *gs_app_get_source_ids (GsApp *app); +void gs_app_set_source_ids (GsApp *app, + GPtrArray *source_ids); +void gs_app_clear_source_ids (GsApp *app); +const gchar *gs_app_get_project_group (GsApp *app); +void gs_app_set_project_group (GsApp *app, + const gchar *project_group); +const gchar *gs_app_get_developer_name (GsApp *app); +void gs_app_set_developer_name (GsApp *app, + const gchar *developer_name); +const gchar *gs_app_get_agreement (GsApp *app); +void gs_app_set_agreement (GsApp *app, + const gchar *agreement); +const gchar *gs_app_get_version (GsApp *app); +const gchar *gs_app_get_version_ui (GsApp *app); +void gs_app_set_version (GsApp *app, + const gchar *version); +const gchar *gs_app_get_summary (GsApp *app); +void gs_app_set_summary (GsApp *app, + GsAppQuality quality, + const gchar *summary); +const gchar *gs_app_get_summary_missing (GsApp *app); +void gs_app_set_summary_missing (GsApp *app, + const gchar *summary_missing); +const gchar *gs_app_get_description (GsApp *app); +void gs_app_set_description (GsApp *app, + GsAppQuality quality, + const gchar *description); +const gchar *gs_app_get_url (GsApp *app, + AsUrlKind kind); +void gs_app_set_url (GsApp *app, + AsUrlKind kind, + const gchar *url); +const gchar *gs_app_get_url_missing (GsApp *app); +void gs_app_set_url_missing (GsApp *app, + const gchar *url); +const gchar *gs_app_get_launchable (GsApp *app, + AsLaunchableKind kind); +void gs_app_set_launchable (GsApp *app, + AsLaunchableKind kind, + const gchar *launchable); +const gchar *gs_app_get_license (GsApp *app); +gboolean gs_app_get_license_is_free (GsApp *app); +void gs_app_set_license (GsApp *app, + GsAppQuality quality, + const gchar *license); +gchar **gs_app_get_menu_path (GsApp *app); +void gs_app_set_menu_path (GsApp *app, + gchar **menu_path); +const gchar *gs_app_get_origin (GsApp *app); +void gs_app_set_origin (GsApp *app, + const gchar *origin); +const gchar *gs_app_get_origin_appstream (GsApp *app); +void gs_app_set_origin_appstream (GsApp *app, + const gchar *origin_appstream); +const gchar *gs_app_get_origin_hostname (GsApp *app); +void gs_app_set_origin_hostname (GsApp *app, + const gchar *origin_hostname); +GPtrArray *gs_app_get_screenshots (GsApp *app); +void gs_app_add_screenshot (GsApp *app, + AsScreenshot *screenshot); +AsScreenshot *gs_app_get_action_screenshot (GsApp *app); +void gs_app_set_action_screenshot (GsApp *app, + AsScreenshot *screenshot); +const gchar *gs_app_get_update_version (GsApp *app); +const gchar *gs_app_get_update_version_ui (GsApp *app); +void gs_app_set_update_version (GsApp *app, + const gchar *update_version); +const gchar *gs_app_get_update_details_markup + (GsApp *app); +void gs_app_set_update_details_markup + (GsApp *app, + const gchar *markup); +void gs_app_set_update_details_text (GsApp *app, + const gchar *text); +AsUrgencyKind gs_app_get_update_urgency (GsApp *app); +void gs_app_set_update_urgency (GsApp *app, + AsUrgencyKind update_urgency); +GsPlugin *gs_app_dup_management_plugin (GsApp *app); +gboolean gs_app_has_management_plugin (GsApp *app, + GsPlugin *plugin); +void gs_app_set_management_plugin (GsApp *app, + GsPlugin *management_plugin); +GIcon *gs_app_get_icon_for_size (GsApp *app, + guint size, + guint scale, + const gchar *fallback_icon_name); +GPtrArray *gs_app_get_icons (GsApp *app); +void gs_app_add_icon (GsApp *app, + GIcon *icon); +void gs_app_remove_all_icons (GsApp *app); +GFile *gs_app_get_local_file (GsApp *app); +void gs_app_set_local_file (GsApp *app, + GFile *local_file); +AsContentRating *gs_app_dup_content_rating (GsApp *app); +void gs_app_set_content_rating (GsApp *app, + AsContentRating *content_rating); +GsApp *gs_app_get_runtime (GsApp *app); +void gs_app_set_runtime (GsApp *app, + GsApp *runtime); +const gchar *gs_app_get_metadata_item (GsApp *app, + const gchar *key); +GVariant *gs_app_get_metadata_variant (GsApp *app, + const gchar *key); +void gs_app_set_metadata (GsApp *app, + const gchar *key, + const gchar *value); +void gs_app_set_metadata_variant (GsApp *app, + const gchar *key, + GVariant *value); +gint gs_app_get_rating (GsApp *app); +void gs_app_set_rating (GsApp *app, + gint rating); +GArray *gs_app_get_review_ratings (GsApp *app); +void gs_app_set_review_ratings (GsApp *app, + GArray *review_ratings); +GPtrArray *gs_app_get_reviews (GsApp *app); +void gs_app_add_review (GsApp *app, + AsReview *review); +void gs_app_remove_review (GsApp *app, + AsReview *review); +GPtrArray *gs_app_get_provided (GsApp *app); +AsProvided *gs_app_get_provided_for_kind (GsApp *app, + AsProvidedKind kind); +void gs_app_add_provided_item (GsApp *app, + AsProvidedKind kind, + const gchar *item); +GsSizeType gs_app_get_size_installed (GsApp *app, + guint64 *size_bytes_out); +void gs_app_set_size_installed (GsApp *app, + GsSizeType size_type, + guint64 size_bytes); +GsSizeType gs_app_get_size_installed_dependencies + (GsApp *app, + guint64 *size_bytes_out); +GsSizeType gs_app_get_size_user_data (GsApp *app, + guint64 *size_bytes_out); +void gs_app_set_size_user_data (GsApp *app, + GsSizeType size_type, + guint64 size_bytes); +GsSizeType gs_app_get_size_cache_data (GsApp *app, + guint64 *size_bytes_out); +void gs_app_set_size_cache_data (GsApp *app, + GsSizeType size_type, + guint64 size_bytes); +GsSizeType gs_app_get_size_download (GsApp *app, + guint64 *size_bytes_out); +void gs_app_set_size_download (GsApp *app, + GsSizeType size_type, + guint64 size_bytes); +GsSizeType gs_app_get_size_download_dependencies + (GsApp *app, + guint64 *size_bytes_out); +void gs_app_add_related (GsApp *app, + GsApp *app2); +void gs_app_add_addons (GsApp *app, + GsAppList *addons); +void gs_app_add_history (GsApp *app, + GsApp *app2); +guint64 gs_app_get_install_date (GsApp *app); +void gs_app_set_install_date (GsApp *app, + guint64 install_date); +guint64 gs_app_get_release_date (GsApp *app); +void gs_app_set_release_date (GsApp *app, + guint64 release_date); +GPtrArray *gs_app_get_categories (GsApp *app); +void gs_app_set_categories (GsApp *app, + GPtrArray *categories); +GArray *gs_app_get_key_colors (GsApp *app); +void gs_app_set_key_colors (GsApp *app, + GArray *key_colors); +void gs_app_add_key_color (GsApp *app, + GdkRGBA *key_color); +gboolean gs_app_get_user_key_colors (GsApp *app); +void gs_app_set_is_update_downloaded (GsApp *app, + gboolean is_update_downloaded); +gboolean gs_app_get_is_update_downloaded (GsApp *app); +gboolean gs_app_has_category (GsApp *app, + const gchar *category); +void gs_app_add_category (GsApp *app, + const gchar *category); +gboolean gs_app_remove_category (GsApp *app, + const gchar *category); +void gs_app_add_kudo (GsApp *app, + GsAppKudo kudo); +void gs_app_remove_kudo (GsApp *app, + GsAppKudo kudo); +gboolean gs_app_has_kudo (GsApp *app, + GsAppKudo kudo); +guint64 gs_app_get_kudos (GsApp *app); +guint gs_app_get_kudos_percentage (GsApp *app); +gboolean gs_app_get_to_be_installed (GsApp *app); +void gs_app_set_to_be_installed (GsApp *app, + gboolean to_be_installed); +void gs_app_set_match_value (GsApp *app, + guint match_value); +guint gs_app_get_match_value (GsApp *app); + +gboolean gs_app_has_quirk (GsApp *app, + GsAppQuirk quirk); +void gs_app_add_quirk (GsApp *app, + GsAppQuirk quirk); +void gs_app_remove_quirk (GsApp *app, + GsAppQuirk quirk); +gboolean gs_app_is_installed (GsApp *app); +gboolean gs_app_is_updatable (GsApp *app); +gchar *gs_app_dup_origin_ui (GsApp *app, + gboolean with_packaging_format); +void gs_app_set_origin_ui (GsApp *app, + const gchar *origin_ui); +gchar *gs_app_get_packaging_format (GsApp *app); +const gchar *gs_app_get_packaging_format_raw(GsApp *app); +void gs_app_subsume_metadata (GsApp *app, + GsApp *donor); +GsAppPermissions * + gs_app_dup_permissions (GsApp *app); +void gs_app_set_permissions (GsApp *app, + GsAppPermissions *permissions); +GsAppPermissions * + gs_app_dup_update_permissions (GsApp *app); +void gs_app_set_update_permissions (GsApp *app, + GsAppPermissions *update_permissions); +GPtrArray *gs_app_get_version_history (GsApp *app); +void gs_app_set_version_history (GsApp *app, + GPtrArray *version_history); +void gs_app_ensure_icons_downloaded (GsApp *app, + SoupSession *soup_session, + guint maximum_icon_size, + GCancellable *cancellable); + +GPtrArray *gs_app_get_relations (GsApp *app); +void gs_app_add_relation (GsApp *app, + AsRelation *relation); +void gs_app_set_relations (GsApp *app, + GPtrArray *relations); + +gboolean gs_app_get_has_translations (GsApp *app); +void gs_app_set_has_translations (GsApp *app, + gboolean has_translations); +gboolean gs_app_is_downloaded (GsApp *app); + +G_END_DECLS diff --git a/lib/gs-appstream.c b/lib/gs-appstream.c new file mode 100644 index 0000000..fa94a6e --- /dev/null +++ b/lib/gs-appstream.c @@ -0,0 +1,2165 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <gnome-software.h> +#include <locale.h> + +#include "gs-appstream.h" + +#define GS_APPSTREAM_MAX_SCREENSHOTS 5 + +GsApp * +gs_appstream_create_app (GsPlugin *plugin, XbSilo *silo, XbNode *component, GError **error) +{ + GsApp *app; + g_autoptr(GsApp) app_new = NULL; + + /* The 'plugin' can be NULL, when creating app for --show-metainfo */ + g_return_val_if_fail (XB_IS_SILO (silo), NULL); + g_return_val_if_fail (XB_IS_NODE (component), NULL); + + app_new = gs_app_new (NULL); + + /* refine enough to get the unique ID */ + if (!gs_appstream_refine_app (plugin, app_new, silo, component, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID, + error)) + return NULL; + + /* never add wildcard apps to the plugin cache, and only add to + * the cache if it’s available */ + if (gs_app_has_quirk (app_new, GS_APP_QUIRK_IS_WILDCARD) || plugin == NULL) + return g_steal_pointer (&app_new); + + if (plugin == NULL) + return g_steal_pointer (&app_new); + + /* look for existing object */ + app = gs_plugin_cache_lookup (plugin, gs_app_get_unique_id (app_new)); + if (app != NULL) + return app; + + /* use the temp object we just created */ + gs_app_set_metadata (app_new, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_plugin_cache_add (plugin, NULL, app_new); + return g_steal_pointer (&app_new); +} + +/* Helper function to do the equivalent of + * *node = xb_node_get_next (*node) + * but with correct reference counting, since xb_node_get_next() returns a new + * ref. */ +static void +node_set_to_next (XbNode **node) +{ + g_autoptr(XbNode) next_node = NULL; + + g_assert (node != NULL); + g_assert (*node != NULL); + + next_node = xb_node_get_next (*node); + g_object_unref (*node); + *node = g_steal_pointer (&next_node); +} + +/* Returns escaped text */ +static gchar * +gs_appstream_format_description_text (XbNode *node) +{ + g_autoptr(GString) str = g_string_new (NULL); + const gchar *node_text; + + if (node == NULL) + return NULL; + + node_text = xb_node_get_text (node); + if (node_text != NULL && *node_text != '\0') { + g_autofree gchar *escaped = g_markup_escape_text (node_text, -1); + g_string_append (str, escaped); + } + + for (g_autoptr(XbNode) n = xb_node_get_child (node); n != NULL; node_set_to_next (&n)) { + const gchar *start_elem = "", *end_elem = ""; + g_autofree gchar *text = NULL; + if (g_strcmp0 (xb_node_get_element (n), "em") == 0) { + start_elem = "<i>"; + end_elem = "</i>"; + } else if (g_strcmp0 (xb_node_get_element (n), "code") == 0) { + start_elem = "<tt>"; + end_elem = "</tt>"; + } + + /* These can be nested */ + text = gs_appstream_format_description_text (n); + if (text != NULL) { + g_string_append_printf (str, "%s%s%s", start_elem, text, end_elem); + } + + node_text = xb_node_get_tail (n); + if (node_text != NULL && *node_text != '\0') { + g_autofree gchar *escaped = g_markup_escape_text (node_text, -1); + g_string_append (str, escaped); + } + } + + if (str->len == 0) + return NULL; + + return g_string_free (g_steal_pointer (&str), FALSE); +} + +static gchar * +gs_appstream_format_description (XbNode *root, GError **error) +{ + g_autoptr(GString) str = g_string_new (NULL); + + for (g_autoptr(XbNode) n = xb_node_get_child (root); n != NULL; node_set_to_next (&n)) { + /* support <p>, <em>, <code>, <ul>, <ol> and <li>, ignore all else */ + if (g_strcmp0 (xb_node_get_element (n), "p") == 0) { + g_autofree gchar *escaped = gs_appstream_format_description_text (n); + /* Treat a self-closing paragraph (`<p/>`) as + * nonexistent. This is consistent with Firefox. */ + if (escaped != NULL) + g_string_append_printf (str, "%s\n\n", escaped); + } else if (g_strcmp0 (xb_node_get_element (n), "ul") == 0) { + g_autoptr(GPtrArray) children = xb_node_get_children (n); + + for (guint i = 0; i < children->len; i++) { + XbNode *nc = g_ptr_array_index (children, i); + if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) { + g_autofree gchar *escaped = gs_appstream_format_description_text (nc); + + /* Treat a self-closing `<li/>` as an empty + * list element (equivalent to `<li></li>`). + * This is consistent with Firefox. */ + g_string_append_printf (str, " • %s\n", + (escaped != NULL) ? escaped : ""); + } + } + g_string_append (str, "\n"); + } else if (g_strcmp0 (xb_node_get_element (n), "ol") == 0) { + g_autoptr(GPtrArray) children = xb_node_get_children (n); + for (guint i = 0; i < children->len; i++) { + XbNode *nc = g_ptr_array_index (children, i); + if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) { + g_autofree gchar *escaped = gs_appstream_format_description_text (nc); + + /* Treat self-closing elements as with `<ul>` above. */ + g_string_append_printf (str, " %u. %s\n", + i + 1, + (escaped != NULL) ? escaped : ""); + } + } + g_string_append (str, "\n"); + } + } + + /* remove extra newlines */ + while (str->len > 0 && str->str[str->len - 1] == '\n') + g_string_truncate (str, str->len - 1); + + /* success */ + return g_string_free (g_steal_pointer (&str), FALSE); +} + +static gchar * +gs_appstream_build_icon_prefix (XbNode *component) +{ + const gchar *origin; + const gchar *tmp; + gint npath; + g_auto(GStrv) path = NULL; + g_autoptr(XbNode) components = NULL; + + /* no parent, e.g. AppData */ + components = xb_node_get_parent (component); + if (components == NULL) + return NULL; + + /* set explicitly */ + tmp = xb_node_query_text (components, "info/icon-prefix", NULL); + if (tmp != NULL) + return g_strdup (tmp); + + /* fall back to origin */ + origin = xb_node_get_attr (components, "origin"); + if (origin == NULL) + return NULL; + + /* no metadata */ + tmp = xb_node_query_text (components, "info/filename", NULL); + if (tmp == NULL) + return NULL; + + /* check format */ + path = g_strsplit (tmp, "/", -1); + npath = g_strv_length (path); + if (npath < 3 || + !(g_strcmp0 (path[npath-2], "xmls") == 0 || + g_strcmp0 (path[npath-2], "yaml") == 0 || + g_strcmp0 (path[npath-2], "xml") == 0)) + return NULL; + + /* fix the new path */ + g_free (path[npath-1]); + g_free (path[npath-2]); + path[npath-1] = g_strdup (origin); + path[npath-2] = g_strdup ("icons"); + return g_strjoinv ("/", path); +} + +/* This function is designed to do no disk or network I/O. */ +static AsIcon * +gs_appstream_new_icon (XbNode *component, XbNode *n, AsIconKind icon_kind, guint sz) +{ + AsIcon *icon = as_icon_new (); + g_autofree gchar *icon_path = NULL; + as_icon_set_kind (icon, icon_kind); + switch (icon_kind) { + case AS_ICON_KIND_LOCAL: + as_icon_set_filename (icon, xb_node_get_text (n)); + break; + case AS_ICON_KIND_REMOTE: + as_icon_set_url (icon, xb_node_get_text (n)); + break; + default: + as_icon_set_name (icon, xb_node_get_text (n)); + } + if (sz == 0) { + guint64 width = xb_node_get_attr_as_uint (n, "width"); + if (width > 0 && width < G_MAXUINT) + sz = width; + } + + if (sz > 0) { + as_icon_set_width (icon, sz); + as_icon_set_height (icon, sz); + } + + if (icon_kind != AS_ICON_KIND_LOCAL && icon_kind != AS_ICON_KIND_REMOTE) { + /* add partial filename for now, we will compose the full one later */ + icon_path = gs_appstream_build_icon_prefix (component); + as_icon_set_filename (icon, icon_path); + } + return icon; +} + +static void +app_add_icon (GsApp *app, + AsIcon *as_icon) +{ + g_autoptr(GIcon) icon = gs_icon_new_for_appstream_icon (as_icon); + if (icon != NULL) + gs_app_add_icon (app, icon); +} + +static void +traverse_component_icons (GsApp *app, + XbNode *component, + GPtrArray *icons) +{ + if (!icons) + return; + + /* This code deliberately does *not* check that the icon files or theme + * icons exist, as that would mean doing disk I/O for all the apps in + * the appstream file, regardless of whether the calling code is + * actually going to use the icons. Better to add all the possible icons + * and let the calling code check which ones exist, if it needs to. */ + for (guint i = 0; i < icons->len; i++) { + XbNode *icon_node = g_ptr_array_index (icons, i); + g_autoptr(AsIcon) icon = NULL; + const gchar *icon_kind_str = xb_node_get_attr (icon_node, "type"); + AsIconKind icon_kind = as_icon_kind_from_string (icon_kind_str); + + if (icon_kind == AS_ICON_KIND_UNKNOWN) { + g_debug ("unknown icon kind ‘%s’", icon_kind_str); + continue; + } + + icon = gs_appstream_new_icon (component, icon_node, icon_kind, 0); + app_add_icon (app, icon); + } +} + +static void +traverse_components_xpath_for_icons (GsApp *app, + XbSilo *silo, + const gchar *xpath, + gboolean try_with_launchable) +{ + g_autoptr(GPtrArray) components = NULL; + g_autoptr(GError) local_error = NULL; + + components = xb_silo_query (silo, xpath, 0, &local_error); + if (components) { + for (guint i = 0; i < components->len; i++) { + g_autoptr(GPtrArray) icons = NULL; /* (element-type XbNode) */ + XbNode *component = g_ptr_array_index (components, i); + g_autofree gchar *xml = xb_node_export (component, 0, NULL); + icons = xb_node_query (component, "icon", 0, NULL); + traverse_component_icons (app, component, icons); + + if (try_with_launchable && gs_app_get_icons (app) == NULL) { + const gchar *launchable_id = xb_node_query_text (component, "launchable[@type='desktop-id']", NULL); + if (launchable_id != NULL) { + g_autofree gchar *xpath2 = NULL; + + /* Inherit the icon from the .desktop file */ + xpath2 = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..", + launchable_id); + traverse_components_xpath_for_icons (app, silo, xpath2, FALSE); + } + } + } + } +} + +static void +gs_appstream_refine_icon (GsApp *app, + XbSilo *silo, + XbNode *component) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(GPtrArray) icons = NULL; /* (element-type XbNode) */ + + icons = xb_node_query (component, "icon", 0, &local_error); + traverse_component_icons (app, component, icons); + g_clear_pointer (&icons, g_ptr_array_unref); + + /* If no icon found, try to inherit the icon from the .desktop file */ + if (gs_app_get_icons (app) == NULL) { + g_autofree gchar *xpath = NULL; + const gchar *launchable_id = xb_node_query_text (component, "launchable[@type='desktop-id']", NULL); + if (launchable_id != NULL) { + xpath = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..", + launchable_id); + traverse_components_xpath_for_icons (app, silo, xpath, FALSE); + g_clear_pointer (&xpath, g_free); + } + + xpath = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..", + gs_app_get_id (app)); + traverse_components_xpath_for_icons (app, silo, xpath, FALSE); + } +} + +static gboolean +gs_appstream_refine_add_addons (GsPlugin *plugin, + GsApp *app, + XbSilo *silo, + GError **error) +{ + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) addons = NULL; + g_autoptr(GsAppList) addons_list = NULL; + + /* get all components */ + xpath = g_strdup_printf ("components/component/extends[text()='%s']/..", + gs_app_get_id (app)); + addons = xb_silo_query (silo, xpath, 0, &error_local); + if (addons == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + addons_list = gs_app_list_new (); + + for (guint i = 0; i < addons->len; i++) { + XbNode *addon = g_ptr_array_index (addons, i); + g_autoptr(GsApp) addon_app = NULL; + + addon_app = gs_appstream_create_app (plugin, silo, addon, error); + if (addon_app == NULL) + return FALSE; + + gs_app_list_add (addons_list, addon_app); + } + + gs_app_add_addons (app, addons_list); + + return TRUE; +} + +static gboolean +gs_appstream_refine_add_images (GsApp *app, + AsScreenshot *ss, + XbNode *screenshot, + gboolean *out_any_added, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) images = NULL; + + /* get all components */ + images = xb_node_query (screenshot, "image", 0, &error_local); + if (images == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < images->len; i++) { + XbNode *image = g_ptr_array_index (images, i); + g_autoptr(AsImage) im = as_image_new (); + as_image_set_height (im, xb_node_get_attr_as_uint (image, "height")); + as_image_set_width (im, xb_node_get_attr_as_uint (image, "width")); + as_image_set_kind (im, as_image_kind_from_string (xb_node_get_attr (image, "type"))); + as_image_set_url (im, xb_node_get_text (image)); + as_screenshot_add_image (ss, im); + } + + *out_any_added = *out_any_added || images->len > 0; + + /* success */ + return TRUE; +} + +static gboolean +gs_appstream_refine_add_videos (GsApp *app, + AsScreenshot *ss, + XbNode *screenshot, + gboolean *out_any_added, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) videos = NULL; + + videos = xb_node_query (screenshot, "video", 0, &error_local); + if (videos == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < videos->len; i++) { + XbNode *video = g_ptr_array_index (videos, i); + g_autoptr(AsVideo) vid = as_video_new (); + as_video_set_height (vid, xb_node_get_attr_as_uint (video, "height")); + as_video_set_width (vid, xb_node_get_attr_as_uint (video, "width")); + as_video_set_codec_kind (vid, as_video_codec_kind_from_string (xb_node_get_attr (video, "codec"))); + as_video_set_container_kind (vid, as_video_container_kind_from_string (xb_node_get_attr (video, "container"))); + as_video_set_url (vid, xb_node_get_text (video)); + as_screenshot_add_video (ss, vid); + } + + *out_any_added = *out_any_added || videos->len > 0; + + return TRUE; +} + +static gboolean +gs_appstream_refine_add_screenshots (GsApp *app, XbNode *component, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) screenshots = NULL; + + /* get all components */ + screenshots = xb_node_query (component, "screenshots/screenshot", 0, &error_local); + if (screenshots == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < screenshots->len; i++) { + XbNode *screenshot = g_ptr_array_index (screenshots, i); + g_autoptr(AsScreenshot) ss = as_screenshot_new (); + gboolean any_added = FALSE; + if (!gs_appstream_refine_add_images (app, ss, screenshot, &any_added, error) || + !gs_appstream_refine_add_videos (app, ss, screenshot, &any_added, error)) + return FALSE; + if (any_added) + gs_app_add_screenshot (app, ss); + } + + /* FIXME: move into no refine flags section? */ + if (screenshots ->len > 0) + gs_app_add_kudo (app, GS_APP_KUDO_HAS_SCREENSHOTS); + + /* success */ + return TRUE; +} + +static gboolean +gs_appstream_refine_add_provides (GsApp *app, XbNode *component, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) provides = NULL; + + /* get all components */ + provides = xb_node_query (component, "provides/*", 0, &error_local); + if (provides == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < provides->len; i++) { + AsProvidedKind kind; + const gchar *element_name; + XbNode *provide = g_ptr_array_index (provides, i); + element_name = xb_node_get_element (provide); + + /* try the simple case */ + kind = as_provided_kind_from_string (element_name); + if (kind == AS_PROVIDED_KIND_UNKNOWN) { + /* try the complex cases */ + + if (g_strcmp0 (element_name, "library") == 0) { + kind = AS_PROVIDED_KIND_LIBRARY; + } else if (g_strcmp0 (element_name, "binary") == 0) { + kind = AS_PROVIDED_KIND_BINARY; + } else if (g_strcmp0 (element_name, "firmware") == 0) { + const gchar *fw_type = xb_node_get_attr (provide, "type"); + if (g_strcmp0 (fw_type, "runtime") == 0) + kind = AS_PROVIDED_KIND_FIRMWARE_RUNTIME; + else if (g_strcmp0 (fw_type, "flashed") == 0) + kind = AS_PROVIDED_KIND_FIRMWARE_FLASHED; + } else if (g_strcmp0 (element_name, "python2") == 0) { + kind = AS_PROVIDED_KIND_PYTHON_2; + } else if (g_strcmp0 (element_name, "python3") == 0) { + kind = AS_PROVIDED_KIND_PYTHON; + } else if (g_strcmp0 (element_name, "dbus") == 0) { + const gchar *dbus_type = xb_node_get_attr (provide, "type"); + if (g_strcmp0 (dbus_type, "system") == 0) + kind = AS_PROVIDED_KIND_DBUS_SYSTEM; + else if ((g_strcmp0 (dbus_type, "user") == 0) || (g_strcmp0 (dbus_type, "session") == 0)) + kind = AS_PROVIDED_KIND_DBUS_USER; + } + } + + if (kind == AS_PROVIDED_KIND_UNKNOWN || + xb_node_get_text (provide) == NULL) { + /* give up */ + g_warning ("ignoring unknown or empty provided item type: %s", element_name); + continue; + } + + gs_app_add_provided_item (app, + kind, + xb_node_get_text (provide)); + } + + /* success */ + return TRUE; +} + +static guint64 +component_get_release_timestamp (XbNode *component) +{ + guint64 timestamp; + const gchar *date_str; + + /* Spec says to prefer `timestamp` over `date` if both are provided: + * https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-releases */ + timestamp = xb_node_query_attr_as_uint (component, "releases/release", "timestamp", NULL); + date_str = xb_node_query_attr (component, "releases/release", "date", NULL); + + if (timestamp != G_MAXUINT64) { + return timestamp; + } else if (date_str != NULL) { + g_autoptr(GDateTime) date = g_date_time_new_from_iso8601 (date_str, NULL); + if (date != NULL) + return g_date_time_to_unix (date); + } + + /* Unknown. */ + return G_MAXUINT64; +} + +static gboolean +gs_appstream_is_recent_release (XbNode *component) +{ + guint64 ts; + gint64 secs; + + /* get newest release */ + ts = component_get_release_timestamp (component); + if (ts == G_MAXUINT64) + return FALSE; + + /* is last build less than one year ago? */ + secs = (g_get_real_time () / G_USEC_PER_SEC) - ts; + return secs / (60 * 60 * 24) < 365; +} + +static gboolean +gs_appstream_copy_metadata (GsApp *app, XbNode *component, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) values = NULL; + + /* get all components */ + values = xb_node_query (component, "custom/value", 0, &error_local); + if (values == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < values->len; i++) { + XbNode *value = g_ptr_array_index (values, i); + const gchar *key = xb_node_get_attr (value, "key"); + if (key == NULL) + continue; + if (gs_app_get_metadata_item (app, key) != NULL) + continue; + gs_app_set_metadata (app, key, xb_node_get_text (value)); + } + return TRUE; +} + +static gboolean +gs_appstream_refine_app_updates (GsApp *app, + XbSilo *silo, + XbNode *component, + GError **error) +{ + AsUrgencyKind urgency_best = AS_URGENCY_KIND_UNKNOWN; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GHashTable) installed = g_hash_table_new (g_str_hash, g_str_equal); + g_autoptr(GPtrArray) releases_inst = NULL; + g_autoptr(GPtrArray) releases = NULL; + g_autoptr(GPtrArray) updates_list = g_ptr_array_new (); + + /* only for UPDATABLE apps */ + if (!gs_app_is_updatable (app)) + return TRUE; + + /* find out which releases are already installed */ + xpath = g_strdup_printf ("component/id[text()='%s']/../releases/*[@version]", + gs_app_get_id (app)); + releases_inst = xb_silo_query (silo, xpath, 0, &error_local); + if (releases_inst == NULL) { + if (!g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } else { + for (guint i = 0; i < releases_inst->len; i++) { + XbNode *release = g_ptr_array_index (releases_inst, i); + g_hash_table_insert (installed, + (gpointer) xb_node_get_attr (release, "version"), + (gpointer) release); + } + } + g_clear_error (&error_local); + + /* get all components */ + releases = xb_node_query (component, "releases/*", 0, &error_local); + if (releases == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < releases->len; i++) { + XbNode *release = g_ptr_array_index (releases, i); + const gchar *version = xb_node_get_attr (release, "version"); + g_autoptr(XbNode) description = NULL; + AsUrgencyKind urgency_tmp; + + /* ignore releases with no version */ + if (version == NULL) + continue; + + /* already installed */ + if (g_hash_table_lookup (installed, version) != NULL) + continue; + + /* limit this to three versions backwards if there has never + * been a detected installed version */ + if (g_hash_table_size (installed) == 0 && i >= 3) + break; + + /* use the 'worst' urgency, e.g. critical over enhancement */ + urgency_tmp = as_urgency_kind_from_string (xb_node_get_attr (release, "urgency")); + if (urgency_tmp > urgency_best) + urgency_best = urgency_tmp; + + /* add updates with a description */ + description = xb_node_query_first (release, "description", NULL); + if (description == NULL) + continue; + g_ptr_array_add (updates_list, release); + } + + /* only set if known */ + if (urgency_best != AS_URGENCY_KIND_UNKNOWN) + gs_app_set_update_urgency (app, urgency_best); + + /* no prefix on each release */ + if (updates_list->len == 1) { + XbNode *release = g_ptr_array_index (updates_list, 0); + g_autoptr(XbNode) n = NULL; + g_autofree gchar *desc = NULL; + n = xb_node_query_first (release, "description", NULL); + desc = gs_appstream_format_description (n, NULL); + gs_app_set_update_details_markup (app, desc); + + /* get the descriptions with a version prefix */ + } else if (updates_list->len > 1) { + const gchar *version = gs_app_get_version (app); + g_autoptr(GString) update_desc = g_string_new (""); + for (guint i = 0; i < updates_list->len; i++) { + XbNode *release = g_ptr_array_index (updates_list, i); + const gchar *release_version = xb_node_get_attr (release, "version"); + g_autofree gchar *desc = NULL; + g_autoptr(XbNode) n = NULL; + + /* skip the currently installed version and all below it */ + if (version != NULL && as_vercmp_simple (version, release_version) >= 0) + continue; + + n = xb_node_query_first (release, "description", NULL); + desc = gs_appstream_format_description (n, NULL); + g_string_append_printf (update_desc, + "Version %s:\n%s\n\n", + xb_node_get_attr (release, "version"), + desc); + } + + /* remove trailing newlines */ + if (update_desc->len > 2) + g_string_truncate (update_desc, update_desc->len - 2); + if (update_desc->len > 0) + gs_app_set_update_details_markup (app, update_desc->str); + } + + /* if there is no already set update version use the newest */ + if (gs_app_get_update_version (app) == NULL && + updates_list->len > 0) { + XbNode *release = g_ptr_array_index (updates_list, 0); + gs_app_set_update_version (app, xb_node_get_attr (release, "version")); + } + + /* success */ + return TRUE; +} + +static gboolean +gs_appstream_refine_add_version_history (GsApp *app, XbNode *component, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) version_history = NULL; /* (element-type AsRelease) */ + g_autoptr(GPtrArray) releases = NULL; /* (element-type XbNode) */ + + /* get all components */ + releases = xb_node_query (component, "releases/*", 0, &error_local); + if (releases == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + version_history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + for (guint i = 0; i < releases->len; i++) { + XbNode *release_node = g_ptr_array_index (releases, i); + const gchar *version = xb_node_get_attr (release_node, "version"); + g_autoptr(XbNode) description_node = NULL; + g_autofree gchar *description = NULL; + guint64 timestamp; + const gchar *date_str; + g_autoptr(AsRelease) release = NULL; + g_autofree char *timestamp_xpath = NULL; + + /* ignore releases with no version */ + if (version == NULL) + continue; + + timestamp_xpath = g_strdup_printf ("releases/release[%u]", i+1); + timestamp = xb_node_query_attr_as_uint (component, timestamp_xpath, "timestamp", NULL); + date_str = xb_node_query_attr (component, timestamp_xpath, "date", NULL); + + /* include updates with or without a description */ + description_node = xb_node_query_first (release_node, "description", NULL); + if (description_node != NULL) + description = gs_appstream_format_description (description_node, NULL); + + release = as_release_new (); + as_release_set_version (release, version); + if (timestamp != G_MAXUINT64) + as_release_set_timestamp (release, timestamp); + else if (date_str != NULL) /* timestamp takes precedence over date */ + as_release_set_date (release, date_str); + if (description != NULL) + as_release_set_description (release, description, NULL); + + g_ptr_array_add (version_history, g_steal_pointer (&release)); + } + + if (version_history->len > 0) + gs_app_set_version_history (app, version_history); + + /* success */ + return TRUE; +} + +/** + * _gs_utils_locale_has_translations: + * @locale: A locale, e.g. `en_GB` or `uz_UZ.utf8@cyrillic` + * + * Looks up if the locale is likely to have translations. + * + * Returns: %TRUE if the locale should have translations + **/ +static gboolean +_gs_utils_locale_has_translations (const gchar *locale) +{ + g_autofree gchar *locale_copy = g_strdup (locale); + gchar *separator; + + /* Strip off the territory, codeset and modifier, if present. */ + separator = strpbrk (locale_copy, "_.@"); + if (separator != NULL) + *separator = '\0'; + + if (g_strcmp0 (locale_copy, "C") == 0) + return FALSE; + if (g_strcmp0 (locale_copy, "en") == 0) + return FALSE; + return TRUE; +} + +static gboolean +gs_appstream_origin_valid (const gchar *origin) +{ + if (origin == NULL) + return FALSE; + if (g_strcmp0 (origin, "") == 0) + return FALSE; + return TRUE; +} + +static gboolean +gs_appstream_is_valid_project_group (const gchar *project_group) +{ + if (project_group == NULL) + return FALSE; + return as_utils_is_desktop_environment (project_group); +} + +static gboolean +gs_appstream_refine_app_content_rating (GsApp *app, + XbNode *content_rating, + GError **error) +{ + g_autoptr(AsContentRating) cr = as_content_rating_new (); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) content_attributes = NULL; + const gchar *content_rating_kind = NULL; + + /* get kind */ + content_rating_kind = xb_node_get_attr (content_rating, "type"); + /* we only really expect/support OARS 1.0 and 1.1 */ + if (content_rating_kind == NULL || + (g_strcmp0 (content_rating_kind, "oars-1.0") != 0 && + g_strcmp0 (content_rating_kind, "oars-1.1") != 0)) { + return TRUE; + } + + as_content_rating_set_kind (cr, content_rating_kind); + + /* get attributes; no attributes being found (i.e. + * `<content_rating type="*"/>`) is OK: it means that all attributes have + * value `none`, as per the + * [OARS semantics](https://github.com/hughsie/oars/blob/HEAD/specification/oars-1.1.md) */ + content_attributes = xb_node_query (content_rating, "content_attribute", 0, &error_local); + if (content_attributes == NULL && + g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_clear_error (&error_local); + } else if (content_attributes == NULL) { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + for (guint i = 0; content_attributes != NULL && i < content_attributes->len; i++) { + XbNode *content_attribute = g_ptr_array_index (content_attributes, i); + as_content_rating_add_attribute (cr, + xb_node_get_attr (content_attribute, "id"), + as_content_rating_value_from_string (xb_node_get_text (content_attribute))); + } + + gs_app_set_content_rating (app, cr); + return TRUE; +} + +static gboolean +gs_appstream_refine_app_content_ratings (GsApp *app, + XbNode *component, + GError **error) +{ + g_autoptr(GPtrArray) content_ratings = NULL; + g_autoptr(GError) error_local = NULL; + + /* find any content ratings */ + content_ratings = xb_node_query (component, "content_rating", 0, &error_local); + if (content_ratings == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < content_ratings->len; i++) { + XbNode *content_rating = g_ptr_array_index (content_ratings, i); + if (!gs_appstream_refine_app_content_rating (app, content_rating, error)) + return FALSE; + } + return TRUE; +} + +static gboolean +gs_appstream_refine_app_relation (GsApp *app, + XbNode *relation_node, + AsRelationKind kind, + GError **error) +{ + /* Iterate over the children, which might be any combination of zero or + * more <id/>, <modalias/>, <kernel/>, <memory/>, <firmware/>, + * <control/> or <display_length/> elements. For the moment, we only + * support some of these. */ + for (g_autoptr(XbNode) child = xb_node_get_child (relation_node); child != NULL; node_set_to_next (&child)) { + const gchar *item_kind = xb_node_get_element (child); + g_autoptr(AsRelation) relation = as_relation_new (); + + as_relation_set_kind (relation, kind); + + if (g_str_equal (item_kind, "control")) { + /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-relations-control */ + as_relation_set_item_kind (relation, AS_RELATION_ITEM_KIND_CONTROL); + as_relation_set_value_control_kind (relation, as_control_kind_from_string (xb_node_get_text (child))); + } else if (g_str_equal (item_kind, "display_length")) { + AsDisplayLengthKind display_length_kind; + const gchar *compare; + + /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-relations-display_length */ + as_relation_set_item_kind (relation, AS_RELATION_ITEM_KIND_DISPLAY_LENGTH); + + compare = xb_node_get_attr (child, "compare"); + as_relation_set_compare (relation, (compare != NULL) ? as_relation_compare_from_string (compare) : AS_RELATION_COMPARE_GE); + + display_length_kind = as_display_length_kind_from_string (xb_node_get_text (child)); + if (display_length_kind != AS_DISPLAY_LENGTH_KIND_UNKNOWN) { + /* Ignore the `side` attribute */ + as_relation_set_value_display_length_kind (relation, display_length_kind); + } else { + const gchar *side = xb_node_get_attr (child, "side"); + as_relation_set_display_side_kind (relation, (side != NULL) ? as_display_side_kind_from_string (side) : AS_DISPLAY_SIDE_KIND_SHORTEST); + as_relation_set_value_px (relation, xb_node_get_text_as_uint (child)); + } + } else { + g_debug ("Relation type ‘%s’ not currently supported for %s; ignoring", + item_kind, gs_app_get_id (app)); + continue; + } + + gs_app_add_relation (app, relation); + } + + return TRUE; +} + +static gboolean +gs_appstream_refine_app_relations (GsApp *app, + XbNode *component, + GError **error) +{ + const struct { + const gchar *element_name; + AsRelationKind relation_kind; + } relation_types[] = { +#if AS_CHECK_VERSION(0, 15, 0) + { "supports", AS_RELATION_KIND_SUPPORTS }, +#endif + { "recommends", AS_RELATION_KIND_RECOMMENDS }, + { "requires", AS_RELATION_KIND_REQUIRES }, + }; + + for (gsize i = 0; i < G_N_ELEMENTS (relation_types); i++) { + g_autoptr(GPtrArray) relations = NULL; + g_autoptr(GError) error_local = NULL; + + /* find any instances of this @element_name */ + relations = xb_node_query (component, relation_types[i].element_name, 0, &error_local); + if (relations == NULL && + !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + for (guint j = 0; relations != NULL && j < relations->len; j++) { + XbNode *relation = g_ptr_array_index (relations, j); + if (!gs_appstream_refine_app_relation (app, relation, relation_types[i].relation_kind, error)) + return FALSE; + } + } + + return TRUE; +} + +gboolean +gs_appstream_refine_app (GsPlugin *plugin, + GsApp *app, + XbSilo *silo, + XbNode *component, + GsPluginRefineFlags refine_flags, + GError **error) +{ + const gchar *tmp; + guint64 timestamp; + g_autoptr(GPtrArray) bundles = NULL; + g_autoptr(GPtrArray) launchables = NULL; + g_autoptr(XbNode) req = NULL; + + /* The 'plugin' can be NULL, when creating app for --show-metainfo */ + g_return_val_if_fail (GS_IS_APP (app), FALSE); + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (XB_IS_NODE (component), FALSE); + + /* is compatible */ + req = xb_node_query_first (component, + "requires/id[@type='id']" + "[text()='org.gnome.Software.desktop']", NULL); + if (req != NULL) { + gint rc = as_vercmp_simple (xb_node_get_attr (req, "version"), + PACKAGE_VERSION); + if (rc > 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "not for this gnome-software"); + return FALSE; + } + } + + /* set id kind */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN || + gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC) { + AsComponentKind kind; + tmp = xb_node_get_attr (component, "type"); + kind = as_component_kind_from_string (tmp); + if (kind != AS_COMPONENT_KIND_UNKNOWN) + gs_app_set_kind (app, kind); + } + + /* types we can never launch */ + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_ADDON: + case AS_COMPONENT_KIND_CODEC: + case AS_COMPONENT_KIND_DRIVER: + case AS_COMPONENT_KIND_FIRMWARE: + case AS_COMPONENT_KIND_FONT: + case AS_COMPONENT_KIND_GENERIC: + case AS_COMPONENT_KIND_INPUT_METHOD: + case AS_COMPONENT_KIND_LOCALIZATION: + case AS_COMPONENT_KIND_OPERATING_SYSTEM: + case AS_COMPONENT_KIND_RUNTIME: + case AS_COMPONENT_KIND_REPOSITORY: + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + break; + default: + break; + } + + /* check if the special metadata affects the not-launchable quirk */ + tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::not-launchable"); + if (tmp != NULL) { + if (g_strcmp0 (tmp, "true") == 0) + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + else if (g_strcmp0 (tmp, "false") == 0) + gs_app_remove_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } + + tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::hide-everywhere"); + if (tmp != NULL) { + if (g_strcmp0 (tmp, "true") == 0) + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + else if (g_strcmp0 (tmp, "false") == 0) + gs_app_remove_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + } + + /* try to detect old-style AppStream 'override' + * files without the merge attribute */ + if (xb_node_query_text (component, "name", NULL) == NULL && + xb_node_query_text (component, "metadata_license", NULL) == NULL) { + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + } + + /* set id */ + tmp = xb_node_query_text (component, "id", NULL); + if (tmp != NULL && gs_app_get_id (app) == NULL) + gs_app_set_id (app, tmp); + + /* set source */ + tmp = xb_node_query_text (component, "info/filename", NULL); + if (tmp == NULL) + tmp = xb_node_query_text (component, "../info/filename", NULL); + if (tmp != NULL && gs_app_get_metadata_item (app, "appstream::source-file") == NULL) { + gs_app_set_metadata (app, "appstream::source-file", tmp); + } + + /* set scope */ + tmp = xb_node_query_text (component, "../info/scope", NULL); + if (tmp != NULL) + gs_app_set_scope (app, as_component_scope_from_string (tmp)); + + /* set content rating */ + if (TRUE) { + if (!gs_appstream_refine_app_content_ratings (app, component, error)) + return FALSE; + } + + /* recommends/requires + * FIXME: Technically this could do with a more specific refine flag, + * but essentially the relations are used on the details page and so + * are the permissions. It would be good to eliminate refine flags at + * some point in the future. */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) { + if (!gs_appstream_refine_app_relations (app, component, error)) + return FALSE; + } + + /* set name */ + tmp = xb_node_query_text (component, "name", NULL); + if (tmp != NULL) + gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, tmp); + + /* set summary */ + tmp = xb_node_query_text (component, "summary", NULL); + if (tmp != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, tmp); + + /* add urls */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) { + g_autoptr(GPtrArray) urls = NULL; + urls = xb_node_query (component, "url", 0, NULL); + if (urls != NULL) { + for (guint i = 0; i < urls->len; i++) { + XbNode *url = g_ptr_array_index (urls, i); + const gchar *kind = xb_node_get_attr (url, "type"); + if (kind == NULL) + continue; + gs_app_set_url (app, + as_url_kind_from_string (kind), + xb_node_get_text (url)); + } + } + } + + /* add launchables */ + launchables = xb_node_query (component, "launchable", 0, NULL); + if (launchables != NULL) { + for (guint i = 0; i < launchables->len; i++) { + XbNode *launchable = g_ptr_array_index (launchables, i); + const gchar *kind = xb_node_get_attr (launchable, "type"); + if (g_strcmp0 (kind, "desktop-id") == 0) { + gs_app_set_launchable (app, + AS_LAUNCHABLE_KIND_DESKTOP_ID, + xb_node_get_text (launchable)); + break; + } else if (g_strcmp0 (kind, "url") == 0) { + gs_app_set_launchable (app, + AS_LAUNCHABLE_KIND_URL, + xb_node_get_text (launchable)); + } + } + } + + /* set license */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0 && + gs_app_get_license (app) == NULL) { + tmp = xb_node_query_text (component, "project_license", NULL); + if (tmp != NULL) + gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, tmp); + } + + /* set description */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) { + g_autofree gchar *description = NULL; + g_autoptr(XbNode) n = xb_node_query_first (component, "description", NULL); + if (n != NULL) + description = gs_appstream_format_description (n, NULL); + if (description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_HIGHEST, description); + } + + /* set icon */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) > 0 && + gs_app_get_icons (app) == NULL) + gs_appstream_refine_icon (app, silo, component); + + /* set categories */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES) { + g_autoptr(GPtrArray) categories = NULL; + categories = xb_node_query (component, "categories/category", 0, NULL); + if (categories != NULL) { + for (guint i = 0; i < categories->len; i++) { + XbNode *category = g_ptr_array_index (categories, i); + gs_app_add_category (app, xb_node_get_text (category)); + + /* Special case: We used to use the `Blacklisted` + * category to hide apps from their .desktop + * file or appdata. We now use a quirk for that. + * This special case can be removed when all + * appstream files no longer use the `Blacklisted` + * category (including external-appstream files + * put together by distributions). */ + if (g_strcmp0 (xb_node_get_text (category), "Blacklisted") == 0) + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + } + } + } + + /* set project group */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP) > 0 && + gs_app_get_project_group (app) == NULL) { + tmp = xb_node_query_text (component, "project_group", NULL); + if (tmp != NULL && gs_appstream_is_valid_project_group (tmp)) + gs_app_set_project_group (app, tmp); + } + + /* set developer name */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) > 0 && + gs_app_get_developer_name (app) == NULL) { + tmp = xb_node_query_text (component, "developer_name", NULL); + if (tmp != NULL) + gs_app_set_developer_name (app, tmp); + } + + /* set the release date */ + timestamp = component_get_release_timestamp (component); + if (timestamp != G_MAXUINT64) + gs_app_set_release_date (app, timestamp); + + /* set the version history */ + if (!gs_appstream_refine_add_version_history (app, component, error)) + return FALSE; + + /* copy all the metadata */ + if (!gs_appstream_copy_metadata (app, component, error)) + return FALSE; + + /* add bundles */ + bundles = xb_node_query (component, "bundle", 0, NULL); + if (bundles != NULL && gs_app_get_sources(app)->len == 0) { + for (guint i = 0; i < bundles->len; i++) { + XbNode *bundle = g_ptr_array_index (bundles, i); + const gchar *kind = xb_node_get_attr (bundle, "type"); + const gchar *bundle_id = xb_node_get_text (bundle); + + if (bundle_id == NULL || kind == NULL) + continue; + + gs_app_add_source (app, bundle_id); + gs_app_set_bundle_kind (app, as_bundle_kind_from_string (kind)); + + /* get the type/name/arch/branch */ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) { + g_auto(GStrv) split = g_strsplit (bundle_id, "/", -1); + if (g_strv_length (split) != 4) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "invalid ID %s for a flatpak ref", + bundle_id); + return FALSE; + } + + /* we only need the branch for the unique ID */ + gs_app_set_branch (app, split[3]); + } + } + } + + /* add legacy package names */ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN) { + g_autoptr(GPtrArray) pkgnames = NULL; + pkgnames = xb_node_query (component, "pkgname", 0, NULL); + if (pkgnames != NULL && gs_app_get_sources(app)->len == 0) { + for (guint i = 0; i < pkgnames->len; i++) { + XbNode *pkgname = g_ptr_array_index (pkgnames, i); + tmp = xb_node_get_text (pkgname); + if (tmp != NULL && tmp[0] != '\0') + gs_app_add_source (app, tmp); + } + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + } + } + + /* set origin */ + tmp = xb_node_query_attr (component, "..", "origin", NULL); + if (gs_appstream_origin_valid (tmp)) { + gs_app_set_origin_appstream (app, tmp); + + if (gs_app_get_origin (app) == NULL && ( + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK || + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE)) { + gs_app_set_origin (app, tmp); + } + } + + /* set addons */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) != 0 && + plugin != NULL && silo != NULL) { + if (!gs_appstream_refine_add_addons (plugin, app, silo, error)) + return FALSE; + } + + /* set screenshots */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) > 0 && + gs_app_get_screenshots(app)->len == 0) { + if (!gs_appstream_refine_add_screenshots (app, component, error)) + return FALSE; + } + + /* set provides */ + if (!gs_appstream_refine_add_provides (app, component, error)) + return FALSE; + + /* add kudos */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) { + g_autoptr(GPtrArray) kudos = NULL; + tmp = setlocale (LC_MESSAGES, NULL); + if (!_gs_utils_locale_has_translations (tmp)) { + gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE); + } else { + + g_autoptr(GString) xpath = g_string_new (NULL); + g_auto(GStrv) variants = g_get_locale_variants (tmp); + + /* @variants includes @tmp */ + for (gsize i = 0; variants[i] != NULL; i++) + xb_string_append_union (xpath, "languages/lang[(text()='%s') and (@percentage>50)]", variants[i]); + + if (xb_node_query_text (component, xpath->str, NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE); + } + + /* Set this under the FLAGS_REQUIRE_KUDOS flag because it’s + * only useful in combination with KUDO_MY_LANGUAGE */ + if (xb_node_query_text (component, "languages/lang", NULL) != NULL) + gs_app_set_has_translations (app, TRUE); + + /* any keywords */ + if (xb_node_query_text (component, "keywords/keyword", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_HAS_KEYWORDS); + + /* HiDPI icon */ + if (xb_node_query_text (component, "icon[@width='128']", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON); + + /* was this application released recently */ + if (gs_appstream_is_recent_release (component)) + gs_app_add_kudo (app, GS_APP_KUDO_RECENT_RELEASE); + + /* add a kudo to featured and popular apps */ + if (xb_node_query_text (component, "kudos/kudo[text()='GnomeSoftware::popular']", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED); + if (xb_node_query_text (component, "categories/category[text()='Featured']", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED); + } + + /* we have an origin in the XML */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) > 0 && + gs_app_get_origin_appstream (app) == NULL) { + g_autoptr(XbNode) parent = xb_node_get_parent (component); + if (parent != NULL) { + tmp = xb_node_get_attr (parent, "origin"); + if (gs_appstream_origin_valid (tmp)) + gs_app_set_origin_appstream (app, tmp); + } + } + + /* is there any update information */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) != 0 && + silo != NULL) { + if (!gs_appstream_refine_app_updates (app, + silo, + component, + error)) + return FALSE; + } + + return TRUE; +} + +typedef struct { + AsSearchTokenMatch match_value; + XbQuery *query; +} GsAppstreamSearchHelper; + +static void +gs_appstream_search_helper_free (GsAppstreamSearchHelper *helper) +{ + g_object_unref (helper->query); + g_free (helper); +} + +static guint16 +gs_appstream_silo_search_component2 (GPtrArray *array, XbNode *component, const gchar *search) +{ + guint16 match_value = 0; + + /* do searches */ + for (guint i = 0; i < array->len; i++) { + g_autoptr(GPtrArray) n = NULL; + GsAppstreamSearchHelper *helper = g_ptr_array_index (array, i); +#if LIBXMLB_CHECK_VERSION(0, 3, 0) + g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT (); + xb_value_bindings_bind_str (xb_query_context_get_bindings (&context), 0, search, NULL); + n = xb_node_query_with_context (component, helper->query, &context, NULL); +#else + xb_query_bind_str (helper->query, 0, search, NULL); + n = xb_node_query_full (component, helper->query, NULL); +#endif + if (n != NULL) + match_value |= helper->match_value; + } + return match_value; +} + +static guint16 +gs_appstream_silo_search_component (GPtrArray *array, XbNode *component, const gchar * const *search) +{ + guint16 matches_sum = 0; + + /* do *all* search keywords match */ + for (guint i = 0; search[i] != NULL; i++) { + guint tmp = gs_appstream_silo_search_component2 (array, component, search[i]); + if (tmp == 0) + return 0; + matches_sum |= tmp; + } + return matches_sum; +} + +typedef struct { + AsSearchTokenMatch match_value; + const gchar *xpath; +} Query; + +static gboolean +gs_appstream_do_search (GsPlugin *plugin, + XbSilo *silo, + const gchar * const *values, + const Query queries[], + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func ((GDestroyNotify) gs_appstream_search_helper_free); + g_autoptr(GPtrArray) components = NULL; + g_autoptr(GTimer) timer = g_timer_new (); + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (values != NULL, FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* add some weighted queries */ + for (guint i = 0; queries[i].xpath != NULL; i++) { + g_autoptr(GError) error_query = NULL; + g_autoptr(XbQuery) query = xb_query_new (silo, queries[i].xpath, &error_query); + if (query != NULL) { + GsAppstreamSearchHelper *helper = g_new0 (GsAppstreamSearchHelper, 1); + helper->match_value = queries[i].match_value; + helper->query = g_steal_pointer (&query); + g_ptr_array_add (array, helper); + } else { + g_debug ("ignoring: %s", error_query->message); + } + } + + /* get all components */ + components = xb_silo_query (silo, "components/component", 0, &error_local); + if (components == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + guint16 match_value = gs_appstream_silo_search_component (array, component, values); + if (match_value != 0) { + g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error); + if (app == NULL) + return FALSE; + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + g_debug ("not returning wildcard %s", + gs_app_get_unique_id (app)); + continue; + } + g_debug ("add %s", gs_app_get_unique_id (app)); + + /* The match value is used for prioritising results. + * Drop the ID token from it as it’s the highest + * numeric value but isn’t visible to the user in the + * UI, which leads to confusing results ordering. */ + gs_app_set_match_value (app, match_value & (~AS_SEARCH_TOKEN_MATCH_ID)); + gs_app_list_add (list, app); + + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_ADDON) { + g_autoptr(GPtrArray) extends = NULL; + + /* add the parent app as a wildcard, to be refined later */ + extends = xb_node_query (component, "extends", 0, NULL); + for (guint jj = 0; extends && jj < extends->len; jj++) { + XbNode *extend = g_ptr_array_index (extends, jj); + g_autoptr(GsApp) app2 = NULL; + const gchar *tmp; + app2 = gs_app_new (xb_node_get_text (extend)); + gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD); + tmp = xb_node_query_attr (extend, "../..", "origin", NULL); + if (gs_appstream_origin_valid (tmp)) + gs_app_set_origin_appstream (app2, tmp); + gs_app_list_add (list, app2); + } + } + } + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + } + g_debug ("search took %fms", g_timer_elapsed (timer, NULL) * 1000); + return TRUE; +} + +/* This tokenises and stems @values internally for comparison against the + * already-stemmed tokens in the libxmlb silo */ +gboolean +gs_appstream_search (GsPlugin *plugin, + XbSilo *silo, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + const Query queries[] = { + { AS_SEARCH_TOKEN_MATCH_MIMETYPE, "mimetypes/mimetype[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_PKGNAME, "pkgname[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_SUMMARY, "summary[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_NAME, "name[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_KEYWORD, "keywords/keyword[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_ID, "id[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_ID, "launchable[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_ORIGIN, "../components[@origin~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_NONE, NULL } + }; + + return gs_appstream_do_search (plugin, silo, values, queries, list, cancellable, error); +} + +gboolean +gs_appstream_search_developer_apps (GsPlugin *plugin, + XbSilo *silo, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + const Query queries[] = { + { AS_SEARCH_TOKEN_MATCH_PKGNAME, "developer_name[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_SUMMARY, "project_group[text()~=stem(?)]" }, + { AS_SEARCH_TOKEN_MATCH_NONE, NULL } + }; + + return gs_appstream_do_search (plugin, silo, values, queries, list, cancellable, error); +} + +gboolean +gs_appstream_add_category_apps (GsPlugin *plugin, + XbSilo *silo, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *desktop_groups; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + desktop_groups = gs_category_get_desktop_groups (category); + if (desktop_groups->len == 0) { + g_warning ("no desktop_groups for %s", gs_category_get_id (category)); + return TRUE; + } + for (guint j = 0; j < desktop_groups->len; j++) { + const gchar *desktop_group = g_ptr_array_index (desktop_groups, j); + g_autofree gchar *xpath = NULL; + g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1); + g_autoptr(GPtrArray) components = NULL; + g_autoptr(GError) error_local = NULL; + + /* generate query */ + if (g_strv_length (split) == 1) { + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../..", + split[0]); + } else if (g_strv_length (split) == 2) { + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../" + "category[text()='%s']/../..", + split[0], split[1]); + } + components = xb_silo_query (silo, xpath, 0, &error_local); + if (components == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + continue; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + /* create app */ + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + g_autoptr(GsApp) app = NULL; + const gchar *id = xb_node_query_text (component, "id", NULL); + if (id == NULL) + continue; + app = gs_app_new (id); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + } + + } + return TRUE; +} + +static guint +gs_appstream_count_component_for_groups (XbSilo *silo, + const gchar *desktop_group) +{ + guint limit = 10; + g_autofree gchar *xpath = NULL; + g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1); + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GError) error_local = NULL; + + if (g_strv_length (split) == 1) { /* "all" group for a parent category */ + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../..", + split[0]); + } else if (g_strv_length (split) == 2) { + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../" + "category[text()='%s']/../..", + split[0], split[1]); + } else { + return 0; + } + + array = xb_silo_query (silo, xpath, limit, &error_local); + if (array == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return 0; + g_warning ("%s", error_local->message); + return 0; + } + return array->len; +} + +/* we're not actually adding categories here, we're just setting the number of + * applications available in each category */ +gboolean +gs_appstream_refine_category_sizes (XbSilo *silo, + GPtrArray *list, + GCancellable *cancellable, + GError **error) +{ + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (list != NULL, FALSE); + + for (guint j = 0; j < list->len; j++) { + GsCategory *parent = GS_CATEGORY (g_ptr_array_index (list, j)); + GPtrArray *children = gs_category_get_children (parent); + + for (guint i = 0; i < children->len; i++) { + GsCategory *cat = g_ptr_array_index (children, i); + GPtrArray *groups = gs_category_get_desktop_groups (cat); + for (guint k = 0; k < groups->len; k++) { + const gchar *group = g_ptr_array_index (groups, k); + guint cnt = gs_appstream_count_component_for_groups (silo, group); + if (cnt > 0) { + gs_category_increment_size (parent, cnt); + if (children->len > 1) { + /* Parent category has multiple groups, so increment + * each group's size too */ + gs_category_increment_size (cat, cnt); + } + } + } + } + continue; + } + return TRUE; +} + +gboolean +gs_appstream_add_installed (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) components = NULL; + g_autoptr(GError) local_error = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* get all installed appdata files (notice no 'components/' prefix...) */ + components = xb_silo_query (silo, "component/description/..", 0, NULL); + if (components == NULL) + return TRUE; + + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error); + if (app == NULL) + return FALSE; + + /* Can get cached GsApp, which has the state already updated */ + if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE && + gs_app_get_state (app) != GS_APP_STATE_UPDATABLE_LIVE) + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_appstream_add_popular (XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = NULL; + + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* find out how many packages are in each category */ + array = xb_silo_query (silo, + "components/component/kudos/" + "kudo[text()='GnomeSoftware::popular']/../..", + 0, &error_local); + if (array == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < array->len; i++) { + g_autoptr(GsApp) app = NULL; + XbNode *component = g_ptr_array_index (array, i); + const gchar *component_id = xb_node_query_text (component, "id", NULL); + if (component_id == NULL) + continue; + app = gs_app_new (component_id); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_appstream_add_recent (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error) +{ + guint64 now = (guint64) g_get_real_time () / G_USEC_PER_SEC; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* use predicate conditions to the max */ + xpath = g_strdup_printf ("components/component/releases/" + "release[@timestamp>%" G_GUINT64_FORMAT "]/../..", + now - age); + array = xb_silo_query (silo, xpath, 0, &error_local); + if (array == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < array->len; i++) { + XbNode *component = g_ptr_array_index (array, i); + g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error); + guint64 timestamp; + if (app == NULL) + return FALSE; + /* set the release date */ + timestamp = component_get_release_timestamp (component); + if (timestamp != G_MAXUINT64) + gs_app_set_release_date (app, timestamp); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_appstream_add_alternates (XbSilo *silo, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *sources = gs_app_get_sources (app); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) ids = NULL; + g_autoptr(GString) xpath = g_string_new (NULL); + + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* probably a package we know nothing about */ + if (gs_app_get_id (app) == NULL) + return TRUE; + + /* actual ID */ + xb_string_append_union (xpath, "components/component/id[text()='%s']", + gs_app_get_id (app)); + + /* new ID -> old ID */ + xb_string_append_union (xpath, "components/component/id[text()='%s']/../provides/id", + gs_app_get_id (app)); + + /* old ID -> new ID */ + xb_string_append_union (xpath, "components/component/provides/id[text()='%s']/../../id", + gs_app_get_id (app)); + + /* find apps that use the same pkgname */ + for (guint j = 0; j < sources->len; j++) { + const gchar *source = g_ptr_array_index (sources, j); + g_autofree gchar *source_safe = xb_string_escape (source); + xb_string_append_union (xpath, + "components/component/pkgname[text()='%s']/../id", + source_safe); + } + + /* do a big query, and return all the unique results */ + ids = xb_silo_query (silo, xpath->str, 0, &error_local); + if (ids == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < ids->len; i++) { + XbNode *n = g_ptr_array_index (ids, i); + g_autoptr(GsApp) app2 = NULL; + const gchar *tmp; + app2 = gs_app_new (xb_node_get_text (n)); + gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD); + + tmp = xb_node_query_attr (n, "../..", "origin", NULL); + if (gs_appstream_origin_valid (tmp)) + gs_app_set_origin_appstream (app2, tmp); + gs_app_list_add (list, app2); + } + return TRUE; +} + +static gboolean +gs_appstream_add_featured_with_query (XbSilo *silo, + const gchar *query, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = NULL; + + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* find out how many packages are in each category */ + array = xb_silo_query (silo, query, 0, &error_local); + if (array == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < array->len; i++) { + g_autoptr(GsApp) app = NULL; + XbNode *component = g_ptr_array_index (array, i); + const gchar *component_id = xb_node_query_text (component, "id", NULL); + if (component_id == NULL) + continue; + app = gs_app_new (component_id); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + if (!gs_appstream_copy_metadata (app, component, error)) + return FALSE; + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_appstream_add_featured (XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + const gchar *query = "components/component/custom/value[@key='GnomeSoftware::FeatureTile']/../..|" + "components/component/custom/value[@key='GnomeSoftware::FeatureTile-css']/../.."; + return gs_appstream_add_featured_with_query (silo, query, list, cancellable, error); +} + +gboolean +gs_appstream_add_deployment_featured (XbSilo *silo, + const gchar * const *deployments, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GString) query = g_string_new (NULL); + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (deployments != NULL, FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + for (guint ii = 0; deployments[ii] != NULL; ii++) { + g_autofree gchar *escaped = xb_string_escape (deployments[ii]); + if (escaped != NULL && *escaped != '\0') { + xb_string_append_union (query, + "components/component/custom/value[@key='GnomeSoftware::DeploymentFeatured'][text()='%s']/../..", + escaped); + } + } + if (!query->len) + return TRUE; + return gs_appstream_add_featured_with_query (silo, query->str, list, cancellable, error); +} + +gboolean +gs_appstream_url_to_app (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *path = NULL; + g_autofree gchar *scheme = NULL; + g_autofree gchar *xpath = NULL; + g_autoptr(GPtrArray) components = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (XB_IS_SILO (silo), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + g_return_val_if_fail (url != NULL, FALSE); + + /* not us */ + scheme = gs_utils_get_url_scheme (url); + if (g_strcmp0 (scheme, "appstream") != 0) + return TRUE; + + path = gs_utils_get_url_path (url); + xpath = g_strdup_printf ("components/component/id[text()='%s']/..", path); + components = xb_silo_query (silo, xpath, 0, NULL); + if (components == NULL) + return TRUE; + + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + g_autoptr(GsApp) app = NULL; + app = gs_appstream_create_app (plugin, silo, component, error); + if (app == NULL) + return FALSE; + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_list_add (list, app); + } + + return TRUE; +} + +void +gs_appstream_component_add_keyword (XbBuilderNode *component, const gchar *str) +{ + g_autoptr(XbBuilderNode) keyword = NULL; + g_autoptr(XbBuilderNode) keywords = NULL; + + g_return_if_fail (XB_IS_BUILDER_NODE (component)); + g_return_if_fail (str != NULL); + + /* create <keywords> if it does not already exist */ + keywords = xb_builder_node_get_child (component, "keywords", NULL); + if (keywords == NULL) + keywords = xb_builder_node_insert (component, "keywords", NULL); + + /* create <keyword>str</keyword> if it does not already exist */ + keyword = xb_builder_node_get_child (keywords, "keyword", str); + if (keyword == NULL) { + keyword = xb_builder_node_insert (keywords, "keyword", NULL); + xb_builder_node_set_text (keyword, str, -1); + } +} + +void +gs_appstream_component_add_provide (XbBuilderNode *component, const gchar *str) +{ + g_autoptr(XbBuilderNode) provide = NULL; + g_autoptr(XbBuilderNode) provides = NULL; + + g_return_if_fail (XB_IS_BUILDER_NODE (component)); + g_return_if_fail (str != NULL); + + /* create <provides> if it does not already exist */ + provides = xb_builder_node_get_child (component, "provides", NULL); + if (provides == NULL) + provides = xb_builder_node_insert (component, "provides", NULL); + + /* create <id>str</id> if it does not already exist */ + provide = xb_builder_node_get_child (provides, "id", str); + if (provide == NULL) { + provide = xb_builder_node_insert (provides, "id", NULL); + xb_builder_node_set_text (provide, str, -1); + } +} + +void +gs_appstream_component_add_category (XbBuilderNode *component, const gchar *str) +{ + g_autoptr(XbBuilderNode) category = NULL; + g_autoptr(XbBuilderNode) categories = NULL; + + g_return_if_fail (XB_IS_BUILDER_NODE (component)); + g_return_if_fail (str != NULL); + + /* create <categories> if it does not already exist */ + categories = xb_builder_node_get_child (component, "categories", NULL); + if (categories == NULL) + categories = xb_builder_node_insert (component, "categories", NULL); + + /* create <category>str</category> if it does not already exist */ + category = xb_builder_node_get_child (categories, "category", str); + if (category == NULL) { + category = xb_builder_node_insert (categories, "category", NULL); + xb_builder_node_set_text (category, str, -1); + } +} + +void +gs_appstream_component_add_icon (XbBuilderNode *component, const gchar *str) +{ + g_autoptr(XbBuilderNode) icon = NULL; + + g_return_if_fail (XB_IS_BUILDER_NODE (component)); + g_return_if_fail (str != NULL); + + /* create <icon>str</icon> if it does not already exist */ + icon = xb_builder_node_get_child (component, "icon", NULL); + if (icon == NULL) { + icon = xb_builder_node_insert (component, "icon", + "type", "stock", + NULL); + xb_builder_node_set_text (icon, str, -1); + } +} + +void +gs_appstream_component_add_extra_info (XbBuilderNode *component) +{ + const gchar *kind; + + g_return_if_fail (XB_IS_BUILDER_NODE (component)); + + kind = xb_builder_node_get_attr (component, "type"); + + /* add the gnome-software-specific 'Addon' group and ensure they + * all have an icon set */ + switch (as_component_kind_from_string (kind)) { + case AS_COMPONENT_KIND_WEB_APP: + gs_appstream_component_add_keyword (component, kind); + break; + case AS_COMPONENT_KIND_FONT: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Font"); + break; + case AS_COMPONENT_KIND_DRIVER: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Driver"); + gs_appstream_component_add_icon (component, "system-component-driver"); + break; + case AS_COMPONENT_KIND_LOCALIZATION: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Localization"); + gs_appstream_component_add_icon (component, "system-component-language"); + break; + case AS_COMPONENT_KIND_CODEC: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Codec"); + gs_appstream_component_add_icon (component, "system-component-codecs"); + break; + case AS_COMPONENT_KIND_INPUT_METHOD: + gs_appstream_component_add_keyword (component, kind); + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "InputSource"); + gs_appstream_component_add_icon (component, "system-component-input-sources"); + break; + case AS_COMPONENT_KIND_FIRMWARE: + gs_appstream_component_add_icon (component, "system-component-firmware"); + break; + default: + break; + } +} + +/* Resolve any media URIs which are actually relative + * paths against the media_baseurl property */ +void +gs_appstream_component_fix_url (XbBuilderNode *component, const gchar *baseurl) +{ + const gchar *text; + g_autofree gchar *url = NULL; + + g_return_if_fail (XB_IS_BUILDER_NODE (component)); + g_return_if_fail (baseurl != NULL); + + text = xb_builder_node_get_text (component); + + if (text == NULL) + return; + + if (g_str_has_prefix (text, "http:") || + g_str_has_prefix (text, "https:")) + return; + + url = g_strconcat (baseurl, "/", text, NULL); + xb_builder_node_set_text (component, url , -1); +} diff --git a/lib/gs-appstream.h b/lib/gs-appstream.h new file mode 100644 index 0000000..d0d6ea2 --- /dev/null +++ b/lib/gs-appstream.h @@ -0,0 +1,95 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <xmlb.h> + +G_BEGIN_DECLS + +GsApp *gs_appstream_create_app (GsPlugin *plugin, + XbSilo *silo, + XbNode *component, + GError **error); +gboolean gs_appstream_refine_app (GsPlugin *plugin, + GsApp *app, + XbSilo *silo, + XbNode *component, + GsPluginRefineFlags flags, + GError **error); +gboolean gs_appstream_search (GsPlugin *plugin, + XbSilo *silo, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_search_developer_apps (GsPlugin *plugin, + XbSilo *silo, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_refine_category_sizes (XbSilo *silo, + GPtrArray *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_category_apps (GsPlugin *plugin, + XbSilo *silo, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_installed (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_popular (XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_featured (XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_deployment_featured (XbSilo *silo, + const gchar * const *deployments, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_alternates (XbSilo *silo, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_recent (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_url_to_app (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error); +void gs_appstream_component_add_extra_info (XbBuilderNode *component); +void gs_appstream_component_add_keyword (XbBuilderNode *component, + const gchar *str); +void gs_appstream_component_add_category (XbBuilderNode *component, + const gchar *str); +void gs_appstream_component_add_icon (XbBuilderNode *component, + const gchar *str); +void gs_appstream_component_add_provide (XbBuilderNode *component, + const gchar *str); +void gs_appstream_component_fix_url (XbBuilderNode *component, + const gchar *baseurl); + +G_END_DECLS diff --git a/lib/gs-build-ident.h.in b/lib/gs-build-ident.h.in new file mode 100644 index 0000000..92cd79a --- /dev/null +++ b/lib/gs-build-ident.h.in @@ -0,0 +1,32 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Matthew Leeds <mwleeds@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> + +G_BEGIN_DECLS + +/** + * SECTION:gs-build-ident + * @title: Build Identifier + * @short_description: Identify a build by unique build identifier + * + * Since: 40 + */ + +/** + * GS_BUILD_IDENTIFIER: + * + * A string containing a tag that defines the version of Software that + * was built. Generally, this will be a small version tag plus some + * information to identify the git commit hash when applicable. + */ +#define GS_BUILD_IDENTIFIER "@VCS_TAG@" + +G_END_DECLS diff --git a/lib/gs-category-manager.c b/lib/gs-category-manager.c new file mode 100644 index 0000000..0e37264 --- /dev/null +++ b/lib/gs-category-manager.c @@ -0,0 +1,151 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-category-manager + * @short_description: A container to store #GsCategory instances in + * + * #GsCategoryManager is a container object which stores #GsCategory instances, + * so that they can be consistently reused by other code, without creating + * multiple #GsCategory instances for the same category ID. + * + * It is intended to be used as a singleton, and typically accessed by calling + * gs_plugin_loader_get_category_manager(). + * + * Since: 40 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> +#include <glib/gi18n.h> + +#include "gs-category-manager.h" +#include "gs-desktop-data.h" + +struct _GsCategoryManager +{ + GObject parent; + + /* Array of #GsCategory instances corresponding to the entries in gs_desktop_get_data() + * The +1 is for a NULL terminator */ + GsCategory *categories[GS_DESKTOP_DATA_N_ENTRIES + 1]; +}; + +G_DEFINE_TYPE (GsCategoryManager, gs_category_manager, G_TYPE_OBJECT) + +static void +gs_category_manager_dispose (GObject *object) +{ + GsCategoryManager *self = GS_CATEGORY_MANAGER (object); + + for (gsize i = 0; i < G_N_ELEMENTS (self->categories); i++) + g_clear_object (&self->categories[i]); + + G_OBJECT_CLASS (gs_category_manager_parent_class)->dispose (object); +} + +static void +gs_category_manager_class_init (GsCategoryManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gs_category_manager_dispose; +} + +static void +gs_category_manager_init (GsCategoryManager *self) +{ + const GsDesktopData *msdata; + + /* Set up the category data, and check our expectations about the length + * of gs_desktop_get_data() match reality. */ + msdata = gs_desktop_get_data (); + for (gsize i = 0; msdata[i].id != NULL; i++) { + g_assert (i < G_N_ELEMENTS (self->categories) - 1); + self->categories[i] = gs_category_new_for_desktop_data (&msdata[i]); + } + + g_assert (self->categories[G_N_ELEMENTS (self->categories) - 2] != NULL); + g_assert (self->categories[G_N_ELEMENTS (self->categories) - 1] == NULL); +} + +/** + * gs_category_manager_new: + * + * Create a new #GsCategoryManager. It will contain all the categories, but + * their sizes will not be set until gs_category_increment_size() is called + * on them. + * + * Returns: (transfer full): a new #GsCategoryManager + * Since: 40 + */ +GsCategoryManager * +gs_category_manager_new (void) +{ + return g_object_new (GS_TYPE_CATEGORY_MANAGER, NULL); +} + +/** + * gs_category_manager_lookup: + * @self: a #GsCategoryManager + * @id: ID of the category to look up + * + * Look up a category by its ID. If the category is not found, %NULL is + * returned. + * + * Returns: (transfer full) (nullable): the #GsCategory, or %NULL + * Since: 40 + */ +GsCategory * +gs_category_manager_lookup (GsCategoryManager *self, + const gchar *id) +{ + g_return_val_if_fail (GS_IS_CATEGORY_MANAGER (self), NULL); + g_return_val_if_fail (id != NULL && *id != '\0', NULL); + + /* There are only on the order of 10 categories, so this is quick */ + for (gsize i = 0; i < G_N_ELEMENTS (self->categories) - 1; i++) { + if (g_str_equal (gs_category_get_id (self->categories[i]), id)) + return g_object_ref (self->categories[i]); + } + + return NULL; +} + +/** + * gs_category_manager_get_categories: + * @self: a #GsCategoryManager + * @out_n_categories: (optional) (out caller-allocates): return location for + * the number of categories in the return value, or %NULL to ignore + * + * Get the full list of categories from the category manager. The returned array + * is %NULL terminated and guaranteed to be non-%NULL (although it may be + * empty). + * + * If @out_n_categories is provided, it will be set to the number of #GsCategory + * objects in the return value, not including the %NULL terminator. + * + * Returns: (array length=out_n_categories) (transfer none) (not nullable): the + * categories; do not free this memory + * Since: 40 + */ +GsCategory * const * +gs_category_manager_get_categories (GsCategoryManager *self, + gsize *out_n_categories) +{ + g_return_val_if_fail (GS_IS_CATEGORY_MANAGER (self), NULL); + + if (out_n_categories != NULL) + *out_n_categories = G_N_ELEMENTS (self->categories) - 1; + + return self->categories; +} diff --git a/lib/gs-category-manager.h b/lib/gs-category-manager.h new file mode 100644 index 0000000..8e85fa3 --- /dev/null +++ b/lib/gs-category-manager.h @@ -0,0 +1,32 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +#include "gs-category.h" + +G_BEGIN_DECLS + +#define GS_TYPE_CATEGORY_MANAGER (gs_category_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (GsCategoryManager, gs_category_manager, GS, CATEGORY_MANAGER, GObject) + +GsCategoryManager *gs_category_manager_new (void); + +GsCategory *gs_category_manager_lookup (GsCategoryManager *self, + const gchar *id); + +GsCategory * const *gs_category_manager_get_categories (GsCategoryManager *self, + gsize *out_n_categories); + +G_END_DECLS diff --git a/lib/gs-category-private.h b/lib/gs-category-private.h new file mode 100644 index 0000000..1c9c292 --- /dev/null +++ b/lib/gs-category-private.h @@ -0,0 +1,21 @@ +/* -*- 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) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-category.h" + +G_BEGIN_DECLS + +void gs_category_sort_children (GsCategory *category); +void gs_category_set_size (GsCategory *category, + guint size); +gchar *gs_category_to_string (GsCategory *category); + +G_END_DECLS diff --git a/lib/gs-category.c b/lib/gs-category.c new file mode 100644 index 0000000..befc167 --- /dev/null +++ b/lib/gs-category.c @@ -0,0 +1,724 @@ +/* -*- 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) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-category + * @short_description: An category that contains applications + * + * This object provides functionality that allows a plugin to create + * a tree structure of categories that each contain #GsApp's. + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-category-private.h" +#include "gs-desktop-data.h" + +struct _GsCategory +{ + GObject parent_instance; + + const GsDesktopData *desktop_data; /* NULL for subcategories */ + const GsDesktopMap *desktop_map; /* NULL for parent categories */ + + GPtrArray *desktop_groups; /* potentially NULL if empty */ + GsCategory *parent; + guint size; /* (atomic) */ + GPtrArray *children; /* potentially NULL if empty */ +}; + +G_DEFINE_TYPE (GsCategory, gs_category, G_TYPE_OBJECT) + +typedef enum { + PROP_ID = 1, + PROP_NAME, + PROP_ICON_NAME, + PROP_SCORE, + PROP_PARENT, + PROP_SIZE, +} GsCategoryProperty; + +static GParamSpec *obj_props[PROP_SIZE + 1] = { NULL, }; + +/** + * gs_category_to_string: + * @category: a #GsCategory + * + * Returns a string representation of the category + * + * Returns: a string + * + * Since: 3.22 + **/ +gchar * +gs_category_to_string (GsCategory *category) +{ + guint i; + GString *str = g_string_new (NULL); + g_string_append_printf (str, "GsCategory[%p]:\n", category); + g_string_append_printf (str, " id: %s\n", + gs_category_get_id (category)); + if (gs_category_get_name (category) != NULL) { + g_string_append_printf (str, " name: %s\n", + gs_category_get_name (category)); + } + if (gs_category_get_icon_name (category) != NULL) { + g_string_append_printf (str, " icon-name: %s\n", + gs_category_get_icon_name (category)); + } + g_string_append_printf (str, " size: %u\n", + gs_category_get_size (category)); + g_string_append_printf (str, " desktop-groups: %u\n", + (category->desktop_groups != NULL) ? category->desktop_groups->len : 0); + if (category->parent != NULL) { + g_string_append_printf (str, " parent: %s\n", + gs_category_get_id (category->parent)); + } + g_string_append_printf (str, " score: %i\n", gs_category_get_score (category)); + if (category->children == NULL || category->children->len == 0) { + g_string_append_printf (str, " children: %u\n", 0u); + } else { + g_string_append_printf (str, " children: %u\n", category->children->len); + for (i = 0; i < category->children->len; i++) { + GsCategory *child = g_ptr_array_index (category->children, i); + g_string_append_printf (str, " - %s\n", + gs_category_get_id (child)); + } + } + return g_string_free (str, FALSE); +} + +/** + * gs_category_get_size: + * @category: a #GsCategory + * + * Returns how many applications the category could contain. + * + * NOTE: This may over-estimate the number if duplicate applications are + * filtered or core applications are not shown. + * + * Returns: the number of apps in the category + * + * Since: 3.22 + **/ +guint +gs_category_get_size (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), 0); + + /* The ‘all’ subcategory is a bit special. */ + if (category->parent != NULL && g_str_equal (gs_category_get_id (category), "all")) + return gs_category_get_size (category->parent); + + return g_atomic_int_get (&category->size); +} + +/** + * gs_category_set_size: + * @category: a #GsCategory + * @size: the number of applications + * + * Sets the number of applications in the category. + * Most plugins do not need to call this function. + * + * Since: 3.22 + **/ +void +gs_category_set_size (GsCategory *category, guint size) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + + g_atomic_int_set (&category->size, size); + g_object_notify_by_pspec (G_OBJECT (category), obj_props[PROP_SIZE]); +} + +/** + * gs_category_increment_size: + * @category: a #GsCategory + * @value: how many to add + * + * Adds @value to the size count. + * + * Since: 3.22 + **/ +void +gs_category_increment_size (GsCategory *category, + guint value) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + + g_atomic_int_add (&category->size, value); + if (value != 0) + g_object_notify_by_pspec (G_OBJECT (category), obj_props[PROP_SIZE]); +} + +/** + * gs_category_get_id: + * @category: a #GsCategory + * + * Gets the category ID. + * + * Returns: the string, e.g. "other" + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_id (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + if (category->desktop_data != NULL) + return category->desktop_data->id; + else if (category->desktop_map != NULL) + return category->desktop_map->id; + g_assert_not_reached (); +} + +/** + * gs_category_get_name: + * @category: a #GsCategory + * + * Gets the category name. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_name (GsCategory *category) +{ + const gchar *category_id; + + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + category_id = gs_category_get_id (category); + + /* special case, we don't want translations in the plugins */ + if (g_strcmp0 (category_id, "other") == 0) { + /* TRANSLATORS: this is where all applications that don't + * fit in other groups are put */ + return _("Other"); + } + if (g_strcmp0 (category_id, "all") == 0) { + /* TRANSLATORS: this is a subcategory matching all the + * different apps in the parent category, e.g. "Games" */ + return C_("Category", "All"); + } + if (g_strcmp0 (category_id, "featured") == 0) { + /* TRANSLATORS: this is a subcategory of featured apps */ + return _("Featured"); + } + + /* normal case */ + if (category->desktop_data != NULL) { + return gettext (category->desktop_data->name); + } else if (category->desktop_map != NULL) { + g_autofree gchar *msgctxt = g_strdup_printf ("Menu of %s", category->parent->desktop_data->name); + return g_dpgettext2 (GETTEXT_PACKAGE, msgctxt, category->desktop_map->name); + } + + g_assert_not_reached (); +} + +/** + * gs_category_get_icon_name: + * @category: a #GsCategory + * + * Gets the category icon name. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_icon_name (GsCategory *category) +{ + const gchar *category_id; + + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + category_id = gs_category_get_id (category); + + /* special case */ + if (g_strcmp0 (category_id, "other") == 0) + return "emblem-system-symbolic"; + if (g_strcmp0 (category_id, "all") == 0) + return "emblem-default-symbolic"; + if (g_strcmp0 (category_id, "featured") == 0) + return "emblem-favorite-symbolic"; + + if (category->desktop_data != NULL) + return category->desktop_data->icon; + else + return NULL; +} + +/** + * gs_category_get_score: + * @category: a #GsCategory + * + * Gets if the category score. + * Important categories may be shown before other categories, or tagged in a + * different way, for example with color or in a different section. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +gint +gs_category_get_score (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE); + + if (category->desktop_data != NULL) + return category->desktop_data->score; + else + return 0; +} + +/** + * gs_category_get_desktop_groups: + * @category: a #GsCategory + * + * Gets the list of AppStream groups for the category. + * + * Returns: (element-type utf8) (transfer none): An array + * + * Since: 3.22 + **/ +GPtrArray * +gs_category_get_desktop_groups (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + if (category->desktop_groups == NULL) + category->desktop_groups = g_ptr_array_new_with_free_func (g_free); + + return category->desktop_groups; +} + +/** + * gs_category_has_desktop_group: + * @category: a #GsCategory + * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player" + * + * Finds out if the category has the specific AppStream desktop group. + * + * Returns: %TRUE if found, %FALSE otherwise + * + * Since: 3.22 + **/ +gboolean +gs_category_has_desktop_group (GsCategory *category, const gchar *desktop_group) +{ + guint i; + + g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE); + g_return_val_if_fail (desktop_group != NULL, FALSE); + + if (category->desktop_groups == NULL) + return FALSE; + + for (i = 0; i < category->desktop_groups->len; i++) { + const gchar *tmp = g_ptr_array_index (category->desktop_groups, i); + if (g_strcmp0 (tmp, desktop_group) == 0) + return TRUE; + } + return FALSE; +} + +/* + * gs_category_add_desktop_group: + * @category: a #GsCategory + * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player" + * + * Adds a desktop group to the category. + * A desktop group is a set of category strings that all must exist. + * + * Since: 3.22 + */ +static void +gs_category_add_desktop_group (GsCategory *category, const gchar *desktop_group) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_return_if_fail (desktop_group != NULL); + + /* add if not already found, and lazily create the groups array + * (since it’s only needed in child categories) */ + if (gs_category_has_desktop_group (category, desktop_group)) + return; + if (category->desktop_groups == NULL) + category->desktop_groups = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (category->desktop_groups, g_strdup (desktop_group)); +} + +/** + * gs_category_find_child: + * @category: a #GsCategory + * @id: a category ID, e.g. "other" + * + * Find a child category with a specific ID. + * + * Returns: (transfer none): the #GsCategory, or %NULL + * + * Since: 3.22 + **/ +GsCategory * +gs_category_find_child (GsCategory *category, const gchar *id) +{ + GsCategory *tmp; + guint i; + + if (category->children == NULL) + return NULL; + + /* find the subcategory */ + for (i = 0; i < category->children->len; i++) { + tmp = GS_CATEGORY (g_ptr_array_index (category->children, i)); + if (g_strcmp0 (id, gs_category_get_id (tmp)) == 0) + return tmp; + } + return NULL; +} + +/** + * gs_category_get_parent: + * @category: a #GsCategory + * + * Gets the parent category. + * + * Returns: the #GsCategory or %NULL + * + * Since: 3.22 + **/ +GsCategory * +gs_category_get_parent (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + return category->parent; +} + +/** + * gs_category_get_children: + * @category: a #GsCategory + * + * Gets the list if children for a category. + * + * Return value: (element-type GsApp) (transfer none): A list of children + * + * Since: 3.22 + **/ +GPtrArray * +gs_category_get_children (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + if (category->children == NULL) + category->children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + return category->children; +} + +/* + * gs_category_add_child: + * @category: a #GsCategory + * @subcategory: a #GsCategory + * + * Adds a child category to a parent category. + * + * Since: 3.22 + */ +static void +gs_category_add_child (GsCategory *category, GsCategory *subcategory) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_return_if_fail (GS_IS_CATEGORY (subcategory)); + + /* lazily create the array to save memory in subcategories, which don’t + * recursively have children */ + if (category->children == NULL) + category->children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + /* FIXME: do we need this? */ + subcategory->parent = category; + g_object_add_weak_pointer (G_OBJECT (subcategory->parent), + (gpointer *) &subcategory->parent); + + g_ptr_array_add (category->children, + g_object_ref (subcategory)); +} + +static gchar * +gs_category_get_sort_key (GsCategory *category) +{ + guint sort_order = 5; + if (g_strcmp0 (gs_category_get_id (category), "featured") == 0) + sort_order = 0; + else if (g_strcmp0 (gs_category_get_id (category), "all") == 0) + sort_order = 2; + else if (g_strcmp0 (gs_category_get_id (category), "other") == 0) + sort_order = 9; + return g_strdup_printf ("%u:%s", + sort_order, + gs_category_get_name (category)); +} + +static gint +gs_category_sort_children_cb (gconstpointer a, gconstpointer b) +{ + GsCategory *ca = GS_CATEGORY (*(GsCategory **) a); + GsCategory *cb = GS_CATEGORY (*(GsCategory **) b); + g_autofree gchar *id_a = gs_category_get_sort_key (ca); + g_autofree gchar *id_b = gs_category_get_sort_key (cb); + return g_strcmp0 (id_a, id_b); +} + +/** + * gs_category_sort_children: + * @category: a #GsCategory + * + * Sorts the list of children. + * + * Since: 3.22 + **/ +void +gs_category_sort_children (GsCategory *category) +{ + if (category->children == NULL) + return; + + g_ptr_array_sort (category->children, + gs_category_sort_children_cb); +} + +static void +gs_category_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsCategory *self = GS_CATEGORY (object); + + switch ((GsCategoryProperty) prop_id) { + case PROP_ID: + g_value_set_string (value, gs_category_get_id (self)); + break; + case PROP_NAME: + g_value_set_string (value, gs_category_get_name (self)); + break; + case PROP_ICON_NAME: + g_value_set_string (value, gs_category_get_icon_name (self)); + break; + case PROP_SCORE: + g_value_set_int (value, gs_category_get_score (self)); + break; + case PROP_PARENT: + g_value_set_object (value, self->parent); + break; + case PROP_SIZE: + g_value_set_uint (value, gs_category_get_size (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_category_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsCategory *self = GS_CATEGORY (object); + + switch ((GsCategoryProperty) prop_id) { + case PROP_ID: + case PROP_NAME: + case PROP_ICON_NAME: + case PROP_SCORE: + case PROP_PARENT: + /* Read only */ + g_assert_not_reached (); + break; + case PROP_SIZE: + gs_category_set_size (self, g_value_get_uint (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_category_finalize (GObject *object) +{ + GsCategory *category = GS_CATEGORY (object); + + if (category->parent != NULL) + g_object_remove_weak_pointer (G_OBJECT (category->parent), + (gpointer *) &category->parent); + g_clear_pointer (&category->children, g_ptr_array_unref); + g_clear_pointer (&category->desktop_groups, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_category_parent_class)->finalize (object); +} + +static void +gs_category_class_init (GsCategoryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gs_category_get_property; + object_class->set_property = gs_category_set_property; + object_class->finalize = gs_category_finalize; + + /** + * GsCategory:id: + * + * A machine readable identifier for the category. Must be non-empty + * and in a valid format to be a + * [desktop category ID](https://specifications.freedesktop.org/menu-spec/latest/). + * + * Since: 40 + */ + obj_props[PROP_ID] = + g_param_spec_string ("id", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:name: + * + * Human readable name for the category. + * + * Since: 40 + */ + obj_props[PROP_NAME] = + g_param_spec_string ("name", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:icon-name: (nullable) + * + * Name of the icon to use for the category, or %NULL if none is set. + * + * Since: 40 + */ + obj_props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:score: + * + * Score for sorting the category. Lower numeric values indicate more + * important categories. + * + * Since: 40 + */ + obj_props[PROP_SCORE] = + g_param_spec_int ("score", NULL, NULL, + G_MININT, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:parent: (nullable) + * + * The parent #GsCategory, or %NULL if this category is at the top + * level. + * + * Since: 40 + */ + obj_props[PROP_PARENT] = + g_param_spec_object ("parent", NULL, NULL, + GS_TYPE_CATEGORY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:size: + * + * Number of apps in this category, including apps in its subcategories. + * + * This has to be initialised externally to the #GsCategory by calling + * gs_category_increment_size(). + * + * Since: 40 + */ + obj_props[PROP_SIZE] = + g_param_spec_uint ("size", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); +} + +static void +gs_category_init (GsCategory *category) +{ +} + +/** + * gs_category_new_for_desktop_data: + * @data: data for the category, which must be static and constant + * + * Create a new #GsCategory instance which wraps the desktop category + * information in @data. Where possible, the static data will be reused, so + * @data must be static and constant across the lifetime of the process. + * + * Returns: (transfer full): a new #GsCategory wrapping @data + * Since: 40 + */ +GsCategory * +gs_category_new_for_desktop_data (const GsDesktopData *data) +{ + g_autoptr(GsCategory) category = NULL; + GsCategory *subcategory_all = NULL; + + /* parent category */ + category = g_object_new (GS_TYPE_CATEGORY, NULL); + category->desktop_data = data; + + /* add subcategories */ + for (gsize j = 0; data->mapping[j].id != NULL; j++) { + const GsDesktopMap *map = &data->mapping[j]; + g_autoptr(GsCategory) sub = g_object_new (GS_TYPE_CATEGORY, NULL); + sub->desktop_map = map; + for (gsize k = 0; map->fdo_cats[k] != NULL; k++) + gs_category_add_desktop_group (sub, map->fdo_cats[k]); + gs_category_add_child (category, sub); + + if (g_str_equal (map->id, "all")) + subcategory_all = sub; + } + + /* set up the ‘all’ subcategory specially, adding all the desktop groups + * from all other child categories to it */ + if (subcategory_all != NULL) { + g_assert (category->children != NULL); + + for (guint i = 0; i < category->children->len; i++) { + GPtrArray *desktop_groups; + GsCategory *child; + + /* ignore the all category */ + child = g_ptr_array_index (category->children, i); + if (child == subcategory_all) + continue; + + /* add all desktop groups */ + desktop_groups = gs_category_get_desktop_groups (child); + for (guint j = 0; j < desktop_groups->len; j++) { + const gchar *tmp = g_ptr_array_index (desktop_groups, j); + gs_category_add_desktop_group (subcategory_all, tmp); + } + } + } + + return g_steal_pointer (&category); +} diff --git a/lib/gs-category.h b/lib/gs-category.h new file mode 100644 index 0000000..1e82591 --- /dev/null +++ b/lib/gs-category.h @@ -0,0 +1,45 @@ +/* -*- 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) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +#include "gs-desktop-data.h" + +G_BEGIN_DECLS + +#define GS_TYPE_CATEGORY (gs_category_get_type ()) + +G_DECLARE_FINAL_TYPE (GsCategory, gs_category, GS, CATEGORY, GObject) + +GsCategory *gs_category_new_for_desktop_data (const GsDesktopData *data); + +const gchar *gs_category_get_id (GsCategory *category); +GsCategory *gs_category_get_parent (GsCategory *category); + +const gchar *gs_category_get_name (GsCategory *category); +const gchar *gs_category_get_icon_name (GsCategory *category); +gint gs_category_get_score (GsCategory *category); + +GPtrArray *gs_category_get_desktop_groups (GsCategory *category); +gboolean gs_category_has_desktop_group (GsCategory *category, + const gchar *desktop_group); + +GsCategory *gs_category_find_child (GsCategory *category, + const gchar *id); +GPtrArray *gs_category_get_children (GsCategory *category); + +guint gs_category_get_size (GsCategory *category); +void gs_category_increment_size (GsCategory *category, + guint value); + +G_END_DECLS diff --git a/lib/gs-cmd.c b/lib/gs-cmd.c new file mode 100644 index 0000000..895e1b8 --- /dev/null +++ b/lib/gs-cmd.c @@ -0,0 +1,847 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <locale.h> + +#include "gnome-software-private.h" + +#include "gs-debug.h" + +typedef struct { + GsPluginLoader *plugin_loader; + guint64 refine_flags; + guint max_results; + gboolean interactive; +} GsCmdSelf; + +static void +gs_cmd_show_results_apps (GsAppList *list) +{ + for (guint j = 0; j < gs_app_list_length (list); j++) { + GsApp *app = gs_app_list_index (list, j); + GsAppList *related = gs_app_get_related (app); + g_autofree gchar *tmp = gs_app_to_string (app); + g_print ("%s\n", tmp); + for (guint i = 0; i < gs_app_list_length (related); i++) { + g_autofree gchar *tmp_rel = NULL; + GsApp *app_rel = GS_APP (gs_app_list_index (related, i)); + tmp_rel = gs_app_to_string (app_rel); + g_print ("\t%s\n", tmp_rel); + } + } +} + +static gchar * +gs_cmd_pad_spaces (const gchar *text, guint length) +{ + gsize i; + GString *str; + str = g_string_sized_new (length + 1); + g_string_append (str, text); + for (i = strlen (text); i < length; i++) + g_string_append_c (str, ' '); + return g_string_free (str, FALSE); +} + +static void +gs_cmd_show_results_categories (GPtrArray *list) +{ + for (guint i = 0; i < list->len; i++) { + GsCategory *cat = GS_CATEGORY (g_ptr_array_index (list, i)); + GsCategory *parent = gs_category_get_parent (cat); + g_autofree gchar *tmp = NULL; + if (parent != NULL){ + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s/%s [%u]", + gs_category_get_id (parent), + gs_category_get_id (cat), + gs_category_get_size (cat)); + tmp = gs_cmd_pad_spaces (id, 32); + g_print ("%s : %s\n", + tmp, gs_category_get_name (cat)); + } else { + GPtrArray *subcats = gs_category_get_children (cat); + tmp = gs_cmd_pad_spaces (gs_category_get_id (cat), 32); + g_print ("%s : %s\n", + tmp, gs_category_get_name (cat)); + gs_cmd_show_results_categories (subcats); + } + } +} + +static GsPluginRefineFlags +gs_cmd_refine_flag_from_string (const gchar *flag, GError **error) +{ + if (g_strcmp0 (flag, "all") == 0) + return G_MAXINT32; + if (g_strcmp0 (flag, "license") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE; + if (g_strcmp0 (flag, "url") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL; + if (g_strcmp0 (flag, "description") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION; + if (g_strcmp0 (flag, "size") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE; + if (g_strcmp0 (flag, "rating") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING; + if (g_strcmp0 (flag, "version") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION; + if (g_strcmp0 (flag, "history") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY; + if (g_strcmp0 (flag, "setup-action") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION; + if (g_strcmp0 (flag, "update-details") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS; + if (g_strcmp0 (flag, "origin") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN; + if (g_strcmp0 (flag, "related") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED; + if (g_strcmp0 (flag, "menu-path") == 0) + /* no longer supported by itself; categories are largely equivalent */ + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES; + if (g_strcmp0 (flag, "upgrade-removed") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED; + if (g_strcmp0 (flag, "provenance") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE; + if (g_strcmp0 (flag, "reviews") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS; + if (g_strcmp0 (flag, "review-ratings") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS; + if (g_strcmp0 (flag, "key-colors") == 0) + /* no longer supported by itself; derived automatically from the icon */ + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON; + if (g_strcmp0 (flag, "icon") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON; + if (g_strcmp0 (flag, "permissions") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS; + if (g_strcmp0 (flag, "origin-hostname") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME; + if (g_strcmp0 (flag, "origin-ui") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI; + if (g_strcmp0 (flag, "runtime") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME; + if (g_strcmp0 (flag, "categories") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES; + if (g_strcmp0 (flag, "project-group") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP; + if (g_strcmp0 (flag, "developer-name") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME; + if (g_strcmp0 (flag, "kudos") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS; + if (g_strcmp0 (flag, "content-rating") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING; + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "GsPluginRefineFlag '%s' not recognised", flag); + return 0; +} + +static guint64 +gs_cmd_parse_refine_flags (const gchar *extra, GError **error) +{ + GsPluginRefineFlags tmp; + guint i; + guint64 refine_flags = GS_PLUGIN_REFINE_FLAGS_NONE; + g_auto(GStrv) split = NULL; + + if (extra == NULL) + return GS_PLUGIN_REFINE_FLAGS_NONE; + + split = g_strsplit (extra, ",", -1); + for (i = 0; split[i] != NULL; i++) { + tmp = gs_cmd_refine_flag_from_string (split[i], error); + if (tmp == 0) + return G_MAXUINT64; + refine_flags |= tmp; + } + return refine_flags; +} + +static guint +gs_cmd_prompt_for_number (guint maxnum) +{ + gint retval; + guint answer = 0; + + do { + char buffer[64]; + + /* swallow the \n at end of line too */ + if (!fgets (buffer, sizeof (buffer), stdin)) + break; + if (strlen (buffer) == sizeof (buffer) - 1) + continue; + + /* get a number */ + retval = sscanf (buffer, "%u", &answer); + + /* positive */ + if (retval == 1 && answer > 0 && answer <= maxnum) + break; + + /* TRANSLATORS: the user isn't reading the question */ + g_print (_("Please enter a number from 1 to %u: "), maxnum); + } while (TRUE); + return answer; +} + +static gboolean +gs_cmd_action_exec (GsCmdSelf *self, GsPluginAction action, const gchar *name, GError **error) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_filtered = NULL; + g_autoptr(GsAppQuery) query = NULL; + g_autoptr(GsPluginJob) plugin_job2 = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + gboolean show_installed = TRUE; + const gchar * const keywords[] = { name, NULL }; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + /* ensure set */ + self->refine_flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON; + self->refine_flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION; + + /* do search */ + query = gs_app_query_new ("keywords", keywords, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, error); + if (list == NULL) + return FALSE; + if (gs_app_list_length (list) == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "no components matched '%s'", + name); + return FALSE; + } + + /* filter */ + if (action == GS_PLUGIN_ACTION_INSTALL) + show_installed = FALSE; + list_filtered = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app_tmp = gs_app_list_index (list, i); + if (gs_app_is_installed (app_tmp) == show_installed) + gs_app_list_add (list_filtered, app_tmp); + } + + /* nothing */ + if (gs_app_list_length (list_filtered) == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "no components were in the correct state for '%s %s'", + gs_plugin_action_to_string (action), name); + return FALSE; + } + + /* get one GsApp */ + if (gs_app_list_length (list_filtered) == 1) { + app = g_object_ref (gs_app_list_index (list_filtered, 0)); + } else { + guint idx; + /* TRANSLATORS: asking the user to choose an app from a list */ + g_print ("%s\n", _("Choose an application:")); + for (guint i = 0; i < gs_app_list_length (list_filtered); i++) { + GsApp *app_tmp = gs_app_list_index (list_filtered, i); + g_print ("%u.\t%s\n", + i + 1, + gs_app_get_unique_id (app_tmp)); + } + idx = gs_cmd_prompt_for_number (gs_app_list_length (list_filtered)); + app = g_object_ref (gs_app_list_index (list_filtered, idx - 1)); + } + + /* install */ + plugin_job2 = gs_plugin_job_newv (action, + "app", app, + "interactive", self->interactive, + NULL); + return gs_plugin_loader_job_action (self->plugin_loader, plugin_job2, + NULL, error); +} + +static void +gs_cmd_self_free (GsCmdSelf *self) +{ + if (self->plugin_loader != NULL) + g_object_unref (self->plugin_loader); + g_free (self); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsCmdSelf, gs_cmd_self_free) + +static gint +app_sort_kind_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + if (gs_app_get_kind (app1) == AS_COMPONENT_KIND_DESKTOP_APP) + return -1; + if (gs_app_get_kind (app2) == AS_COMPONENT_KIND_DESKTOP_APP) + return 1; + return 0; +} + +int +main (int argc, char **argv) +{ + g_autoptr(GOptionContext) context = NULL; + gboolean prefer_local = FALSE; + gboolean ret; + gboolean show_results = FALSE; + gboolean verbose = FALSE; + gint i; + guint64 cache_age_secs = 0; + gint repeat = 1; + g_auto(GStrv) plugin_blocklist = NULL; + g_auto(GStrv) plugin_allowlist = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GPtrArray) categories = NULL; + g_autoptr(GsDebug) debug = gs_debug_new_from_environment (); + g_autofree gchar *plugin_blocklist_str = NULL; + g_autofree gchar *plugin_allowlist_str = NULL; + g_autofree gchar *refine_flags_str = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsCmdSelf) self = g_new0 (GsCmdSelf, 1); + const GOptionEntry options[] = { + { "show-results", '\0', 0, G_OPTION_ARG_NONE, &show_results, + "Show the results for the action", NULL }, + { "refine-flags", '\0', 0, G_OPTION_ARG_STRING, &refine_flags_str, + "Set any refine flags required for the action", NULL }, + { "repeat", '\0', 0, G_OPTION_ARG_INT, &repeat, + "Repeat the action this number of times", NULL }, + { "cache-age", '\0', 0, G_OPTION_ARG_INT64, &cache_age_secs, + "Use this maximum cache age in seconds", NULL }, + { "max-results", '\0', 0, G_OPTION_ARG_INT, &self->max_results, + "Return a maximum number of results", NULL }, + { "prefer-local", '\0', 0, G_OPTION_ARG_NONE, &prefer_local, + "Prefer local file sources to AppStream", NULL }, + { "plugin-blocklist", '\0', 0, G_OPTION_ARG_STRING, &plugin_blocklist_str, + "Do not load specific plugins", NULL }, + { "plugin-allowlist", '\0', 0, G_OPTION_ARG_STRING, &plugin_allowlist_str, + "Only load specific plugins", NULL }, + { "verbose", '\0', 0, G_OPTION_ARG_NONE, &verbose, + "Show verbose debugging information", NULL }, + { "interactive", 'i', 0, G_OPTION_ARG_NONE, &self->interactive, + "Allow interactive authentication", NULL }, + { NULL} + }; + + setlocale (LC_ALL, ""); + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + gtk_init (); + + context = g_option_context_new (NULL); + g_option_context_set_summary (context, "GNOME Software Test Program"); + g_option_context_add_main_entries (context, options, NULL); + ret = g_option_context_parse (context, &argc, &argv, &error); + if (!ret) { + g_print ("Failed to parse options: %s\n", error->message); + return EXIT_FAILURE; + } + gs_debug_set_verbose (debug, verbose); + + /* prefer local sources */ + if (prefer_local) + g_setenv ("GNOME_SOFTWARE_PREFER_LOCAL", "true", TRUE); + + /* parse any refine flags */ + self->refine_flags = gs_cmd_parse_refine_flags (refine_flags_str, &error); + if (self->refine_flags == G_MAXUINT64) { + g_print ("Flag unknown: %s\n", error->message); + return EXIT_FAILURE; + } + + /* load plugins */ + self->plugin_loader = gs_plugin_loader_new (NULL, NULL); + if (g_file_test (LOCALPLUGINDIR, G_FILE_TEST_EXISTS)) + gs_plugin_loader_add_location (self->plugin_loader, LOCALPLUGINDIR); + if (plugin_allowlist_str != NULL) + plugin_allowlist = g_strsplit (plugin_allowlist_str, ",", -1); + if (plugin_blocklist_str != NULL) + plugin_blocklist = g_strsplit (plugin_blocklist_str, ",", -1); + ret = gs_plugin_loader_setup (self->plugin_loader, + (const gchar * const *) plugin_allowlist, + (const gchar * const *) plugin_blocklist, + NULL, + &error); + if (!ret) { + g_print ("Failed to setup plugins: %s\n", error->message); + return EXIT_FAILURE; + } + gs_plugin_loader_dump_state (self->plugin_loader); + + /* ensure that at least some metadata of any age is present, and also + * spin up the plugins enough as to prime caches */ + if (g_getenv ("GS_CMD_NO_INITIAL_REFRESH") == NULL) { + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPluginRefreshMetadataFlags refresh_metadata_flags = GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE; + + if (self->interactive) + refresh_metadata_flags |= GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64, refresh_metadata_flags); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (!ret) { + g_print ("Failed to refresh plugins: %s\n", error->message); + return EXIT_FAILURE; + } + } + + /* do action */ + if (argc == 2 && g_strcmp0 (argv[1], "installed") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsAppQuery) query = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + query = gs_app_query_new ("is-installed", GS_APP_QUERY_TRISTATE_TRUE, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 3 && g_strcmp0 (argv[1], "search") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsAppQuery) query = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + const gchar *keywords[2] = { argv[2], NULL }; + + if (list != NULL) + g_object_unref (list); + + query = gs_app_query_new ("keywords", keywords, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 3 && g_strcmp0 (argv[1], "get-alternates") == 0) { + app = gs_app_new (argv[2]); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + for (i = 0; i < repeat; i++) { + g_autoptr(GsAppQuery) query = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + query = gs_app_query_new ("alternate-of", app, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_priority, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 4 && g_strcmp0 (argv[1], "action") == 0) { + GsPluginAction action = gs_plugin_action_from_string (argv[2]); + if (action == GS_PLUGIN_ACTION_UNKNOWN) { + ret = FALSE; + g_set_error (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Did not recognise action '%s'", argv[2]); + } else { + ret = gs_cmd_action_exec (self, action, argv[3], &error); + } + } else if (argc == 3 && g_strcmp0 (argv[1], "action-upgrade-download") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + app = gs_app_new (argv[2]); + gs_app_set_kind (app, AS_COMPONENT_KIND_OPERATING_SYSTEM); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "app", app, + "interactive", self->interactive, + NULL); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (ret) + gs_app_list_add (list, app); + } else if (argc == 3 && g_strcmp0 (argv[1], "refine") == 0) { + app = gs_app_new (argv[2]); + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_refine_new_for_app (app, self->refine_flags); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (!ret) + break; + } + list = gs_app_list_new (); + gs_app_list_add (list, app); + } else if (argc == 3 && g_strcmp0 (argv[1], "launch") == 0) { + app = gs_app_new (argv[2]); + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_LAUNCH, + "app", app, + "interactive", self->interactive, + NULL); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (!ret) + break; + } + } else if (argc == 3 && g_strcmp0 (argv[1], "filename-to-app") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + file = g_file_new_for_path (argv[2]); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "interactive", self->interactive, + NULL); + app = gs_plugin_loader_job_process_app (self->plugin_loader, plugin_job, NULL, &error); + if (app == NULL) { + ret = FALSE; + } else { + list = gs_app_list_new (); + gs_app_list_add (list, app); + } + } else if (argc == 3 && g_strcmp0 (argv[1], "url-to-app") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP, + "search", argv[2], + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "interactive", self->interactive, + NULL); + app = gs_plugin_loader_job_process_app (self->plugin_loader, plugin_job, + NULL, &error); + if (app == NULL) { + ret = FALSE; + } else { + list = gs_app_list_new (); + gs_app_list_add (list, app); + } + } else if (argc == 2 && g_strcmp0 (argv[1], "updates") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "interactive", self->interactive, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "upgrades") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPluginListDistroUpgradesFlags upgrades_flags = GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + if (self->interactive) + upgrades_flags |= GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_distro_upgrades_new (upgrades_flags, self->refine_flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "sources") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "interactive", self->interactive, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, + plugin_job, + NULL, + &error); + if (list == NULL) + ret = FALSE; + } else if (argc == 2 && g_strcmp0 (argv[1], "popular") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + query = gs_app_query_new ("is-curated", GS_APP_QUERY_TRISTATE_TRUE, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "sort-func", app_sort_kind_cb, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "featured") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + query = gs_app_query_new ("is-featured", GS_APP_QUERY_TRISTATE_TRUE, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 3 && g_strcmp0 (argv[1], "deployment-featured") == 0) { + g_auto(GStrv) split = g_strsplit (argv[2], ",", -1); + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + query = gs_app_query_new ("deployment-featured", split, + "refine-flags", self->refine_flags, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_KEY_ID, + "max-results", self->max_results, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "recent") == 0) { + if (cache_age_secs == 0) + cache_age_secs = 60 * 60 * 24 * 60; + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GDateTime) now = NULL; + g_autoptr(GDateTime) released_since = NULL; + g_autoptr(GsAppQuery) query = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + now = g_date_time_new_now_local (); + released_since = g_date_time_add_seconds (now, -cache_age_secs); + query = gs_app_query_new ("released-since", released_since, + "refine-flags", self->refine_flags, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_KEY_ID, + "max-results", self->max_results, + "sort-func", app_sort_kind_cb, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "get-categories") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPluginRefineCategoriesFlags flags = GS_PLUGIN_REFINE_CATEGORIES_FLAGS_SIZE; + + if (categories != NULL) + g_ptr_array_unref (categories); + + if (self->interactive) + flags |= GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_categories_new (flags); + if (!gs_plugin_loader_job_action (self->plugin_loader, plugin_job, NULL, &error)) { + ret = FALSE; + break; + } + + categories = g_ptr_array_ref (gs_plugin_job_list_categories_get_result_list (GS_PLUGIN_JOB_LIST_CATEGORIES (plugin_job))); + } + } else if (argc == 3 && g_strcmp0 (argv[1], "get-category-apps") == 0) { + g_autoptr(GsCategory) category_owned = NULL; + GsCategory *category = NULL; + g_auto(GStrv) split = NULL; + GsCategoryManager *manager = gs_plugin_loader_get_category_manager (self->plugin_loader); + + split = g_strsplit (argv[2], "/", 2); + if (g_strv_length (split) == 1) { + category_owned = gs_category_manager_lookup (manager, split[0]); + category = category_owned; + } else { + g_autoptr(GsCategory) parent = gs_category_manager_lookup (manager, split[0]); + if (parent != NULL) + category = gs_category_find_child (parent, split[1]); + } + + if (category == NULL) { + g_printerr ("Error: Could not find category ‘%s’\n", argv[2]); + return EXIT_FAILURE; + } + + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + GsPluginListAppsFlags flags = GS_PLUGIN_LIST_APPS_FLAGS_NONE; + + if (list != NULL) + g_object_unref (list); + + query = gs_app_query_new ("category", category, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + "sort-func", gs_utils_app_sort_name, + NULL); + + if (self->interactive) + flags |= GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_list_apps_new (query, flags); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc >= 2 && g_strcmp0 (argv[1], "refresh") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPluginRefreshMetadataFlags refresh_metadata_flags = GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE; + + if (self->interactive) + refresh_metadata_flags |= GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE; + + plugin_job = gs_plugin_job_refresh_metadata_new (cache_age_secs, refresh_metadata_flags); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + } else if (argc >= 1 && g_strcmp0 (argv[1], "user-hash") == 0) { + g_autofree gchar *user_hash = gs_utils_get_user_hash (&error); + if (user_hash == NULL) { + ret = FALSE; + } else { + g_print ("%s\n", user_hash); + ret = TRUE; + } + } else { + ret = FALSE; + g_set_error_literal (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Did not recognise option, use 'installed', " + "'updates', 'popular', 'get-categories', " + "'get-category-apps', 'get-alternates', 'filename-to-app', " + "'action install', 'action remove', " + "'sources', 'refresh', 'launch' or 'search'"); + } + if (!ret) { + g_print ("Failed: %s\n", error->message); + return EXIT_FAILURE; + } + + if (show_results) { + if (list != NULL) + gs_cmd_show_results_apps (list); + if (categories != NULL) + gs_cmd_show_results_categories (categories); + } + return EXIT_SUCCESS; +} diff --git a/lib/gs-debug.c b/lib/gs-debug.c new file mode 100644 index 0000000..f76788a --- /dev/null +++ b/lib/gs-debug.c @@ -0,0 +1,295 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <stdio.h> +#include <unistd.h> + +#include "gs-os-release.h" +#include "gs-debug.h" + +struct _GsDebug +{ + GObject parent_instance; + + gchar **domains; /* (owned) (nullable), read-only after construction, guaranteed to be %NULL if empty */ + gboolean verbose; /* (atomic) */ + gboolean use_time; /* read-only after construction */ +}; + +G_DEFINE_TYPE (GsDebug, gs_debug, G_TYPE_OBJECT) + +static GLogWriterOutput +gs_log_writer_console (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + GsDebug *debug = GS_DEBUG (user_data); + gboolean verbose; + const gchar * const *domains = NULL; + const gchar *log_domain = NULL; + const gchar *log_message = NULL; + g_autofree gchar *tmp = NULL; + g_autoptr(GString) domain = NULL; + + domains = (const gchar * const *) debug->domains; + verbose = g_atomic_int_get (&debug->verbose); + + /* check enabled, fast path without parsing fields */ + if ((log_level == G_LOG_LEVEL_DEBUG || + log_level == G_LOG_LEVEL_INFO) && + !verbose && + debug->domains == NULL) + return G_LOG_WRITER_HANDLED; + + /* get data from arguments */ + for (gsize i = 0; i < n_fields; i++) { + if (g_strcmp0 (fields[i].key, "MESSAGE") == 0) { + log_message = fields[i].value; + continue; + } + if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0) { + log_domain = fields[i].value; + continue; + } + } + + /* check enabled, slower path */ + if ((log_level == G_LOG_LEVEL_DEBUG || + log_level == G_LOG_LEVEL_INFO) && + !verbose && + debug->domains != NULL && + g_strcmp0 (debug->domains[0], "all") != 0 && + (log_domain == NULL || !g_strv_contains (domains, log_domain))) + return G_LOG_WRITER_HANDLED; + + /* this is really verbose */ + if ((g_strcmp0 (log_domain, "dconf") == 0 || + g_strcmp0 (log_domain, "GLib-GIO") == 0 || + g_strcmp0 (log_domain, "GLib-Net") == 0 || + g_strcmp0 (log_domain, "GdkPixbuf") == 0) && + log_level == G_LOG_LEVEL_DEBUG) + return G_LOG_WRITER_HANDLED; + + /* time header */ + if (debug->use_time) { + g_autoptr(GDateTime) dt = g_date_time_new_now_utc (); + tmp = g_strdup_printf ("%02i:%02i:%02i:%03i", + g_date_time_get_hour (dt), + g_date_time_get_minute (dt), + g_date_time_get_second (dt), + g_date_time_get_microsecond (dt) / 1000); + } + + /* make these shorter */ + if (g_strcmp0 (log_domain, "PackageKit") == 0) { + log_domain = "PK"; + } else if (g_strcmp0 (log_domain, "GsPlugin") == 0) { + log_domain = "Gs"; + } + + /* pad out domain */ + domain = g_string_new (log_domain); + for (guint i = domain->len; i < 3; i++) + g_string_append (domain, " "); + + switch (log_level) { + case G_LOG_LEVEL_ERROR: + case G_LOG_LEVEL_CRITICAL: + case G_LOG_LEVEL_WARNING: + /* to screen */ + if (isatty (fileno (stderr)) == 1) { + /* critical in red */ + if (tmp != NULL) + g_printerr ("%c[%dm%s ", 0x1B, 32, tmp); + g_printerr ("%s ", domain->str); + g_printerr ("%c[%dm%s\n%c[%dm", 0x1B, 31, log_message, 0x1B, 0); + } else { /* to file */ + if (tmp != NULL) + g_printerr ("%s ", tmp); + g_printerr ("%s ", domain->str); + g_printerr ("%s\n", log_message); + } + break; + default: + /* to screen */ + if (isatty (fileno (stdout)) == 1) { + /* debug in blue */ + if (tmp != NULL) + g_print ("%c[%dm%s ", 0x1B, 32, tmp); + g_print ("%s ", domain->str); + g_print ("%c[%dm%s\n%c[%dm", 0x1B, 34, log_message, 0x1B, 0); + break; + } else { /* to file */ + if (tmp != NULL) + g_print ("%s ", tmp); + g_print ("%s ", domain->str); + g_print ("%s\n", log_message); + } + } + + /* success */ + return G_LOG_WRITER_HANDLED; +} + +static GLogWriterOutput +gs_log_writer_journald (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + /* important enough to force to the journal */ + switch (log_level) { + case G_LOG_LEVEL_ERROR: + case G_LOG_LEVEL_CRITICAL: + case G_LOG_LEVEL_WARNING: + case G_LOG_LEVEL_INFO: + return g_log_writer_journald (log_level, fields, n_fields, user_data); + break; + default: + break; + } + + return G_LOG_WRITER_UNHANDLED; +} + +static GLogWriterOutput +gs_debug_log_writer (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + if (g_log_writer_is_journald (fileno (stderr))) + return gs_log_writer_journald (log_level, fields, n_fields, user_data); + else + return gs_log_writer_console (log_level, fields, n_fields, user_data); +} + +static void +gs_debug_finalize (GObject *object) +{ + GsDebug *debug = GS_DEBUG (object); + + g_clear_pointer (&debug->domains, g_strfreev); + + G_OBJECT_CLASS (gs_debug_parent_class)->finalize (object); +} + +static void +gs_debug_class_init (GsDebugClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_debug_finalize; +} + +static void +gs_debug_init (GsDebug *debug) +{ + g_log_set_writer_func (gs_debug_log_writer, + g_object_ref (debug), + (GDestroyNotify) g_object_unref); +} + +/** + * gs_debug_new: + * @domains: (transfer full) (nullable): a #GStrv of debug log domains to output, + * or `{ "all", NULL }` to output all debug log domains; %NULL is equivalent + * to an empty array + * @verbose: whether to output log debug messages + * @use_time: whether to output a timestamp with each log message + * + * Create a new #GsDebug with the given configuration. + * + * Ownership of @domains is transferred to this function. It will be freed with + * g_strfreev() when the #GsDebug is destroyed. + * + * Returns: (transfer full): a new #GsDebug + * Since: 40 + */ +GsDebug * +gs_debug_new (gchar **domains, + gboolean verbose, + gboolean use_time) +{ + g_autoptr(GsDebug) debug = g_object_new (GS_TYPE_DEBUG, NULL); + + /* Strictly speaking these should be set before g_log_set_writer_func() + * is called, but threads probably haven’t been started at this point. */ + debug->domains = (domains != NULL && domains[0] != NULL) ? g_steal_pointer (&domains) : NULL; + debug->verbose = verbose; + debug->use_time = use_time; + + return g_steal_pointer (&debug); +} + +/** + * gs_debug_new_from_environment: + * + * Create a new #GsDebug with its configuration loaded from environment + * variables. + * + * Returns: (transfer full): a new #GsDebug + * Since: 40 + */ +GsDebug * +gs_debug_new_from_environment (void) +{ + g_auto(GStrv) domains = NULL; + gboolean verbose, use_time; + + if (g_getenv ("G_MESSAGES_DEBUG") != NULL) { + domains = g_strsplit (g_getenv ("G_MESSAGES_DEBUG"), " ", -1); + if (domains[0] == NULL) + g_clear_pointer (&domains, g_strfreev); + } + + verbose = (g_getenv ("GS_DEBUG") != NULL); + use_time = (g_getenv ("GS_DEBUG_NO_TIME") == NULL); + + return gs_debug_new (g_steal_pointer (&domains), verbose, use_time); +} + +/** + * gs_debug_set_verbose: + * @self: a #GsDebug + * @verbose: whether to output log debug messages + * + * Enable or disable verbose logging mode. + * + * This can be called at any time, from any thread. + * + * Since: 40 + */ +void +gs_debug_set_verbose (GsDebug *self, + gboolean verbose) +{ + g_return_if_fail (GS_IS_DEBUG (self)); + + /* If we’re changing from !verbose → verbose, print OS information. + * This is helpful in verbose logs when people file bug reports. */ + if (g_atomic_int_compare_and_exchange (&self->verbose, !verbose, verbose) && + verbose) { + g_autoptr(GsOsRelease) os_release = NULL; + g_autoptr(GError) error = NULL; + + g_debug (PACKAGE_NAME " " PACKAGE_VERSION); + + os_release = gs_os_release_new (&error); + if (os_release) { + g_debug ("OS: %s; %s", + gs_os_release_get_name (os_release), + gs_os_release_get_version (os_release)); + } else { + g_debug ("Failed to get OS Release information: %s", error->message); + } + } +} diff --git a/lib/gs-debug.h b/lib/gs-debug.h new file mode 100644 index 0000000..d927f81 --- /dev/null +++ b/lib/gs-debug.h @@ -0,0 +1,27 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_DEBUG (gs_debug_get_type ()) + +G_DECLARE_FINAL_TYPE (GsDebug, gs_debug, GS, DEBUG, GObject) + +GsDebug *gs_debug_new (gchar **domains, + gboolean verbose, + gboolean use_time); +GsDebug *gs_debug_new_from_environment (void); +void gs_debug_set_verbose (GsDebug *self, + gboolean verbose); + +G_END_DECLS diff --git a/lib/gs-desktop-data.c b/lib/gs-desktop-data.c new file mode 100644 index 0000000..c2ee93b --- /dev/null +++ b/lib/gs-desktop-data.c @@ -0,0 +1,320 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-desktop-data.h" + +static const GsDesktopMap map_create[] = { + { "all", NC_("Menu of Graphics & Photography", "All"), + { "Graphics", + "AudioVideo", + NULL } }, + { "featured", NC_("Menu of Graphics & Photography", "Featured"), + { "Graphics::Featured", + "AudioVideo::Featured", + NULL} }, + { "3d", NC_("Menu of Graphics & Photography", "3D Graphics"), + { "Graphics::3DGraphics", + NULL} }, + { "photography", NC_("Menu of Graphics & Photography", "Photography"), + { "Graphics::Photography", + NULL} }, + { "scanning", NC_("Menu of Graphics & Photography", "Scanning"), + { "Graphics::Scanning", + NULL} }, + { "vector", NC_("Menu of Graphics & Photography", "Vector Graphics"), + { "Graphics::VectorGraphics", + NULL} }, + { "viewers", NC_("Menu of Graphics & Photography", "Viewers"), + { "Graphics::Viewer", + NULL} }, + { "creation-editing", NC_("Menu of Audio & Video", "Audio Creation & Editing"), + { "AudioVideo::AudioVideoEditing", + "AudioVideo::Midi", + "AudioVideo::DiscBurning", + "AudioVideo::Sequencer", + NULL} }, + { "music-players", NC_("Menu of Audio & Video", "Music Players"), + { "AudioVideo::Music", + "AudioVideo::Player", + NULL} }, + { NULL } +}; + +static const GsDesktopMap map_work[] = { + { "all", NC_("Menu of Productivity", "All"), + { "Office", + "Utility", + "Network::WebBrowser", + NULL } }, + { "featured", NC_("Menu of Productivity", "Featured"), + { "Office::Featured", + "Utility::Featured", + NULL} }, + { "calendar", NC_("Menu of Productivity", "Calendar"), + { "Office::Calendar", + "Office::ProjectManagement", + NULL} }, + { "database", NC_("Menu of Productivity", "Database"), + { "Office::Database", + NULL} }, + { "finance", NC_("Menu of Productivity", "Finance"), + { "Office::Finance", + "Office::Spreadsheet", + NULL} }, + { "word-processor", NC_("Menu of Productivity", "Word Processor"), + { "Office::WordProcessor", + "Office::Dictionary", + NULL} }, + { "text-editors", NC_("Menu of Utilities", "Text Editors"), + { "Utility::TextEditor", + NULL} }, + { "web-browsers", NC_("Menu of Communication & News", "Web Browsers"), + { "Network::WebBrowser", + NULL} }, + { NULL } +}; + +static const GsDesktopMap map_play[] = { + { "all", NC_("Menu of Audio & Video", "All"), + { "Game", + NULL } }, + { "featured", NC_("Menu of Audio & Video", "Featured"), + { "Game::Featured", + NULL} }, + { "action", NC_("Menu of Games", "Action"), + { "Game::ActionGame", + NULL} }, + { "adventure", NC_("Menu of Games", "Adventure"), + { "Game::AdventureGame", + NULL} }, + { "arcade", NC_("Menu of Games", "Arcade"), + { "Game::ArcadeGame", + NULL} }, + { "blocks", NC_("Menu of Games", "Blocks"), + { "Game::BlocksGame", + NULL} }, + { "board", NC_("Menu of Games", "Board"), + { "Game::BoardGame", + NULL} }, + { "card", NC_("Menu of Games", "Card"), + { "Game::CardGame", + NULL} }, + { "emulator", NC_("Menu of Games", "Emulators"), + { "Game::Emulator", + NULL} }, + { "kids", NC_("Menu of Games", "Kids"), + { "Game::KidsGame", + NULL} }, + { "logic", NC_("Menu of Games", "Logic"), + { "Game::LogicGame", + "Game::Simulation", + NULL} }, + { "role-playing", NC_("Menu of Games", "Role Playing"), + { "Game::RolePlaying", + NULL} }, + { "sports", NC_("Menu of Games", "Sports"), + { "Game::SportsGame", + NULL} }, + { "strategy", NC_("Menu of Games", "Strategy"), + { "Game::StrategyGame", + NULL} }, + { NULL } +}; + +static const GsDesktopMap map_socialize[] = { + { "all", NC_("Menu of Communication & News", "All"), + { "Network", + NULL } }, + { "featured", NC_("Menu of Communication & News", "Featured"), + { "Network::Featured", + NULL} }, + { "chat", NC_("Menu of Communication & News", "Chat"), + { "Network::Chat", + "Network::IRCClient", + "Network::Telephony", + "Network::VideoConference", + "Network::Email", + NULL} }, + { NULL } +}; + +static const GsDesktopMap map_learn[] = { + { "all", NC_("Menu of Education & Science", "All"), + { "Education", + "Science", + "Reference", + "Network::Feed", + "Network::News", + NULL } }, + { "featured", NC_("Menu of Education & Science", "Featured"), + { "Education::Featured", + "Science::Featured", + "Reference::Featured", + NULL} }, + { "artificial-intelligence", NC_("Menu of Education & Science", "Artificial Intelligence"), + { "Science::ArtificialIntelligence", + NULL} }, + { "astronomy", NC_("Menu of Education & Science", "Astronomy"), + { "Education::Astronomy", + "Science::Astronomy", + NULL} }, + { "chemistry", NC_("Menu of Education & Science", "Chemistry"), + { "Education::Chemistry", + "Science::Chemistry", + NULL} }, + { "languages", NC_("Menu of Education & Science", "Languages"), + { "Education::Languages", + "Education::Literature", + NULL} }, + { "math", NC_("Menu of Education & Science", "Math"), + { "Education::Math", + "Education::NumericalAnalysis", + "Science::Math", + "Science::Physics", + "Science::NumericalAnalysis", + NULL} }, + { "news", NC_("Menu of Communication & News", "News"), + { "Network::Feed", + "Network::News", + NULL} }, + { "robotics", NC_("Menu of Education & Science", "Robotics"), + { "Science::Robotics", + NULL} }, + { "art", NC_("Menu of Art", "Art"), + { "Reference::Art", + NULL} }, + { "biography", NC_("Menu of Reference", "Biography"), + { "Reference::Biography", + NULL} }, + { "comics", NC_("Menu of Reference", "Comics"), + { "Reference::Comics", + NULL} }, + { "fiction", NC_("Menu of Reference", "Fiction"), + { "Reference::Fiction", + NULL} }, + { "health", NC_("Menu of Reference", "Health"), + { "Reference::Health", + NULL} }, + { "history", NC_("Menu of Reference", "History"), + { "Reference::History", + NULL} }, + { "lifestyle", NC_("Menu of Reference", "Lifestyle"), + { "Reference::Lifestyle", + NULL} }, + { "politics", NC_("Menu of Reference", "Politics"), + { "Reference::Politics", + NULL} }, + { "sports", NC_("Menu of Reference", "Sports"), + { "Reference::Sports", + NULL} }, + { NULL } +}; + +static const GsDesktopMap map_develop[] = { + { "all", NC_("Menu of Developer Tools", "All"), + { "Development", + NULL } }, + { "featured", NC_("Menu of Developer Tools", "Featured"), + { "Development::Featured", + NULL} }, + { "debuggers", NC_("Menu of Developer Tools", "Debuggers"), + { "Development::Debugger", + NULL} }, + { "ide", NC_("Menu of Developer Tools", "IDEs"), + { "Development::IDE", + "Development::GUIDesigner", + NULL} }, + { NULL } +}; + +static const GsDesktopMap map_addon_codecs[] = { + { "all", NC_("Menu of Add-ons", "Codecs"), + { "Addon::Codec", + NULL } }, + { NULL } +}; + +static const GsDesktopMap map_addon_drivers[] = { + { "all", NC_("Menu of Add-ons", "Hardware Drivers"), + { "Addon::Driver", + NULL } }, + { NULL } +}; + +static const GsDesktopMap map_addon_fonts[] = { + { "all", NC_("Menu of Add-ons", "Fonts"), + { "Addon::Font", + NULL } }, + { NULL } +}; + +static const GsDesktopMap map_addon_input_sources[] = { + { "all", NC_("Menu of Add-ons", "Input Sources"), + { "Addon::InputSource", + NULL } }, + { NULL } +}; + +static const GsDesktopMap map_addon_language_packs[] = { + { "all", NC_("Menu of Add-ons", "Language Packs"), + { "Addon::LanguagePack", + NULL } }, + { NULL } +}; + +static const GsDesktopMap map_addon_localization[] = { + { "all", NC_("Menu of Add-ons", "Localization"), + { "Addon::Localization", + NULL } }, + { NULL } +}; + +/* main categories */ +/* Please keep category name and subcategory context synchronized!!! */ +static const GsDesktopData msdata[] = { + /* Translators: this is a menu category */ + { "create", map_create, N_("Create"), "org.gnome.Software.Create", 100 }, + /* Translators: this is a menu category */ + { "work", map_work, N_("Work"), "org.gnome.Software.Work", 90 }, + /* Translators: this is a menu category */ + { "play", map_play, N_("Play"), "org.gnome.Software.Play", 80 }, + /* Translators: this is a menu category */ + { "socialize", map_socialize, N_("Socialize"), "org.gnome.Software.Socialize", 70 }, + /* Translators: this is a menu category */ + { "learn", map_learn, N_("Learn"), "org.gnome.Software.Learn", 60 }, + /* Translators: this is a menu category */ + { "develop", map_develop, N_("Develop"), "org.gnome.Software.Develop", 50 }, + + /* Translators: this is a menu category */ + { "codecs", map_addon_codecs, N_("Codecs"), NULL, 10 }, + /* Translators: this is a menu category */ + { "drivers", map_addon_drivers, N_("Hardware Drivers"), NULL, 10 }, + /* Translators: this is a menu category */ + { "fonts", map_addon_fonts, N_("Fonts"), NULL, 10 }, + /* Translators: this is a menu category */ + { "input-sources", map_addon_input_sources, N_("Input Sources"), NULL, 10 }, + /* Translators: this is a menu category */ + { "language-packs", map_addon_language_packs, N_("Language Packs"), NULL, 10 }, + /* Translators: this is a menu category */ + { "localization", map_addon_localization, N_("Localization"), NULL, 10 }, + + { NULL } +}; + +/* the -1 is for the NULL terminator */ +G_STATIC_ASSERT (G_N_ELEMENTS (msdata) - 1 == GS_DESKTOP_DATA_N_ENTRIES); + +const GsDesktopData * +gs_desktop_get_data (void) +{ + return msdata; +} diff --git a/lib/gs-desktop-data.h b/lib/gs-desktop-data.h new file mode 100644 index 0000000..049656a --- /dev/null +++ b/lib/gs-desktop-data.h @@ -0,0 +1,43 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2011-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct { + const gchar *id; + const gchar *name; + const gchar *fdo_cats[16]; +} GsDesktopMap; + +typedef struct { + const gchar *id; + const GsDesktopMap *mapping; + const gchar *name; + const gchar *icon; + gint score; +} GsDesktopData; + +const GsDesktopData *gs_desktop_get_data (void); + +/** + * GS_DESKTOP_DATA_N_ENTRIES: + * + * Number of entries in the array returned by gs_desktop_get_data(). This is + * static and guaranteed to be up to date. It’s intended to be used when + * defining static arrays which need to be the same size as the array returned + * by gs_desktop_get_data(). + * + * Since: 40 + */ +#define GS_DESKTOP_DATA_N_ENTRIES 12 + +G_END_DECLS diff --git a/lib/gs-download-utils.c b/lib/gs-download-utils.c new file mode 100644 index 0000000..4949064 --- /dev/null +++ b/lib/gs-download-utils.c @@ -0,0 +1,870 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021, 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-download-utils + * @short_description: Download and HTTP utilities + * + * A set of utilities for downloading things and doing HTTP requests. + * + * Since: 42 + */ + +#include "config.h" + +#include <gio/gio.h> +#include <glib.h> +#include <glib-object.h> +#include <glib/gi18n.h> +#include <libsoup/soup.h> + +#include "gs-download-utils.h" +#include "gs-utils.h" + +G_DEFINE_QUARK (gs-download-error-quark, gs_download_error) + +/** + * gs_build_soup_session: + * + * Build a new #SoupSession configured with the gnome-software user agent. + * + * A new #SoupSession should be used for each independent download context, such + * as in different plugins. Each #SoupSession caches HTTP connections and + * authentication information, and these likely needn’t be shared between + * plugins. Using separate sessions reduces thread contention. + * + * Returns: (transfer full): a new #SoupSession + * Since: 42 + */ +SoupSession * +gs_build_soup_session (void) +{ + return soup_session_new_with_options ("user-agent", gs_user_agent (), + "timeout", 10, + NULL); +} + +/* See https://httpwg.org/specs/rfc7231.html#http.date + * For example: Sun, 06 Nov 1994 08:49:37 GMT */ +static gchar * +date_time_to_rfc7231 (GDateTime *date_time) +{ +#if SOUP_CHECK_VERSION(3, 0, 0) + return soup_date_time_to_string (date_time, SOUP_DATE_HTTP); +#else + const gchar *day_names[] = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }; + const gchar *month_names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + /* We can’t just use g_date_time_format() here because its output + * (particularly day and month names) is locale-dependent. + * #SoupDate is also a pain to use because there’s no easy way to + * convert from a #GDateTime with libsoup-2.4, while preserving the timezone. */ + g_autofree gchar *time_str = g_date_time_format (date_time, "%H:%M:%S %Z"); + + return g_strdup_printf ("%s, %02d %s %d %s", + day_names[g_date_time_get_day_of_week (date_time) - 1], + g_date_time_get_day_of_month (date_time), + month_names[g_date_time_get_month (date_time) - 1], + g_date_time_get_year (date_time), + time_str); +#endif +} + +static GDateTime * +date_time_from_rfc7231 (const gchar *rfc7231_str) +{ +#if SOUP_CHECK_VERSION(3, 0, 0) + return soup_date_time_new_from_http_string (rfc7231_str); +#else + g_autoptr(SoupDate) soup_date = NULL; + g_autoptr(GTimeZone) tz = NULL; + + soup_date = soup_date_new_from_string (rfc7231_str); + if (soup_date == NULL) + return NULL; + + if (soup_date->utc) + tz = g_time_zone_new_utc (); + else + tz = g_time_zone_new_offset (soup_date->offset * 60); + + return g_date_time_new (tz, soup_date->year, soup_date->month, + soup_date->day, soup_date->hour, + soup_date->minute, soup_date->second); +#endif +} + +typedef struct { + /* Input data. */ + gchar *uri; /* (not nullable) (owned) */ + GInputStream *input_stream; /* (nullable) (owned) */ + GOutputStream *output_stream; /* (nullable) (owned) */ + gsize buffer_size_bytes; + gchar *last_etag; /* (nullable) (owned) */ + GDateTime *last_modified_date; /* (nullable) (owned) */ + int io_priority; + GsDownloadProgressCallback progress_callback; /* (nullable) */ + gpointer progress_user_data; + + /* In-progress state. */ + SoupMessage *message; /* (nullable) (owned) */ + gboolean close_input_stream; + gboolean close_output_stream; + gboolean discard_output_stream; + gsize total_read_bytes; + gsize total_written_bytes; + gsize expected_stream_size_bytes; + GBytes *currently_unwritten_chunk; /* (nullable) (owned) */ + + /* Output data. */ + gchar *new_etag; /* (nullable) (owned) */ + GDateTime *new_last_modified_date; /* (nullable) (owned) */ + GError *error; /* (nullable) (owned) */ +} DownloadData; + +static void +download_data_free (DownloadData *data) +{ + g_assert (data->input_stream == NULL || g_input_stream_is_closed (data->input_stream)); + g_assert (data->output_stream == NULL || g_output_stream_is_closed (data->output_stream)); + + g_assert (data->currently_unwritten_chunk == NULL || data->error != NULL); + + g_clear_object (&data->input_stream); + g_clear_object (&data->output_stream); + + g_clear_pointer (&data->last_etag, g_free); + g_clear_pointer (&data->last_modified_date, g_date_time_unref); + g_clear_object (&data->message); + g_clear_pointer (&data->uri, g_free); + g_clear_pointer (&data->new_etag, g_free); + g_clear_pointer (&data->new_last_modified_date, g_date_time_unref); + g_clear_pointer (&data->currently_unwritten_chunk, g_bytes_unref); + g_clear_error (&data->error); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DownloadData, download_data_free) + +static void open_input_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void read_bytes_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void write_bytes_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_download (GTask *task, + GError *error); +static void close_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void download_progress (GTask *task); + +/** + * gs_download_stream_async: + * @soup_session: a #SoupSession + * @uri: (not nullable): the URI to download + * @output_stream: (not nullable): an output stream to write the download to + * @last_etag: (nullable): the last-known ETag of the URI, or %NULL if unknown + * @last_modified_date: (nullable): the last-known Last-Modified date of the + * URI, or %NULL if unknown + * @io_priority: I/O priority to download and write at + * @progress_callback: (nullable): callback to call with progress information + * @progress_user_data: (nullable) (closure progress_callback): data to pass + * to @progress_callback + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: callback to call once the operation is complete + * @user_data: (closure callback): data to pass to @callback + * + * Download @uri and write it to @output_stream asynchronously. + * + * If @last_etag is non-%NULL or @last_modified_date is non-%NULL, they will be + * sent to the server, which may return a ‘not modified’ response. If so, + * @output_stream will not be written to, and will be closed with a cancelled + * close operation. This will ensure that the existing content of the output + * stream (if it’s a file, for example) will not be overwritten. + * + * Note that @last_etag must be the ETag value returned by the server last time + * the file was downloaded, not the local file ETag generated by GLib. + * + * If specified, @progress_callback will be called zero or more times until + * @callback is called, providing progress updates on the download. + * + * Since: 43 + */ +void +gs_download_stream_async (SoupSession *soup_session, + const gchar *uri, + GOutputStream *output_stream, + const gchar *last_etag, + GDateTime *last_modified_date, + int io_priority, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(SoupMessage) msg = NULL; + DownloadData *data; + g_autoptr(DownloadData) data_owned = NULL; + + g_return_if_fail (SOUP_IS_SESSION (soup_session)); + g_return_if_fail (uri != NULL); + g_return_if_fail (G_IS_OUTPUT_STREAM (output_stream)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (soup_session, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_download_stream_async); + + data = data_owned = g_new0 (DownloadData, 1); + data->uri = g_strdup (uri); + data->output_stream = g_object_ref (output_stream); + data->close_output_stream = TRUE; + data->buffer_size_bytes = 8192; /* arbitrarily chosen */ + data->io_priority = io_priority; + data->progress_callback = progress_callback; + data->progress_user_data = progress_user_data; + + g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) download_data_free); + + /* local */ + if (g_str_has_prefix (uri, "file://")) { + g_autoptr(GFile) local_file = g_file_new_for_path (uri + strlen ("file://")); + g_file_read_async (local_file, io_priority, cancellable, open_input_stream_cb, g_steal_pointer (&task)); + return; + } + + /* remote */ + g_debug ("Downloading %s to %s", uri, G_OBJECT_TYPE_NAME (output_stream)); + msg = soup_message_new (SOUP_METHOD_GET, uri); + if (msg == NULL) { + finish_download (task, + g_error_new (G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "Failed to parse URI ‘%s’", uri)); + return; + } + + data->message = g_object_ref (msg); + + /* Caching support. Prefer ETags to modification dates, as the latter + * have problems with rapid updates and clock drift. */ + if (last_etag != NULL && *last_etag == '\0') + last_etag = NULL; + data->last_etag = g_strdup (last_etag); + + if (last_modified_date != NULL) + data->last_modified_date = g_date_time_ref (last_modified_date); + + if (last_etag != NULL) { +#if SOUP_CHECK_VERSION(3, 0, 0) + soup_message_headers_append (soup_message_get_request_headers (msg), "If-None-Match", last_etag); +#else + soup_message_headers_append (msg->request_headers, "If-None-Match", last_etag); +#endif + } else if (last_modified_date != NULL) { + g_autofree gchar *last_modified_date_str = date_time_to_rfc7231 (last_modified_date); +#if SOUP_CHECK_VERSION(3, 0, 0) + soup_message_headers_append (soup_message_get_request_headers (msg), "If-Modified-Since", last_modified_date_str); +#else + soup_message_headers_append (msg->request_headers, "If-Modified-Since", last_modified_date_str); +#endif + } + +#if SOUP_CHECK_VERSION(3, 0, 0) + soup_session_send_async (soup_session, msg, data->io_priority, cancellable, open_input_stream_cb, g_steal_pointer (&task)); +#else + soup_session_send_async (soup_session, msg, cancellable, open_input_stream_cb, g_steal_pointer (&task)); +#endif +} + +static void +open_input_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + DownloadData *data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GInputStream) input_stream = NULL; + g_autoptr(GError) local_error = NULL; + + /* This function can be called as a result of either reading a local + * file, or sending an HTTP request, so @source_object’s type can vary. */ + if (G_IS_FILE (source_object)) { + GFile *local_file = G_FILE (source_object); + + /* Local file. */ + input_stream = G_INPUT_STREAM (g_file_read_finish (local_file, result, &local_error)); + + if (input_stream == NULL) { + g_prefix_error (&local_error, "Failed to read ‘%s’: ", + g_file_peek_path (local_file)); + finish_download (task, g_steal_pointer (&local_error)); + return; + } + + g_assert (data->input_stream == NULL); + data->input_stream = g_object_ref (input_stream); + data->close_input_stream = TRUE; + } else if (SOUP_IS_SESSION (source_object)) { + SoupSession *soup_session = SOUP_SESSION (source_object); + guint status_code; + const gchar *new_etag, *new_last_modified_str; + + /* HTTP request. */ +#if SOUP_CHECK_VERSION(3, 0, 0) + input_stream = soup_session_send_finish (soup_session, result, &local_error); + status_code = soup_message_get_status (data->message); +#else + input_stream = soup_session_send_finish (soup_session, result, &local_error); + status_code = data->message->status_code; +#endif + + if (input_stream != NULL) { + g_assert (data->input_stream == NULL); + data->input_stream = g_object_ref (input_stream); + data->close_input_stream = TRUE; + } + + if (status_code == SOUP_STATUS_NOT_MODIFIED) { + /* If the file has not been modified from the ETag or + * Last-Modified date we have, finish the download + * early. Ensure to close the output stream so that its + * existing content is *not* overwritten. + * + * Preserve the existing ETag. */ + data->discard_output_stream = TRUE; + data->new_etag = g_strdup (data->last_etag); + data->new_last_modified_date = (data->last_modified_date != NULL) ? g_date_time_ref (data->last_modified_date) : NULL; + finish_download (task, + g_error_new (GS_DOWNLOAD_ERROR, + GS_DOWNLOAD_ERROR_NOT_MODIFIED, + "Skipped downloading ‘%s’: %s", + data->uri, soup_status_get_phrase (status_code))); + return; + } else 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 (local_error != NULL) { + g_string_append (str, ": "); + g_string_append (str, local_error->message); + } + + finish_download (task, + g_error_new (G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to download ‘%s’: %s", + data->uri, str->str)); + return; + } + + g_assert (input_stream != NULL); + + /* Get the expected download size. */ +#if SOUP_CHECK_VERSION(3, 0, 0) + data->expected_stream_size_bytes = soup_message_headers_get_content_length (soup_message_get_response_headers (data->message)); +#else + data->expected_stream_size_bytes = soup_message_headers_get_content_length (data->message->response_headers); +#endif + + /* Store the new ETag for later use. */ +#if SOUP_CHECK_VERSION(3, 0, 0) + new_etag = soup_message_headers_get_one (soup_message_get_response_headers (data->message), "ETag"); +#else + new_etag = soup_message_headers_get_one (data->message->response_headers, "ETag"); +#endif + if (new_etag != NULL && *new_etag == '\0') + new_etag = NULL; + data->new_etag = g_strdup (new_etag); + + /* Store the Last-Modified date for later use. */ +#if SOUP_CHECK_VERSION(3, 0, 0) + new_last_modified_str = soup_message_headers_get_one (soup_message_get_response_headers (data->message), "Last-Modified"); +#else + new_last_modified_str = soup_message_headers_get_one (data->message->response_headers, "Last-Modified"); +#endif + if (new_last_modified_str != NULL && *new_last_modified_str == '\0') + new_last_modified_str = NULL; + if (new_last_modified_str != NULL) + data->new_last_modified_date = date_time_from_rfc7231 (new_last_modified_str); + } else { + g_assert_not_reached (); + } + + /* Splice in an asynchronous loop. We unfortunately can’t use + * g_output_stream_splice_async() here, as it doesn’t provide a progress + * callback. The approach is the same though. */ + g_input_stream_read_bytes_async (input_stream, data->buffer_size_bytes, data->io_priority, + cancellable, read_bytes_cb, g_steal_pointer (&task)); +} + +static void +read_bytes_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GInputStream *input_stream = G_INPUT_STREAM (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + DownloadData *data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GError) local_error = NULL; + + bytes = g_input_stream_read_bytes_finish (input_stream, result, &local_error); + + if (bytes == NULL) { + finish_download (task, g_steal_pointer (&local_error)); + return; + } + + /* Report progress. */ + data->total_read_bytes += g_bytes_get_size (bytes); + data->expected_stream_size_bytes = MAX (data->expected_stream_size_bytes, data->total_read_bytes); + download_progress (task); + + /* Write the downloaded data. */ + if (g_bytes_get_size (bytes) > 0) { + g_clear_pointer (&data->currently_unwritten_chunk, g_bytes_unref); + data->currently_unwritten_chunk = g_bytes_ref (bytes); + + g_output_stream_write_bytes_async (data->output_stream, bytes, data->io_priority, + cancellable, write_bytes_cb, g_steal_pointer (&task)); + } else { + finish_download (task, NULL); + } +} + +static void +write_bytes_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GOutputStream *output_stream = G_OUTPUT_STREAM (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + DownloadData *data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + gssize bytes_written_signed; + gsize bytes_written; + g_autoptr(GError) local_error = NULL; + + bytes_written_signed = g_output_stream_write_bytes_finish (output_stream, result, &local_error); + + if (bytes_written_signed < 0) { + finish_download (task, g_steal_pointer (&local_error)); + return; + } + + /* We know this is non-negative now. */ + bytes_written = (gsize) bytes_written_signed; + + /* Report progress. */ + data->total_written_bytes += bytes_written; + download_progress (task); + + g_assert (data->currently_unwritten_chunk != NULL); + + if (bytes_written < g_bytes_get_size (data->currently_unwritten_chunk)) { + /* Partial write; try again with the remaining bytes. */ + g_autoptr(GBytes) sub_bytes = g_bytes_new_from_bytes (data->currently_unwritten_chunk, bytes_written, g_bytes_get_size (data->currently_unwritten_chunk) - bytes_written); + g_assert (bytes_written > 0); + + g_clear_pointer (&data->currently_unwritten_chunk, g_bytes_unref); + data->currently_unwritten_chunk = g_bytes_ref (sub_bytes); + + g_output_stream_write_bytes_async (output_stream, sub_bytes, data->io_priority, + cancellable, write_bytes_cb, g_steal_pointer (&task)); + } else { + /* Full write succeeded. Start the next read. */ + g_clear_pointer (&data->currently_unwritten_chunk, g_bytes_unref); + + g_input_stream_read_bytes_async (data->input_stream, data->buffer_size_bytes, data->io_priority, + cancellable, read_bytes_cb, g_steal_pointer (&task)); + } +} + +static inline gboolean +is_not_modidifed_error (GError *error) +{ + return g_error_matches (error, GS_DOWNLOAD_ERROR, GS_DOWNLOAD_ERROR_NOT_MODIFIED); +} + +/* error is (transfer full) */ +static void +finish_download (GTask *task, + GError *error) +{ + DownloadData *data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + + /* Final progress update. */ + if (error == NULL || is_not_modidifed_error (error)) { + data->expected_stream_size_bytes = data->total_read_bytes; + download_progress (task); + } + + /* Record the error from the operation, if set. */ + g_assert (data->error == NULL); + data->error = g_steal_pointer (&error); + + g_assert (!data->discard_output_stream || data->close_output_stream); + + if (data->close_output_stream) { + g_autoptr(GCancellable) output_cancellable = NULL; + + g_assert (data->output_stream != NULL); + + /* If there’s been a prior error, or we are aborting writing the + * output stream (perhaps because of a cache hit), close the + * output stream but cancel the close operation so that the old + * output file is not overwritten. */ + if ((data->error != NULL && !is_not_modidifed_error (data->error)) || data->discard_output_stream) { + output_cancellable = g_cancellable_new (); + g_cancellable_cancel (output_cancellable); + } else if (g_task_get_cancellable (task) != NULL) { + output_cancellable = g_object_ref (g_task_get_cancellable (task)); + } + + g_output_stream_close_async (data->output_stream, data->io_priority, output_cancellable, close_stream_cb, g_object_ref (task)); + } + + if (data->close_input_stream && data->input_stream != NULL) { + g_input_stream_close_async (data->input_stream, data->io_priority, cancellable, close_stream_cb, g_object_ref (task)); + } + + /* Check in case both streams are already closed. */ + close_stream_cb (NULL, NULL, g_object_ref (task)); +} + +static void +close_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + DownloadData *data = g_task_get_task_data (task); + g_autoptr(GError) local_error = NULL; + + if (G_IS_INPUT_STREAM (source_object)) { + /* Errors in closing the input stream are not fatal. */ + if (!g_input_stream_close_finish (G_INPUT_STREAM (source_object), + result, &local_error)) + g_debug ("Error closing input stream: %s", local_error->message); + g_clear_error (&local_error); + + data->close_input_stream = FALSE; + } else if (G_IS_OUTPUT_STREAM (source_object)) { + /* Errors in closing the output stream are fatal, but don’t + * overwrite errors set earlier in the operation. */ + if (!g_output_stream_close_finish (G_OUTPUT_STREAM (source_object), + result, &local_error)) { + /* If we are aborting writing the output stream (perhaps + * because of a cache hit), don’t report the error at + * all. */ + if (data->discard_output_stream && + g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_clear_error (&local_error); + else if (data->error == NULL) + data->error = g_steal_pointer (&local_error); + else if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Error closing output stream: %s", local_error->message); + } + g_clear_error (&local_error); + + data->close_output_stream = FALSE; + data->discard_output_stream = FALSE; + } else { + /* finish_download() calls this with a NULL source_object */ + } + + /* Still waiting for one of the streams to close? */ + if (data->close_input_stream || data->close_output_stream) + return; + + if (data->error != NULL) { + g_task_return_error (task, g_error_copy (data->error)); + } else { + g_task_return_boolean (task, TRUE); + } +} + +static void +download_progress (GTask *task) +{ + DownloadData *data = g_task_get_task_data (task); + + if (data->progress_callback != NULL) { + /* This should be guaranteed by the rest of the download code. */ + g_assert (data->expected_stream_size_bytes >= data->total_written_bytes); + + data->progress_callback (data->total_written_bytes, data->expected_stream_size_bytes, + data->progress_user_data); + } +} + +/** + * gs_download_stream_finish: + * @soup_session: a #SoupSession + * @result: result of the asynchronous operation + * @new_etag_out: (out callee-allocates) (transfer full) (optional) (nullable): + * return location for the ETag of the downloaded file (which may be %NULL), + * or %NULL to ignore it + * @new_last_modified_date_out: (out callee-allocates) (transfer full) (optional) (nullable): + * return location for the new Last-Modified date of the downloaded file + * (which may be %NULL), or %NULL to ignore it + * @error: return location for a #GError + * + * Finish an asynchronous download operation started with + * gs_download_stream_async(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 43 + */ +gboolean +gs_download_stream_finish (SoupSession *soup_session, + GAsyncResult *result, + gchar **new_etag_out, + GDateTime **new_last_modified_date_out, + GError **error) +{ + DownloadData *data; + + g_return_val_if_fail (g_task_is_valid (result, soup_session), FALSE); + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_download_stream_async, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + data = g_task_get_task_data (G_TASK (result)); + + if (new_etag_out != NULL) + *new_etag_out = g_strdup (data->new_etag); + if (new_last_modified_date_out != NULL) + *new_last_modified_date_out = (data->new_last_modified_date != NULL) ? g_date_time_ref (data->new_last_modified_date) : NULL; + + return g_task_propagate_boolean (G_TASK (result), error); +} + +typedef struct { + /* Input data. */ + gchar *uri; /* (not nullable) (owned) */ + GFile *output_file; /* (not nullable) (owned) */ + int io_priority; + GsDownloadProgressCallback progress_callback; + gpointer progress_user_data; + + /* In-progress data. */ + gchar *last_etag; /* (nullable) (owned) */ + GDateTime *last_modified_date; /* (nullable) (owned) */ +} DownloadFileData; + +static void +download_file_data_free (DownloadFileData *data) +{ + g_free (data->uri); + g_clear_object (&data->output_file); + g_free (data->last_etag); + g_clear_pointer (&data->last_modified_date, g_date_time_unref); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DownloadFileData, download_file_data_free) + +static void download_replace_file_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void download_file_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/** + * gs_download_file_async: + * @soup_session: a #SoupSession + * @uri: (not nullable): the URI to download + * @output_file: (not nullable): an output file to write the download to + * @io_priority: I/O priority to download and write at + * @progress_callback: (nullable): callback to call with progress information + * @progress_user_data: (nullable) (closure progress_callback): data to pass + * to @progress_callback + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: callback to call once the operation is complete + * @user_data: (closure callback): data to pass to @callback + * + * Download @uri and write it to @output_file asynchronously, overwriting the + * existing content of @output_file. + * + * The ETag and modification time of @output_file will be queried and, if known, + * used to skip the download if @output_file is already up to date. + * + * If specified, @progress_callback will be called zero or more times until + * @callback is called, providing progress updates on the download. + * + * Since: 42 + */ +void +gs_download_file_async (SoupSession *soup_session, + const gchar *uri, + GFile *output_file, + int io_priority, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + DownloadFileData *data; + g_autoptr(DownloadFileData) data_owned = NULL; + g_autoptr(GFile) output_file_parent = NULL; + g_autoptr(GError) local_error = NULL; + + g_return_if_fail (SOUP_IS_SESSION (soup_session)); + g_return_if_fail (uri != NULL); + g_return_if_fail (G_IS_FILE (output_file)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (soup_session, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_download_file_async); + + data = data_owned = g_new0 (DownloadFileData, 1); + data->uri = g_strdup (uri); + data->output_file = g_object_ref (output_file); + data->io_priority = io_priority; + data->progress_callback = progress_callback; + data->progress_user_data = progress_user_data; + g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) download_file_data_free); + + /* Create the destination file’s directory. + * FIXME: This should be made async; it hasn’t done for now as it’s + * likely to be fast. */ + output_file_parent = g_file_get_parent (output_file); + + if (output_file_parent != NULL && + !g_file_make_directory_with_parents (output_file_parent, cancellable, &local_error) && + !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_clear_error (&local_error); + + /* Query the old ETag and modification date if the file already exists. */ + data->last_etag = gs_utils_get_file_etag (output_file, &data->last_modified_date, cancellable); + + /* Create the output file. + * + * Note that `data->last_etag` is *not* passed in here, as the ETag from + * the server and the file modification ETag that GLib uses are + * different things. For g_file_replace_async(), GLib always uses an + * ETag it generates internally based on the file mtime (see + * _g_local_file_info_create_etag()), which will never match what the + * server returns in its ETag header. + * + * This is fine, as we are using the ETag to avoid an unnecessary HTTP + * download if possible. We don’t care about tracking changes to the + * file on disk. */ + g_file_replace_async (output_file, + NULL, /* ETag */ + FALSE, /* make_backup */ + G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION, + io_priority, + cancellable, + download_replace_file_cb, + g_steal_pointer (&task)); +} + +static void +download_replace_file_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GFile *output_file = G_FILE (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + SoupSession *soup_session = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + DownloadFileData *data = g_task_get_task_data (task); + g_autoptr(GFileOutputStream) output_stream = NULL; + g_autoptr(GError) local_error = NULL; + + output_stream = g_file_replace_finish (output_file, result, &local_error); + + if (output_stream == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Do the download. */ + gs_download_stream_async (soup_session, data->uri, G_OUTPUT_STREAM (output_stream), + data->last_etag, data->last_modified_date, data->io_priority, + data->progress_callback, data->progress_user_data, + cancellable, download_file_cb, g_steal_pointer (&task)); +} + +static void +download_file_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SoupSession *soup_session = SOUP_SESSION (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GCancellable *cancellable = g_task_get_cancellable (task); + DownloadFileData *data = g_task_get_task_data (task); + g_autofree gchar *new_etag = NULL; + g_autoptr(GError) local_error = NULL; + + if (!gs_download_stream_finish (soup_session, result, &new_etag, NULL, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Update the stored HTTP ETag. + * + * Under the assumption that this code is only ever used for locally + * cached copies of remote files (i.e. the local copies are never + * modified except by downloading an updated version from the server), + * it’s safe to use the local file modification date for Last-Modified, + * and save having to update that explicitly. This is because the + * modification time of the local file equals when gnome-software last + * checked for updates to it — which is correct to send as the + * If-Modified-Since the next time gnome-software checks for updates to + * the file. */ + gs_utils_set_file_etag (data->output_file, new_etag, cancellable); + + g_task_return_boolean (task, TRUE); +} + +/** + * gs_download_file_finish: + * @soup_session: a #SoupSession + * @result: result of the asynchronous operation + * @error: return location for a #GError + * + * Finish an asynchronous download operation started with + * gs_download_file_async(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_download_file_finish (SoupSession *soup_session, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, soup_session), FALSE); + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_download_file_async, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/lib/gs-download-utils.h b/lib/gs-download-utils.h new file mode 100644 index 0000000..cd9dd46 --- /dev/null +++ b/lib/gs-download-utils.h @@ -0,0 +1,88 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> +#include <libsoup/soup.h> + +G_BEGIN_DECLS + +SoupSession *gs_build_soup_session (void); + +/** + * GsDownloadProgressCallback: + * @bytes_downloaded: number of bytes downloaded so far + * @total_download_size: the total size of the download, in bytes + * @user_data: data passed to the calling function + * + * A progress callback to indicate how far a download has progressed. + * + * @total_download_size may be zero (for example, at the start of the download), + * so implementations of this callback must be careful to avoid division by zero + * errors. + * + * @total_download_size is guaranteed to always be greater than or equal to + * @bytes_downloaded. + * + * Since: 42 + */ +typedef void (*GsDownloadProgressCallback) (gsize bytes_downloaded, + gsize total_download_size, + gpointer user_data); + +/** + * GsExternalAppstreamError: + * @GS_DOWNLOAD_ERROR_NOT_MODIFIED: The ETag matches that of the server file. + * + * Error codes for download operations. + * + * Since: 44 + */ +typedef enum { + GS_DOWNLOAD_ERROR_NOT_MODIFIED, +} GsDownloadError; + +#define GS_DOWNLOAD_ERROR gs_download_error_quark () +GQuark gs_download_error_quark (void); + +void gs_download_stream_async (SoupSession *soup_session, + const gchar *uri, + GOutputStream *output_stream, + const gchar *last_etag, + GDateTime *last_modified_date, + int io_priority, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_download_stream_finish (SoupSession *soup_session, + GAsyncResult *result, + gchar **new_etag_out, + GDateTime **new_last_modified_date_out, + GError **error); + +void gs_download_file_async (SoupSession *soup_session, + const gchar *uri, + GFile *output_file, + int io_priority, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_download_file_finish (SoupSession *soup_session, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/lib/gs-external-appstream-utils.c b/lib/gs-external-appstream-utils.c new file mode 100644 index 0000000..24ce3a8 --- /dev/null +++ b/lib/gs-external-appstream-utils.c @@ -0,0 +1,627 @@ + /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Endless Mobile, Inc. + * + * Authors: Joaquim Rocha <jrocha@endlessm.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/* + * SECTION:gs-external-appstream-urls + * @short_description: Provides support for downloading external AppStream files. + * + * This downloads the set of configured external AppStream files, and caches + * them locally. + * + * According to the `external-appstream-system-wide` GSetting, the files will + * either be downloaded to a per-user cache, or to a system-wide cache. In the + * case of a system-wide cache, they are downloaded to a temporary file writable + * by the user, and then the suexec binary `gnome-software-install-appstream` is + * run to copy them to the system location. + * + * All the downloads are done in the default #GMainContext for the thread which + * calls gs_external_appstream_refresh_async(). They are done in parallel and + * the async refresh function will only complete once the last download is + * complete. + * + * Progress data is reported via a callback, and gives the total progress of all + * parallel downloads. Internally this is done by updating #ProgressTuple + * structs as each download progresses. A periodic timeout callback sums these + * and reports the total progress to the caller. That means that progress + * reports from gs_external_appstream_refresh_async() are done at a constant + * frequency. + * + * To test this code locally you will probably want to change your GSettings + * configuration to add some external AppStream URIs: + * ``` + * gsettings set org.gnome.software external-appstream-urls '["https://example.com/appdata.xml.gz"]' + * ``` + * + * When you are done with development, run the following command to use the real + * external AppStream list again: + * ``` + * gsettings reset org.gnome.software external-appstream-urls + * ``` + * + * Since: 42 + */ + +#include <errno.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <libsoup/soup.h> + +#include "gs-external-appstream-utils.h" + +#define APPSTREAM_SYSTEM_DIR LOCALSTATEDIR "/cache/swcatalog/xml" + +G_DEFINE_QUARK (gs-external-appstream-error-quark, gs_external_appstream_error) + +gchar * +gs_external_appstream_utils_get_file_cache_path (const gchar *file_name) +{ + g_autofree gchar *prefixed_file_name = g_strdup_printf (EXTERNAL_APPSTREAM_PREFIX "-%s", + file_name); + return g_build_filename (APPSTREAM_SYSTEM_DIR, prefixed_file_name, NULL); +} + +/* To be able to delete old files, when the path changed */ +gchar * +gs_external_appstream_utils_get_legacy_file_cache_path (const gchar *file_name) +{ + g_autofree gchar *prefixed_file_name = g_strdup_printf (EXTERNAL_APPSTREAM_PREFIX "-%s", + file_name); + return g_build_filename (LOCALSTATEDIR "/cache/app-info/xmls", prefixed_file_name, NULL); +} + +const gchar * +gs_external_appstream_utils_get_system_dir (void) +{ + return APPSTREAM_SYSTEM_DIR; +} + +static gboolean +gs_external_appstream_check (GFile *appstream_file, + guint64 cache_age_secs) +{ + guint64 appstream_file_age = gs_utils_get_file_age (appstream_file); + return appstream_file_age >= cache_age_secs; +} + +static gboolean +gs_external_appstream_install (const gchar *appstream_file, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GSubprocess) subprocess = NULL; + const gchar *argv[] = { "pkexec", + LIBEXECDIR "/gnome-software-install-appstream", + appstream_file, NULL}; + g_debug ("Installing the appstream file %s in the system", + appstream_file); + subprocess = g_subprocess_newv (argv, + G_SUBPROCESS_FLAGS_STDOUT_PIPE | + G_SUBPROCESS_FLAGS_STDIN_PIPE, error); + if (subprocess == NULL) + return FALSE; + return g_subprocess_wait_check (subprocess, cancellable, error); +} + +static void download_replace_file_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void download_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/* A tuple to store the last-received progress data for a single download. + * Each download (refresh_url_async()) has a pointer to the relevant + * #ProgressTuple for its download. These are stored in an array in #RefreshData + * and a timeout callback periodically sums them all and reports progress to the + * caller. */ +typedef struct { + gsize bytes_downloaded; + gsize total_download_size; +} ProgressTuple; + +typedef struct { + /* Input data. */ + gchar *url; /* (not nullable) (owned) */ + GTask *task; /* (not nullable) (owned) */ + GFile *output_file; /* (not nullable) (owned) */ + ProgressTuple *progress_tuple; /* (not nullable) */ + SoupSession *soup_session; /* (not nullable) (owned) */ + gboolean system_wide; + + /* In-progress data. */ + gchar *last_etag; /* (nullable) (owned) */ + GDateTime *last_modified_date; /* (nullable) (owned) */ +} DownloadAppStreamData; + +static void +download_appstream_data_free (DownloadAppStreamData *data) +{ + g_free (data->url); + g_clear_object (&data->task); + g_clear_object (&data->output_file); + g_clear_object (&data->soup_session); + g_free (data->last_etag); + g_clear_pointer (&data->last_modified_date, g_date_time_unref); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DownloadAppStreamData, download_appstream_data_free) + +static void +refresh_url_progress_cb (gsize bytes_downloaded, + gsize total_download_size, + gpointer user_data) +{ + ProgressTuple *tuple = user_data; + + tuple->bytes_downloaded = bytes_downloaded; + tuple->total_download_size = total_download_size; + + /* The timeout callback in progress_cb() periodically sums these. No + * need to notify of progress from here. */ +} + +static void +refresh_url_async (GSettings *settings, + const gchar *url, + SoupSession *soup_session, + guint64 cache_age_secs, + ProgressTuple *progress_tuple, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autofree gchar *basename = NULL; + g_autofree gchar *basename_url = g_path_get_basename (url); + /* make sure different uris with same basenames differ */ + g_autofree gchar *hash = NULL; + g_autofree gchar *target_file_path = NULL; + g_autoptr(GFile) target_file = NULL; + g_autoptr(GFile) tmp_file_parent = NULL; + g_autoptr(GFile) tmp_file = NULL; + g_autoptr(GsApp) app_dl = gs_app_new ("external-appstream"); + g_autoptr(GError) local_error = NULL; + DownloadAppStreamData *data; + gboolean system_wide; + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, refresh_url_async); + + /* Calculate the basename of the target file. */ + hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, url, -1); + if (hash == NULL) { + g_task_return_new_error (task, + GS_EXTERNAL_APPSTREAM_ERROR, + GS_EXTERNAL_APPSTREAM_ERROR_DOWNLOADING, + "Failed to hash URI ‘%s’", url); + return; + } + basename = g_strdup_printf ("%s-%s", hash, basename_url); + + /* Are we downloading for the user, or the system? */ + system_wide = g_settings_get_boolean (settings, "external-appstream-system-wide"); + + /* Check cache file age. */ + if (system_wide) { + target_file_path = gs_external_appstream_utils_get_file_cache_path (basename); + } else { + g_autofree gchar *legacy_file_path = NULL; + + target_file_path = g_build_filename (g_get_user_data_dir (), + "swcatalog", + "xml", + basename, + NULL); + + /* Delete an old file, from a legacy location */ + legacy_file_path = g_build_filename (g_get_user_data_dir (), + "app-info", + "xmls", + basename, + NULL); + + if (g_unlink (legacy_file_path) == -1) { + int errn = errno; + if (errn != ENOENT) + g_debug ("Failed to unlink '%s': %s", legacy_file_path, g_strerror (errn)); + + } + } + + target_file = g_file_new_for_path (target_file_path); + + if (!gs_external_appstream_check (target_file, cache_age_secs)) { + g_debug ("skipping updating external appstream file %s: " + "cache age is older than file", + target_file_path); + g_task_return_boolean (task, TRUE); + return; + } + + /* If downloading system wide, write the download contents into a + * temporary file that will be copied into the system location later. */ + if (system_wide) { + g_autofree gchar *tmp_file_path = NULL; + + tmp_file_path = gs_utils_get_cache_filename ("external-appstream", + basename, + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + &local_error); + if (tmp_file_path == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + tmp_file = g_file_new_for_path (tmp_file_path); + } else { + tmp_file = g_object_ref (target_file); + } + + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading extra metadata files…")); + + data = g_new0 (DownloadAppStreamData, 1); + data->url = g_strdup (url); + data->task = g_object_ref (task); + data->output_file = g_object_ref (tmp_file); + data->progress_tuple = progress_tuple; + data->soup_session = g_object_ref (soup_session); + data->system_wide = system_wide; + g_task_set_task_data (task, data, (GDestroyNotify) download_appstream_data_free); + + /* Create the destination file’s directory. + * FIXME: This should be made async; it hasn’t done for now as it’s + * likely to be fast. */ + tmp_file_parent = g_file_get_parent (tmp_file); + + if (tmp_file_parent != NULL && + !g_file_make_directory_with_parents (tmp_file_parent, cancellable, &local_error) && + !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_clear_error (&local_error); + + /* Query the ETag and modification date of the target file, if the file already exists. For + * system-wide installations, this is the ETag of the AppStream file installed system-wide. + * For local installations, this is just the local output file. */ + data->last_etag = gs_utils_get_file_etag (target_file, &data->last_modified_date, cancellable); + g_debug ("Queried ETag of file %s: %s", g_file_peek_path (target_file), data->last_etag); + + /* Create the output file */ + g_file_replace_async (tmp_file, + NULL, /* ETag */ + FALSE, /* make_backup */ + G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION, + G_PRIORITY_LOW, + cancellable, + download_replace_file_cb, + g_steal_pointer (&task)); +} + +static void +download_replace_file_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GFile *output_file = G_FILE (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GCancellable *cancellable = g_task_get_cancellable (task); + DownloadAppStreamData *data = g_task_get_task_data (task); + g_autoptr(GFileOutputStream) output_stream = NULL; + g_autoptr(GError) local_error = NULL; + + output_stream = g_file_replace_finish (output_file, result, &local_error); + + if (output_stream == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Do the download. */ + gs_download_stream_async (data->soup_session, + data->url, + G_OUTPUT_STREAM (output_stream), + data->last_etag, + data->last_modified_date, + G_PRIORITY_LOW, + refresh_url_progress_cb, + data->progress_tuple, + cancellable, + download_stream_cb, + g_steal_pointer (&task)); +} + +static void +download_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SoupSession *soup_session = SOUP_SESSION (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GCancellable *cancellable = g_task_get_cancellable (task); + DownloadAppStreamData *data = g_task_get_task_data (task); + g_autoptr(GError) local_error = NULL; + g_autofree gchar *new_etag = NULL; + + if (!gs_download_stream_finish (soup_session, result, &new_etag, NULL, &local_error)) { + if (data->system_wide && g_error_matches (local_error, GS_DOWNLOAD_ERROR, GS_DOWNLOAD_ERROR_NOT_MODIFIED)) { + g_debug ("External AppStream file not modified, removing temporary download file %s", + g_file_peek_path (data->output_file)); + + /* System-wide installs should delete the empty file created when preparing to + * download the external AppStream file. */ + g_file_delete_async (data->output_file, G_PRIORITY_LOW, NULL, NULL, NULL); + g_task_return_boolean (task, TRUE); + } else if (!g_network_monitor_get_network_available (g_network_monitor_get_default ())) { + g_task_return_new_error (task, + GS_EXTERNAL_APPSTREAM_ERROR, + GS_EXTERNAL_APPSTREAM_ERROR_NO_NETWORK, + "External AppStream could not be downloaded due to being offline"); + } else { + g_task_return_new_error (task, + GS_EXTERNAL_APPSTREAM_ERROR, + GS_EXTERNAL_APPSTREAM_ERROR_DOWNLOADING, + "Server returned no data for external AppStream file: %s", + local_error->message); + } + return; + } + + g_debug ("Downloaded appstream file %s", g_file_peek_path (data->output_file)); + + gs_utils_set_file_etag (data->output_file, new_etag, cancellable); + + if (data->system_wide) { + /* install file systemwide */ + if (!gs_external_appstream_install (g_file_peek_path (data->output_file), + cancellable, + &local_error)) { + g_task_return_new_error (task, + GS_EXTERNAL_APPSTREAM_ERROR, + GS_EXTERNAL_APPSTREAM_ERROR_INSTALLING_ON_SYSTEM, + "Error installing external AppStream file on system: %s", local_error->message); + return; + } + g_debug ("Installed appstream file %s", g_file_peek_path (data->output_file)); + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +refresh_url_finish (GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void refresh_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static gboolean progress_cb (gpointer user_data); +static void finish_refresh_op (GTask *task, + GError *error); + +typedef struct { + /* Input data. */ + guint64 cache_age_secs; + + /* In-progress data. */ + guint n_pending_ops; + GError *error; /* (nullable) (owned) */ + gsize n_appstream_urls; + GsDownloadProgressCallback progress_callback; /* (nullable) */ + gpointer progress_user_data; /* (closure progress_callback) */ + ProgressTuple *progress_tuples; /* (array length=n_appstream_urls) (owned) */ + GSource *progress_source; /* (owned) */ +} RefreshData; + +static void +refresh_data_free (RefreshData *data) +{ + g_assert (data->n_pending_ops == 0); + + /* If this was set it should have been stolen for g_task_return_error() + * by now. */ + g_assert (data->error == NULL); + + /* Similarly, progress reporting should have been stopped by now. */ + g_assert (g_source_is_destroyed (data->progress_source)); + g_source_unref (data->progress_source); + + g_free (data->progress_tuples); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (RefreshData, refresh_data_free) + +/** + * gs_external_appstream_refresh_async: + * @cache_age_secs: cache age, in seconds, as passed to #GsPluginClass.refresh_metadata_async() + * @progress_callback: (nullable): callback to call with progress information + * @progress_user_data: (nullable) (closure progress_callback): data to pass + * to @progress_callback + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: function call when the asynchronous operation is complete + * @user_data: data to pass to @callback + * + * Refresh any configured external appstream files, if the cache is too old. + * + * Since: 42 + */ +void +gs_external_appstream_refresh_async (guint64 cache_age_secs, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GSettings) settings = NULL; + g_auto(GStrv) appstream_urls = NULL; + gsize n_appstream_urls; + g_autoptr(SoupSession) soup_session = NULL; + g_autoptr(GTask) task = NULL; + RefreshData *data; + g_autoptr(RefreshData) data_owned = NULL; + + /* Chosen to allow a few UI updates per second without updating the + * progress label so often it’s unreadable. */ + const guint progress_update_period_ms = 300; + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_external_appstream_refresh_async); + + settings = g_settings_new ("org.gnome.software"); + soup_session = gs_build_soup_session (); + appstream_urls = g_settings_get_strv (settings, + "external-appstream-urls"); + n_appstream_urls = g_strv_length (appstream_urls); + + data = data_owned = g_new0 (RefreshData, 1); + data->progress_callback = progress_callback; + data->progress_user_data = progress_user_data; + data->n_appstream_urls = n_appstream_urls; + data->progress_tuples = g_new0 (ProgressTuple, n_appstream_urls); + data->progress_source = g_timeout_source_new (progress_update_period_ms); + g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) refresh_data_free); + + /* Set up the progress timeout. This periodically sums up the progress + * tuples in `data->progress_tuples` and reports them to the calling + * function via @progress_callback, giving an overall progress for all + * the parallel operations. */ + g_source_set_callback (data->progress_source, progress_cb, g_object_ref (task), g_object_unref); + g_source_attach (data->progress_source, g_main_context_get_thread_default ()); + + /* Refresh all the URIs in parallel. */ + data->n_pending_ops = 1; + + for (gsize i = 0; i < n_appstream_urls; i++) { + if (!g_str_has_prefix (appstream_urls[i], "https")) { + g_warning ("Not considering %s as an external " + "appstream source: please use an https URL", + appstream_urls[i]); + continue; + } + + data->n_pending_ops++; + refresh_url_async (settings, + appstream_urls[i], + soup_session, + cache_age_secs, + &data->progress_tuples[i], + cancellable, + refresh_cb, + g_object_ref (task)); + } + + finish_refresh_op (task, NULL); +} + +static void +refresh_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + refresh_url_finish (result, &local_error); + finish_refresh_op (task, g_steal_pointer (&local_error)); +} + +static gboolean +progress_cb (gpointer user_data) +{ + GTask *task = G_TASK (user_data); + RefreshData *data = g_task_get_task_data (task); + gsize parallel_bytes_downloaded = 0, parallel_total_download_size = 0; + + /* Sum up the progress numerator and denominator for all parallel + * downloads. */ + for (gsize i = 0; i < data->n_appstream_urls; i++) { + const ProgressTuple *progress_tuple = &data->progress_tuples[i]; + + if (!g_size_checked_add (¶llel_bytes_downloaded, + parallel_bytes_downloaded, + progress_tuple->bytes_downloaded)) + parallel_bytes_downloaded = G_MAXSIZE; + if (!g_size_checked_add (¶llel_total_download_size, + parallel_total_download_size, + progress_tuple->total_download_size)) + parallel_total_download_size = G_MAXSIZE; + } + + /* Report progress to the calling function. */ + if (data->progress_callback != NULL) + data->progress_callback (parallel_bytes_downloaded, + parallel_total_download_size, + data->progress_user_data); + + return G_SOURCE_CONTINUE; +} + +/* @error is (transfer full) if non-%NULL */ +static void +finish_refresh_op (GTask *task, + GError *error) +{ + RefreshData *data = g_task_get_task_data (task); + g_autoptr(GError) error_owned = g_steal_pointer (&error); + + if (data->error == NULL && error_owned != NULL) + data->error = g_steal_pointer (&error_owned); + else if (error_owned != NULL) + g_debug ("Additional error while refreshing external appstream: %s", error_owned->message); + + g_assert (data->n_pending_ops > 0); + data->n_pending_ops--; + + if (data->n_pending_ops > 0) + return; + + /* Emit one final progress update, then stop any further ones. */ + progress_cb (task); + g_source_destroy (data->progress_source); + + /* All complete. */ + if (data->error != NULL) + g_task_return_error (task, g_steal_pointer (&data->error)); + else + g_task_return_boolean (task, TRUE); +} + +/** + * gs_external_appstream_refresh_finish: + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous refresh operation started with + * gs_external_appstream_refresh_async(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_external_appstream_refresh_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/lib/gs-external-appstream-utils.h b/lib/gs-external-appstream-utils.h new file mode 100644 index 0000000..63e67f8 --- /dev/null +++ b/lib/gs-external-appstream-utils.h @@ -0,0 +1,50 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Endless Mobile, Inc. + * + * Authors: Joaquim Rocha <jrocha@endlessm.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "config.h" + +#include <glib.h> +#include <gnome-software.h> + +#define EXTERNAL_APPSTREAM_PREFIX "org.gnome.Software" + +/** + * GsExternalAppstreamError: + * @GS_EXTERNAL_APPSTREAM_ERROR_DOWNLOADING: Error while downloading external appstream data. + * @GS_EXTERNAL_APPSTREAM_ERROR_NO_NETWORK: Offline or network unavailable. + * @GS_EXTERNAL_APPSTREAM_ERROR_INSTALLING_ON_SYSTEM: Error while installing an external AppStream file system-wide. + * + * Error codes for external appstream operations. + * + * Since: 42 + */ +typedef enum { + GS_EXTERNAL_APPSTREAM_ERROR_DOWNLOADING, + GS_EXTERNAL_APPSTREAM_ERROR_NO_NETWORK, + GS_EXTERNAL_APPSTREAM_ERROR_INSTALLING_ON_SYSTEM, +} GsExternalAppstreamError; + +#define GS_EXTERNAL_APPSTREAM_ERROR gs_external_appstream_error_quark () +GQuark gs_external_appstream_error_quark (void); + +const gchar *gs_external_appstream_utils_get_system_dir (void); +gchar *gs_external_appstream_utils_get_file_cache_path (const gchar *file_name); +gchar *gs_external_appstream_utils_get_legacy_file_cache_path (const gchar *file_name); + +void gs_external_appstream_refresh_async (guint64 cache_age_secs, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_external_appstream_refresh_finish (GAsyncResult *result, + GError **error); diff --git a/lib/gs-fedora-third-party.c b/lib/gs-fedora-third-party.c new file mode 100644 index 0000000..6afebc0 --- /dev/null +++ b/lib/gs-fedora-third-party.c @@ -0,0 +1,497 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Red Hat <www.redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <glib.h> +#include <gio/gio.h> + +#include "gs-fedora-third-party.h" + +struct _GsFedoraThirdParty +{ + GObject parent_instance; + GMutex lock; + gchar *executable; + GHashTable *repos; /* gchar *name ~> gchar *packaging format */ + gint64 last_update; +}; + +G_DEFINE_TYPE (GsFedoraThirdParty, gs_fedora_third_party, G_TYPE_OBJECT) + +static GObject * +gs_fedora_third_party_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties) +{ + static GWeakRef singleton; + GObject *result; + + result = g_weak_ref_get (&singleton); + if (result == NULL) { + result = G_OBJECT_CLASS (gs_fedora_third_party_parent_class)->constructor (type, n_construct_properties, construct_properties); + + if (result) + g_weak_ref_set (&singleton, result); + } + + return result; +} + +static void +gs_fedora_third_party_finalize (GObject *object) +{ + GsFedoraThirdParty *self = GS_FEDORA_THIRD_PARTY (object); + + g_clear_pointer (&self->executable, g_free); + g_clear_pointer (&self->repos, g_hash_table_unref); + g_mutex_clear (&self->lock); + + /* Chain up to parent's method. */ + G_OBJECT_CLASS (gs_fedora_third_party_parent_class)->finalize (object); +} + +static void +gs_fedora_third_party_class_init (GsFedoraThirdPartyClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->constructor = gs_fedora_third_party_constructor; + object_class->finalize = gs_fedora_third_party_finalize; +} + +static void +gs_fedora_third_party_init (GsFedoraThirdParty *self) +{ + g_mutex_init (&self->lock); +} + +GsFedoraThirdParty * +gs_fedora_third_party_new (void) +{ + return g_object_new (GS_TYPE_FEDORA_THIRD_PARTY, NULL); +} + +static gboolean +gs_fedora_third_party_ensure_executable_locked (GsFedoraThirdParty *self, + GError **error) +{ + if (self->executable == NULL) + self->executable = g_find_program_in_path ("fedora-third-party"); + + if (self->executable == NULL) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "File 'fedora-third-party' not found"); + return FALSE; + } + + return TRUE; +} + +gboolean +gs_fedora_third_party_is_available (GsFedoraThirdParty *self) +{ + gboolean res; + + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + + g_mutex_lock (&self->lock); + res = gs_fedora_third_party_ensure_executable_locked (self, NULL); + g_mutex_unlock (&self->lock); + + return res; +} + +void +gs_fedora_third_party_invalidate (GsFedoraThirdParty *self) +{ + g_return_if_fail (GS_IS_FEDORA_THIRD_PARTY (self)); + + g_mutex_lock (&self->lock); + g_clear_pointer (&self->executable, g_free); + g_clear_pointer (&self->repos, g_hash_table_unref); + self->last_update = 0; + g_mutex_unlock (&self->lock); +} + +typedef struct _AsyncData +{ + gboolean enable; + gboolean config_only; +} AsyncData; + +static AsyncData * +async_data_new (gboolean enable, + gboolean config_only) +{ + AsyncData *async_data = g_slice_new0 (AsyncData); + async_data->enable = enable; + async_data->config_only = config_only; + return async_data; +} + +static void +async_data_free (gpointer ptr) +{ + AsyncData *async_data = ptr; + if (async_data != NULL) + g_slice_free (AsyncData, async_data); +} + +static void +gs_fedora_third_party_query_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GError) error = NULL; + GsFedoraThirdPartyState state; + if (gs_fedora_third_party_query_sync (GS_FEDORA_THIRD_PARTY (source_object), &state, cancellable, &error)) + g_task_return_int (task, state); + else + g_task_return_error (task, g_steal_pointer (&error)); +} + +void +gs_fedora_third_party_query (GsFedoraThirdParty *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (GS_IS_FEDORA_THIRD_PARTY (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_fedora_third_party_query); + g_task_run_in_thread (task, gs_fedora_third_party_query_thread); +} + +gboolean +gs_fedora_third_party_query_finish (GsFedoraThirdParty *self, + GAsyncResult *result, + GsFedoraThirdPartyState *out_state, + GError **error) +{ + GError *local_error = NULL; + GsFedoraThirdPartyState state = GS_FEDORA_THIRD_PARTY_STATE_UNKNOWN; + + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + + state = g_task_propagate_int (G_TASK (result), &local_error); + if (local_error) { + g_propagate_error (error, local_error); + return FALSE; + } + + if (out_state) + *out_state = state; + + return TRUE; +} + +gboolean +gs_fedora_third_party_query_sync (GsFedoraThirdParty *self, + GsFedoraThirdPartyState *out_state, + GCancellable *cancellable, + GError **error) +{ + const gchar *args[] = { + "", /* executable */ + "query", + "--quiet", + NULL + }; + gboolean success = FALSE; + + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + + g_mutex_lock (&self->lock); + if (gs_fedora_third_party_ensure_executable_locked (self, error)) { + gint wait_status = -1; + args[0] = self->executable; + success = g_spawn_sync (NULL, (gchar **) args, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL, &wait_status, error); + if (success) { + GsFedoraThirdPartyState state = GS_FEDORA_THIRD_PARTY_STATE_UNKNOWN; + /* See https://pagure.io/fedora-third-party/blob/main/f/doc/fedora-third-party.1.md */ + switch (WEXITSTATUS (wait_status)) { + case 0: + state = GS_FEDORA_THIRD_PARTY_STATE_ENABLED; + break; + case 1: + state = GS_FEDORA_THIRD_PARTY_STATE_DISABLED; + break; + case 2: + state = GS_FEDORA_THIRD_PARTY_STATE_ASK; + break; + default: + break; + } + if (out_state) + *out_state = state; + } + } + g_mutex_unlock (&self->lock); + + return success; +} + +static void +gs_fedora_third_party_switch_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GError) error = NULL; + AsyncData *async_data = task_data; + if (gs_fedora_third_party_switch_sync (GS_FEDORA_THIRD_PARTY (source_object), async_data->enable, async_data->config_only, cancellable, &error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&error)); +} + +void +gs_fedora_third_party_switch (GsFedoraThirdParty *self, + gboolean enable, + gboolean config_only, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (GS_IS_FEDORA_THIRD_PARTY (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_fedora_third_party_switch); + g_task_set_task_data (task, async_data_new (enable, config_only), async_data_free); + g_task_run_in_thread (task, gs_fedora_third_party_switch_thread); +} + +gboolean +gs_fedora_third_party_switch_finish (GsFedoraThirdParty *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_fedora_third_party_switch_sync (GsFedoraThirdParty *self, + gboolean enable, + gboolean config_only, + GCancellable *cancellable, + GError **error) +{ + const gchar *args[] = { + "pkexec", + "", /* executable */ + "", /* command */ + "", /* config-only */ + NULL + }; + gboolean success = FALSE; + + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + + g_mutex_lock (&self->lock); + if (gs_fedora_third_party_ensure_executable_locked (self, error)) { + gint wait_status = -1; + args[1] = self->executable; + args[2] = enable ? "enable" : "disable"; + args[3] = config_only ? "--config-only" : NULL; + success = g_spawn_sync (NULL, (gchar **) args, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL, &wait_status, error) && + g_spawn_check_wait_status (wait_status, error); + } + g_mutex_unlock (&self->lock); + + return success; +} + +static void +gs_fedora_third_party_opt_out_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GError) error = NULL; + if (gs_fedora_third_party_opt_out_sync (GS_FEDORA_THIRD_PARTY (source_object), cancellable, &error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&error)); +} + +void +gs_fedora_third_party_opt_out (GsFedoraThirdParty *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (GS_IS_FEDORA_THIRD_PARTY (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_fedora_third_party_opt_out); + g_task_run_in_thread (task, gs_fedora_third_party_opt_out_thread); +} + +gboolean +gs_fedora_third_party_opt_out_finish (GsFedoraThirdParty *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_fedora_third_party_opt_out_sync (GsFedoraThirdParty *self, + GCancellable *cancellable, + GError **error) +{ + /* fedora-third-party-opt-out is a single-purpose script that changes + * the third-party status from unset => disabled. It exists to allow + * a different pkexec configuration for opting-out and thus avoid + * admin users needing to authenticate to opt-out. + */ + const gchar *args[] = { + "pkexec", + "/usr/lib/fedora-third-party/fedora-third-party-opt-out", + NULL + }; + gboolean success = FALSE; + + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + + g_mutex_lock (&self->lock); + if (gs_fedora_third_party_ensure_executable_locked (self, error)) { + gint wait_status = -1; + success = g_spawn_sync (NULL, (gchar **) args, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL, &wait_status, error) && + g_spawn_check_wait_status (wait_status, error); + } + g_mutex_unlock (&self->lock); + + return success; +} + +static void +gs_fedora_third_party_list_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GHashTable) repos = NULL; + if (gs_fedora_third_party_list_sync (GS_FEDORA_THIRD_PARTY (source_object), &repos, cancellable, &error)) + g_task_return_pointer (task, g_steal_pointer (&repos), (GDestroyNotify) g_hash_table_unref); + else + g_task_return_error (task, g_steal_pointer (&error)); +} + +void +gs_fedora_third_party_list (GsFedoraThirdParty *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (GS_IS_FEDORA_THIRD_PARTY (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_fedora_third_party_list); + g_task_run_in_thread (task, gs_fedora_third_party_list_thread); +} + +gboolean +gs_fedora_third_party_list_finish (GsFedoraThirdParty *self, + GAsyncResult *result, + GHashTable **out_repos, /* gchar *name ~> gchar *management_plugin */ + GError **error) +{ + g_autoptr(GHashTable) repos = NULL; + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + repos = g_task_propagate_pointer (G_TASK (result), error); + if (repos == NULL) + return FALSE; + if (out_repos) + *out_repos = g_steal_pointer (&repos); + return TRUE; +} + +gboolean +gs_fedora_third_party_list_sync (GsFedoraThirdParty *self, + GHashTable **out_repos, /* gchar *name ~> gchar *management_plugin */ + GCancellable *cancellable, + GError **error) +{ + const gchar *args[] = { + "", /* executable */ + "list", + "--csv", + "--columns=type,name", + NULL + }; + gboolean success = FALSE; + + g_return_val_if_fail (GS_IS_FEDORA_THIRD_PARTY (self), FALSE); + + g_mutex_lock (&self->lock); + /* Auto-recheck only twice a day */ + if (self->repos == NULL || (g_get_real_time () / G_USEC_PER_SEC) - self->last_update > 12 * 60 * 60) { + g_clear_pointer (&self->repos, g_hash_table_unref); + if (gs_fedora_third_party_ensure_executable_locked (self, error)) { + gint wait_status = -1; + g_autofree gchar *stdoutput = NULL; + args[0] = self->executable; + if (g_spawn_sync (NULL, (gchar **) args, NULL, G_SPAWN_DEFAULT, NULL, NULL, &stdoutput, NULL, &wait_status, error) && + g_spawn_check_wait_status (wait_status, error)) { + GHashTable *repos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_auto(GStrv) lines = NULL; + + lines = g_strsplit (stdoutput != NULL ? stdoutput : "", "\n", -1); + + for (gsize ii = 0; lines != NULL && lines[ii]; ii++) { + g_auto(GStrv) tokens = g_strsplit (lines[ii], ",", 2); + if (tokens != NULL && tokens[0] != NULL && tokens[1] != NULL) { + const gchar *repo_type = tokens[0]; + /* The 'dnf' means 'packagekit' here */ + if (g_str_equal (repo_type, "dnf")) + repo_type = "packagekit"; + /* Hash them by name, which cannot clash between types */ + g_hash_table_insert (repos, g_strdup (tokens[1]), g_strdup (repo_type)); + } + } + + self->repos = repos; + } + } + self->last_update = g_get_real_time () / G_USEC_PER_SEC; + } + success = self->repos != NULL; + if (success && out_repos) + *out_repos = g_hash_table_ref (self->repos); + g_mutex_unlock (&self->lock); + + return success; +} + +gboolean +gs_fedora_third_party_util_is_third_party_repo (GHashTable *third_party_repos, + const gchar *origin, + const gchar *management_plugin) +{ + const gchar *expected_management_plugin; + + if (third_party_repos == NULL || origin == NULL) + return FALSE; + + expected_management_plugin = g_hash_table_lookup (third_party_repos, origin); + if (expected_management_plugin == NULL) + return FALSE; + + return g_strcmp0 (management_plugin, expected_management_plugin) == 0; +} diff --git a/lib/gs-fedora-third-party.h b/lib/gs-fedora-third-party.h new file mode 100644 index 0000000..11a0144 --- /dev/null +++ b/lib/gs-fedora-third-party.h @@ -0,0 +1,93 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Red Hat <www.redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FEDORA_THIRD_PARTY (gs_fedora_third_party_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFedoraThirdParty, gs_fedora_third_party, GS, FEDORA_THIRD_PARTY, GObject) + +typedef enum _GsFedoraThirdPartyState { + GS_FEDORA_THIRD_PARTY_STATE_UNKNOWN, + GS_FEDORA_THIRD_PARTY_STATE_ENABLED, + GS_FEDORA_THIRD_PARTY_STATE_DISABLED, + GS_FEDORA_THIRD_PARTY_STATE_ASK +} GsFedoraThirdPartyState; + +GsFedoraThirdParty * + gs_fedora_third_party_new (void); +gboolean gs_fedora_third_party_is_available + (GsFedoraThirdParty *self); +void gs_fedora_third_party_invalidate(GsFedoraThirdParty *self); +void gs_fedora_third_party_query (GsFedoraThirdParty *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_fedora_third_party_query_finish + (GsFedoraThirdParty *self, + GAsyncResult *result, + GsFedoraThirdPartyState *out_state, + GError **error); +gboolean gs_fedora_third_party_query_sync(GsFedoraThirdParty *self, + GsFedoraThirdPartyState *out_state, + GCancellable *cancellable, + GError **error); +void gs_fedora_third_party_switch (GsFedoraThirdParty *self, + gboolean enable, + gboolean config_only, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_fedora_third_party_switch_finish + (GsFedoraThirdParty *self, + GAsyncResult *result, + GError **error); +gboolean gs_fedora_third_party_switch_sync + (GsFedoraThirdParty *self, + gboolean enable, + gboolean config_only, + GCancellable *cancellable, + GError **error); +void gs_fedora_third_party_opt_out (GsFedoraThirdParty *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_fedora_third_party_opt_out_finish + (GsFedoraThirdParty *self, + GAsyncResult *result, + GError **error); +gboolean gs_fedora_third_party_opt_out_sync + (GsFedoraThirdParty *self, + GCancellable *cancellable, + GError **error); +void gs_fedora_third_party_list (GsFedoraThirdParty *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_fedora_third_party_list_finish + (GsFedoraThirdParty *self, + GAsyncResult *result, + GHashTable **out_repos, + GError **error); +gboolean gs_fedora_third_party_list_sync (GsFedoraThirdParty *self, + GHashTable **out_repos, + GCancellable *cancellable, + GError **error); + +/* Utility functions */ +gboolean gs_fedora_third_party_util_is_third_party_repo + (GHashTable *third_party_repos, + const gchar *origin, + const gchar *management_plugin); + +G_END_DECLS diff --git a/lib/gs-icon.c b/lib/gs-icon.c new file mode 100644 index 0000000..955ab02 --- /dev/null +++ b/lib/gs-icon.c @@ -0,0 +1,315 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation, Inc + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-icon + * @short_description: Utilities for handling #GIcons + * + * This file provides several utilities for creating and handling #GIcon + * instances. #GIcon is used for representing icon sources throughout + * gnome-software, as it has low memory overheads, and allows the most + * appropriate icon data to be loaded when it’s needed to be used in a UI. + * + * gnome-software uses various classes which implement #GIcon, mostly the + * built-in ones provided by GIO, but also #GsRemoteIcon. All of them are tagged + * with `width` and `height` metadata (when that data was available at + * construction time). See gs_icon_get_width(). + * + * Since: 40 + */ + +#include "config.h" + +#include <appstream.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <gio/gio.h> +#include <glib.h> +#include <glib-object.h> +#include <gtk/gtk.h> + +#include "gs-icon.h" +#include "gs-remote-icon.h" + +/** + * gs_icon_get_width: + * @icon: a #GIcon + * + * Get the width of an icon, if it was attached as metadata when the #GIcon was + * created from an #AsIcon. + * + * Returns: width of the icon (in device pixels), or `0` if unknown + * Since: 40 + */ +guint +gs_icon_get_width (GIcon *icon) +{ + g_return_val_if_fail (G_IS_ICON (icon), 0); + + return GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (icon), "width")); +} + +/** + * gs_icon_set_width: + * @icon: a #GIcon + * @width: width of the icon, in device pixels + * + * Set the width of an icon. See gs_icon_get_width(). + * + * Since: 40 + */ +void +gs_icon_set_width (GIcon *icon, + guint width) +{ + g_return_if_fail (G_IS_ICON (icon)); + + g_object_set_data (G_OBJECT (icon), "width", GUINT_TO_POINTER (width)); +} + +/** + * gs_icon_get_height: + * @icon: a #GIcon + * + * Get the height of an icon, if it was attached as metadata when the #GIcon was + * created from an #AsIcon. + * + * Returns: height of the icon (in device pixels), or `0` if unknown + * Since: 40 + */ +guint +gs_icon_get_height (GIcon *icon) +{ + g_return_val_if_fail (G_IS_ICON (icon), 0); + + return GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (icon), "height")); +} + +/** + * gs_icon_set_height: + * @icon: a #GIcon + * @height: height of the icon, in device pixels + * + * Set the height of an icon. See gs_icon_get_height(). + * + * Since: 40 + */ +void +gs_icon_set_height (GIcon *icon, + guint height) +{ + g_return_if_fail (G_IS_ICON (icon)); + + g_object_set_data (G_OBJECT (icon), "height", GUINT_TO_POINTER (height)); +} + +/** + * gs_icon_get_scale: + * @icon: a #GIcon + * + * Get the scale of an icon, if it was attached as metadata when the #GIcon was + * created from an #AsIcon. + * + * See gtk_widget_get_scale_factor() for more information about scales. + * + * Returns: scale of the icon, or `1` if unknown; guaranteed to always be + * greater than or equal to 1 + * Since: 40 + */ +guint +gs_icon_get_scale (GIcon *icon) +{ + g_return_val_if_fail (G_IS_ICON (icon), 0); + + return MAX (1, GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (icon), "scale"))); +} + +/** + * gs_icon_set_scale: + * @icon: a #GIcon + * @scale: scale of the icon, which must be greater than or equal to 1 + * + * Set the scale of an icon. See gs_icon_get_scale(). + * + * Since: 40 + */ +void +gs_icon_set_scale (GIcon *icon, + guint scale) +{ + g_return_if_fail (G_IS_ICON (icon)); + g_return_if_fail (scale >= 1); + + g_object_set_data (G_OBJECT (icon), "scale", GUINT_TO_POINTER (scale)); +} + +static GIcon * +gs_icon_load_local (AsIcon *icon) +{ + const gchar *filename = as_icon_get_filename (icon); + g_autoptr(GFile) file = NULL; + + if (filename == NULL) + return NULL; + + file = g_file_new_for_path (filename); + return g_file_icon_new (file); +} + +static GIcon * +gs_icon_load_stock (AsIcon *icon) +{ + const gchar *name = as_icon_get_name (icon); + + if (name == NULL) + return NULL; + + return g_themed_icon_new (name); +} + +static GIcon * +gs_icon_load_remote (AsIcon *icon) +{ + const gchar *url = as_icon_get_url (icon); + + if (url == NULL) + return NULL; + + /* Load local files directly. */ + if (g_str_has_prefix (url, "file:")) { + g_autoptr(GFile) file = g_file_new_for_path (url + strlen ("file:")); + return g_file_icon_new (file); + } + + /* Only HTTP and HTTPS are supported. */ + if (!g_str_has_prefix (url, "http:") && + !g_str_has_prefix (url, "https:")) + return NULL; + + return gs_remote_icon_new (url); +} + +static GIcon * +gs_icon_load_cached (AsIcon *icon) +{ + const gchar *filename = as_icon_get_filename (icon); + const gchar *name = as_icon_get_name (icon); + g_autofree gchar *name_allocated = NULL; + g_autofree gchar *full_filename = NULL; + g_autoptr(GFile) file = NULL; + + if (filename == NULL || name == NULL) + return NULL; + + /* FIXME: Work around https://github.com/hughsie/appstream-glib/pull/390 + * where appstream files generated with appstream-builder from + * appstream-glib, with its hidpi option enabled, will contain an + * unnecessary size subdirectory in the icon name. */ + if (g_str_has_prefix (name, "64x64/")) + name = name_allocated = g_strdup (name + strlen ("64x64/")); + else if (g_str_has_prefix (name, "128x128/")) + name = name_allocated = g_strdup (name + strlen ("128x128/")); + + if (!g_str_has_suffix (filename, name)) { + /* Spec: https://www.freedesktop.org/software/appstream/docs/sect-AppStream-IconCache.html#spec-iconcache-location */ + if (as_icon_get_scale (icon) <= 1) { + full_filename = g_strdup_printf ("%s/%ux%u/%s", + filename, + as_icon_get_width (icon), + as_icon_get_height (icon), + name); + } else { + full_filename = g_strdup_printf ("%s/%ux%u@%u/%s", + filename, + as_icon_get_width (icon), + as_icon_get_height (icon), + as_icon_get_scale (icon), + name); + } + + filename = full_filename; + } + + file = g_file_new_for_path (filename); + return g_file_icon_new (file); +} + +/** + * gs_icon_new_for_appstream_icon: + * @appstream_icon: an #AsIcon + * + * Create a new #GIcon representing the given #AsIcon. The actual type of the + * returned icon will vary depending on the #AsIconKind of @appstream_icon. + * + * If the width or height of the icon are set on the #AsIcon, they are stored + * as the `width` and `height` data associated with the returned object, using + * g_object_set_data(). + * + * This can fail (and return %NULL) if the @appstream_icon has invalid or + * missing properties. + * + * Returns: (transfer full) (nullable): the #GIcon, or %NULL + * Since: 40 + */ +GIcon * +gs_icon_new_for_appstream_icon (AsIcon *appstream_icon) +{ + g_autoptr(GIcon) icon = NULL; + + g_return_val_if_fail (AS_IS_ICON (appstream_icon), NULL); + + switch (as_icon_get_kind (appstream_icon)) { + case AS_ICON_KIND_LOCAL: + icon = gs_icon_load_local (appstream_icon); + break; + case AS_ICON_KIND_STOCK: + icon = gs_icon_load_stock (appstream_icon); + break; + case AS_ICON_KIND_REMOTE: + icon = gs_icon_load_remote (appstream_icon); + break; + case AS_ICON_KIND_CACHED: + icon = gs_icon_load_cached (appstream_icon); + break; + default: + g_assert_not_reached (); + } + + if (icon == NULL) { + g_debug ("Error creating GIcon for AsIcon of kind %s", + as_icon_kind_to_string (as_icon_get_kind (appstream_icon))); + return NULL; + } + + /* Store the width, height and scale as associated metadata (if + * available) so that #GsApp can sort icons by size and return the most + * appropriately sized one in gs_app_get_icon_by_size(). + * + * FIXME: Ideally we’d store these as properties on the objects, but + * GIO currently doesn’t allow subclassing of its #GIcon classes. If we + * were to implement a #GLoadableIcon with these as properties, all the + * fast paths in GTK for loading icon data (particularly named icons) + * would be ignored. + * + * Storing the width and height as associated metadata means GObject + * creates a hash table for each GIcon object. This is a waste of memory + * (compared to using properties), but seems like the least-worst + * option. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2345 + */ + if (as_icon_get_width (appstream_icon) != 0 || as_icon_get_height (appstream_icon) != 0) { + gs_icon_set_width (icon, as_icon_get_width (appstream_icon)); + gs_icon_set_height (icon, as_icon_get_height (appstream_icon)); + } + if (as_icon_get_scale (appstream_icon) != 0) + gs_icon_set_scale (icon, as_icon_get_scale (appstream_icon)); + + return g_steal_pointer (&icon); +} diff --git a/lib/gs-icon.h b/lib/gs-icon.h new file mode 100644 index 0000000..b1b7a89 --- /dev/null +++ b/lib/gs-icon.h @@ -0,0 +1,32 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation, Inc + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <appstream.h> +#include <gio/gio.h> +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +guint gs_icon_get_width (GIcon *icon); +void gs_icon_set_width (GIcon *icon, + guint width); +guint gs_icon_get_height (GIcon *icon); +void gs_icon_set_height (GIcon *icon, + guint height); +guint gs_icon_get_scale (GIcon *icon); +void gs_icon_set_scale (GIcon *icon, + guint scale); + +GIcon *gs_icon_new_for_appstream_icon (AsIcon *appstream_icon); + +G_END_DECLS diff --git a/lib/gs-ioprio.c b/lib/gs-ioprio.c new file mode 100644 index 0000000..3a63d07 --- /dev/null +++ b/lib/gs-ioprio.c @@ -0,0 +1,197 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2005, Novell, Inc. + * Copyright (C) 2006, Jamie McCracken <jamiemcc@gnome.org> + * Copyright (C) 2006, Anders Aagaard + * + * Based mostly on code by Robert Love <rml@novell.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#ifdef __linux__ + +#include <stdio.h> +#include <errno.h> + +#ifdef HAVE_LINUX_UNISTD_H +#include <linux/unistd.h> +#endif + +#include <sys/syscall.h> +#include <unistd.h> + +#include <glib/gstdio.h> + +#endif /* __linux__ */ + +#include "gs-ioprio.h" + +/* We assume ALL linux architectures have the syscalls defined here */ +#ifdef __linux__ + +/* Make sure the system call is supported */ +#ifndef __NR_ioprio_set + +#if defined(__i386__) +#define __NR_ioprio_set 289 +#define __NR_ioprio_get 290 +#elif defined(__powerpc__) || defined(__powerpc64__) +#define __NR_ioprio_set 273 +#define __NR_ioprio_get 274 +#elif defined(__x86_64__) +#define __NR_ioprio_set 251 +#define __NR_ioprio_get 252 +#elif defined(__ia64__) +#define __NR_ioprio_set 1274 +#define __NR_ioprio_get 1275 +#elif defined(__alpha__) +#define __NR_ioprio_set 442 +#define __NR_ioprio_get 443 +#elif defined(__s390x__) || defined(__s390__) +#define __NR_ioprio_set 282 +#define __NR_ioprio_get 283 +#elif defined(__SH4__) +#define __NR_ioprio_set 288 +#define __NR_ioprio_get 289 +#elif defined(__SH5__) +#define __NR_ioprio_set 316 +#define __NR_ioprio_get 317 +#elif defined(__sparc__) || defined(__sparc64__) +#define __NR_ioprio_set 196 +#define __NR_ioprio_get 218 +#elif defined(__arm__) +#define __NR_ioprio_set 314 +#define __NR_ioprio_get 315 +#else +#error "Unsupported architecture!" +#endif + +#endif /* __NR_ioprio_set */ + +enum { + IOPRIO_CLASS_NONE, + IOPRIO_CLASS_RT, + IOPRIO_CLASS_BE, + IOPRIO_CLASS_IDLE, +}; + +enum { + IOPRIO_WHO_PROCESS = 1, + IOPRIO_WHO_PGRP, + IOPRIO_WHO_USER, +}; + +#define IOPRIO_CLASS_SHIFT 13 + +static inline int +ioprio_set (int which, int who, int ioprio_val) +{ + return syscall (__NR_ioprio_set, which, who, ioprio_val); +} + +static int +set_io_priority (int ioprio, + int ioclass) +{ + return ioprio_set (IOPRIO_WHO_PROCESS, 0, ioprio | (ioclass << IOPRIO_CLASS_SHIFT)); +} + +static const gchar * +ioclass_to_string (int ioclass) +{ + switch (ioclass) { + case IOPRIO_CLASS_IDLE: + return "IDLE"; + case IOPRIO_CLASS_BE: + return "BE"; + default: + return "unknown"; + } +} + +/** + * gs_ioprio_set: + * @priority: I/O priority, with higher numeric values indicating lower priority; + * use %G_PRIORITY_DEFAULT as the default + * + * Set the I/O priority of the current thread using the `ioprio_set()` syscall. + * + * The @priority is quantised before being passed to the kernel. + * + * This function may fail if the process doesn’t have permission to change its + * I/O priority to the given value. If so, a warning will be printed, as the + * quantised priority values are chosen so they shouldn’t typically require + * permissions to set. + */ +void +gs_ioprio_set (gint priority) +{ + int ioprio, ioclass; + + /* If the priority is lower than default, use an idle I/O priority. The + * condition looks wrong because higher integers indicate lower priority + * in GLib. + * + * Otherwise use a default best-effort priority, which is the same as + * what all new threads get (in the absence of an I/O context with + * `CLONE_IO`). */ + if (priority > G_PRIORITY_DEFAULT) { + ioprio = 7; + ioclass = IOPRIO_CLASS_IDLE; + } else if (priority == G_PRIORITY_DEFAULT) { + ioprio = 4; /* this is the default priority in the BE class */ + ioclass = IOPRIO_CLASS_BE; + } else { + ioprio = 0; /* this is the highest priority in the BE class */ + ioclass = IOPRIO_CLASS_BE; + } + + g_debug ("Setting I/O priority of thread %p to %s, %d", + g_thread_self (), ioclass_to_string (ioclass), ioprio); + + if (set_io_priority (ioprio, ioclass) == -1) { + g_warning ("Could not set I/O priority to %s, %d", + ioclass_to_string (ioclass), ioprio); + + /* If we were trying to set to idle priority, try again with the + * lowest-possible best-effort priority. This is because kernels + * older than 2.6.25 required `CAP_SYS_ADMIN` to set + * `IOPRIO_CLASS_IDLE`. Newer kernels do not. */ + if (ioclass == IOPRIO_CLASS_IDLE) { + ioprio = 7; /* this is the lowest priority in the BE class */ + ioclass = IOPRIO_CLASS_BE; + + if (set_io_priority (ioprio, ioclass) == -1) + g_warning ("Could not set best effort IO priority either, giving up"); + } + } +} + +#else /* __linux__ */ + +void +gs_ioprio_set (gint priority) +{ +} + +#endif /* __linux__ */ diff --git a/lib/gs-ioprio.h b/lib/gs-ioprio.h new file mode 100644 index 0000000..e630b8a --- /dev/null +++ b/lib/gs-ioprio.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2006, Anders Aagaard + * Copyright (C) 2008, Nokia <ivan.frade@nokia.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include <glib.h> + +G_BEGIN_DECLS + +void gs_ioprio_set (gint priority); + +G_END_DECLS diff --git a/lib/gs-key-colors.c b/lib/gs-key-colors.c new file mode 100644 index 0000000..fb0850c --- /dev/null +++ b/lib/gs-key-colors.c @@ -0,0 +1,334 @@ +/* -*- 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) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Authors: + * - Richard Hughes <richard@hughsie.com> + * - Kalev Lember <klember@redhat.com> + * - Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-key-colors + * @short_description: Helper functions for calculating key colors + * + * Key colors are RGB colors which represent an app, and they are derived from + * the app’s icon, or manually specified as an override. + * + * Use gs_calculate_key_colors() to calculate the key colors from an app’s icon. + * + * Since: 40 + */ + +#include "config.h" + +#include <glib.h> +#include <gdk/gdk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include "gs-key-colors.h" + +/* Hard-code the number of clusters to split the icon color space into. This + * gives the maximum number of key colors returned for an icon. This number has + * been chosen by examining 1000 icons to subjectively see how many key colors + * each has. The number of key colors ranged from 1 to 6, but the mode was + * definitely 3. */ +const guint n_clusters = 3; + +/* Discard pixels with less than this level of alpha. Almost all icons have a + * transparent background/border at 100% transparency, and a blending fringe + * with some intermediate level of transparency which should be ignored for + * choosing key colors. A number of icons have partially-transparent colored + * sections in the main body of the icon, which should be used if they’re above + * this threshold. About 1% of icons have no completely opaque pixels, so we + * can’t discard non-opaque pixels entirely. */ +const guint minimum_alpha = 0.5 * 255; + +typedef struct { + guint8 red; + guint8 green; + guint8 blue; +} Pixel8; + +typedef struct { + Pixel8 color; + union { + guint8 alpha; + guint8 cluster; + }; +} ClusterPixel8; + +typedef struct { + guint red; + guint green; + guint blue; + guint n_members; +} CentroidAccumulator; + +static inline ClusterPixel8 * +get_pixel (guint8 *pixels, + guint x, + guint y, + guint rowstride, + guint n_channels) +{ + return (ClusterPixel8 *) (pixels + y * rowstride + x * n_channels); +} + +static inline guint +color_distance (const Pixel8 *a, + const Pixel8 *b) +{ + /* Take the absolute value rather than the square root to save some + * time, as the caller is comparing distances. + * + * The arithmetic here can’t overflow, as the R/G/B components have a + * maximum value of 255 but the arithmetic is done in (at least) 32-bit + * variables.*/ + gint dr = b->red - a->red; + gint dg = b->green - a->green; + gint db = b->blue - a->blue; + + return abs (dr * dr + dg * dg + db * db); +} + +/* NOTE: This has to return stable results when more than one cluster is + * equidistant from the @pixel, or the k_means() function may not terminate. */ +static inline gsize +nearest_cluster (const Pixel8 *pixel, + const Pixel8 *cluster_centres, + gsize n_cluster_centres) +{ + gsize nearest_cluster = 0; + guint nearest_cluster_distance = color_distance (&cluster_centres[0], pixel); + + for (gsize i = 1; i < n_cluster_centres; i++) { + guint distance = color_distance (&cluster_centres[i], pixel); + if (distance < nearest_cluster_distance) { + nearest_cluster = i; + nearest_cluster_distance = distance; + } + } + + return nearest_cluster; +} + +/* A variant of g_random_int_range() which chooses without replacement, + * tracking the used integers in @used_ints and @n_used_ints. + * Once all integers in 0..max_ints have been used once, it will choose + * with replacement. */ +static gint32 +random_int_range_no_replacement (guint max_ints, + gboolean *used_ints, + guint *n_used_ints) +{ + gint32 random_value = g_random_int_range (0, (gint32) max_ints); + + if (*n_used_ints < max_ints) { + while (used_ints[random_value]) + random_value = (random_value + 1) % max_ints; + + used_ints[random_value] = TRUE; + *n_used_ints = *n_used_ints + 1; + } + + return random_value; +} + +/* Extract the key colors from @pb by clustering the pixels in RGB space. + * Clustering is done using k-means, with initialisation using a + * Random Partition. + * + * This approach can be thought of as plotting every pixel in @pb in a + * three-dimensional color space, with red, green and blue axes (alpha is + * clipped to 0 (pixel is ignored) or 1 (pixel is used)). The key colors for + * the image are the ones where a large number of pixels are plotted in a group + * in the color space — either a lot of pixels with an identical color + * (repeated use of exactly the same color in the image) or a lot of pixels in + * a rough group (use of a lot of similar shades of the same color in the + * image). + * + * By transforming to a color space, information about the X and Y positions of + * each color is ignored, so a thin outline in the image of a single color + * will appear in the color space as a cluster, just as a contiguous block of + * one color would. + * + * The k-means clustering algorithm is then used to find these clusters. k-means + * is used, rather than (say) principal component analysis, because it + * inherently calculates the centroid for each cluster. In a color space, the + * centroid is itself a color, which can then be used as the key color to + * return. + * + * The number of clusters is limited to @n_clusters, as a subjective survey of + * 1000 icons found that they commonly used this number of key colors. + * + * Various other shortcuts have been taken which make this approach quite + * specific to key color extraction from icons, with the aim of making it + * faster. That’s fine — it doesn’t matter if the results this function produces + * are optimal, only that they’re good enough. */ +static void +k_means (GArray *colors, + GdkPixbuf *pb) +{ + gint rowstride, n_channels; + gint width, height; + guint8 *raw_pixels; + ClusterPixel8 *pixels; + const ClusterPixel8 *pixels_end; + Pixel8 cluster_centres[n_clusters]; + CentroidAccumulator cluster_accumulators[n_clusters]; + gboolean used_clusters[n_clusters]; + guint n_used_clusters = 0; + guint n_assignments_changed; + guint n_iterations = 0; + guint assignments_termination_limit; + + n_channels = gdk_pixbuf_get_n_channels (pb); + rowstride = gdk_pixbuf_get_rowstride (pb); + raw_pixels = gdk_pixbuf_get_pixels (pb); + width = gdk_pixbuf_get_width (pb); + height = gdk_pixbuf_get_height (pb); + + /* The pointer arithmetic over pixels can be simplified if we can assume + * there are no gaps in the @raw_pixels data. Since the caller is + * downsizing the #GdkPixbuf, this is a reasonable assumption. */ + g_assert (rowstride == width * n_channels); + g_assert (n_channels == 4); + + pixels = (ClusterPixel8 *) raw_pixels; + pixels_end = &pixels[height * width]; + + memset (cluster_centres, 0, sizeof (cluster_centres)); + memset (used_clusters, 0, sizeof (used_clusters)); + + /* Initialise the clusters using the Random Partition method: randomly + * assign a starting cluster to each pixel. + * + * The Forgy method (choosing random pixels as the starting cluster + * centroids) is not appropriate as the checks required to make sure + * they aren’t transparent or duplicated colors mean that the + * initialisation step may never complete. Consider the case of an icon + * which is a block of solid color. */ + for (ClusterPixel8 *p = pixels; p < pixels_end; p++) { + if (p->alpha < minimum_alpha) + p->cluster = G_N_ELEMENTS (cluster_centres); + else + p->cluster = random_int_range_no_replacement (G_N_ELEMENTS (cluster_centres), used_clusters, &n_used_clusters); + } + + /* Iterate until every cluster is relatively settled. This is determined + * by the number of pixels whose assignment to a cluster changes in + * each iteration — if the number of pixels is less than 1% of the image + * then subsequent iterations are not going to significantly affect the + * results. + * + * As we’re choosing key colors, finding the optimal result is not + * needed. We just need to find one which is good enough, quickly. + * + * A second termination condition is set on the number of iterations, to + * avoid a potential infinite loop. This termination condition is never + * normally expected to be hit — typically an icon will require 5–10 + * iterations to terminate based on @n_assignments_changed. */ + assignments_termination_limit = width * height * 0.01; + n_iterations = 0; + do { + /* Update step. Re-calculate the centroid of each cluster from + * the colors which are in it. */ + memset (cluster_accumulators, 0, sizeof (cluster_accumulators)); + + for (const ClusterPixel8 *p = pixels; p < pixels_end; p++) { + if (p->cluster >= G_N_ELEMENTS (cluster_centres)) + continue; + + cluster_accumulators[p->cluster].red += p->color.red; + cluster_accumulators[p->cluster].green += p->color.green; + cluster_accumulators[p->cluster].blue += p->color.blue; + cluster_accumulators[p->cluster].n_members++; + } + + for (gsize i = 0; i < G_N_ELEMENTS (cluster_centres); i++) { + if (cluster_accumulators[i].n_members == 0) + continue; + + cluster_centres[i].red = cluster_accumulators[i].red / cluster_accumulators[i].n_members; + cluster_centres[i].green = cluster_accumulators[i].green / cluster_accumulators[i].n_members; + cluster_centres[i].blue = cluster_accumulators[i].blue / cluster_accumulators[i].n_members; + } + + /* Update assignments of colors to clusters. */ + n_assignments_changed = 0; + for (ClusterPixel8 *p = pixels; p < pixels_end; p++) { + gsize new_cluster; + + if (p->cluster >= G_N_ELEMENTS (cluster_centres)) + continue; + + new_cluster = nearest_cluster (&p->color, cluster_centres, G_N_ELEMENTS (cluster_centres)); + if (new_cluster != p->cluster) + n_assignments_changed++; + p->cluster = new_cluster; + } + + n_iterations++; + } while (n_assignments_changed > assignments_termination_limit && n_iterations < 50); + + /* Output the cluster centres: these are the icon’s key colors. */ + for (gsize i = 0; i < G_N_ELEMENTS (cluster_centres); i++) { + GdkRGBA color; + + if (cluster_accumulators[i].n_members == 0) + continue; + + color.red = (gdouble) cluster_centres[i].red / 255.0; + color.green = (gdouble) cluster_centres[i].green / 255.0; + color.blue = (gdouble) cluster_centres[i].blue / 255.0; + color.alpha = 1.0; + g_array_append_val (colors, color); + } +} + +/** + * gs_calculate_key_colors: + * @pixbuf: an app icon to calculate key colors from + * + * Calculate the set of key colors present in @pixbuf. These are the colors + * which stand out the most, and they are subjective. This function does not + * guarantee to return perfect results, but should return workable results for + * most icons. + * + * @pixbuf will be scaled down to 32×32 pixels, so if it can be provided at + * that resolution by the caller, this function will return faster. + * + * Returns: (transfer full) (element-type GdkRGBA): key colors for @pixbuf + * Since: 40 + */ +GArray * +gs_calculate_key_colors (GdkPixbuf *pixbuf) +{ + g_autoptr(GdkPixbuf) pb_small = NULL; + g_autoptr(GArray) colors = g_array_new (FALSE, FALSE, sizeof (GdkRGBA)); + + /* people almost always use BILINEAR scaling with pixbufs, but we can + * use NEAREST here since we only care about the rough colour data, not + * whether the edges in the image are smooth and visually appealing; + * NEAREST is twice as fast as BILINEAR */ + pb_small = gdk_pixbuf_scale_simple (pixbuf, 32, 32, GDK_INTERP_NEAREST); + + /* require an alpha channel for storing temporary values; most images + * have one already, about 2% don’t */ + if (gdk_pixbuf_get_n_channels (pixbuf) != 4) { + g_autoptr(GdkPixbuf) temp = g_steal_pointer (&pb_small); + pb_small = gdk_pixbuf_add_alpha (temp, FALSE, 0, 0, 0); + } + + /* get a list of key colors */ + k_means (colors, pb_small); + + return g_steal_pointer (&colors); +} diff --git a/lib/gs-key-colors.h b/lib/gs-key-colors.h new file mode 100644 index 0000000..de1baee --- /dev/null +++ b/lib/gs-key-colors.h @@ -0,0 +1,27 @@ +/* -*- 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) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Authors: + * - Richard Hughes <richard@hughsie.com> + * - Kalev Lember <klember@redhat.com> + * - Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <gdk/gdk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +G_BEGIN_DECLS + +GArray *gs_calculate_key_colors (GdkPixbuf *pixbuf); + +G_END_DECLS diff --git a/lib/gs-metered.c b/lib/gs-metered.c new file mode 100644 index 0000000..e6a82d9 --- /dev/null +++ b/lib/gs-metered.c @@ -0,0 +1,312 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Endless Mobile, Inc. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-metered + * @title: Metered Data Utilities + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Utility functions to help with metered data handling + * + * Metered data handling is provided by Mogwai, which implements a download + * scheduler to control when, and in which order, large downloads happen on + * the system. + * + * All large downloads from gs_plugin_download() or gs_plugin_download_app() + * calls should be scheduled using Mogwai, which will notify gnome-software + * when those downloads can start and stop, according to system policy. + * + * The functions in this file make interacting with the scheduling daemon a + * little simpler. Since all #GsPlugin method calls happen in worker threads, + * typically without a #GMainContext, all interaction with the scheduler should + * be blocking. libmogwai-schedule-client was designed to be asynchronous; so + * these helpers make it synchronous. + * + * Since: 3.34 + */ + +#include "config.h" + +#include <glib.h> + +#ifdef HAVE_MOGWAI +#include <libmogwai-schedule-client/scheduler.h> +#endif + +#include "gs-metered.h" +#include "gs-utils.h" + + +#ifdef HAVE_MOGWAI + +typedef struct +{ + gboolean *out_download_now; /* (unowned) */ + GMainContext *context; /* (unowned) */ +} DownloadNowData; + +static void +download_now_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + DownloadNowData *data = user_data; + *data->out_download_now = mwsc_schedule_entry_get_download_now (MWSC_SCHEDULE_ENTRY (obj)); + g_main_context_wakeup (data->context); +} + +typedef struct +{ + GError **out_error; /* (unowned) */ + GMainContext *context; /* (unowned) */ +} InvalidatedData; + +static void +invalidated_cb (MwscScheduleEntry *entry, + const GError *error, + gpointer user_data) +{ + InvalidatedData *data = user_data; + *data->out_error = g_error_copy (error); + g_main_context_wakeup (data->context); +} + +#endif /* HAVE_MOGWAI */ + +/** + * gs_metered_block_on_download_scheduler: + * @parameters: (nullable): a #GVariant of type `a{sv}` specifying parameters + * for the schedule entry, or %NULL to pass no parameters + * @schedule_entry_handle_out: (out) (not optional): return location for a + * handle to the resulting schedule entry + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Create a schedule entry with the given @parameters, and block until + * permission is given to download. + * + * FIXME: This will currently ignore later revocations of that download + * permission, and does not support creating a schedule entry per app. + * The schedule entry must later be removed from the schedule by passing + * the handle from @schedule_entry_handle_out to + * gs_metered_remove_from_download_scheduler(), otherwise resources will leak. + * This is an opaque handle and should not be inspected. + * + * If a schedule entry cannot be created, or if @cancellable is cancelled, + * an error will be set and %FALSE returned. + * + * The keys understood by @parameters are listed in the documentation for + * mwsc_scheduler_schedule_async(). + * + * This function will likely be called from a #GsPluginLoader worker thread. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 3.38 + */ +gboolean +gs_metered_block_on_download_scheduler (GVariant *parameters, + gpointer *schedule_entry_handle_out, + GCancellable *cancellable, + GError **error) +{ +#ifdef HAVE_MOGWAI + g_autoptr(MwscScheduler) scheduler = NULL; + g_autoptr(MwscScheduleEntry) schedule_entry = NULL; + g_autofree gchar *parameters_str = NULL; + g_autoptr(GMainContext) context = NULL; + g_autoptr(GMainContextPusher) pusher = NULL; + + g_return_val_if_fail (schedule_entry_handle_out != NULL, FALSE); + + /* set this in case of error */ + *schedule_entry_handle_out = NULL; + + parameters_str = (parameters != NULL) ? g_variant_print (parameters, TRUE) : g_strdup ("(none)"); + g_debug ("%s: Waiting with parameters: %s", G_STRFUNC, parameters_str); + + /* Push the context early so that the #MwscScheduler is created to run within it. */ + context = g_main_context_new (); + pusher = g_main_context_pusher_new (context); + + /* Wait until the download can be scheduled. + * FIXME: In future, downloads could be split up by app, so they can all + * be scheduled separately and, for example, higher priority ones could + * be scheduled with a higher priority. This would have to be aware of + * dependencies. */ + scheduler = mwsc_scheduler_new (cancellable, error); + if (scheduler == NULL) + return FALSE; + + /* Create a schedule entry for the group of downloads. + * FIXME: The underlying OSTree code supports resuming downloads + * (at a granularity of individual objects), so it should be + * possible to plumb through here. */ + schedule_entry = mwsc_scheduler_schedule (scheduler, parameters, cancellable, + error); + if (schedule_entry == NULL) + return FALSE; + + /* Wait until the download is allowed to proceed. */ + if (!mwsc_schedule_entry_get_download_now (schedule_entry)) { + gboolean download_now = FALSE; + g_autoptr(GError) invalidated_error = NULL; + gulong notify_id, invalidated_id; + DownloadNowData download_now_data = { &download_now, context }; + InvalidatedData invalidated_data = { &invalidated_error, context }; + + notify_id = g_signal_connect (schedule_entry, "notify::download-now", + (GCallback) download_now_cb, &download_now_data); + invalidated_id = g_signal_connect (schedule_entry, "invalidated", + (GCallback) invalidated_cb, &invalidated_data); + + while (!download_now && invalidated_error == NULL && + !g_cancellable_is_cancelled (cancellable)) + g_main_context_iteration (context, TRUE); + + g_signal_handler_disconnect (schedule_entry, invalidated_id); + g_signal_handler_disconnect (schedule_entry, notify_id); + + if (!download_now && invalidated_error != NULL) { + /* no need to remove the schedule entry as it’s been + * invalidated */ + g_propagate_error (error, g_steal_pointer (&invalidated_error)); + return FALSE; + } else if (!download_now && g_cancellable_set_error_if_cancelled (cancellable, error)) { + /* remove the schedule entry and fail */ + gs_metered_remove_from_download_scheduler (schedule_entry, NULL, NULL); + return FALSE; + } + + g_assert (download_now); + } + + *schedule_entry_handle_out = g_object_ref (schedule_entry); + + g_debug ("%s: Allowed to download", G_STRFUNC); +#else /* if !HAVE_MOGWAI */ + g_debug ("%s: Allowed to download (Mogwai support compiled out)", G_STRFUNC); +#endif /* !HAVE_MOGWAI */ + + return TRUE; +} + +/** + * gs_metered_remove_from_download_scheduler: + * @schedule_entry_handle: (transfer full) (nullable): schedule entry handle as + * returned by gs_metered_block_on_download_scheduler() + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Remove a schedule entry previously created by + * gs_metered_block_on_download_scheduler(). This must be called after + * gs_metered_block_on_download_scheduler() has successfully returned, or + * resources will leak. It should be called once the corresponding download is + * complete. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 3.38 + */ +gboolean +gs_metered_remove_from_download_scheduler (gpointer schedule_entry_handle, + GCancellable *cancellable, + GError **error) +{ +#ifdef HAVE_MOGWAI + g_autoptr(MwscScheduleEntry) schedule_entry = schedule_entry_handle; +#endif + + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + g_debug ("Removing schedule entry handle %p", schedule_entry_handle); + + if (schedule_entry_handle == NULL) + return TRUE; + +#ifdef HAVE_MOGWAI + return mwsc_schedule_entry_remove (schedule_entry, cancellable, error); +#else + return TRUE; +#endif +} + +/** + * gs_metered_block_app_on_download_scheduler: + * @app: a #GsApp to get the scheduler parameters from + * @schedule_entry_handle_out: (out) (not optional): return location for a + * handle to the resulting schedule entry + * @cancellable: a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Version of gs_metered_block_on_download_scheduler() which extracts the + * download parameters from the given @app. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 3.38 + */ +gboolean +gs_metered_block_app_on_download_scheduler (GsApp *app, + gpointer *schedule_entry_handle_out, + GCancellable *cancellable, + GError **error) +{ + g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL); + g_autoptr(GVariant) parameters = NULL; + guint64 download_size; + + /* Currently no plugins support resumable downloads. This may change in + * future, in which case this parameter should be refactored. */ + g_variant_dict_insert (¶meters_dict, "resumable", "b", FALSE); + + if (gs_app_get_size_download (app, &download_size) == GS_SIZE_TYPE_VALID) { + g_variant_dict_insert (¶meters_dict, "size-minimum", "t", download_size); + g_variant_dict_insert (¶meters_dict, "size-maximum", "t", download_size); + } + + parameters = g_variant_ref_sink (g_variant_dict_end (¶meters_dict)); + + return gs_metered_block_on_download_scheduler (parameters, schedule_entry_handle_out, cancellable, error); +} + +/** + * gs_metered_block_app_list_on_download_scheduler: + * @app_list: a #GsAppList to get the scheduler parameters from + * @schedule_entry_handle_out: (out) (not optional): return location for a + * handle to the resulting schedule entry + * @cancellable: a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Version of gs_metered_block_on_download_scheduler() which extracts the + * download parameters from the apps in the given @app_list. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 3.38 + */ +gboolean +gs_metered_block_app_list_on_download_scheduler (GsAppList *app_list, + gpointer *schedule_entry_handle_out, + GCancellable *cancellable, + GError **error) +{ + g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL); + g_autoptr(GVariant) parameters = NULL; + + /* Currently no plugins support resumable downloads. This may change in + * future, in which case this parameter should be refactored. */ + g_variant_dict_insert (¶meters_dict, "resumable", "b", FALSE); + + /* FIXME: Currently this creates a single Mogwai schedule entry for the + * entire app list. Eventually, we probably want one schedule entry per + * app being downloaded, so that they can be individually prioritised. + * However, that requires much deeper integration into the download + * code, and Mogwai does not currently support that level of + * prioritisation, so go with this simple implementation for now. */ + parameters = g_variant_ref_sink (g_variant_dict_end (¶meters_dict)); + + return gs_metered_block_on_download_scheduler (parameters, schedule_entry_handle_out, cancellable, error); +} diff --git a/lib/gs-metered.h b/lib/gs-metered.h new file mode 100644 index 0000000..f81d171 --- /dev/null +++ b/lib/gs-metered.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Endless Mobile, Inc. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-app.h" +#include "gs-app-list.h" + +G_BEGIN_DECLS + +gboolean gs_metered_block_on_download_scheduler (GVariant *parameters, + gpointer *schedule_entry_handle_out, + GCancellable *cancellable, + GError **error); +gboolean gs_metered_remove_from_download_scheduler (gpointer schedule_entry_handle, + GCancellable *cancellable, + GError **error); +gboolean gs_metered_block_app_on_download_scheduler (GsApp *app, + gpointer *schedule_entry_handle_out, + GCancellable *cancellable, + GError **error); +gboolean gs_metered_block_app_list_on_download_scheduler (GsAppList *app_list, + gpointer *schedule_entry_handle_out, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/lib/gs-odrs-provider.c b/lib/gs-odrs-provider.c new file mode 100644 index 0000000..cb9d37d --- /dev/null +++ b/lib/gs-odrs-provider.c @@ -0,0 +1,1998 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016-2018 Kalev Lember <klember@redhat.com> + * Copyright (C) 2021 Endless OS Foundation LLC + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/* + * SECTION:gs-odrs-provider + * @short_description: Provides review data from the Open Desktop Ratings Service. + * + * To test this plugin locally you will probably want to build and run the + * `odrs-web` container, following the instructions in the + * [`odrs-web` repository](https://gitlab.gnome.org/Infrastructure/odrs-web/-/blob/HEAD/app_data/README.md), + * and then get gnome-software to use your local review server by running: + * ``` + * gsettings set org.gnome.software review-server 'http://127.0.0.1:5000/1.0/reviews/api' + * ``` + * + * When you are done with development, run the following command to use the real + * ODRS server again: + * ``` + * gsettings reset org.gnome.software review-server + * ``` + * + * Since: 41 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> +#include <glib/gi18n.h> +#include <gnome-software.h> +#include <json-glib/json-glib.h> +#include <libsoup/soup.h> +#include <locale.h> +#include <math.h> +#include <string.h> + +G_DEFINE_QUARK (gs-odrs-provider-error-quark, gs_odrs_provider_error) + +/* Element in self->ratings, all allocated in one big block and sorted + * alphabetically to reduce the number of allocations and fragmentation. */ +typedef struct { + gchar *app_id; /* (owned) */ + guint32 n_star_ratings[6]; +} GsOdrsRating; + +static int +rating_compare (const GsOdrsRating *a, const GsOdrsRating *b) +{ + return g_strcmp0 (a->app_id, b->app_id); +} + +static void +rating_clear (GsOdrsRating *rating) +{ + g_free (rating->app_id); +} + +struct _GsOdrsProvider +{ + GObject parent_instance; + + gchar *distro; /* (not nullable) (owned) */ + gchar *user_hash; /* (not nullable) (owned) */ + gchar *review_server; /* (not nullable) (owned) */ + GArray *ratings; /* (element-type GsOdrsRating) (mutex ratings_mutex) (owned) (nullable) */ + GMutex ratings_mutex; + guint64 max_cache_age_secs; + guint n_results_max; + SoupSession *session; /* (owned) (not nullable) */ +}; + +G_DEFINE_TYPE (GsOdrsProvider, gs_odrs_provider, G_TYPE_OBJECT) + +typedef enum { + PROP_REVIEW_SERVER = 1, + PROP_USER_HASH, + PROP_DISTRO, + PROP_MAX_CACHE_AGE_SECS, + PROP_N_RESULTS_MAX, + PROP_SESSION, +} GsOdrsProviderProperty; + +static GParamSpec *obj_props[PROP_SESSION + 1] = { NULL, }; + +static gboolean +gs_odrs_provider_load_ratings_for_app (JsonObject *json_app, + const gchar *app_id, + GsOdrsRating *rating_out) +{ + guint i; + const gchar *names[] = { "star0", "star1", "star2", "star3", + "star4", "star5", NULL }; + + for (i = 0; names[i] != NULL; i++) { + if (!json_object_has_member (json_app, names[i])) + return FALSE; + rating_out->n_star_ratings[i] = (guint64) json_object_get_int_member (json_app, names[i]); + } + + rating_out->app_id = g_strdup (app_id); + + return TRUE; +} + +static gboolean +gs_odrs_provider_load_ratings (GsOdrsProvider *self, + const gchar *filename, + GError **error) +{ + JsonNode *json_root; + JsonObject *json_item; + g_autoptr(JsonParser) json_parser = NULL; + const gchar *app_id; + JsonNode *json_app_node; + JsonObjectIter iter; + g_autoptr(GArray) new_ratings = NULL; + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GError) local_error = NULL; + + /* parse the data and find the success */ + json_parser = json_parser_new_immutable (); + if (!json_parser_load_from_mapped_file (json_parser, filename, &local_error)) { + g_set_error (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "Error parsing ODRS data: %s", local_error->message); + return FALSE; + } + json_root = json_parser_get_root (json_parser); + if (json_root == NULL) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no ratings root"); + return FALSE; + } + if (json_node_get_node_type (json_root) != JSON_NODE_OBJECT) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no ratings array"); + return FALSE; + } + + json_item = json_node_get_object (json_root); + + new_ratings = g_array_sized_new (FALSE, /* don’t zero-terminate */ + FALSE, /* don’t clear */ + sizeof (GsOdrsRating), + json_object_get_size (json_item)); + g_array_set_clear_func (new_ratings, (GDestroyNotify) rating_clear); + + /* parse each app */ + json_object_iter_init (&iter, json_item); + while (json_object_iter_next (&iter, &app_id, &json_app_node)) { + GsOdrsRating rating; + JsonObject *json_app; + + if (!JSON_NODE_HOLDS_OBJECT (json_app_node)) + continue; + json_app = json_node_get_object (json_app_node); + + if (gs_odrs_provider_load_ratings_for_app (json_app, app_id, &rating)) + g_array_append_val (new_ratings, rating); + } + + /* Allow for binary searches later. */ + g_array_sort (new_ratings, (GCompareFunc) rating_compare); + + /* Update the shared state */ + locker = g_mutex_locker_new (&self->ratings_mutex); + g_clear_pointer (&self->ratings, g_array_unref); + self->ratings = g_steal_pointer (&new_ratings); + + return TRUE; +} + +static AsReview * +gs_odrs_provider_parse_review_object (JsonObject *item) +{ + AsReview *rev = as_review_new (); + + /* date */ + if (json_object_has_member (item, "date_created")) { + gint64 timestamp; + g_autoptr(GDateTime) dt = NULL; + timestamp = json_object_get_int_member (item, "date_created"); + dt = g_date_time_new_from_unix_utc (timestamp); + as_review_set_date (rev, dt); + } + + /* assemble review */ + if (json_object_has_member (item, "rating")) + as_review_set_rating (rev, (gint) json_object_get_int_member (item, "rating")); + if (json_object_has_member (item, "score")) { + as_review_set_priority (rev, (gint) json_object_get_int_member (item, "score")); + } else if (json_object_has_member (item, "karma_up") && + json_object_has_member (item, "karma_down")) { + gdouble ku = (gdouble) json_object_get_int_member (item, "karma_up"); + gdouble kd = (gdouble) json_object_get_int_member (item, "karma_down"); + gdouble wilson = 0.f; + + /* from http://www.evanmiller.org/how-not-to-sort-by-average-rating.html */ + if (ku > 0 || kd > 0) { + wilson = ((ku + 1.9208) / (ku + kd) - + 1.96 * sqrt ((ku * kd) / (ku + kd) + 0.9604) / + (ku + kd)) / (1 + 3.8416 / (ku + kd)); + wilson *= 100.f; + } + as_review_set_priority (rev, (gint) wilson); + } + if (json_object_has_member (item, "user_hash")) + as_review_set_reviewer_id (rev, json_object_get_string_member (item, "user_hash")); + if (json_object_has_member (item, "user_display")) { + g_autofree gchar *user_display = g_strdup (json_object_get_string_member (item, "user_display")); + if (user_display) + g_strstrip (user_display); + as_review_set_reviewer_name (rev, user_display); + } + if (json_object_has_member (item, "summary")) { + g_autofree gchar *summary = g_strdup (json_object_get_string_member (item, "summary")); + if (summary) + g_strstrip (summary); + as_review_set_summary (rev, summary); + } + if (json_object_has_member (item, "description")) { + g_autofree gchar *description = g_strdup (json_object_get_string_member (item, "description")); + if (description) + g_strstrip (description); + as_review_set_description (rev, description); + } + if (json_object_has_member (item, "version")) + as_review_set_version (rev, json_object_get_string_member (item, "version")); + + /* add extra metadata for the plugin */ + if (json_object_has_member (item, "user_skey")) { + as_review_add_metadata (rev, "user_skey", + json_object_get_string_member (item, "user_skey")); + } + if (json_object_has_member (item, "app_id")) { + as_review_add_metadata (rev, "app_id", + json_object_get_string_member (item, "app_id")); + } + if (json_object_has_member (item, "review_id")) { + g_autofree gchar *review_id = NULL; + review_id = g_strdup_printf ("%" G_GINT64_FORMAT, + json_object_get_int_member (item, "review_id")); + as_review_set_id (rev, review_id); + } + + /* don't allow multiple votes */ + if (json_object_has_member (item, "vote_id")) + as_review_add_flags (rev, AS_REVIEW_FLAG_VOTED); + + return rev; +} + +/* json_parser_load*() must have been called on @json_parser before calling + * this function. */ +static GPtrArray * +gs_odrs_provider_parse_reviews (GsOdrsProvider *self, + JsonParser *json_parser, + GError **error) +{ + JsonArray *json_reviews; + JsonNode *json_root; + guint i; + g_autoptr(GHashTable) reviewer_ids = NULL; + g_autoptr(GPtrArray) reviews = NULL; + g_autoptr(GError) local_error = NULL; + + json_root = json_parser_get_root (json_parser); + if (json_root == NULL) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no root"); + return NULL; + } + if (json_node_get_node_type (json_root) != JSON_NODE_ARRAY) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no array"); + return NULL; + } + + /* parse each rating */ + reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + json_reviews = json_node_get_array (json_root); + reviewer_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + for (i = 0; i < json_array_get_length (json_reviews); i++) { + JsonNode *json_review; + JsonObject *json_item; + const gchar *reviewer_id; + g_autoptr(AsReview) review = NULL; + + /* extract the data */ + json_review = json_array_get_element (json_reviews, i); + if (json_node_get_node_type (json_review) != JSON_NODE_OBJECT) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no object type"); + return NULL; + } + json_item = json_node_get_object (json_review); + if (json_item == NULL) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no object"); + return NULL; + } + + /* create review */ + review = gs_odrs_provider_parse_review_object (json_item); + + reviewer_id = as_review_get_reviewer_id (review); + if (reviewer_id == NULL) + continue; + + /* dedupe each on the user_hash */ + if (g_hash_table_lookup (reviewer_ids, reviewer_id) != NULL) { + g_debug ("duplicate review %s, skipping", reviewer_id); + continue; + } + g_hash_table_add (reviewer_ids, g_strdup (reviewer_id)); + g_ptr_array_add (reviews, g_object_ref (review)); + } + return g_steal_pointer (&reviews); +} + +static gboolean +gs_odrs_provider_parse_success (GInputStream *input_stream, + GError **error) +{ + JsonNode *json_root; + JsonObject *json_item; + const gchar *msg = NULL; + g_autoptr(JsonParser) json_parser = NULL; + g_autoptr(GError) local_error = NULL; + + /* parse the data and find the success + * FIXME: This should probably eventually be refactored and made async */ + json_parser = json_parser_new_immutable (); + if (!json_parser_load_from_stream (json_parser, input_stream, NULL, &local_error)) { + g_set_error (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "Error parsing ODRS data: %s", local_error->message); + return FALSE; + } + json_root = json_parser_get_root (json_parser); + if (json_root == NULL) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no error root"); + return FALSE; + } + if (json_node_get_node_type (json_root) != JSON_NODE_OBJECT) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no error object"); + return FALSE; + } + json_item = json_node_get_object (json_root); + if (json_item == NULL) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "no error object"); + return FALSE; + } + + /* failed? */ + if (json_object_has_member (json_item, "msg")) + msg = json_object_get_string_member (json_item, "msg"); + if (!json_object_get_boolean_member (json_item, "success")) { + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + msg != NULL ? msg : "unknown failure"); + return FALSE; + } + + /* just for the console */ + if (msg != NULL) + g_debug ("success: %s", msg); + return TRUE; +} + +#if SOUP_CHECK_VERSION(3, 0, 0) +typedef struct { + GInputStream *input_stream; + gssize length; + goffset read_from; +} MessageData; + +static MessageData * +message_data_new (GInputStream *input_stream, + gssize length) +{ + MessageData *md; + + md = g_slice_new0 (MessageData); + md->input_stream = g_object_ref (input_stream); + md->length = length; + + if (G_IS_SEEKABLE (input_stream)) + md->read_from = g_seekable_tell (G_SEEKABLE (input_stream)); + + return md; +} + +static void +message_data_free (gpointer ptr, + GClosure *closure) +{ + MessageData *md = ptr; + + if (md) { + g_object_unref (md->input_stream); + g_slice_free (MessageData, md); + } +} + +static void +g_odrs_provider_message_restarted_cb (SoupMessage *message, + gpointer user_data) +{ + MessageData *md = user_data; + + if (G_IS_SEEKABLE (md->input_stream) && md->read_from != g_seekable_tell (G_SEEKABLE (md->input_stream))) + g_seekable_seek (G_SEEKABLE (md->input_stream), md->read_from, G_SEEK_SET, NULL, NULL); + + soup_message_set_request_body (message, NULL, md->input_stream, md->length); +} + +static void +g_odrs_provider_set_message_request_body (SoupMessage *message, + const gchar *content_type, + gconstpointer data, + gsize length) +{ + MessageData *md; + GInputStream *input_stream; + + g_return_if_fail (SOUP_IS_MESSAGE (message)); + g_return_if_fail (data != NULL); + + input_stream = g_memory_input_stream_new_from_data (g_memdup2 (data, length), length, g_free); + md = message_data_new (input_stream, length); + + g_signal_connect_data (message, "restarted", + G_CALLBACK (g_odrs_provider_message_restarted_cb), md, message_data_free, 0); + + soup_message_set_request_body (message, content_type, input_stream, length); + + g_object_unref (input_stream); +} +#endif + +static gboolean +gs_odrs_provider_json_post (SoupSession *session, + const gchar *uri, + const gchar *data, + GCancellable *cancellable, + GError **error) +{ + guint status_code; + g_autoptr(SoupMessage) msg = NULL; + gconstpointer downloaded_data; + gsize downloaded_data_length; + g_autoptr(GInputStream) input_stream = NULL; +#if SOUP_CHECK_VERSION(3, 0, 0) + g_autoptr(GBytes) bytes = NULL; +#endif + /* create the GET data */ + g_debug ("Sending ODRS request to %s: %s", uri, data); + msg = soup_message_new (SOUP_METHOD_POST, uri); +#if SOUP_CHECK_VERSION(3, 0, 0) + g_odrs_provider_set_message_request_body (msg, "application/json; charset=utf-8", + data, strlen (data)); + bytes = soup_session_send_and_read (session, msg, cancellable, error); + if (bytes == NULL) + return FALSE; + + downloaded_data = g_bytes_get_data (bytes, &downloaded_data_length); + status_code = soup_message_get_status (msg); +#else + soup_message_set_request (msg, "application/json; charset=utf-8", + SOUP_MEMORY_COPY, data, strlen (data)); + + /* set sync request */ + status_code = soup_session_send_message (session, msg); + downloaded_data = msg->response_body ? msg->response_body->data : NULL; + downloaded_data_length = msg->response_body ? msg->response_body->length : 0; +#endif + g_debug ("ODRS server returned status %u: %.*s", status_code, (gint) downloaded_data_length, (const gchar *) downloaded_data); + if (status_code != SOUP_STATUS_OK) { + g_warning ("Failed to set rating on ODRS: %s", + soup_status_get_phrase (status_code)); + g_set_error (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_SERVER_ERROR, + "Failed to submit review to ODRS: %s", soup_status_get_phrase (status_code)); + return FALSE; + } + + /* process returned JSON */ + input_stream = g_memory_input_stream_new_from_data (downloaded_data, downloaded_data_length, NULL); + return gs_odrs_provider_parse_success (input_stream, error); +} + +static GPtrArray * +_gs_app_get_reviewable_ids (GsApp *app) +{ + GPtrArray *ids = g_ptr_array_new_with_free_func (g_free); + GPtrArray *provided = gs_app_get_provided (app); + + /* add the main component id */ + g_ptr_array_add (ids, g_strdup (gs_app_get_id (app))); + + /* add any ID provides */ + for (guint i = 0; i < provided->len; i++) { + GPtrArray *items; + AsProvided *prov = g_ptr_array_index (provided, i); + if (as_provided_get_kind (prov) != AS_PROVIDED_KIND_ID) + continue; + + items = as_provided_get_items (prov); + for (guint j = 0; j < items->len; j++) { + const gchar *value = (const gchar *) g_ptr_array_index (items, j); + if (value == NULL) + continue; + g_ptr_array_add (ids, g_strdup (value)); + } + } + return ids; +} + +static gboolean +gs_odrs_provider_refine_ratings (GsOdrsProvider *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + gint rating; + guint32 ratings_raw[6] = { 0, 0, 0, 0, 0, 0 }; + guint cnt = 0; + g_autoptr(GArray) review_ratings = NULL; + g_autoptr(GPtrArray) reviewable_ids = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + /* get ratings for each reviewable ID */ + reviewable_ids = _gs_app_get_reviewable_ids (app); + + locker = g_mutex_locker_new (&self->ratings_mutex); + + if (!self->ratings) { + g_autofree gchar *cache_filename = NULL; + + g_clear_pointer (&locker, g_mutex_locker_free); + + /* Load from the local cache, if available, when in offline or + when refresh/download disabled on start */ + cache_filename = gs_utils_get_cache_filename ("odrs", + "ratings.json", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + error); + + if (!cache_filename) + return TRUE; + + if (!gs_odrs_provider_load_ratings (self, cache_filename, NULL)) { + g_autoptr(GFile) cache_file = g_file_new_for_path (cache_filename); + g_debug ("Failed to load cache file ‘%s’, deleting it", cache_filename); + g_file_delete (cache_file, NULL, NULL); + return TRUE; + } + + locker = g_mutex_locker_new (&self->ratings_mutex); + + if (!self->ratings) + return TRUE; + } + + for (guint i = 0; i < reviewable_ids->len; i++) { + const gchar *id = g_ptr_array_index (reviewable_ids, i); + const GsOdrsRating search_rating = { (gchar *) id, { 0, }}; + guint found_index; + const GsOdrsRating *found_rating; + + if (!g_array_binary_search (self->ratings, &search_rating, + (GCompareFunc) rating_compare, &found_index)) + continue; + + found_rating = &g_array_index (self->ratings, GsOdrsRating, found_index); + + /* copy into accumulator array */ + for (guint j = 0; j < 6; j++) + ratings_raw[j] += found_rating->n_star_ratings[j]; + cnt++; + } + if (cnt == 0) + return TRUE; + + /* Done with self->ratings now */ + g_clear_pointer (&locker, g_mutex_locker_free); + + /* merge to accumulator array back to one GArray blob */ + review_ratings = g_array_sized_new (FALSE, TRUE, sizeof(guint32), 6); + for (guint i = 0; i < 6; i++) + g_array_append_val (review_ratings, ratings_raw[i]); + gs_app_set_review_ratings (app, review_ratings); + + /* find the wilson rating */ + rating = gs_utils_get_wilson_rating (g_array_index (review_ratings, guint32, 1), + g_array_index (review_ratings, guint32, 2), + g_array_index (review_ratings, guint32, 3), + g_array_index (review_ratings, guint32, 4), + g_array_index (review_ratings, guint32, 5)); + if (rating > 0) + gs_app_set_rating (app, rating); + return TRUE; +} + +static JsonNode * +gs_odrs_provider_get_compat_ids (GsApp *app) +{ + GPtrArray *provided = gs_app_get_provided (app); + g_autoptr(GHashTable) ids = NULL; + g_autoptr(JsonArray) json_array = json_array_new (); + g_autoptr(JsonNode) json_node = json_node_new (JSON_NODE_ARRAY); + + ids = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); + for (guint i = 0; i < provided->len; i++) { + GPtrArray *items; + AsProvided *prov = g_ptr_array_index (provided, i); + + if (as_provided_get_kind (prov) != AS_PROVIDED_KIND_ID) + continue; + + items = as_provided_get_items (prov); + for (guint j = 0; j < items->len; j++) { + const gchar *value = g_ptr_array_index (items, j); + if (value == NULL) + continue; + + if (g_hash_table_add (ids, (gpointer) value)) + json_array_add_string_element (json_array, value); + } + } + if (json_array_get_length (json_array) == 0) + return NULL; + json_node_set_array (json_node, json_array); + return g_steal_pointer (&json_node); +} + +static void open_input_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void parse_reviews_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void set_reviews_on_app (GsOdrsProvider *self, + GsApp *app, + GPtrArray *reviews); + +typedef struct { + GsApp *app; /* (not nullable) (owned) */ + gchar *cache_filename; /* (not nullable) (owned) */ + SoupMessage *message; /* (nullable) (owned) */ +} FetchReviewsForAppData; + +static void +fetch_reviews_for_app_data_free (FetchReviewsForAppData *data) +{ + g_clear_object (&data->app); + g_free (data->cache_filename); + g_clear_object (&data->message); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (FetchReviewsForAppData, fetch_reviews_for_app_data_free) + +static void +gs_odrs_provider_fetch_reviews_for_app_async (GsOdrsProvider *self, + GsApp *app, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + JsonNode *json_compat_ids; + const gchar *version; + g_autofree gchar *cachefn_basename = NULL; + g_autofree gchar *cachefn = NULL; + g_autofree gchar *request_body = NULL; + g_autofree gchar *uri = NULL; + g_autoptr(GFile) cachefn_file = NULL; + g_autoptr(GPtrArray) reviews = NULL; + g_autoptr(JsonBuilder) builder = NULL; + g_autoptr(JsonParser) json_parser = NULL; + g_autoptr(JsonGenerator) json_generator = NULL; + g_autoptr(JsonNode) json_root = NULL; + g_autoptr(SoupMessage) msg = NULL; +#if SOUP_CHECK_VERSION(3, 0, 0) + g_autoptr(GBytes) bytes = NULL; +#endif + g_autoptr(GTask) task = NULL; + FetchReviewsForAppData *data; + g_autoptr(FetchReviewsForAppData) data_owned = NULL; + g_autoptr(GError) local_error = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_odrs_provider_fetch_reviews_for_app_async); + + data = data_owned = g_new0 (FetchReviewsForAppData, 1); + data->app = g_object_ref (app); + g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) fetch_reviews_for_app_data_free); + + /* look in the cache */ + cachefn_basename = g_strdup_printf ("%s.json", gs_app_get_id (app)); + cachefn = gs_utils_get_cache_filename ("odrs", + cachefn_basename, + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + &local_error); + if (cachefn == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + data->cache_filename = g_strdup (cachefn); + cachefn_file = g_file_new_for_path (cachefn); + if (gs_utils_get_file_age (cachefn_file) < self->max_cache_age_secs) { + g_debug ("got review data for %s from %s", + gs_app_get_id (app), cachefn); + + /* parse the data and find the array of ratings */ + json_parser = json_parser_new_immutable (); + if (!json_parser_load_from_mapped_file (json_parser, cachefn, &local_error)) { + g_task_return_new_error (task, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "Error parsing ODRS data: %s", local_error->message); + return; + } + + reviews = gs_odrs_provider_parse_reviews (self, json_parser, &local_error); + if (reviews == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + } else { + set_reviews_on_app (self, app, reviews); + g_task_return_boolean (task, TRUE); + } + + return; + } + + /* not always available */ + version = gs_app_get_version (app); + if (version == NULL) + version = "unknown"; + + /* create object with review data */ + builder = json_builder_new (); + json_builder_begin_object (builder); + json_builder_set_member_name (builder, "user_hash"); + json_builder_add_string_value (builder, self->user_hash); + json_builder_set_member_name (builder, "app_id"); + json_builder_add_string_value (builder, gs_app_get_id (app)); + json_builder_set_member_name (builder, "locale"); + json_builder_add_string_value (builder, setlocale (LC_MESSAGES, NULL)); + json_builder_set_member_name (builder, "distro"); + json_builder_add_string_value (builder, self->distro); + json_builder_set_member_name (builder, "version"); + json_builder_add_string_value (builder, version); + json_builder_set_member_name (builder, "limit"); + json_builder_add_int_value (builder, self->n_results_max); + json_compat_ids = gs_odrs_provider_get_compat_ids (app); + if (json_compat_ids != NULL) { + json_builder_set_member_name (builder, "compat_ids"); + json_builder_add_value (builder, json_compat_ids); + } + json_builder_end_object (builder); + + /* export as a string */ + json_root = json_builder_get_root (builder); + json_generator = json_generator_new (); + json_generator_set_pretty (json_generator, TRUE); + json_generator_set_root (json_generator, json_root); + request_body = json_generator_to_data (json_generator, NULL); + + uri = g_strdup_printf ("%s/fetch", self->review_server); + g_debug ("Updating ODRS cache for %s from %s to %s; request %s", gs_app_get_id (app), + uri, cachefn, request_body); + msg = soup_message_new (SOUP_METHOD_POST, uri); + data->message = g_object_ref (msg); + +#if SOUP_CHECK_VERSION(3, 0, 0) + g_odrs_provider_set_message_request_body (msg, "application/json; charset=utf-8", + request_body, strlen (request_body)); + soup_session_send_async (self->session, msg, G_PRIORITY_DEFAULT, + cancellable, open_input_stream_cb, g_steal_pointer (&task)); +#else + soup_message_set_request (msg, "application/json; charset=utf-8", + SOUP_MEMORY_COPY, request_body, strlen (request_body)); + soup_session_send_async (self->session, msg, cancellable, + open_input_stream_cb, g_steal_pointer (&task)); +#endif +} + +static void +open_input_stream_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SoupSession *soup_session = SOUP_SESSION (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + FetchReviewsForAppData *data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GInputStream) input_stream = NULL; + guint status_code; + g_autoptr(JsonParser) json_parser = NULL; + g_autoptr(GError) local_error = NULL; + +#if SOUP_CHECK_VERSION(3, 0, 0) + input_stream = soup_session_send_finish (soup_session, result, &local_error); + status_code = soup_message_get_status (data->message); +#else + input_stream = soup_session_send_finish (soup_session, result, &local_error); + status_code = data->message->status_code; +#endif + + if (input_stream == NULL) { + if (!g_network_monitor_get_network_available (g_network_monitor_get_default ())) + g_task_return_new_error (task, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_NO_NETWORK, + "server couldn't be reached"); + else + g_task_return_new_error (task, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "server returned no data"); + return; + } + + if (status_code != SOUP_STATUS_OK) { + if (!gs_odrs_provider_parse_success (input_stream, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* not sure what to do here */ + g_task_return_new_error (task, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_DOWNLOADING, + "status code invalid"); + return; + } + + /* parse the data and find the array of ratings */ + json_parser = json_parser_new_immutable (); + json_parser_load_from_stream_async (json_parser, input_stream, cancellable, parse_reviews_cb, g_steal_pointer (&task)); +} + +static void +parse_reviews_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + JsonParser *json_parser = JSON_PARSER (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsOdrsProvider *self = g_task_get_source_object (task); + FetchReviewsForAppData *data = g_task_get_task_data (task); + g_autoptr(GPtrArray) reviews = NULL; + g_autoptr(JsonGenerator) cache_generator = NULL; + g_autoptr(GError) local_error = NULL; + + if (!json_parser_load_from_stream_finish (json_parser, result, &local_error)) { + g_task_return_new_error (task, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "Error parsing ODRS data: %s", local_error->message); + return; + } + + reviews = gs_odrs_provider_parse_reviews (self, json_parser, &local_error); + if (reviews == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* save to the cache */ + cache_generator = json_generator_new (); + json_generator_set_pretty (cache_generator, FALSE); + json_generator_set_root (cache_generator, json_parser_get_root (json_parser)); + + if (!json_generator_to_file (cache_generator, data->cache_filename, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + set_reviews_on_app (self, data->app, reviews); + + /* success */ + g_task_return_boolean (task, TRUE); +} + +static void +set_reviews_on_app (GsOdrsProvider *self, + GsApp *app, + GPtrArray *reviews) +{ + for (guint i = 0; i < reviews->len; i++) { + AsReview *review = g_ptr_array_index (reviews, i); + + /* save this on the application object so we can use it for + * submitting a new review */ + if (i == 0) { + gs_app_set_metadata (app, "ODRS::user_skey", + as_review_get_metadata_item (review, "user_skey")); + } + + /* ignore invalid reviews */ + if (as_review_get_rating (review) == 0) + continue; + + /* the user_hash matches, so mark this as our own review */ + if (g_strcmp0 (as_review_get_reviewer_id (review), + self->user_hash) == 0) { + as_review_set_flags (review, AS_REVIEW_FLAG_SELF); + } + gs_app_add_review (app, review); + } +} + +static gboolean +gs_odrs_provider_fetch_reviews_for_app_finish (GsOdrsProvider *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gchar * +gs_odrs_provider_trim_version (const gchar *version) +{ + gchar *str; + gchar *tmp; + + /* nothing set */ + if (version == NULL) + return g_strdup ("unknown"); + + /* remove epoch */ + str = g_strrstr (version, ":"); + if (str != NULL) + version = str + 1; + + /* remove release */ + tmp = g_strdup (version); + g_strdelimit (tmp, "-", '\0'); + + /* remove '+dfsg' suffix */ + str = g_strstr_len (tmp, -1, "+dfsg"); + if (str != NULL) + *str = '\0'; + + return tmp; +} + +static gboolean +gs_odrs_provider_invalidate_cache (AsReview *review, GError **error) +{ + g_autofree gchar *cachefn_basename = NULL; + g_autofree gchar *cachefn = NULL; + g_autoptr(GFile) cachefn_file = NULL; + + /* look in the cache */ + cachefn_basename = g_strdup_printf ("%s.json", + as_review_get_metadata_item (review, "app_id")); + cachefn = gs_utils_get_cache_filename ("odrs", + cachefn_basename, + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + error); + if (cachefn == NULL) + return FALSE; + cachefn_file = g_file_new_for_path (cachefn); + if (!g_file_query_exists (cachefn_file, NULL)) + return TRUE; + return g_file_delete (cachefn_file, NULL, error); +} + +static gboolean +gs_odrs_provider_vote (GsOdrsProvider *self, + AsReview *review, + const gchar *uri, + GCancellable *cancellable, + GError **error) +{ + const gchar *tmp; + g_autofree gchar *data = NULL; + g_autoptr(JsonBuilder) builder = NULL; + g_autoptr(JsonGenerator) json_generator = NULL; + g_autoptr(JsonNode) json_root = NULL; + + /* create object with vote data */ + builder = json_builder_new (); + json_builder_begin_object (builder); + + json_builder_set_member_name (builder, "user_hash"); + json_builder_add_string_value (builder, self->user_hash); + json_builder_set_member_name (builder, "user_skey"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "user_skey")); + json_builder_set_member_name (builder, "app_id"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "app_id")); + tmp = as_review_get_id (review); + if (tmp != NULL) { + gint64 review_id; + json_builder_set_member_name (builder, "review_id"); + review_id = g_ascii_strtoll (tmp, NULL, 10); + json_builder_add_int_value (builder, review_id); + } + json_builder_end_object (builder); + + /* export as a string */ + json_root = json_builder_get_root (builder); + json_generator = json_generator_new (); + json_generator_set_pretty (json_generator, TRUE); + json_generator_set_root (json_generator, json_root); + data = json_generator_to_data (json_generator, NULL); + if (data == NULL) + return FALSE; + + /* clear cache */ + if (!gs_odrs_provider_invalidate_cache (review, error)) + return FALSE; + + /* send to server */ + if (!gs_odrs_provider_json_post (self->session, uri, data, cancellable, error)) + return FALSE; + + /* mark as voted */ + as_review_add_flags (review, AS_REVIEW_FLAG_VOTED); + + /* success */ + return TRUE; +} + +static GsApp * +gs_odrs_provider_create_app_dummy (const gchar *id) +{ + GsApp *app = gs_app_new (id); + g_autoptr(GString) str = NULL; + str = g_string_new (id); + as_gstring_replace (str, ".desktop", ""); + g_string_prepend (str, "No description is available for "); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, "Unknown Application"); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, "Application not found"); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, str->str); + return app; +} + +static void +gs_odrs_provider_init (GsOdrsProvider *self) +{ + g_mutex_init (&self->ratings_mutex); +} + +static void +gs_odrs_provider_constructed (GObject *object) +{ + GsOdrsProvider *self = GS_ODRS_PROVIDER (object); + + G_OBJECT_CLASS (gs_odrs_provider_parent_class)->constructed (object); + + /* Check all required properties have been set. */ + g_assert (self->review_server != NULL); + g_assert (self->user_hash != NULL); + g_assert (self->distro != NULL); +} + +static void +gs_odrs_provider_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsOdrsProvider *self = GS_ODRS_PROVIDER (object); + + switch ((GsOdrsProviderProperty) prop_id) { + case PROP_REVIEW_SERVER: + g_value_set_string (value, self->review_server); + break; + case PROP_USER_HASH: + g_value_set_string (value, self->user_hash); + break; + case PROP_DISTRO: + g_value_set_string (value, self->distro); + break; + case PROP_MAX_CACHE_AGE_SECS: + g_value_set_uint64 (value, self->max_cache_age_secs); + break; + case PROP_N_RESULTS_MAX: + g_value_set_uint (value, self->n_results_max); + break; + case PROP_SESSION: + g_value_set_object (value, self->session); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_odrs_provider_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsOdrsProvider *self = GS_ODRS_PROVIDER (object); + + switch ((GsOdrsProviderProperty) prop_id) { + case PROP_REVIEW_SERVER: + /* Construct-only */ + g_assert (self->review_server == NULL); + self->review_server = g_value_dup_string (value); + break; + case PROP_USER_HASH: + /* Construct-only */ + g_assert (self->user_hash == NULL); + self->user_hash = g_value_dup_string (value); + break; + case PROP_DISTRO: + /* Construct-only */ + g_assert (self->distro == NULL); + self->distro = g_value_dup_string (value); + break; + case PROP_MAX_CACHE_AGE_SECS: + /* Construct-only */ + g_assert (self->max_cache_age_secs == 0); + self->max_cache_age_secs = g_value_get_uint64 (value); + break; + case PROP_N_RESULTS_MAX: + /* Construct-only */ + g_assert (self->n_results_max == 0); + self->n_results_max = g_value_get_uint (value); + break; + case PROP_SESSION: + /* Construct-only */ + g_assert (self->session == NULL); + self->session = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_odrs_provider_dispose (GObject *object) +{ + GsOdrsProvider *self = GS_ODRS_PROVIDER (object); + + g_clear_object (&self->session); + + G_OBJECT_CLASS (gs_odrs_provider_parent_class)->dispose (object); +} + +static void +gs_odrs_provider_finalize (GObject *object) +{ + GsOdrsProvider *self = GS_ODRS_PROVIDER (object); + + g_free (self->user_hash); + g_free (self->distro); + g_free (self->review_server); + g_clear_pointer (&self->ratings, g_array_unref); + g_mutex_clear (&self->ratings_mutex); + + G_OBJECT_CLASS (gs_odrs_provider_parent_class)->finalize (object); +} + +static void +gs_odrs_provider_class_init (GsOdrsProviderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = gs_odrs_provider_constructed; + object_class->get_property = gs_odrs_provider_get_property; + object_class->set_property = gs_odrs_provider_set_property; + object_class->dispose = gs_odrs_provider_dispose; + object_class->finalize = gs_odrs_provider_finalize; + + /** + * GsOdrsProvider:review-server: (not nullable) + * + * The URI of the ODRS review server to contact. + * + * Since: 41 + */ + obj_props[PROP_REVIEW_SERVER] = + g_param_spec_string ("review-server", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * GsOdrsProvider:user-hash: (not nullable) + * + * An opaque hash of the user identifier, used to identify the user on + * the server. + * + * Since: 41 + */ + obj_props[PROP_USER_HASH] = + g_param_spec_string ("user-hash", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * GsOdrsProvider:distro: (not nullable) + * + * A human readable string identifying the current distribution. + * + * Since: 41 + */ + obj_props[PROP_DISTRO] = + g_param_spec_string ("distro", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * GsOdrsProvider:max-cache-age-secs: + * + * The maximum age of the ODRS cache files, in seconds. Older files will + * be refreshed on demand. + * + * Since: 41 + */ + obj_props[PROP_MAX_CACHE_AGE_SECS] = + g_param_spec_uint64 ("max-cache-age-secs", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * GsOdrsProvider:n-results-max: + * + * Maximum number of reviews or ratings to download. The default value + * of 0 means no limit is applied. + * + * Since: 41 + */ + obj_props[PROP_N_RESULTS_MAX] = + g_param_spec_uint ("n-results-max", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * GsOdrsProvider:session: (not nullable) + * + * #SoupSession to use for downloading things. + * + * Since: 41 + */ + obj_props[PROP_SESSION] = + g_param_spec_object ("session", NULL, NULL, + SOUP_TYPE_SESSION, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); +} + +/** + * gs_odrs_provider_new: + * @review_server: (not nullable): value for #GsOdrsProvider:review-server + * @user_hash: (not nullable): value for #GsOdrsProvider:user-hash + * @distro: (not nullable): value for #GsOdrsProvider:distro + * @max_cache_age_secs: value for #GsOdrsProvider:max-cache-age-secs + * @n_results_max: value for #GsOdrsProvider:n-results-max + * @session: value for #GsOdrsProvider:session + * + * Create a new #GsOdrsProvider. This does no network activity. + * + * Returns: (transfer full): a new #GsOdrsProvider + * Since: 41 + */ +GsOdrsProvider * +gs_odrs_provider_new (const gchar *review_server, + const gchar *user_hash, + const gchar *distro, + guint64 max_cache_age_secs, + guint n_results_max, + SoupSession *session) +{ + g_return_val_if_fail (review_server != NULL && *review_server != '\0', NULL); + g_return_val_if_fail (user_hash != NULL && *user_hash != '\0', NULL); + g_return_val_if_fail (distro != NULL && *distro != '\0', NULL); + g_return_val_if_fail (SOUP_IS_SESSION (session), NULL); + + return g_object_new (GS_TYPE_ODRS_PROVIDER, + "review-server", review_server, + "user-hash", user_hash, + "distro", distro, + "max-cache-age-secs", max_cache_age_secs, + "n-results-max", n_results_max, + "session", session, + NULL); +} + +static void download_ratings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/** + * gs_odrs_provider_refresh_ratings_async: + * @self: a #GsOdrsProvider + * @cache_age_secs: cache age, in seconds, as passed to #GsPluginClass.refresh_metadata_async() + * @progress_callback: (nullable): callback to call with progress information + * @progress_user_data: (nullable) (closure progress_callback): data to pass + * to @progress_callback + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: function to call when the asynchronous operation is complete + * @user_data: data to pass to @callback + * + * Refresh the cached ODRS ratings and re-load them asynchronously. + * + * Since: 42 + */ +void +gs_odrs_provider_refresh_ratings_async (GsOdrsProvider *self, + guint64 cache_age_secs, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autofree gchar *cache_filename = NULL; + g_autoptr(GFile) cache_file = NULL; + g_autofree gchar *uri = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_odrs_provider_refresh_ratings_async); + + /* check cache age */ + cache_filename = gs_utils_get_cache_filename ("odrs", + "ratings.json", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + &error_local); + if (cache_filename == NULL) { + g_task_return_error (task, g_steal_pointer (&error_local)); + return; + } + + cache_file = g_file_new_for_path (cache_filename); + g_task_set_task_data (task, g_object_ref (cache_file), g_object_unref); + + if (cache_age_secs > 0) { + guint64 tmp; + + tmp = gs_utils_get_file_age (cache_file); + if (tmp < cache_age_secs) { + g_debug ("%s is only %" G_GUINT64_FORMAT " seconds old, so ignoring refresh", + cache_filename, tmp); + if (!gs_odrs_provider_load_ratings (self, cache_filename, &error_local)) { + g_debug ("Failed to load cache file ‘%s’, deleting it", cache_filename); + g_file_delete (cache_file, NULL, NULL); + + g_task_return_error (task, g_steal_pointer (&error_local)); + } else { + g_task_return_boolean (task, TRUE); + } + return; + } + } + + /* download the complete file */ + uri = g_strdup_printf ("%s/ratings", self->review_server); + g_debug ("Updating ODRS cache from %s to %s", uri, cache_filename); + + gs_download_file_async (self->session, uri, cache_file, G_PRIORITY_LOW, + progress_callback, progress_user_data, + cancellable, download_ratings_cb, g_steal_pointer (&task)); +} + +static void +download_ratings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SoupSession *soup_session = SOUP_SESSION (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsOdrsProvider *self = g_task_get_source_object (task); + GFile *cache_file = g_task_get_task_data (task); + const gchar *cache_file_path = NULL; + g_autoptr(GError) local_error = NULL; + + if (!gs_download_file_finish (soup_session, result, &local_error) && + !g_error_matches (local_error, GS_DOWNLOAD_ERROR, GS_DOWNLOAD_ERROR_NOT_MODIFIED)) { + g_task_return_new_error (task, GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_DOWNLOADING, + "%s", local_error->message); + return; + } + + g_clear_error (&local_error); + + cache_file_path = g_file_peek_path (cache_file); + if (!gs_odrs_provider_load_ratings (self, cache_file_path, &local_error)) { + g_debug ("Failed to load cache file ‘%s’, deleting it", cache_file_path); + g_file_delete (cache_file, NULL, NULL); + + g_task_return_new_error (task, GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "%s", local_error->message); + } else { + g_task_return_boolean (task, TRUE); + } +} + +/** + * gs_odrs_provider_refresh_ratings_finish: + * @self: a #GsOdrsProvider + * @result: result of the asynchronous operation + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous refresh operation started with + * gs_odrs_provider_refresh_ratings_async(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_odrs_provider_refresh_ratings_finish (GsOdrsProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GS_IS_ODRS_PROVIDER (self), FALSE); + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_odrs_provider_refresh_ratings_async, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void refine_app_op (GsOdrsProvider *self, + GTask *task, + GsApp *app, + GsOdrsProviderRefineFlags flags, + GCancellable *cancellable); +static void refine_reviews_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_refine_op (GTask *task, + GError *error); + +typedef struct { + /* Input data. */ + GsAppList *list; /* (owned) (not nullable) */ + GsOdrsProviderRefineFlags flags; + + /* In-progress data. */ + guint n_pending_ops; + GError *error; /* (nullable) (owned) */ +} RefineData; + +static void +refine_data_free (RefineData *data) +{ + g_assert (data->n_pending_ops == 0); + + g_clear_object (&data->list); + g_clear_error (&data->error); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (RefineData, refine_data_free) + +/** + * gs_odrs_provider_refine_async: + * @self: a #GsOdrsProvider + * @list: list of apps to refine + * @flags: refine flags + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: callback for asynchronous completion + * @user_data: data to pass to @callback + * + * Asynchronously refine the given @list of apps to add ratings and review data + * to them, as specified in @flags. + * + * Since: 42 + */ +void +gs_odrs_provider_refine_async (GsOdrsProvider *self, + GsAppList *list, + GsOdrsProviderRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(RefineData) data = NULL; + RefineData *data_unowned = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_odrs_provider_refine_async); + + data_unowned = data = g_new0 (RefineData, 1); + data->list = g_object_ref (list); + data->flags = flags; + g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) refine_data_free); + + if ((flags & (GS_ODRS_PROVIDER_REFINE_FLAGS_GET_RATINGS | + GS_ODRS_PROVIDER_REFINE_FLAGS_GET_REVIEWS)) == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + /* Mark one operation as pending while all the operations are started, + * so the overall operation can’t complete while things are still being + * started. */ + data_unowned->n_pending_ops++; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* not valid */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_ADDON) + continue; + if (gs_app_get_id (app) == NULL) + continue; + + data_unowned->n_pending_ops++; + refine_app_op (self, task, app, flags, cancellable); + } + + finish_refine_op (task, NULL); +} + +static void +refine_app_op (GsOdrsProvider *self, + GTask *task, + GsApp *app, + GsOdrsProviderRefineFlags flags, + GCancellable *cancellable) +{ + g_autoptr(GError) local_error = NULL; + + /* add ratings if possible */ + if ((flags & GS_ODRS_PROVIDER_REFINE_FLAGS_GET_RATINGS) && + gs_app_get_review_ratings (app) == NULL) { + if (!gs_odrs_provider_refine_ratings (self, app, cancellable, &local_error)) { + if (g_error_matches (local_error, GS_ODRS_PROVIDER_ERROR, GS_ODRS_PROVIDER_ERROR_NO_NETWORK)) { + g_debug ("failed to refine app %s: %s", + gs_app_get_unique_id (app), local_error->message); + } else { + g_prefix_error (&local_error, "failed to refine app: "); + finish_refine_op (task, g_steal_pointer (&local_error)); + return; + } + } + } + + /* add reviews if possible */ + if ((flags & GS_ODRS_PROVIDER_REFINE_FLAGS_GET_REVIEWS) && + gs_app_get_reviews (app)->len == 0) { + /* get from server asynchronously */ + gs_odrs_provider_fetch_reviews_for_app_async (self, app, cancellable, refine_reviews_cb, g_object_ref (task)); + } else { + finish_refine_op (task, NULL); + } +} + +static void +refine_reviews_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsOdrsProvider *self = GS_ODRS_PROVIDER (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + if (!gs_odrs_provider_fetch_reviews_for_app_finish (self, result, &local_error)) { + if (g_error_matches (local_error, GS_ODRS_PROVIDER_ERROR, GS_ODRS_PROVIDER_ERROR_NO_NETWORK)) { + g_debug ("failed to refine app: %s", local_error->message); + } else { + g_prefix_error (&local_error, "failed to refine app: "); + finish_refine_op (task, g_steal_pointer (&local_error)); + return; + } + } + + finish_refine_op (task, NULL); +} + +/* @error is (transfer full) if non-NULL. */ +static void +finish_refine_op (GTask *task, + GError *error) +{ + RefineData *data = g_task_get_task_data (task); + g_autoptr(GError) error_owned = g_steal_pointer (&error); + + if (data->error == NULL && error_owned != NULL) + data->error = g_steal_pointer (&error_owned); + else if (error_owned != NULL) + g_debug ("Additional error while refining ODRS data: %s", error_owned->message); + + g_assert (data->n_pending_ops > 0); + data->n_pending_ops--; + + if (data->n_pending_ops == 0) { + if (data->error != NULL) + g_task_return_error (task, g_steal_pointer (&data->error)); + else + g_task_return_boolean (task, TRUE); + } +} + +/** + * gs_odrs_provider_refine_finish: + * @self: a #GsOdrsProvider + * @result: result of the asynchronous operation + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous refine operation started with + * gs_odrs_provider_refine_finish(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_odrs_provider_refine_finish (GsOdrsProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GS_IS_ODRS_PROVIDER (self), FALSE); + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, gs_odrs_provider_refine_async), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * gs_odrs_provider_submit_review: + * @self: a #GsOdrsProvider + * @app: the app being reviewed + * @review: the review + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError + * + * Submit a new @review for @app. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_odrs_provider_submit_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *data = NULL; + g_autofree gchar *uri = NULL; + g_autofree gchar *version = NULL; + g_autoptr(JsonBuilder) builder = NULL; + g_autoptr(JsonGenerator) json_generator = NULL; + g_autoptr(JsonNode) json_root = NULL; + + /* save as we don't re-request the review from the server */ + as_review_add_flags (review, AS_REVIEW_FLAG_SELF); + as_review_set_reviewer_name (review, g_get_real_name ()); + as_review_add_metadata (review, "app_id", gs_app_get_id (app)); + as_review_add_metadata (review, "user_skey", + gs_app_get_metadata_item (app, "ODRS::user_skey")); + + /* create object with review data */ + builder = json_builder_new (); + json_builder_begin_object (builder); + json_builder_set_member_name (builder, "user_hash"); + json_builder_add_string_value (builder, self->user_hash); + json_builder_set_member_name (builder, "user_skey"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "user_skey")); + json_builder_set_member_name (builder, "app_id"); + json_builder_add_string_value (builder, + as_review_get_metadata_item (review, "app_id")); + json_builder_set_member_name (builder, "locale"); + json_builder_add_string_value (builder, setlocale (LC_MESSAGES, NULL)); + json_builder_set_member_name (builder, "distro"); + json_builder_add_string_value (builder, self->distro); + json_builder_set_member_name (builder, "version"); + version = gs_odrs_provider_trim_version (as_review_get_version (review)); + json_builder_add_string_value (builder, version); + json_builder_set_member_name (builder, "user_display"); + json_builder_add_string_value (builder, as_review_get_reviewer_name (review)); + json_builder_set_member_name (builder, "summary"); + json_builder_add_string_value (builder, as_review_get_summary (review)); + json_builder_set_member_name (builder, "description"); + json_builder_add_string_value (builder, as_review_get_description (review)); + json_builder_set_member_name (builder, "rating"); + json_builder_add_int_value (builder, as_review_get_rating (review)); + json_builder_end_object (builder); + + /* export as a string */ + json_root = json_builder_get_root (builder); + json_generator = json_generator_new (); + json_generator_set_pretty (json_generator, TRUE); + json_generator_set_root (json_generator, json_root); + data = json_generator_to_data (json_generator, NULL); + + /* clear cache */ + if (!gs_odrs_provider_invalidate_cache (review, error)) + return FALSE; + + /* POST */ + uri = g_strdup_printf ("%s/submit", self->review_server); + if (!gs_odrs_provider_json_post (self->session, uri, data, cancellable, error)) + return FALSE; + + /* modify the local app */ + gs_app_add_review (app, review); + + return TRUE; +} + +/** + * gs_odrs_provider_report_review: + * @self: a #GsOdrsProvider + * @app: the app whose review is being reported + * @review: the review to report + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError + * + * Report the given @review on @app for being incorrect or breaking the code of + * conduct. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_odrs_provider_report_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/report", self->review_server); + return gs_odrs_provider_vote (self, review, uri, cancellable, error); +} + +/** + * gs_odrs_provider_upvote_review: + * @self: a #GsOdrsProvider + * @app: the app whose review is being upvoted + * @review: the review to upvote + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError + * + * Add one vote to @review on @app. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_odrs_provider_upvote_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/upvote", self->review_server); + return gs_odrs_provider_vote (self, review, uri, cancellable, error); +} + +/** + * gs_odrs_provider_downvote_review: + * @self: a #GsOdrsProvider + * @app: the app whose review is being downvoted + * @review: the review to downvote + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError + * + * Remove one vote from @review on @app. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_odrs_provider_downvote_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/downvote", self->review_server); + return gs_odrs_provider_vote (self, review, uri, cancellable, error); +} + +/** + * gs_odrs_provider_dismiss_review: + * @self: a #GsOdrsProvider + * @app: the app whose review is being dismissed + * @review: the review to dismiss + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError + * + * Dismiss (ignore) @review on @app when moderating. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_odrs_provider_dismiss_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/dismiss", self->review_server); + return gs_odrs_provider_vote (self, review, uri, cancellable, error); +} + +/** + * gs_odrs_provider_remove_review: + * @self: a #GsOdrsProvider + * @app: the app whose review is being removed + * @review: the review to remove + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError + * + * Remove a @review written by the user, from @app. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_odrs_provider_remove_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *uri = NULL; + uri = g_strdup_printf ("%s/remove", self->review_server); + if (!gs_odrs_provider_vote (self, review, uri, cancellable, error)) + return FALSE; + + /* update the local app */ + gs_app_remove_review (app, review); + + return TRUE; +} + +/** + * gs_odrs_provider_add_unvoted_reviews: + * @self: a #GsOdrsProvider + * @list: list of apps to add unvoted reviews to + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError + * + * Add the unmoderated reviews for each app in @list to the apps. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 41 + */ +gboolean +gs_odrs_provider_add_unvoted_reviews (GsOdrsProvider *self, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + guint status_code; + guint i; + gconstpointer downloaded_data; + gsize downloaded_data_length; + g_autofree gchar *uri = NULL; + g_autoptr(GHashTable) hash = NULL; + g_autoptr(JsonParser) json_parser = NULL; + g_autoptr(GPtrArray) reviews = NULL; + g_autoptr(SoupMessage) msg = NULL; +#if SOUP_CHECK_VERSION(3, 0, 0) + g_autoptr(GBytes) bytes = NULL; +#endif + g_autoptr(GError) local_error = NULL; + + /* create the GET data *with* the machine hash so we can later + * review the application ourselves */ + uri = g_strdup_printf ("%s/moderate/%s/%s", + self->review_server, + self->user_hash, + setlocale (LC_MESSAGES, NULL)); + msg = soup_message_new (SOUP_METHOD_GET, uri); +#if SOUP_CHECK_VERSION(3, 0, 0) + bytes = soup_session_send_and_read (self->session, msg, cancellable, error); + if (bytes == NULL) + return FALSE; + + downloaded_data = g_bytes_get_data (bytes, &downloaded_data_length); + status_code = soup_message_get_status (msg); +#else + status_code = soup_session_send_message (self->session, msg); + downloaded_data = msg->response_body ? msg->response_body->data : NULL; + downloaded_data_length = msg->response_body ? msg->response_body->length : 0; +#endif + if (status_code != SOUP_STATUS_OK) { + g_autoptr(GInputStream) input_stream = g_memory_input_stream_new_from_data (downloaded_data, downloaded_data_length, NULL); + if (!gs_odrs_provider_parse_success (input_stream, error)) + return FALSE; + /* not sure what to do here */ + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_DOWNLOADING, + "status code invalid"); + return FALSE; + } + g_debug ("odrs returned: %.*s", (gint) downloaded_data_length, (const gchar *) downloaded_data); + + /* nothing */ + if (downloaded_data == NULL) { + if (!g_network_monitor_get_network_available (g_network_monitor_get_default ())) + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_NO_NETWORK, + "server couldn't be reached"); + else + g_set_error_literal (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "server returned no data"); + return FALSE; + } + + /* parse the data and find the array of ratings */ + json_parser = json_parser_new_immutable (); + if (!json_parser_load_from_data (json_parser, downloaded_data, downloaded_data_length, &local_error)) { + g_set_error (error, + GS_ODRS_PROVIDER_ERROR, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + "Error parsing ODRS data: %s", local_error->message); + return FALSE; + } + + reviews = gs_odrs_provider_parse_reviews (self, json_parser, error); + if (reviews == NULL) + return FALSE; + + /* look at all the reviews; faking application objects */ + hash = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + for (i = 0; i < reviews->len; i++) { + GsApp *app; + AsReview *review; + const gchar *app_id; + + /* same app? */ + review = g_ptr_array_index (reviews, i); + app_id = as_review_get_metadata_item (review, "app_id"); + app = g_hash_table_lookup (hash, app_id); + if (app == NULL) { + app = gs_odrs_provider_create_app_dummy (app_id); + gs_app_list_add (list, app); + g_hash_table_insert (hash, g_strdup (app_id), app); + } + gs_app_add_review (app, review); + } + + return TRUE; +} diff --git a/lib/gs-odrs-provider.h b/lib/gs-odrs-provider.h new file mode 100644 index 0000000..f02ffde --- /dev/null +++ b/lib/gs-odrs-provider.h @@ -0,0 +1,125 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <libsoup/soup.h> + +#include "gs-app-list.h" +#include "gs-download-utils.h" + +G_BEGIN_DECLS + +/** + * GsOdrsProviderError: + * @GS_ODRS_PROVIDER_ERROR_DOWNLOADING: Error while downloading ODRS data. + * @GS_ODRS_PROVIDER_ERROR_PARSING_DATA: Problem parsing downloaded ODRS data. + * @GS_ODRS_PROVIDER_ERROR_NO_NETWORK: Offline or network unavailable. + * @GS_ODRS_PROVIDER_ERROR_SERVER_ERROR: Server rejected ODRS submission or returned an error. + * + * Error codes for #GsOdrsProvider. + * + * Since: 42 + */ +typedef enum { + GS_ODRS_PROVIDER_ERROR_DOWNLOADING, + GS_ODRS_PROVIDER_ERROR_PARSING_DATA, + GS_ODRS_PROVIDER_ERROR_NO_NETWORK, + GS_ODRS_PROVIDER_ERROR_SERVER_ERROR, +} GsOdrsProviderError; + +#define GS_ODRS_PROVIDER_ERROR gs_odrs_provider_error_quark () +GQuark gs_odrs_provider_error_quark (void); + +/** + * GsOdrsProviderRefineFlags: + * @GS_ODRS_PROVIDER_REFINE_FLAGS_GET_RATINGS: Get the numerical ratings for the app. + * @GS_ODRS_PROVIDER_REFINE_FLAGS_GET_REVIEWS: Get the written reviews for the app. + * + * The flags for refining apps to get their reviews or ratings. + * + * Since: 42 + */ +typedef enum { + GS_ODRS_PROVIDER_REFINE_FLAGS_GET_RATINGS = (1 << 0), + GS_ODRS_PROVIDER_REFINE_FLAGS_GET_REVIEWS = (1 << 1), +} GsOdrsProviderRefineFlags; + +#define GS_TYPE_ODRS_PROVIDER (gs_odrs_provider_get_type ()) + +G_DECLARE_FINAL_TYPE (GsOdrsProvider, gs_odrs_provider, GS, ODRS_PROVIDER, GObject) + +GsOdrsProvider *gs_odrs_provider_new (const gchar *review_server, + const gchar *user_hash, + const gchar *distro, + guint64 max_cache_age_secs, + guint n_results_max, + SoupSession *session); + +void gs_odrs_provider_refresh_ratings_async (GsOdrsProvider *self, + guint64 cache_age_secs, + GsDownloadProgressCallback progress_callback, + gpointer progress_user_data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_odrs_provider_refresh_ratings_finish(GsOdrsProvider *self, + GAsyncResult *result, + GError **error); + +void gs_odrs_provider_refine_async (GsOdrsProvider *self, + GsAppList *list, + GsOdrsProviderRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_odrs_provider_refine_finish (GsOdrsProvider *self, + GAsyncResult *result, + GError **error); + +gboolean gs_odrs_provider_submit_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); +gboolean gs_odrs_provider_report_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); +gboolean gs_odrs_provider_upvote_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); +gboolean gs_odrs_provider_downvote_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); +gboolean gs_odrs_provider_dismiss_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); +gboolean gs_odrs_provider_remove_review (GsOdrsProvider *self, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); + +gboolean gs_odrs_provider_add_unvoted_reviews (GsOdrsProvider *self, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/lib/gs-os-release.c b/lib/gs-os-release.c new file mode 100644 index 0000000..191a385 --- /dev/null +++ b/lib/gs-os-release.c @@ -0,0 +1,347 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/** + * SECTION:gs-os-release + * @title: GsOsRelease + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Data from os-release + * + * This object allows plugins to parse /etc/os-release for distribution + * metadata information. + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-os-release.h" + +struct _GsOsRelease +{ + GObject parent_instance; + gchar *name; + gchar *version; + gchar *id; + gchar **id_like; + gchar *version_id; + gchar *pretty_name; + gchar *cpe_name; + gchar *distro_codename; + gchar *home_url; +}; + +static void gs_os_release_initable_iface_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GsOsRelease, gs_os_release, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, gs_os_release_initable_iface_init)) + +static gboolean +gs_os_release_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + GsOsRelease *os_release = GS_OS_RELEASE (initable); + const gchar *filename; + g_autofree gchar *data = NULL; + g_auto(GStrv) lines = NULL; + guint i; + + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* get contents */ + filename = g_getenv ("GS_SELF_TEST_OS_RELEASE_FILENAME"); + if (filename == NULL) { + filename = "/etc/os-release"; + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) + filename = "/usr/lib/os-release"; + } + if (!g_file_get_contents (filename, &data, NULL, error)) + return FALSE; + + /* parse */ + lines = g_strsplit (data, "\n", -1); + for (i = 0; lines[i] != NULL; i++) { + gchar *tmp; + + /* split the line up into two halves */ + tmp = g_strstr_len (lines[i], -1, "="); + if (tmp == NULL) + continue; + *tmp = '\0'; + tmp++; + + /* ignore trailing quote */ + if (tmp[0] == '\"') + tmp++; + + /* ignore trailing quote */ + g_strdelimit (tmp, "\"", '\0'); + + /* match fields we're interested in */ + if (g_strcmp0 (lines[i], "NAME") == 0) { + os_release->name = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "VERSION") == 0) { + os_release->version = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "ID") == 0) { + os_release->id = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "ID_LIKE") == 0) { + os_release->id_like = g_strsplit (tmp, " ", 0); + continue; + } + if (g_strcmp0 (lines[i], "VERSION_ID") == 0) { + os_release->version_id = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "PRETTY_NAME") == 0) { + os_release->pretty_name = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "CPE_NAME") == 0) { + os_release->cpe_name = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "UBUNTU_CODENAME") == 0) { + os_release->distro_codename = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "HOME_URL") == 0) { + os_release->home_url = g_strdup (tmp); + continue; + } + } + return TRUE; +} + +/** + * gs_os_release_get_name: + * @os_release: A #GsOsRelease + * + * Gets the name from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_name (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->name; +} + +/** + * gs_os_release_get_version: + * @os_release: A #GsOsRelease + * + * Gets the version from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_version (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->version; +} + +/** + * gs_os_release_get_id: + * @os_release: A #GsOsRelease + * + * Gets the ID from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_id (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->id; +} + +/** + * gs_os_release_get_id_like: + * @os_release: A #GsOsRelease + * + * Gets the ID_LIKE from the os-release parser. This is a list of operating + * systems that are "closely related" to the local operating system, possibly + * by being a derivative distribution. + * + * Returns: a %NULL terminated list + * + * Since: 3.26.2 + **/ +const gchar * const * +gs_os_release_get_id_like (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return (const gchar * const *) os_release->id_like; +} + +/** + * gs_os_release_get_version_id: + * @os_release: A #GsOsRelease + * + * Gets the version ID from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_version_id (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->version_id; +} + +/** + * gs_os_release_get_pretty_name: + * @os_release: A #GsOsRelease + * + * Gets the pretty name from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_pretty_name (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->pretty_name; +} + +/** + * gs_os_release_get_cpe_name: + * @os_release: A #GsOsRelease + * + * Gets the pretty name from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_cpe_name (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->cpe_name; +} + +/** + * gs_os_release_get_distro_codename: + * @os_release: A #GsOsRelease + * + * Gets the distro codename from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_distro_codename (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->distro_codename; +} + +/** + * gs_os_release_get_home_url: + * @os_release: A #GsOsRelease + * + * Gets the home URL from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_home_url (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->home_url; +} + +static void +gs_os_release_finalize (GObject *object) +{ + GsOsRelease *os_release = GS_OS_RELEASE (object); + g_free (os_release->name); + g_free (os_release->version); + g_free (os_release->id); + g_strfreev (os_release->id_like); + g_free (os_release->version_id); + g_free (os_release->pretty_name); + g_free (os_release->cpe_name); + g_free (os_release->distro_codename); + g_free (os_release->home_url); + G_OBJECT_CLASS (gs_os_release_parent_class)->finalize (object); +} + +static void +gs_os_release_class_init (GsOsReleaseClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_os_release_finalize; +} + +static void +gs_os_release_initable_iface_init (GInitableIface *iface) +{ + iface->init = gs_os_release_initable_init; +} + +static void +gs_os_release_init (GsOsRelease *os_release) +{ +} + +/** + * gs_os_release_new: + * @error: a #GError, or %NULL + * + * Returns a new reference to a #GsOsRelease. The information may be cached. + * + * Returns: (transfer full): A new reference to a #GsOsRelease, or %NULL for error + * + * Since: 3.22 + **/ +GsOsRelease * +gs_os_release_new (GError **error) +{ + static gsize initialised = 0; + static GsOsRelease *os_release = NULL; + static GError *os_release_error = NULL; + + if (g_once_init_enter (&initialised)) { + os_release = g_initable_new (GS_TYPE_OS_RELEASE, NULL, &os_release_error, NULL); + g_once_init_leave (&initialised, 1); + } + + if (os_release != NULL) { + return g_object_ref (os_release); + } else { + g_propagate_error (error, g_error_copy (os_release_error)); + return NULL; + } +} diff --git a/lib/gs-os-release.h b/lib/gs-os-release.h new file mode 100644 index 0000000..336f17c --- /dev/null +++ b/lib/gs-os-release.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" + +G_BEGIN_DECLS + +#define GS_TYPE_OS_RELEASE (gs_os_release_get_type ()) + +G_DECLARE_FINAL_TYPE (GsOsRelease, gs_os_release, GS, OS_RELEASE, GObject) + +GsOsRelease *gs_os_release_new (GError **error); +const gchar *gs_os_release_get_name (GsOsRelease *os_release); +const gchar *gs_os_release_get_version (GsOsRelease *os_release); +const gchar *gs_os_release_get_id (GsOsRelease *os_release); +const gchar * const *gs_os_release_get_id_like (GsOsRelease *os_release); +const gchar *gs_os_release_get_version_id (GsOsRelease *os_release); +const gchar *gs_os_release_get_pretty_name (GsOsRelease *os_release); +const gchar *gs_os_release_get_cpe_name (GsOsRelease *os_release); +const gchar *gs_os_release_get_distro_codename (GsOsRelease *os_release); +const gchar *gs_os_release_get_home_url (GsOsRelease *os_release); + +G_END_DECLS diff --git a/lib/gs-plugin-event.c b/lib/gs-plugin-event.c new file mode 100644 index 0000000..699e529 --- /dev/null +++ b/lib/gs-plugin-event.c @@ -0,0 +1,453 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-event + * @title: GsPluginEvent + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Information about a plugin event + * + * These functions provide a way for plugins to tell the UI layer about events + * that may require displaying to the user. Plugins should not assume that a + * specific event is actually shown to the user as it may be ignored + * automatically. + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-enums.h" +#include "gs-plugin-private.h" +#include "gs-plugin-event.h" +#include "gs-plugin-job.h" +#include "gs-utils.h" + +struct _GsPluginEvent +{ + GObject parent_instance; + GsApp *app; + GsApp *origin; + GsPluginAction action; + GsPluginJob *job; /* (owned) (nullable) */ + GError *error; + GsPluginEventFlag flags; + gchar *unique_id; +}; + +G_DEFINE_TYPE (GsPluginEvent, gs_plugin_event, G_TYPE_OBJECT) + +typedef enum { + PROP_APP = 1, + PROP_ORIGIN, + PROP_ACTION, + PROP_JOB, + PROP_ERROR, +} GsPluginEventProperty; + +static GParamSpec *props[PROP_ERROR + 1] = { NULL, }; + +/** + * gs_plugin_event_get_app: + * @event: A #GsPluginEvent + * + * Gets an application that created the event. + * + * Returns: (transfer none): a #GsApp, or %NULL if unset + * + * Since: 3.22 + **/ +GsApp * +gs_plugin_event_get_app (GsPluginEvent *event) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL); + return event->app; +} + +/** + * gs_plugin_event_get_origin: + * @event: A #GsPluginEvent + * + * Gets an origin that created the event. + * + * Returns: (transfer none): a #GsApp, or %NULL if unset + * + * Since: 3.22 + **/ +GsApp * +gs_plugin_event_get_origin (GsPluginEvent *event) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL); + return event->origin; +} + +/** + * gs_plugin_event_get_action: + * @event: A #GsPluginEvent + * + * Gets an action that created the event. + * + * Returns: (transfer none): a #GsPluginAction, e.g. %GS_PLUGIN_ACTION_UPDATE + * + * Since: 3.22 + **/ +GsPluginAction +gs_plugin_event_get_action (GsPluginEvent *event) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), 0); + return event->action; +} + +/** + * gs_plugin_event_get_job: + * @event: A #GsPluginEvent + * + * Gets the job that created the event. + * + * Returns: (transfer none) (nullable): a #GsPluginJob + * + * Since: 42 + **/ +GsPluginJob * +gs_plugin_event_get_job (GsPluginEvent *event) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL); + return event->job; +} + +/** + * gs_plugin_event_get_unique_id: + * @event: A #GsPluginEvent + * + * Gets the unique ID for the event. In most cases (if an app has been set) + * this will just be the actual #GsApp unique-id. In the cases where only error + * has been set a virtual (but plausible) ID will be generated. + * + * Returns: a string, or %NULL for invalid + * + * Since: 3.22 + **/ +const gchar * +gs_plugin_event_get_unique_id (GsPluginEvent *event) +{ + /* just proxy */ + if (event->origin != NULL && + gs_app_get_unique_id (event->origin) != NULL) { + return gs_app_get_unique_id (event->origin); + } + if (event->app != NULL && + gs_app_get_unique_id (event->app) != NULL) { + return gs_app_get_unique_id (event->app); + } + + /* generate from error */ + if (event->error != NULL) { + if (event->unique_id == NULL) { + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s.error", + gs_plugin_error_to_string (event->error->code)); + event->unique_id = gs_utils_build_unique_id (AS_COMPONENT_SCOPE_UNKNOWN, + AS_BUNDLE_KIND_UNKNOWN, + NULL, + id, + NULL); + } + return event->unique_id; + } + + /* failed */ + return NULL; +} + +/** + * gs_plugin_event_get_kind: + * @event: A #GsPluginEvent + * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID + * + * Adds a flag to the event. + * + * Since: 3.22 + **/ +void +gs_plugin_event_add_flag (GsPluginEvent *event, GsPluginEventFlag flag) +{ + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + event->flags |= flag; +} + +/** + * gs_plugin_event_set_kind: + * @event: A #GsPluginEvent + * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID + * + * Removes a flag from the event. + * + * Since: 3.22 + **/ +void +gs_plugin_event_remove_flag (GsPluginEvent *event, GsPluginEventFlag flag) +{ + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + event->flags &= ~flag; +} + +/** + * gs_plugin_event_has_flag: + * @event: A #GsPluginEvent + * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID + * + * Finds out if the event has a specific flag. + * + * Returns: %TRUE if the flag is set + * + * Since: 3.22 + **/ +gboolean +gs_plugin_event_has_flag (GsPluginEvent *event, GsPluginEventFlag flag) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), FALSE); + return ((event->flags & flag) > 0); +} + +/** + * gs_plugin_event_get_error: + * @event: A #GsPluginEvent + * + * Gets the event error. + * + * Returns: a #GError, or %NULL for unset + * + * Since: 3.22 + **/ +const GError * +gs_plugin_event_get_error (GsPluginEvent *event) +{ + return event->error; +} + +static void +gs_plugin_event_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsPluginEvent *self = GS_PLUGIN_EVENT (object); + + switch ((GsPluginEventProperty) prop_id) { + case PROP_APP: + g_value_set_object (value, self->app); + break; + case PROP_ORIGIN: + g_value_set_object (value, self->origin); + break; + case PROP_ACTION: + g_value_set_enum (value, self->action); + break; + case PROP_JOB: + g_value_set_object (value, self->job); + break; + case PROP_ERROR: + g_value_set_boxed (value, self->error); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_event_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsPluginEvent *self = GS_PLUGIN_EVENT (object); + + switch ((GsPluginEventProperty) prop_id) { + case PROP_APP: + /* Construct only. */ + g_assert (self->app == NULL); + self->app = g_value_dup_object (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_ORIGIN: + /* Construct only. */ + g_assert (self->origin == NULL); + self->origin = g_value_dup_object (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_ACTION: + /* Construct only. */ + g_assert (self->action == GS_PLUGIN_ACTION_UNKNOWN); + self->action = g_value_get_enum (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_JOB: + /* Construct only. */ + g_assert (self->job == NULL); + self->job = g_value_dup_object (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_ERROR: + /* Construct only. */ + g_assert (self->error == NULL); + self->error = g_value_dup_boxed (value); + if (self->error) { + /* Just in case the caller left there any D-Bus remote error notes */ + g_dbus_error_strip_remote_error (self->error); + } + g_object_notify_by_pspec (object, props[prop_id]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_event_dispose (GObject *object) +{ + GsPluginEvent *event = GS_PLUGIN_EVENT (object); + + g_clear_object (&event->app); + g_clear_object (&event->origin); + g_clear_object (&event->job); + + G_OBJECT_CLASS (gs_plugin_event_parent_class)->dispose (object); +} + +static void +gs_plugin_event_finalize (GObject *object) +{ + GsPluginEvent *event = GS_PLUGIN_EVENT (object); + + g_clear_error (&event->error); + g_free (event->unique_id); + + G_OBJECT_CLASS (gs_plugin_event_parent_class)->finalize (object); +} + +static void +gs_plugin_event_class_init (GsPluginEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gs_plugin_event_get_property; + object_class->set_property = gs_plugin_event_set_property; + object_class->dispose = gs_plugin_event_dispose; + object_class->finalize = gs_plugin_event_finalize; + + /** + * GsPluginEvent:app: (nullable) + * + * The application (or source, or whatever component) that caused the + * event to be created. + * + * Since: 42 + */ + props[PROP_APP] = + g_param_spec_object ("app", "App", + "The application (or source, or whatever component) that caused the event to be created.", + GS_TYPE_APP, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginEvent:origin: (nullable) + * + * The origin that caused the event to be created. + * + * Since: 42 + */ + props[PROP_ORIGIN] = + g_param_spec_object ("origin", "Origin", + "The origin that caused the event to be created.", + GS_TYPE_APP, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginEvent:action: + * + * The action that caused the event to be created. + * + * Since: 42 + */ + props[PROP_ACTION] = + g_param_spec_enum ("action", "Action", + "The action that caused the event to be created.", + GS_TYPE_PLUGIN_ACTION, GS_PLUGIN_ACTION_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginEvent:job: (nullable) + * + * The job that caused the event to be created. + * + * Since: 42 + */ + props[PROP_JOB] = + g_param_spec_object ("job", "Job", + "The job that caused the event to be created.", + GS_TYPE_PLUGIN_JOB, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginEvent:error: (nullable) + * + * The error the event is reporting. + * + * Since: 42 + */ + props[PROP_ERROR] = + g_param_spec_boxed ("error", "Error", + "The error the event is reporting.", + G_TYPE_ERROR, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static void +gs_plugin_event_init (GsPluginEvent *event) +{ +} + +/** + * gs_plugin_event_new: + * @first_property_name: the name of the first property + * @...: the value of the first property, followed by zero or more pairs of + * property name/value pairs, then %NULL + * + * Creates a new event. + * + * The arguments are as for g_object_new(): property name/value pairs to set + * the properties of the event. + * + * Returns: (transfer full): A newly allocated #GsPluginEvent + * + * Since: 42 + **/ +GsPluginEvent * +gs_plugin_event_new (const gchar *first_property_name, + ...) +{ + GsPluginEvent *event; + va_list args; + + va_start (args, first_property_name); + event = GS_PLUGIN_EVENT (g_object_new_valist (GS_TYPE_PLUGIN_EVENT, first_property_name, args)); + va_end (args); + + return GS_PLUGIN_EVENT (event); +} diff --git a/lib/gs-plugin-event.h b/lib/gs-plugin-event.h new file mode 100644 index 0000000..949eb85 --- /dev/null +++ b/lib/gs-plugin-event.h @@ -0,0 +1,61 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_EVENT (gs_plugin_event_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginEvent, gs_plugin_event, GS, PLUGIN_EVENT, GObject) + +typedef struct _GsPluginJob GsPluginJob; + +/** + * GsPluginEventFlag: + * @GS_PLUGIN_EVENT_FLAG_NONE: No special flags set + * @GS_PLUGIN_EVENT_FLAG_INVALID: Event is no longer valid, e.g. was dismissed + * @GS_PLUGIN_EVENT_FLAG_VISIBLE: Event is is visible on the screen + * @GS_PLUGIN_EVENT_FLAG_WARNING: Event should be shown with more urgency + * @GS_PLUGIN_EVENT_FLAG_INTERACTIVE: The plugin job was created with interactive=True + * + * Any flags an event can have. + **/ +typedef enum { + GS_PLUGIN_EVENT_FLAG_NONE = 0, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_INVALID = 1 << 0, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_VISIBLE = 1 << 1, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_WARNING = 1 << 2, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_INTERACTIVE = 1 << 3, /* Since: 3.30 */ + GS_PLUGIN_EVENT_FLAG_LAST /*< skip >*/ +} GsPluginEventFlag; + +GsPluginEvent *gs_plugin_event_new (const gchar *first_property_name, + ...) G_GNUC_NULL_TERMINATED; + +const gchar *gs_plugin_event_get_unique_id (GsPluginEvent *event); + +GsApp *gs_plugin_event_get_app (GsPluginEvent *event); +GsApp *gs_plugin_event_get_origin (GsPluginEvent *event); +GsPluginAction gs_plugin_event_get_action (GsPluginEvent *event); +GsPluginJob *gs_plugin_event_get_job (GsPluginEvent *event); +const GError *gs_plugin_event_get_error (GsPluginEvent *event); + +void gs_plugin_event_add_flag (GsPluginEvent *event, + GsPluginEventFlag flag); +void gs_plugin_event_remove_flag (GsPluginEvent *event, + GsPluginEventFlag flag); +gboolean gs_plugin_event_has_flag (GsPluginEvent *event, + GsPluginEventFlag flag); + +G_END_DECLS diff --git a/lib/gs-plugin-helpers.c b/lib/gs-plugin-helpers.c new file mode 100644 index 0000000..40c8a61 --- /dev/null +++ b/lib/gs-plugin-helpers.c @@ -0,0 +1,338 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-helpers + * @short_description: Helpers for storing call closures for #GsPlugin vfuncs + * + * The helpers in this file each create a context structure to store the + * arguments passed to a standard #GsPlugin vfunc. + * + * These are intended to be used by plugin implementations to easily create + * #GTasks for handling #GsPlugin vfunc calls, without all having to write the + * same code to create a structure to wrap the vfunc arguments. + * + * Since: 42 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> + +#include "gs-plugin-helpers.h" + +/** + * gs_plugin_refine_data_new: + * @list: list of #GsApps to refine + * @flags: refine flags + * + * Context data for a call to #GsPluginClass.refine_async. + * + * Returns: (transfer full): context data structure + * Since: 42 + */ +GsPluginRefineData * +gs_plugin_refine_data_new (GsAppList *list, + GsPluginRefineFlags flags) +{ + g_autoptr(GsPluginRefineData) data = g_new0 (GsPluginRefineData, 1); + data->list = g_object_ref (list); + data->flags = flags; + + return g_steal_pointer (&data); +} + +/** + * gs_plugin_refine_data_new_task: + * @source_object: task source object + * @list: list of #GsApps to refine + * @flags: refine flags + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: function to call once asynchronous operation is finished + * @user_data: data to pass to @callback + * + * Create a #GTask for a refine operation with the given arguments. The task + * data will be set to a #GsPluginRefineData containing the given context. + * + * This is essentially a combination of gs_plugin_refine_data_new(), + * g_task_new() and g_task_set_task_data(). + * + * Returns: (transfer full): new #GTask with the given context data + * Since: 42 + */ +GTask * +gs_plugin_refine_data_new_task (gpointer source_object, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = g_task_new (source_object, cancellable, callback, user_data); + g_task_set_task_data (task, gs_plugin_refine_data_new (list, flags), (GDestroyNotify) gs_plugin_refine_data_free); + return g_steal_pointer (&task); +} + +/** + * gs_plugin_refine_data_free: + * @data: (transfer full): a #GsPluginRefineData + * + * Free the given @data. + * + * Since: 42 + */ +void +gs_plugin_refine_data_free (GsPluginRefineData *data) +{ + g_clear_object (&data->list); + g_free (data); +} + +/** + * gs_plugin_refresh_metadata_data_new: + * @cache_age_secs: maximum allowed age of the cache in order for it to remain valid, in seconds + * @flags: refresh metadata flags + * + * Context data for a call to #GsPluginClass.refresh_metadata_async. + * + * Returns: (transfer full): context data structure + * Since: 42 + */ +GsPluginRefreshMetadataData * +gs_plugin_refresh_metadata_data_new (guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags) +{ + g_autoptr(GsPluginRefreshMetadataData) data = g_new0 (GsPluginRefreshMetadataData, 1); + data->cache_age_secs = cache_age_secs; + data->flags = flags; + + return g_steal_pointer (&data); +} + +/** + * gs_plugin_refresh_metadata_data_free: + * @data: (transfer full): a #GsPluginRefreshMetadataData + * + * Free the given @data. + * + * Since: 42 + */ +void +gs_plugin_refresh_metadata_data_free (GsPluginRefreshMetadataData *data) +{ + g_free (data); +} + +/** + * gs_plugin_list_apps_data_new: + * @query: (nullable) (transfer none): a query to filter apps, or %NULL for + * no filtering + * @flags: list apps flags + * + * Context data for a call to #GsPluginClass.list_apps_async. + * + * Returns: (transfer full): context data structure + * Since: 43 + */ +GsPluginListAppsData * +gs_plugin_list_apps_data_new (GsAppQuery *query, + GsPluginListAppsFlags flags) +{ + g_autoptr(GsPluginListAppsData) data = g_new0 (GsPluginListAppsData, 1); + data->query = (query != NULL) ? g_object_ref (query) : NULL; + data->flags = flags; + + return g_steal_pointer (&data); +} + +/** + * gs_plugin_list_apps_data_new_task: + * @source_object: task source object + * @query: (nullable) (transfer none): a query to filter apps, or %NULL for + * no filtering + * @flags: list apps flags + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: function to call once asynchronous operation is finished + * @user_data: data to pass to @callback + * + * Create a #GTask for a list apps operation with the given arguments. The task + * data will be set to a #GsPluginListAppsData containing the given context. + * + * This is essentially a combination of gs_plugin_list_apps_data_new(), + * g_task_new() and g_task_set_task_data(). + * + * Returns: (transfer full): new #GTask with the given context data + * Since: 43 + */ +GTask * +gs_plugin_list_apps_data_new_task (gpointer source_object, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = g_task_new (source_object, cancellable, callback, user_data); + g_task_set_task_data (task, gs_plugin_list_apps_data_new (query, flags), (GDestroyNotify) gs_plugin_list_apps_data_free); + return g_steal_pointer (&task); +} + +/** + * gs_plugin_list_apps_data_free: + * @data: (transfer full): a #GsPluginListAppsData + * + * Free the given @data. + * + * Since: 43 + */ +void +gs_plugin_list_apps_data_free (GsPluginListAppsData *data) +{ + g_clear_object (&data->query); + g_free (data); +} + +/** + * gs_plugin_manage_repository_data_new: + * @repository: (not-nullable) (transfer none): a repository to manage + * @flags: manage repository flags + * + * Common context data for a call to #GsPluginClass.install_repository_async, + * #GsPluginClass.remove_repository_async, #GsPluginClass.enable_repository_async + * and #GsPluginClass.disable_repository_async. + * + * Returns: (transfer full): context data structure + * Since: 43 + */ +GsPluginManageRepositoryData * +gs_plugin_manage_repository_data_new (GsApp *repository, + GsPluginManageRepositoryFlags flags) +{ + g_autoptr(GsPluginManageRepositoryData) data = g_new0 (GsPluginManageRepositoryData, 1); + data->repository = g_object_ref (repository); + data->flags = flags; + + return g_steal_pointer (&data); +} + +/** + * gs_plugin_manage_repository_data_new_task: + * @source_object: task source object + * @repository: (not-nullable) (transfer none): a repository to manage + * @flags: manage repository flags + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: function to call once asynchronous operation is finished + * @user_data: data to pass to @callback + * + * Create a #GTask for a manage repository operation with the given arguments. The task + * data will be set to a #GsPluginManageRepositoryData containing the given context. + * + * This is essentially a combination of gs_plugin_manage_repository_data_new(), + * g_task_new() and g_task_set_task_data(). + * + * Returns: (transfer full): new #GTask with the given context data + * Since: 43 + */ +GTask * +gs_plugin_manage_repository_data_new_task (gpointer source_object, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = g_task_new (source_object, cancellable, callback, user_data); + g_task_set_task_data (task, gs_plugin_manage_repository_data_new (repository, flags), (GDestroyNotify) gs_plugin_manage_repository_data_free); + return g_steal_pointer (&task); +} + +/** + * gs_plugin_manage_repository_data_free: + * @data: (transfer full): a #GsPluginManageRepositoryData + * + * Free the given @data. + * + * Since: 43 + */ +void +gs_plugin_manage_repository_data_free (GsPluginManageRepositoryData *data) +{ + g_clear_object (&data->repository); + g_free (data); +} + +/** + * gs_plugin_refine_categories_data_new: + * @list: (element-type GsCategory): list of #GsCategory objects to refine + * @flags: refine flags + * + * Context data for a call to #GsPluginClass.refine_categories_async. + * + * Returns: (transfer full): context data structure + * Since: 43 + */ +GsPluginRefineCategoriesData * +gs_plugin_refine_categories_data_new (GPtrArray *list, + GsPluginRefineCategoriesFlags flags) +{ + g_autoptr(GsPluginRefineCategoriesData) data = g_new0 (GsPluginRefineCategoriesData, 1); + data->list = g_ptr_array_ref (list); + data->flags = flags; + + return g_steal_pointer (&data); +} + +/** + * gs_plugin_refine_categories_data_new_task: + * @source_object: task source object + * @list: (element-type GsCategory): list of #GsCategory objects to refine + * @flags: refine flags + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: function to call once asynchronous operation is finished + * @user_data: data to pass to @callback + * + * Create a #GTask for a refine categories operation with the given arguments. + * The task data will be set to a #GsPluginRefineCategoriesData containing the + * given context. + * + * This is essentially a combination of gs_plugin_refine_categories_data_new(), + * g_task_new() and g_task_set_task_data(). + * + * Returns: (transfer full): new #GTask with the given context data + * Since: 43 + */ +GTask * +gs_plugin_refine_categories_data_new_task (gpointer source_object, + GPtrArray *list, + GsPluginRefineCategoriesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = g_task_new (source_object, cancellable, callback, user_data); + g_task_set_task_data (task, gs_plugin_refine_categories_data_new (list, flags), (GDestroyNotify) gs_plugin_refine_categories_data_free); + return g_steal_pointer (&task); +} + +/** + * gs_plugin_refine_categories_data_free: + * @data: (transfer full): a #GsPluginRefineCategoriesData + * + * Free the given @data. + * + * Since: 43 + */ +void +gs_plugin_refine_categories_data_free (GsPluginRefineCategoriesData *data) +{ + g_clear_pointer (&data->list, g_ptr_array_unref); + g_free (data); +} diff --git a/lib/gs-plugin-helpers.h b/lib/gs-plugin-helpers.h new file mode 100644 index 0000000..b4a19f3 --- /dev/null +++ b/lib/gs-plugin-helpers.h @@ -0,0 +1,96 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gio/gio.h> +#include <glib.h> +#include <glib-object.h> + +#include <gnome-software.h> + +G_BEGIN_DECLS + +typedef struct { + GsAppList *list; /* (owned) (not nullable) */ + GsPluginRefineFlags flags; +} GsPluginRefineData; + +GsPluginRefineData *gs_plugin_refine_data_new (GsAppList *list, + GsPluginRefineFlags flags); +GTask *gs_plugin_refine_data_new_task (gpointer source_object, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void gs_plugin_refine_data_free (GsPluginRefineData *data); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginRefineData, gs_plugin_refine_data_free) + +typedef struct { + guint64 cache_age_secs; + GsPluginRefreshMetadataFlags flags; +} GsPluginRefreshMetadataData; + +GsPluginRefreshMetadataData *gs_plugin_refresh_metadata_data_new (guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags); +void gs_plugin_refresh_metadata_data_free (GsPluginRefreshMetadataData *data); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginRefreshMetadataData, gs_plugin_refresh_metadata_data_free) + +typedef struct { + GsAppQuery *query; /* (owned) (nullable) */ + GsPluginListAppsFlags flags; +} GsPluginListAppsData; + +GsPluginListAppsData *gs_plugin_list_apps_data_new (GsAppQuery *query, + GsPluginListAppsFlags flags); +GTask *gs_plugin_list_apps_data_new_task (gpointer source_object, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void gs_plugin_list_apps_data_free (GsPluginListAppsData *data); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginListAppsData, gs_plugin_list_apps_data_free) + +typedef struct { + GsApp *repository; /* (owned) (nullable) */ + GsPluginManageRepositoryFlags flags; +} GsPluginManageRepositoryData; + +GsPluginManageRepositoryData * + gs_plugin_manage_repository_data_new (GsApp *repository, + GsPluginManageRepositoryFlags flags); +GTask * gs_plugin_manage_repository_data_new_task (gpointer source_object, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void gs_plugin_manage_repository_data_free (GsPluginManageRepositoryData *data); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginManageRepositoryData, gs_plugin_manage_repository_data_free) + +typedef struct { + GPtrArray *list; /* (element-type GsCategory) (owned) (not nullable) */ + GsPluginRefineCategoriesFlags flags; +} GsPluginRefineCategoriesData; + +GsPluginRefineCategoriesData *gs_plugin_refine_categories_data_new (GPtrArray *list, + GsPluginRefineCategoriesFlags flags); +GTask *gs_plugin_refine_categories_data_new_task (gpointer source_object, + GPtrArray *list, + GsPluginRefineCategoriesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +void gs_plugin_refine_categories_data_free (GsPluginRefineCategoriesData *data); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginRefineCategoriesData, gs_plugin_refine_categories_data_free) + +G_END_DECLS diff --git a/lib/gs-plugin-job-list-apps.c b/lib/gs-plugin-job-list-apps.c new file mode 100644 index 0000000..56a4e14 --- /dev/null +++ b/lib/gs-plugin-job-list-apps.c @@ -0,0 +1,516 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-job-list-apps + * @short_description: A plugin job to list apps according to a search query + * + * #GsPluginJobListApps is a #GsPluginJob representing an operation to + * list apps which match a given query, from all #GsPlugins. + * + * The known properties on the set of apps returned by this operation can be + * controlled with the #GsAppQuery:refine-flags property of the query. All + * results will be refined using the given set of refine flags. See + * #GsPluginJobRefine. + * + * This class is a wrapper around #GsPluginClass.list_apps_async, + * calling it for all loaded plugins, with #GsPluginJobRefine used to refine + * them. + * + * Retrieve the resulting #GsAppList using + * gs_plugin_job_list_apps_get_result_list(). + * + * See also: #GsPluginClass.list_apps_async + * Since: 43 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> + +#include "gs-app.h" +#include "gs-app-list-private.h" +#include "gs-app-query.h" +#include "gs-enums.h" +#include "gs-plugin-job.h" +#include "gs-plugin-job-list-apps.h" +#include "gs-plugin-job-private.h" +#include "gs-plugin-job-refine.h" +#include "gs-plugin-private.h" +#include "gs-plugin-types.h" +#include "gs-utils.h" + +struct _GsPluginJobListApps +{ + GsPluginJob parent; + + /* Input arguments. */ + GsAppQuery *query; /* (owned) (nullable) */ + GsPluginListAppsFlags flags; + + /* In-progress data. */ + GsAppList *merged_list; /* (owned) (nullable) */ + GError *saved_error; /* (owned) (nullable) */ + guint n_pending_ops; + + /* Results. */ + GsAppList *result_list; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPluginJobListApps, gs_plugin_job_list_apps, GS_TYPE_PLUGIN_JOB) + +typedef enum { + PROP_QUERY = 1, + PROP_FLAGS, +} GsPluginJobListAppsProperty; + +static GParamSpec *props[PROP_FLAGS + 1] = { NULL, }; + +static void +gs_plugin_job_list_apps_dispose (GObject *object) +{ + GsPluginJobListApps *self = GS_PLUGIN_JOB_LIST_APPS (object); + + g_assert (self->merged_list == NULL); + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + g_clear_object (&self->result_list); + g_clear_object (&self->query); + + G_OBJECT_CLASS (gs_plugin_job_list_apps_parent_class)->dispose (object); +} + +static void +gs_plugin_job_list_apps_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsPluginJobListApps *self = GS_PLUGIN_JOB_LIST_APPS (object); + + switch ((GsPluginJobListAppsProperty) prop_id) { + case PROP_QUERY: + g_value_set_object (value, self->query); + break; + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_list_apps_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsPluginJobListApps *self = GS_PLUGIN_JOB_LIST_APPS (object); + + switch ((GsPluginJobListAppsProperty) prop_id) { + case PROP_QUERY: + /* Construct only. */ + g_assert (self->query == NULL); + self->query = g_value_dup_object (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_FLAGS: + /* Construct only. */ + g_assert (self->flags == 0); + self->flags = g_value_get_flags (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gboolean +filter_valid_apps (GsApp *app, + gpointer user_data) +{ + GsPluginJobListApps *self = GS_PLUGIN_JOB_LIST_APPS (user_data); + GsPluginRefineFlags refine_flags = GS_PLUGIN_REFINE_FLAGS_NONE; + + if (self->query) + refine_flags = gs_app_query_get_refine_flags (self->query); + + return gs_plugin_loader_app_is_valid (app, refine_flags); +} + +static gboolean +app_filter_qt_for_gtk_and_compatible (GsApp *app, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data); + + /* hide the QT versions in preference to the GTK ones */ + if (g_strcmp0 (gs_app_get_id (app), "transmission-qt.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "nntpgrab_qt.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "gimagereader-qt4.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "gimagereader-qt5.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "nntpgrab_server_qt.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "hotot-qt.desktop") == 0) { + g_debug ("removing QT version of %s", + gs_app_get_unique_id (app)); + return FALSE; + } + + /* hide the KDE version in preference to the GTK one */ + if (g_strcmp0 (gs_app_get_id (app), "qalculate_kde.desktop") == 0) { + g_debug ("removing KDE version of %s", + gs_app_get_unique_id (app)); + return FALSE; + } + + /* hide the KDE version in preference to the Qt one */ + if (g_strcmp0 (gs_app_get_id (app), "kid3.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "kchmviewer.desktop") == 0) { + g_debug ("removing KDE version of %s", + gs_app_get_unique_id (app)); + return FALSE; + } + + return gs_plugin_loader_app_is_compatible (plugin_loader, app); +} + +static void plugin_list_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_op (GTask *task, + GError *error); +static void refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_task (GTask *task, + GsAppList *merged_list); + +static void +gs_plugin_job_list_apps_run_async (GsPluginJob *job, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginJobListApps *self = GS_PLUGIN_JOB_LIST_APPS (job); + g_autoptr(GTask) task = NULL; + GPtrArray *plugins; /* (element-type GsPlugin) */ + gboolean anything_ran = FALSE; + + task = g_task_new (job, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_job_list_apps_run_async); + g_task_set_task_data (task, g_object_ref (plugin_loader), (GDestroyNotify) g_object_unref); + + /* run each plugin, keeping a counter of pending operations which is + * initialised to 1 until all the operations are started */ + self->n_pending_ops = 1; + self->merged_list = gs_app_list_new (); + plugins = gs_plugin_loader_get_plugins (plugin_loader); + + for (guint i = 0; i < plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugins, i); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + + if (!gs_plugin_get_enabled (plugin)) + continue; + if (plugin_class->list_apps_async == NULL) + continue; + + /* at least one plugin supports this vfunc */ + anything_ran = TRUE; + + /* run the plugin */ + self->n_pending_ops++; + plugin_class->list_apps_async (plugin, self->query, self->flags, cancellable, plugin_list_apps_cb, g_object_ref (task)); + } + + if (!anything_ran) + g_debug ("no plugin could handle listing apps"); + + finish_op (task, NULL); +} + +static void +plugin_list_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginJobListApps *self = g_task_get_source_object (task); + g_autoptr(GsAppList) plugin_apps = NULL; + g_autoptr(GError) local_error = NULL; + + plugin_apps = plugin_class->list_apps_finish (plugin, result, &local_error); + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + + if (plugin_apps != NULL) + gs_app_list_add_list (self->merged_list, plugin_apps); + + /* Since #GsAppQuery supports a number of different query parameters, + * not all plugins will support all of them. Ignore errors related to + * that. */ + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) + g_clear_error (&local_error); + + finish_op (task, g_steal_pointer (&local_error)); +} + +/* @error is (transfer full) if non-%NULL */ +static void +finish_op (GTask *task, + GError *error) +{ + GsPluginJobListApps *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + GsPluginLoader *plugin_loader = g_task_get_task_data (task); + g_autoptr(GsAppList) merged_list = NULL; + GsPluginRefineFlags refine_flags = GS_PLUGIN_REFINE_FLAGS_NONE; + g_autoptr(GError) error_owned = g_steal_pointer (&error); + + if (error_owned != NULL && self->saved_error == NULL) + self->saved_error = g_steal_pointer (&error_owned); + else if (error_owned != NULL) + g_debug ("Additional error while listing apps: %s", error_owned->message); + + g_assert (self->n_pending_ops > 0); + self->n_pending_ops--; + + if (self->n_pending_ops > 0) + return; + + /* Get the results of the parallel ops. */ + merged_list = g_steal_pointer (&self->merged_list); + + if (self->saved_error != NULL) { + g_task_return_error (task, g_steal_pointer (&self->saved_error)); + return; + } + + /* run refine() on each one if required */ + if (self->query != NULL) + refine_flags = gs_app_query_get_refine_flags (self->query); + + if (merged_list != NULL && + gs_app_list_length (merged_list) > 0 && + refine_flags != GS_PLUGIN_REFINE_FLAGS_NONE) { + g_autoptr(GsPluginJob) refine_job = NULL; + + refine_job = gs_plugin_job_refine_new (merged_list, + refine_flags | + GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING); + gs_plugin_loader_job_process_async (plugin_loader, refine_job, + cancellable, + refine_cb, + g_object_ref (task)); + } else { + g_debug ("No apps to refine"); + finish_task (task, merged_list); + } +} + +static void +refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GsAppList) new_list = NULL; + g_autoptr(GError) local_error = NULL; + + new_list = gs_plugin_loader_job_process_finish (plugin_loader, result, &local_error); + if (new_list == NULL) { + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + finish_task (task, new_list); +} + +static void +finish_task (GTask *task, + GsAppList *merged_list) +{ + GsPluginJobListApps *self = g_task_get_source_object (task); + GsPluginLoader *plugin_loader = g_task_get_task_data (task); + GsAppListFilterFlags dedupe_flags = GS_APP_LIST_FILTER_FLAG_NONE; + GsAppListSortFunc sort_func = NULL; + gpointer sort_func_data = NULL; + GsAppListFilterFunc filter_func = NULL; + gpointer filter_func_data = NULL; + guint max_results = 0; + g_autofree gchar *job_debug = NULL; + + /* Standard filtering. + * + * FIXME: It feels like this filter should be done in a different layer. */ + gs_app_list_filter (merged_list, filter_valid_apps, self); + gs_app_list_filter (merged_list, app_filter_qt_for_gtk_and_compatible, plugin_loader); + + /* Caller-specified filtering. */ + if (self->query != NULL) + filter_func = gs_app_query_get_filter_func (self->query, &filter_func_data); + + if (filter_func != NULL) + gs_app_list_filter (merged_list, filter_func, filter_func_data); + + /* Filter duplicates with priority, taking into account the source name + * & version, so we combine available updates with the installed app */ + if (self->query != NULL) + dedupe_flags = gs_app_query_get_dedupe_flags (self->query); + + if (dedupe_flags != GS_APP_LIST_FILTER_FLAG_NONE) + gs_app_list_filter_duplicates (merged_list, dedupe_flags); + + /* Sort the results. The refine may have added useful metadata. */ + if (self->query != NULL) + sort_func = gs_app_query_get_sort_func (self->query, &sort_func_data); + + if (sort_func != NULL) { + gs_app_list_sort (merged_list, sort_func, sort_func_data); + } else { + g_debug ("no ->sort_func() set, using random!"); + gs_app_list_randomize (merged_list); + } + + /* Truncate the results if needed. */ + if (self->query != NULL) + max_results = gs_app_query_get_max_results (self->query); + + if (max_results > 0 && gs_app_list_length (merged_list) > max_results) { + g_debug ("truncating results from %u to %u", + gs_app_list_length (merged_list), max_results); + gs_app_list_truncate (merged_list, max_results); + } + + /* show elapsed time */ + job_debug = gs_plugin_job_to_string (GS_PLUGIN_JOB (self)); + g_debug ("%s", job_debug); + + /* Check the intermediate working values are all cleared. */ + g_assert (self->merged_list == NULL); + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + /* success */ + g_set_object (&self->result_list, merged_list); + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_job_list_apps_run_finish (GsPluginJob *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_job_list_apps_class_init (GsPluginJobListAppsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginJobClass *job_class = GS_PLUGIN_JOB_CLASS (klass); + + object_class->dispose = gs_plugin_job_list_apps_dispose; + object_class->get_property = gs_plugin_job_list_apps_get_property; + object_class->set_property = gs_plugin_job_list_apps_set_property; + + job_class->run_async = gs_plugin_job_list_apps_run_async; + job_class->run_finish = gs_plugin_job_list_apps_run_finish; + + /** + * GsPluginJobListApps:query: (nullable) + * + * A #GsAppQuery defining the query parameters. + * + * If this is %NULL, all apps will be returned. + * + * Since: 43 + */ + props[PROP_QUERY] = + g_param_spec_object ("query", "Query", + "A #GsAppQuery defining the query parameters.", + GS_TYPE_APP_QUERY, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginJobListApps:flags: + * + * Flags to specify how the operation should run. + * + * Since: 43 + */ + props[PROP_FLAGS] = + g_param_spec_flags ("flags", "Flags", + "Flags to specify how the operation should run.", + GS_TYPE_PLUGIN_LIST_APPS_FLAGS, + GS_PLUGIN_LIST_APPS_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static void +gs_plugin_job_list_apps_init (GsPluginJobListApps *self) +{ +} + +/** + * gs_plugin_job_list_apps_new: + * @query: (nullable) (transfer none): query to affect which apps to return + * @flags: flags affecting how the operation runs + * + * Create a new #GsPluginJobListApps for listing apps according to the given + * @query. + * + * Returns: (transfer full): a new #GsPluginJobListApps + * Since: 43 + */ +GsPluginJob * +gs_plugin_job_list_apps_new (GsAppQuery *query, + GsPluginListAppsFlags flags) +{ + g_return_val_if_fail (query == NULL || GS_IS_APP_QUERY (query), NULL); + + return g_object_new (GS_TYPE_PLUGIN_JOB_LIST_APPS, + "query", query, + "flags", flags, + NULL); +} + +/** + * gs_plugin_job_list_apps_get_result_list: + * @self: a #GsPluginJobListApps + * + * Get the full list of apps matching the query. + * + * If this is called before the job is complete, %NULL will be returned. + * + * Returns: (transfer none) (nullable): the job results, or %NULL on error + * or if called before the job has completed + * Since: 43 + */ +GsAppList * +gs_plugin_job_list_apps_get_result_list (GsPluginJobListApps *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB_LIST_APPS (self), NULL); + + return self->result_list; +} diff --git a/lib/gs-plugin-job-list-apps.h b/lib/gs-plugin-job-list-apps.h new file mode 100644 index 0000000..e6aec46 --- /dev/null +++ b/lib/gs-plugin-job-list-apps.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-app-query.h" +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB_LIST_APPS (gs_plugin_job_list_apps_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginJobListApps, gs_plugin_job_list_apps, GS, PLUGIN_JOB_LIST_APPS, GsPluginJob) + +GsPluginJob *gs_plugin_job_list_apps_new (GsAppQuery *query, + GsPluginListAppsFlags flags); + +GsAppList *gs_plugin_job_list_apps_get_result_list (GsPluginJobListApps *self); + +G_END_DECLS diff --git a/lib/gs-plugin-job-list-categories.c b/lib/gs-plugin-job-list-categories.c new file mode 100644 index 0000000..f2d1644 --- /dev/null +++ b/lib/gs-plugin-job-list-categories.c @@ -0,0 +1,347 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-job-list-categories + * @short_description: A plugin job to list categories + * + * #GsPluginJobListCategories is a #GsPluginJob representing an operation to + * list categories. + * + * All results will be refined using the given set of refine flags, similarly to + * how #GsPluginJobRefine refines apps. + * + * This class is a wrapper around #GsPluginClass.refine_categories_async, + * calling it for all loaded plugins on the list of categories exposed by a + * #GsCategoryManager. + * + * Retrieve the resulting #GPtrArray of #GsCategory objects using + * gs_plugin_job_list_categories_get_result_list(). + * + * See also: #GsPluginClass.refine_categories_async + * Since: 43 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> + +#include "gs-category.h" +#include "gs-category-private.h" +#include "gs-enums.h" +#include "gs-plugin-job.h" +#include "gs-plugin-job-list-categories.h" +#include "gs-plugin-job-private.h" +#include "gs-plugin-private.h" +#include "gs-plugin-types.h" +#include "gs-utils.h" + +struct _GsPluginJobListCategories +{ + GsPluginJob parent; + + /* Input arguments. */ + GsPluginRefineCategoriesFlags flags; + + /* In-progress data. */ + GPtrArray *category_list; /* (element-type GsCategory) (owned) (nullable) */ + GError *saved_error; /* (owned) (nullable) */ + guint n_pending_ops; + + /* Results. */ + GPtrArray *result_list; /* (element-type GsCategory) (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPluginJobListCategories, gs_plugin_job_list_categories, GS_TYPE_PLUGIN_JOB) + +typedef enum { + PROP_FLAGS = 1, +} GsPluginJobListCategoriesProperty; + +static GParamSpec *props[PROP_FLAGS + 1] = { NULL, }; + +static void +gs_plugin_job_list_categories_dispose (GObject *object) +{ + GsPluginJobListCategories *self = GS_PLUGIN_JOB_LIST_CATEGORIES (object); + + g_assert (self->category_list == NULL); + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + g_clear_pointer (&self->result_list, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_plugin_job_list_categories_parent_class)->dispose (object); +} + +static void +gs_plugin_job_list_categories_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsPluginJobListCategories *self = GS_PLUGIN_JOB_LIST_CATEGORIES (object); + + switch ((GsPluginJobListCategoriesProperty) prop_id) { + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_list_categories_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsPluginJobListCategories *self = GS_PLUGIN_JOB_LIST_CATEGORIES (object); + + switch ((GsPluginJobListCategoriesProperty) prop_id) { + case PROP_FLAGS: + /* Construct only. */ + g_assert (self->flags == 0); + self->flags = g_value_get_flags (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void plugin_refine_categories_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_op (GTask *task, + GError *error); + +static void +gs_plugin_job_list_categories_run_async (GsPluginJob *job, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginJobListCategories *self = GS_PLUGIN_JOB_LIST_CATEGORIES (job); + g_autoptr(GTask) task = NULL; + GPtrArray *plugins; /* (element-type GsPlugin) */ + gboolean anything_ran = FALSE; + GsCategory * const *categories = NULL; + gsize n_categories; + + task = g_task_new (job, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_job_list_categories_run_async); + g_task_set_task_data (task, g_object_ref (plugin_loader), (GDestroyNotify) g_object_unref); + + /* get the categories */ + categories = gs_category_manager_get_categories (gs_plugin_loader_get_category_manager (plugin_loader), &n_categories); + self->category_list = g_ptr_array_new_full (n_categories, (GDestroyNotify) g_object_unref); + + for (gsize i = 0; i < n_categories; i++) + g_ptr_array_add (self->category_list, g_object_ref (categories[i])); + + /* run each plugin, keeping a counter of pending operations which is + * initialised to 1 until all the operations are started */ + self->n_pending_ops = 1; + plugins = gs_plugin_loader_get_plugins (plugin_loader); + + for (guint i = 0; i < plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugins, i); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + + if (!gs_plugin_get_enabled (plugin)) + continue; + if (plugin_class->refine_categories_async == NULL) + continue; + + /* at least one plugin supports this vfunc */ + anything_ran = TRUE; + + /* run the plugin */ + self->n_pending_ops++; + plugin_class->refine_categories_async (plugin, self->category_list, self->flags, cancellable, plugin_refine_categories_cb, g_object_ref (task)); + } + + if (!anything_ran) + g_debug ("no plugin could handle listing categories"); + + finish_op (task, NULL); +} + +static void +plugin_refine_categories_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GError) local_error = NULL; + + if (!plugin_class->refine_categories_finish (plugin, result, &local_error)) { + finish_op (task, g_steal_pointer (&local_error)); + return; + } + + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + + finish_op (task, g_steal_pointer (&local_error)); +} + +static gint +category_sort_cb (gconstpointer a, + gconstpointer b) +{ + GsCategory *category_a = GS_CATEGORY (*(GsCategory **) a); + GsCategory *category_b = GS_CATEGORY (*(GsCategory **) b); + gint score_a = gs_category_get_score (category_a); + gint score_b = gs_category_get_score (category_b); + + if (score_a != score_b) + return score_b - score_a; + return gs_utils_sort_strcmp (gs_category_get_name (category_a), + gs_category_get_name (category_b)); +} + +/* @error is (transfer full) if non-%NULL */ +static void +finish_op (GTask *task, + GError *error) +{ + GsPluginJobListCategories *self = g_task_get_source_object (task); + g_autoptr(GPtrArray) category_list = NULL; + g_autoptr(GError) error_owned = g_steal_pointer (&error); + g_autofree gchar *job_debug = NULL; + + if (error_owned != NULL && self->saved_error == NULL) + self->saved_error = g_steal_pointer (&error_owned); + else if (error_owned != NULL) + g_debug ("Additional error while listing categories: %s", error_owned->message); + + g_assert (self->n_pending_ops > 0); + self->n_pending_ops--; + + if (self->n_pending_ops > 0) + return; + + /* Get the results of the parallel ops. */ + category_list = g_steal_pointer (&self->category_list); + + if (self->saved_error != NULL) { + g_task_return_error (task, g_steal_pointer (&self->saved_error)); + return; + } + + /* sort by name */ + g_ptr_array_sort (category_list, category_sort_cb); + for (guint i = 0; i < category_list->len; i++) { + GsCategory *category = GS_CATEGORY (g_ptr_array_index (category_list, i)); + gs_category_sort_children (category); + } + + /* show elapsed time */ + job_debug = gs_plugin_job_to_string (GS_PLUGIN_JOB (self)); + g_debug ("%s", job_debug); + + /* Check the intermediate working values are all cleared. */ + g_assert (self->category_list == NULL); + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + /* success */ + self->result_list = g_ptr_array_ref (category_list); + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_job_list_categories_run_finish (GsPluginJob *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_job_list_categories_class_init (GsPluginJobListCategoriesClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginJobClass *job_class = GS_PLUGIN_JOB_CLASS (klass); + + object_class->dispose = gs_plugin_job_list_categories_dispose; + object_class->get_property = gs_plugin_job_list_categories_get_property; + object_class->set_property = gs_plugin_job_list_categories_set_property; + + job_class->run_async = gs_plugin_job_list_categories_run_async; + job_class->run_finish = gs_plugin_job_list_categories_run_finish; + + /** + * GsPluginJobListCategories:flags: + * + * Flags to specify how the operation should run. + * + * Since: 43 + */ + props[PROP_FLAGS] = + g_param_spec_flags ("flags", "Flags", + "Flags to specify how the operation should run.", + GS_TYPE_PLUGIN_REFINE_CATEGORIES_FLAGS, + GS_PLUGIN_REFINE_CATEGORIES_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static void +gs_plugin_job_list_categories_init (GsPluginJobListCategories *self) +{ +} + +/** + * gs_plugin_job_list_categories_new: + * @flags: flags affecting how the operation runs + * + * Create a new #GsPluginJobListCategories for listing categories. + * + * Returns: (transfer full): a new #GsPluginJobListCategories + * Since: 43 + */ +GsPluginJob * +gs_plugin_job_list_categories_new (GsPluginRefineCategoriesFlags flags) +{ + return g_object_new (GS_TYPE_PLUGIN_JOB_LIST_CATEGORIES, + "flags", flags, + NULL); +} + +/** + * gs_plugin_job_list_categories_get_result_list: + * @self: a #GsPluginJobListCategories + * + * Get the full list of categories. + * + * If this is called before the job is complete, %NULL will be returned. + * + * Returns: (transfer none) (nullable) (element-type GsCategory): the job + * results, or %NULL on error or if called before the job has completed + * Since: 43 + */ +GPtrArray * +gs_plugin_job_list_categories_get_result_list (GsPluginJobListCategories *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB_LIST_CATEGORIES (self), NULL); + + return self->result_list; +} diff --git a/lib/gs-plugin-job-list-categories.h b/lib/gs-plugin-job-list-categories.h new file mode 100644 index 0000000..49857f3 --- /dev/null +++ b/lib/gs-plugin-job-list-categories.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB_LIST_CATEGORIES (gs_plugin_job_list_categories_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginJobListCategories, gs_plugin_job_list_categories, GS, PLUGIN_JOB_LIST_CATEGORIES, GsPluginJob) + +GsPluginJob *gs_plugin_job_list_categories_new (GsPluginRefineCategoriesFlags flags); + +GPtrArray *gs_plugin_job_list_categories_get_result_list (GsPluginJobListCategories *self); + +G_END_DECLS diff --git a/lib/gs-plugin-job-list-distro-upgrades.c b/lib/gs-plugin-job-list-distro-upgrades.c new file mode 100644 index 0000000..618bb2a --- /dev/null +++ b/lib/gs-plugin-job-list-distro-upgrades.c @@ -0,0 +1,428 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-job-list-distro-upgrades + * @short_description: A plugin job to list distro upgrades + * + * #GsPluginJobListDistroUpgrades is a #GsPluginJob representing an operation to + * list available upgrades for the distro, from all #GsPlugins. + * + * Upgrades for the distro are large upgrades, such as from Fedora 34 to + * Fedora 35. They are not small package updates. + * + * This job will list the available upgrades, but will not download them or + * install them. Due to the typical size of an upgrade, these should not be + * downloaded until the user has explicitly requested it. + * + * The known properties on the set of apps returned by this operation can be + * controlled with the #GsPluginJobListDistroUpgrades:refine-flags property. All + * results will be refined using %GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION + * plus the given set of refine flags. See #GsPluginJobRefine. + * + * This class is a wrapper around #GsPluginClass.list_distro_upgrades_async, + * calling it for all loaded plugins, with some additional filtering + * done on the results and #GsPluginJobRefine used to refine them. + * + * Retrieve the resulting #GsAppList using + * gs_plugin_job_list_distro_upgrades_get_result_list(). Components in the list + * are expected to be of type %AS_COMPONENT_KIND_OPERATING_SYSTEM. + * + * See also: #GsPluginClass.list_distro_upgrades_async + * Since: 42 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> +#include <glib/gi18n.h> + +#include "gs-app.h" +#include "gs-app-list-private.h" +#include "gs-enums.h" +#include "gs-plugin-job-private.h" +#include "gs-plugin-job-list-distro-upgrades.h" +#include "gs-plugin-job-refine.h" +#include "gs-plugin-private.h" +#include "gs-plugin-types.h" +#include "gs-utils.h" + +struct _GsPluginJobListDistroUpgrades +{ + GsPluginJob parent; + + /* Input arguments. */ + GsPluginListDistroUpgradesFlags flags; + GsPluginRefineFlags refine_flags; + + /* In-progress data. */ + GsAppList *merged_list; /* (owned) (nullable) */ + GError *saved_error; /* (owned) (nullable) */ + guint n_pending_ops; + + /* Results. */ + GsAppList *result_list; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPluginJobListDistroUpgrades, gs_plugin_job_list_distro_upgrades, GS_TYPE_PLUGIN_JOB) + +typedef enum { + PROP_REFINE_FLAGS = 1, + PROP_FLAGS, +} GsPluginJobListDistroUpgradesProperty; + +static GParamSpec *props[PROP_FLAGS + 1] = { NULL, }; + +static void +gs_plugin_job_list_distro_upgrades_dispose (GObject *object) +{ + GsPluginJobListDistroUpgrades *self = GS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (object); + + g_assert (self->merged_list == NULL); + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + g_clear_object (&self->result_list); + + G_OBJECT_CLASS (gs_plugin_job_list_distro_upgrades_parent_class)->dispose (object); +} + +static void +gs_plugin_job_list_distro_upgrades_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsPluginJobListDistroUpgrades *self = GS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (object); + + switch ((GsPluginJobListDistroUpgradesProperty) prop_id) { + case PROP_REFINE_FLAGS: + g_value_set_flags (value, self->refine_flags); + break; + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_list_distro_upgrades_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsPluginJobListDistroUpgrades *self = GS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (object); + + switch ((GsPluginJobListDistroUpgradesProperty) prop_id) { + case PROP_REFINE_FLAGS: + /* Construct only. */ + g_assert (self->refine_flags == 0); + self->refine_flags = g_value_get_flags (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_FLAGS: + /* Construct only. */ + g_assert (self->flags == 0); + self->flags = g_value_get_flags (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gint +app_sort_version_cb (GsApp *app1, + GsApp *app2, + gpointer user_data) +{ + return as_vercmp_simple (gs_app_get_version (app1), + gs_app_get_version (app2)); +} + +static void plugin_list_distro_upgrades_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_op (GTask *task, + GError *error); +static void refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_task (GTask *task, + GsAppList *merged_list); + +static void +gs_plugin_job_list_distro_upgrades_run_async (GsPluginJob *job, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginJobListDistroUpgrades *self = GS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (job); + g_autoptr(GTask) task = NULL; + GPtrArray *plugins; /* (element-type GsPlugin) */ + gboolean anything_ran = FALSE; + + /* check required args */ + task = g_task_new (job, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_job_list_distro_upgrades_run_async); + g_task_set_task_data (task, g_object_ref (plugin_loader), (GDestroyNotify) g_object_unref); + + /* run each plugin, keeping a counter of pending operations which is + * initialised to 1 until all the operations are started */ + self->n_pending_ops = 1; + self->merged_list = gs_app_list_new (); + plugins = gs_plugin_loader_get_plugins (plugin_loader); + + for (guint i = 0; i < plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugins, i); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + + if (!gs_plugin_get_enabled (plugin)) + continue; + if (plugin_class->list_distro_upgrades_async == NULL) + continue; + + /* at least one plugin supports this vfunc */ + anything_ran = TRUE; + + /* run the plugin */ + self->n_pending_ops++; + plugin_class->list_distro_upgrades_async (plugin, self->flags, cancellable, plugin_list_distro_upgrades_cb, g_object_ref (task)); + } + + if (!anything_ran) + g_debug ("no plugin could handle listing distro upgrades"); + + finish_op (task, NULL); +} + +static void +plugin_list_distro_upgrades_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginJobListDistroUpgrades *self = g_task_get_source_object (task); + g_autoptr(GsAppList) plugin_apps = NULL; + g_autoptr(GError) local_error = NULL; + + plugin_apps = plugin_class->list_distro_upgrades_finish (plugin, result, &local_error); + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + + if (plugin_apps != NULL) + gs_app_list_add_list (self->merged_list, plugin_apps); + + finish_op (task, g_steal_pointer (&local_error)); +} + +/* @error is (transfer full) if non-%NULL */ +static void +finish_op (GTask *task, + GError *error) +{ + GsPluginJobListDistroUpgrades *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + GsPluginLoader *plugin_loader = g_task_get_task_data (task); + g_autoptr(GsAppList) merged_list = NULL; + g_autoptr(GError) error_owned = g_steal_pointer (&error); + + if (error_owned != NULL && self->saved_error == NULL) + self->saved_error = g_steal_pointer (&error_owned); + else if (error_owned != NULL) + g_debug ("Additional error while listing distro upgrades: %s", error_owned->message); + + g_assert (self->n_pending_ops > 0); + self->n_pending_ops--; + + if (self->n_pending_ops > 0) + return; + + /* Get the results of the parallel ops. */ + merged_list = g_steal_pointer (&self->merged_list); + + if (self->saved_error != NULL) { + g_task_return_error (task, g_steal_pointer (&self->saved_error)); + return; + } + + /* run refine() on each one if required */ + if (merged_list != NULL && + gs_app_list_length (merged_list) > 0) { + g_autoptr(GsPluginJob) refine_job = NULL; + + /* Always specify REQUIRE_SETUP_ACTION, as that requires enough + * information to be able to install the upgrade later if + * requested. */ + refine_job = gs_plugin_job_refine_new (merged_list, + self->refine_flags | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING); + gs_plugin_loader_job_process_async (plugin_loader, refine_job, + cancellable, + refine_cb, + g_object_ref (task)); + } else { + g_debug ("No distro upgrades to refine"); + finish_task (task, merged_list); + } +} + +static void +refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GsAppList) new_list = NULL; + g_autoptr(GError) local_error = NULL; + + new_list = gs_plugin_loader_job_process_finish (plugin_loader, result, &local_error); + if (new_list == NULL) { + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + finish_task (task, new_list); +} + +static void +finish_task (GTask *task, + GsAppList *merged_list) +{ + GsPluginJobListDistroUpgrades *self = g_task_get_source_object (task); + g_autofree gchar *job_debug = NULL; + + /* Sort the results. The refine may have added useful metadata. */ + gs_app_list_sort (merged_list, app_sort_version_cb, NULL); + + /* show elapsed time */ + job_debug = gs_plugin_job_to_string (GS_PLUGIN_JOB (self)); + g_debug ("%s", job_debug); + + /* Check the intermediate working values are all cleared. */ + g_assert (self->merged_list == NULL); + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + /* success */ + g_set_object (&self->result_list, merged_list); + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_job_list_distro_upgrades_run_finish (GsPluginJob *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_job_list_distro_upgrades_class_init (GsPluginJobListDistroUpgradesClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginJobClass *job_class = GS_PLUGIN_JOB_CLASS (klass); + + object_class->dispose = gs_plugin_job_list_distro_upgrades_dispose; + object_class->get_property = gs_plugin_job_list_distro_upgrades_get_property; + object_class->set_property = gs_plugin_job_list_distro_upgrades_set_property; + + job_class->run_async = gs_plugin_job_list_distro_upgrades_run_async; + job_class->run_finish = gs_plugin_job_list_distro_upgrades_run_finish; + + /** + * GsPluginJobListDistroUpgrades:refine-flags: + * + * Flags to specify how to refine the returned apps. + * + * %GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION will always be used. + * + * Since: 42 + */ + props[PROP_REFINE_FLAGS] = + g_param_spec_flags ("refine-flags", "Refine Flags", + "Flags to specify how to refine the returned apps.", + GS_TYPE_PLUGIN_REFINE_FLAGS, GS_PLUGIN_REFINE_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginJobListDistroUpgrades:flags: + * + * Flags to specify how the operation should run. + * + * Since: 42 + */ + props[PROP_FLAGS] = + g_param_spec_flags ("flags", "Flags", + "Flags to specify how the operation should run.", + GS_TYPE_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS, + GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static void +gs_plugin_job_list_distro_upgrades_init (GsPluginJobListDistroUpgrades *self) +{ +} + +/** + * gs_plugin_job_list_distro_upgrades_new: + * @flags: flags affecting how the operation runs + * @refine_flags: flags to affect how the results are refined + * + * Create a new #GsPluginJobListDistroUpgrades for listing the available distro + * upgrades. + * + * Returns: (transfer full): a new #GsPluginJobListDistroUpgrades + * Since: 42 + */ +GsPluginJob * +gs_plugin_job_list_distro_upgrades_new (GsPluginListDistroUpgradesFlags flags, + GsPluginRefineFlags refine_flags) +{ + return g_object_new (GS_TYPE_PLUGIN_JOB_LIST_DISTRO_UPGRADES, + "refine-flags", refine_flags, + "flags", flags, + NULL); +} + +/** + * gs_plugin_job_list_distro_upgrades_get_result_list: + * @self: a #GsPluginJobListDistroUpgrades + * + * Get the full list of available distro upgrades. + * + * If this is called before the job is complete, %NULL will be returned. + * + * Returns: (transfer none) (nullable): the job results, or %NULL on error + * or if called before the job has completed + * Since: 42 + */ +GsAppList * +gs_plugin_job_list_distro_upgrades_get_result_list (GsPluginJobListDistroUpgrades *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (self), NULL); + + return self->result_list; +} diff --git a/lib/gs-plugin-job-list-distro-upgrades.h b/lib/gs-plugin-job-list-distro-upgrades.h new file mode 100644 index 0000000..0d3b0a2 --- /dev/null +++ b/lib/gs-plugin-job-list-distro-upgrades.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB_LIST_DISTRO_UPGRADES (gs_plugin_job_list_distro_upgrades_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginJobListDistroUpgrades, gs_plugin_job_list_distro_upgrades, GS, PLUGIN_JOB_LIST_DISTRO_UPGRADES, GsPluginJob) + +GsPluginJob *gs_plugin_job_list_distro_upgrades_new (GsPluginListDistroUpgradesFlags flags, + GsPluginRefineFlags refine_flags); + +GsAppList *gs_plugin_job_list_distro_upgrades_get_result_list (GsPluginJobListDistroUpgrades *self); + +G_END_DECLS diff --git a/lib/gs-plugin-job-manage-repository.c b/lib/gs-plugin-job-manage-repository.c new file mode 100644 index 0000000..2b8ee66 --- /dev/null +++ b/lib/gs-plugin-job-manage-repository.c @@ -0,0 +1,365 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Red Hat <www.redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-job-manage-repository + * @short_description: A plugin job on a repository + * + * #GsPluginJobManageRepository is a #GsPluginJob representing an operation on + * a repository, like install, remove, enable and disable it. + * + * This class is a wrapper around #GsPluginClass.install_repository_async, + * #GsPluginClass.remove_repository_async, #GsPluginClass.enable_repository_async + * and #GsPluginClass.disable_repository_async calling it for all loaded plugins. + * + * Since: 43 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> + +#include "gs-app.h" +#include "gs-app-collation.h" +#include "gs-enums.h" +#include "gs-plugin-job.h" +#include "gs-plugin-job-manage-repository.h" +#include "gs-plugin-job-private.h" +#include "gs-plugin-types.h" + +struct _GsPluginJobManageRepository +{ + GsPluginJob parent; + + /* Input arguments. */ + GsApp *repository; /* (owned) (not nullable) */ + GsPluginManageRepositoryFlags flags; + + /* In-progress data. */ + GError *saved_error; /* (owned) (nullable) */ + guint n_pending_ops; +}; + +G_DEFINE_TYPE (GsPluginJobManageRepository, gs_plugin_job_manage_repository, GS_TYPE_PLUGIN_JOB) + +typedef enum { + PROP_FLAGS = 1, + PROP_REPOSITORY, +} GsPluginJobManageRepositoryProperty; + +static GParamSpec *props[PROP_REPOSITORY + 1] = { NULL, }; + +static void +gs_plugin_job_manage_repository_dispose (GObject *object) +{ + GsPluginJobManageRepository *self = GS_PLUGIN_JOB_MANAGE_REPOSITORY (object); + + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + g_clear_object (&self->repository); + + G_OBJECT_CLASS (gs_plugin_job_manage_repository_parent_class)->dispose (object); +} + +static void +gs_plugin_job_manage_repository_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsPluginJobManageRepository *self = GS_PLUGIN_JOB_MANAGE_REPOSITORY (object); + + switch ((GsPluginJobManageRepositoryProperty) prop_id) { + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + case PROP_REPOSITORY: + g_value_set_object (value, self->repository); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_manage_repository_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsPluginJobManageRepository *self = GS_PLUGIN_JOB_MANAGE_REPOSITORY (object); + + switch ((GsPluginJobManageRepositoryProperty) prop_id) { + case PROP_FLAGS: + /* Construct only. */ + g_assert (self->flags == 0); + self->flags = g_value_get_flags (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_REPOSITORY: + /* Construct only. */ + g_assert (self->repository == NULL); + self->repository = g_value_dup_object (value); + g_assert (self->repository != NULL); + g_object_notify_by_pspec (object, props[prop_id]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void plugin_repository_func_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_op (GTask *task, + GError *error); + +static void +gs_plugin_job_manage_repository_run_async (GsPluginJob *job, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginJobManageRepository *self = GS_PLUGIN_JOB_MANAGE_REPOSITORY (job); + g_autoptr(GTask) task = NULL; + GPtrArray *plugins; /* (element-type GsPlugin) */ + gboolean anything_ran = FALSE; + + task = g_task_new (job, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_job_manage_repository_run_async); + g_task_set_task_data (task, g_object_ref (plugin_loader), (GDestroyNotify) g_object_unref); + + /* run each plugin, keeping a counter of pending operations which is + * initialised to 1 until all the operations are started */ + self->n_pending_ops = 1; + plugins = gs_plugin_loader_get_plugins (plugin_loader); + + for (guint i = 0; i < plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugins, i); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + void (* repository_func_async) (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) = NULL; + + if (!gs_plugin_get_enabled (plugin)) + continue; + if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL) != 0) + repository_func_async = plugin_class->install_repository_async; + else if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE) != 0) + repository_func_async = plugin_class->remove_repository_async; + else if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE) != 0) + repository_func_async = plugin_class->enable_repository_async; + else if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE) != 0) + repository_func_async = plugin_class->disable_repository_async; + else + g_assert_not_reached (); + + if (repository_func_async == NULL) + continue; + + /* at least one plugin supports this vfunc */ + anything_ran = TRUE; + + /* run the plugin */ + self->n_pending_ops++; + repository_func_async (plugin, self->repository, self->flags, cancellable, plugin_repository_func_cb, g_object_ref (task)); + } + + if (!anything_ran) + g_debug ("no plugin could handle repository operation"); + + finish_op (task, NULL); +} + +static void +plugin_repository_func_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginJobManageRepository *self = g_task_get_source_object (task); + gboolean success; + g_autoptr(GError) local_error = NULL; + gboolean (* repository_func_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error) = NULL; + + if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL) != 0) + repository_func_finish = plugin_class->install_repository_finish; + else if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE) != 0) + repository_func_finish = plugin_class->remove_repository_finish; + else if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE) != 0) + repository_func_finish = plugin_class->enable_repository_finish; + else if ((self->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE) != 0) + repository_func_finish = plugin_class->disable_repository_finish; + else + g_assert_not_reached (); + + success = repository_func_finish (plugin, result, &local_error); + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + + g_assert (success || local_error != NULL); + + finish_op (task, g_steal_pointer (&local_error)); +} + +static void +reset_app_progress (GsApp *app) +{ + g_autoptr(GsAppList) addons = gs_app_dup_addons (app); + GsAppList *related = gs_app_get_related (app); + + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + + for (guint i = 0; addons != NULL && i < gs_app_list_length (addons); i++) { + GsApp *app_addons = gs_app_list_index (addons, i); + gs_app_set_progress (app_addons, GS_APP_PROGRESS_UNKNOWN); + } + for (guint i = 0; i < gs_app_list_length (related); i++) { + GsApp *app_related = gs_app_list_index (related, i); + gs_app_set_progress (app_related, GS_APP_PROGRESS_UNKNOWN); + } +} + +/* @error is (transfer full) if non-%NULL */ +static void +finish_op (GTask *task, + GError *error) +{ + GsPluginJobManageRepository *self = g_task_get_source_object (task); + g_autoptr(GError) error_owned = g_steal_pointer (&error); + g_autofree gchar *job_debug = NULL; + + if (error_owned != NULL && self->saved_error == NULL) + self->saved_error = g_steal_pointer (&error_owned); + else if (error_owned != NULL) + g_debug ("Additional error while managing repository: %s", error_owned->message); + + g_assert (self->n_pending_ops > 0); + self->n_pending_ops--; + + if (self->n_pending_ops > 0) + return; + + /* show elapsed time */ + job_debug = gs_plugin_job_to_string (GS_PLUGIN_JOB (self)); + g_debug ("%s", job_debug); + + reset_app_progress (self->repository); + + if (self->saved_error != NULL) + g_task_return_error (task, g_steal_pointer (&self->saved_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_job_manage_repository_run_finish (GsPluginJob *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_job_manage_repository_class_init (GsPluginJobManageRepositoryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginJobClass *job_class = GS_PLUGIN_JOB_CLASS (klass); + + object_class->dispose = gs_plugin_job_manage_repository_dispose; + object_class->get_property = gs_plugin_job_manage_repository_get_property; + object_class->set_property = gs_plugin_job_manage_repository_set_property; + + job_class->run_async = gs_plugin_job_manage_repository_run_async; + job_class->run_finish = gs_plugin_job_manage_repository_run_finish; + + /** + * GsPluginJobManageRepository:repository: (not nullable) + * + * A #GsApp describing the repository to run the operation on. + * + * Since: 43 + */ + props[PROP_REPOSITORY] = + g_param_spec_object ("repository", "Repository", + "A #GsApp describing the repository to run the operation on.", + GS_TYPE_APP, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginJobManageRepository:flags: + * + * Flags to specify how and which the operation should run. + * Only one of the %GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL, + * %GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE, %GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE and + * %GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE can be specified. + * + * Since: 43 + */ + props[PROP_FLAGS] = + g_param_spec_flags ("flags", "Flags", + "Flags to specify how and which the operation should run.", + GS_TYPE_PLUGIN_MANAGE_REPOSITORY_FLAGS, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static void +gs_plugin_job_manage_repository_init (GsPluginJobManageRepository *self) +{ +} + +/** + * gs_plugin_job_manage_repository_new: + * @repository: (not nullable) (transfer none): a repository to run the operation on + * @flags: flags affecting how the operation runs + * + * Create a new #GsPluginJobManageRepository to manage the given @repository. + * + * Returns: (transfer full): a new #GsPluginJobManageRepository + * Since: 43 + */ +GsPluginJob * +gs_plugin_job_manage_repository_new (GsApp *repository, + GsPluginManageRepositoryFlags flags) +{ + guint nops = 0; + + g_return_val_if_fail (GS_IS_APP (repository), NULL); + + if ((flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL) != 0) + nops++; + if ((flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE) != 0) + nops++; + if ((flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE) != 0) + nops++; + if ((flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE) != 0) + nops++; + + g_return_val_if_fail (nops == 1, NULL); + + return g_object_new (GS_TYPE_PLUGIN_JOB_MANAGE_REPOSITORY, + "repository", repository, + "flags", flags, + NULL); +} diff --git a/lib/gs-plugin-job-manage-repository.h b/lib/gs-plugin-job-manage-repository.h new file mode 100644 index 0000000..86233b7 --- /dev/null +++ b/lib/gs-plugin-job-manage-repository.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Red Hat <www.redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB_MANAGE_REPOSITORY (gs_plugin_job_manage_repository_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginJobManageRepository, gs_plugin_job_manage_repository, GS, PLUGIN_JOB_MANAGE_REPOSITORY, GsPluginJob) + +GsPluginJob *gs_plugin_job_manage_repository_new (GsApp *repository, + GsPluginManageRepositoryFlags flags); + +G_END_DECLS diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h new file mode 100644 index 0000000..9cdbbf3 --- /dev/null +++ b/lib/gs-plugin-job-private.h @@ -0,0 +1,40 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +GsPluginAction gs_plugin_job_get_action (GsPluginJob *self); +GsAppListFilterFlags gs_plugin_job_get_dedupe_flags (GsPluginJob *self); +GsPluginRefineFlags gs_plugin_job_get_refine_flags (GsPluginJob *self); +gboolean gs_plugin_job_has_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +void gs_plugin_job_add_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +void gs_plugin_job_remove_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +gboolean gs_plugin_job_get_interactive (GsPluginJob *self); +gboolean gs_plugin_job_get_propagate_error (GsPluginJob *self); +guint gs_plugin_job_get_max_results (GsPluginJob *self); +GsAppListSortFunc gs_plugin_job_get_sort_func (GsPluginJob *self, + gpointer *user_data_out); +const gchar *gs_plugin_job_get_search (GsPluginJob *self); +GsApp *gs_plugin_job_get_app (GsPluginJob *self); +GsAppList *gs_plugin_job_get_list (GsPluginJob *self); +GFile *gs_plugin_job_get_file (GsPluginJob *self); +GsPlugin *gs_plugin_job_get_plugin (GsPluginJob *self); +gchar *gs_plugin_job_to_string (GsPluginJob *self); +void gs_plugin_job_set_action (GsPluginJob *self, + GsPluginAction action); + +G_END_DECLS diff --git a/lib/gs-plugin-job-refine.c b/lib/gs-plugin-job-refine.c new file mode 100644 index 0000000..f9618cd --- /dev/null +++ b/lib/gs-plugin-job-refine.c @@ -0,0 +1,860 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-job-refine + * @short_description: A plugin job to refine #GsApps and add more data + * + * #GsPluginJobRefine is a #GsPluginJob representing a refine operation. + * + * It’s used to query and add more data to a set of #GsApps. The data to be set + * is controlled by the #GsPluginRefineFlags, and is looked up for all the apps + * in a #GsAppList by the loaded plugins. + * + * This class is a wrapper around #GsPluginClass.refine_async, calling it for + * all loaded plugins, with some additional refinements done on the results. + * + * In particular, if an app in the #GsAppList has %GS_APP_QUIRK_IS_WILDCARD, + * refining it will replace it with zero or more non-wildcard #GsApps in the + * #GsAppList, all of which are candidates for what the wildcard represents. + * For example, they may have the same ID as the wildcard, or match its name. + * Refining is the canonical process for resolving wildcards. + * + * This means that the #GsAppList at the end of the refine operation may not + * match the #GsAppList passed in as input. Retrieve the final #GsAppList using + * gs_plugin_job_refine_get_result_list(). The #GsAppList which was passed + * into the job will not be modified. + * + * Internally, the #GsPluginClass.refine_async() functions are called on all + * the plugins in series, and in series with a call to + * gs_odrs_provider_refine_async(). Once all of those calls are finished, + * zero or more recursive calls to run_refine_internal_async() are made in + * parallel to do a similar refine process on the addons, runtime and related + * components for all the components in the input #GsAppList. The refine job is + * complete once all these recursive calls complete. + * + * FIXME: Ideally, the #GsPluginClass.refine_async() calls would happen in + * parallel, but this cannot be the case until the results of the refine_async() + * call in one plugin don’t depend on the results of refine_async() in another. + * This still happens with several pairs of plugins. + * + * ``` + * run_async() + * | + * v + * /-----------------------+-------------+----------------\ + * | | | | + * plugin->refine_async() | | | + * v plugin->refine_async() | | + * | v … | + * | | v gs_odrs_provider_refine_async() + * | | | v + * | | | | + * \-----------------------+-------------+----------------/ + * | + * finish_refine_internal_op() + * | + * v + * /----------------------------+-----------------\ + * | | | + * run_refine_internal_async() run_refine_internal_async() … + * | | | + * v v v + * \----------------------------+-----------------/ + * | + * finish_refine_internal_recursion() + * ``` + * + * See also: #GsPluginClass.refine_async + * Since: 42 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> +#include <glib/gi18n.h> + +#include "gs-app.h" +#include "gs-app-collation.h" +#include "gs-app-private.h" +#include "gs-app-list-private.h" +#include "gs-enums.h" +#include "gs-plugin-job-private.h" +#include "gs-plugin-job-refine.h" +#include "gs-utils.h" + +struct _GsPluginJobRefine +{ + GsPluginJob parent; + + /* Input data. */ + GsAppList *app_list; /* (owned) */ + GsPluginRefineFlags flags; + + /* Output data. */ + GsAppList *result_list; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPluginJobRefine, gs_plugin_job_refine, GS_TYPE_PLUGIN_JOB) + +typedef enum { + PROP_APP_LIST = 1, + PROP_FLAGS, +} GsPluginJobRefineProperty; + +static GParamSpec *props[PROP_FLAGS + 1] = { NULL, }; + +static void +gs_plugin_job_refine_dispose (GObject *object) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (object); + + g_clear_object (&self->app_list); + g_clear_object (&self->result_list); + + G_OBJECT_CLASS (gs_plugin_job_refine_parent_class)->dispose (object); +} + +static void +gs_plugin_job_refine_constructed (GObject *object) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (object); + + G_OBJECT_CLASS (gs_plugin_job_refine_parent_class)->constructed (object); + + /* FIXME: the plugins should specify this, rather than hardcoding */ + if (self->flags & (GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME)) + self->flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN; + if (self->flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) + self->flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME; +} + +static void +gs_plugin_job_refine_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (object); + + switch ((GsPluginJobRefineProperty) prop_id) { + case PROP_APP_LIST: + g_value_set_object (value, self->app_list); + break; + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_refine_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (object); + + switch ((GsPluginJobRefineProperty) prop_id) { + case PROP_APP_LIST: + /* Construct only. */ + g_assert (self->app_list == NULL); + self->app_list = g_value_dup_object (value); + g_object_notify_by_pspec (object, props[PROP_APP_LIST]); + break; + case PROP_FLAGS: + /* Construct only. */ + g_assert (self->flags == 0); + self->flags = g_value_get_flags (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gboolean +app_is_valid_filter (GsApp *app, + gpointer user_data) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (user_data); + + return gs_plugin_loader_app_is_valid (app, self->flags); +} + +static gint +review_score_sort_cb (gconstpointer a, gconstpointer b) +{ + AsReview *ra = *((AsReview **) a); + AsReview *rb = *((AsReview **) b); + if (as_review_get_priority (ra) < as_review_get_priority (rb)) + return 1; + if (as_review_get_priority (ra) > as_review_get_priority (rb)) + return -1; + return 0; +} + +static gboolean +app_is_non_wildcard (GsApp *app, gpointer user_data) +{ + return !gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD); +} + +static void plugin_refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void odrs_provider_refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_refine_internal_op (GTask *task, + GError *error); +static void recursive_internal_refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_refine_internal_recursion (GTask *task, + GError *error); +static gboolean run_refine_internal_finish (GsPluginJobRefine *self, + GAsyncResult *result, + GError **error); + +typedef struct { + /* Input data. */ + GsPluginLoader *plugin_loader; /* (not nullable) (owned) */ + GsAppList *list; /* (not nullable) (owned) */ + GsPluginRefineFlags flags; + + /* In-progress data. */ + guint n_pending_ops; + guint n_pending_recursions; + guint next_plugin_index; + + /* Output data. */ + GError *error; /* (nullable) (owned) */ +} RefineInternalData; + +static void +refine_internal_data_free (RefineInternalData *data) +{ + g_clear_object (&data->plugin_loader); + g_clear_object (&data->list); + + g_assert (data->n_pending_ops == 0); + g_assert (data->n_pending_recursions == 0); + + /* If an error occurred, it should have been stolen to pass to + * g_task_return_error() by now. */ + g_assert (data->error == NULL); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (RefineInternalData, refine_internal_data_free) + +static void +run_refine_internal_async (GsPluginJobRefine *self, + GsPluginLoader *plugin_loader, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GPtrArray *plugins; /* (element-type GsPlugin) */ + g_autoptr(GTask) task = NULL; + RefineInternalData *data; + g_autoptr(RefineInternalData) data_owned = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, run_refine_internal_async); + + data = data_owned = g_new0 (RefineInternalData, 1); + data->plugin_loader = g_object_ref (plugin_loader); + data->list = g_object_ref (list); + data->flags = flags; + g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) refine_internal_data_free); + + /* try to adopt each application with a plugin */ + gs_plugin_loader_run_adopt (plugin_loader, list); + + data->n_pending_ops = 0; + + /* run each plugin + * + * FIXME: For now, we have to run these vfuncs sequentially rather than + * all in parallel. This is because there are still dependencies between + * some of the plugins, where the code to refine an app in one plugin + * depends on the results of refining it in another plugin first. + * + * Eventually, the plugins should all be changed/removed so that they + * can operate independently. At that point, this code can be reverted + * so that the refine_async() vfuncs are called in parallel. */ + plugins = gs_plugin_loader_get_plugins (plugin_loader); + + for (guint i = 0; i < plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugins, i); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + + if (!gs_plugin_get_enabled (plugin)) + continue; + if (plugin_class->refine_async == NULL) + continue; + + /* FIXME: The next refine_async() call is made in + * finish_refine_internal_op(). */ + data->next_plugin_index = i + 1; + + /* run the batched plugin symbol */ + data->n_pending_ops++; + plugin_class->refine_async (plugin, list, flags, + cancellable, plugin_refine_cb, g_object_ref (task)); + + /* FIXME: The next refine_async() call is made in + * finish_refine_internal_op(). */ + return; + } + + data->n_pending_ops++; + finish_refine_internal_op (task, NULL); +} + +static void +plugin_refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + g_autoptr(GError) local_error = NULL; + + if (!plugin_class->refine_finish (plugin, result, &local_error)) { + finish_refine_internal_op (task, g_steal_pointer (&local_error)); + return; + } + + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + + finish_refine_internal_op (task, NULL); +} + +static void +odrs_provider_refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsOdrsProvider *odrs_provider = GS_ODRS_PROVIDER (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + gs_odrs_provider_refine_finish (odrs_provider, result, &local_error); + finish_refine_internal_op (task, g_steal_pointer (&local_error)); +} + +/* @error is (transfer full) if non-NULL */ +static void +finish_refine_internal_op (GTask *task, + GError *error) +{ + GsPluginJobRefine *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GError) error_owned = g_steal_pointer (&error); + RefineInternalData *data = g_task_get_task_data (task); + GsPluginLoader *plugin_loader = data->plugin_loader; + GsAppList *list = data->list; + GsPluginRefineFlags flags = data->flags; + GsOdrsProvider *odrs_provider; + GsOdrsProviderRefineFlags odrs_refine_flags = 0; + GPtrArray *plugins; /* (element-type GsPlugin) */ + + if (data->error == NULL && error_owned != NULL) { + data->error = g_steal_pointer (&error_owned); + } else if (error_owned != NULL) { + g_debug ("Additional error while refining: %s", error_owned->message); + } + + g_assert (data->n_pending_ops > 0); + data->n_pending_ops--; + + plugins = gs_plugin_loader_get_plugins (plugin_loader); + + for (guint i = data->next_plugin_index; i < plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugins, i); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + + if (!gs_plugin_get_enabled (plugin)) + continue; + if (plugin_class->refine_async == NULL) + continue; + + /* FIXME: The next refine_async() call is made in + * finish_refine_internal_op(). */ + data->next_plugin_index = i + 1; + + /* run the batched plugin symbol */ + data->n_pending_ops++; + plugin_class->refine_async (plugin, list, flags, + cancellable, plugin_refine_cb, g_object_ref (task)); + + /* FIXME: The next refine_async() call is made in + * finish_refine_internal_op(). */ + return; + } + + if (data->next_plugin_index == plugins->len) { + /* Avoid the ODRS refine being run multiple times. */ + data->next_plugin_index++; + + /* Add ODRS data if needed */ + odrs_provider = gs_plugin_loader_get_odrs_provider (plugin_loader); + + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) + odrs_refine_flags |= GS_ODRS_PROVIDER_REFINE_FLAGS_GET_REVIEWS; + if (flags & (GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING)) + odrs_refine_flags |= GS_ODRS_PROVIDER_REFINE_FLAGS_GET_RATINGS; + + if (odrs_provider != NULL && odrs_refine_flags != 0) { + data->n_pending_ops++; + gs_odrs_provider_refine_async (odrs_provider, list, odrs_refine_flags, + cancellable, odrs_provider_refine_cb, g_object_ref (task)); + } + } + + if (data->n_pending_ops > 0) + return; + + /* At this point, all the plugin->refine() calls are complete and the + * gs_odrs_provider_refine_async() call is also complete. If an error + * occurred during those calls, return with it now rather than + * proceeding to the recursive calls below. */ + if (data->error != NULL) { + g_task_return_error (task, g_steal_pointer (&data->error)); + return; + } + + /* filter any wildcard apps left in the list */ + gs_app_list_filter (list, app_is_non_wildcard, NULL); + + /* ensure these are sorted by score */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) { + GPtrArray *reviews; + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + reviews = gs_app_get_reviews (app); + g_ptr_array_sort (reviews, review_score_sort_cb); + } + } + + /* Now run several recursive calls to run_refine_internal_async() in + * parallel, to refine related components. */ + data->n_pending_recursions = 1; + + /* refine addons one layer deep */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) { + g_autoptr(GsAppList) addons_list = gs_app_list_new (); + GsPluginRefineFlags addons_flags = flags; + + addons_flags &= ~(GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + g_autoptr(GsAppList) addons = gs_app_dup_addons (app); + + for (guint j = 0; addons != NULL && j < gs_app_list_length (addons); j++) { + GsApp *addon = gs_app_list_index (addons, j); + g_debug ("refining app %s addon %s", + gs_app_get_id (app), + gs_app_get_id (addon)); + gs_app_list_add (addons_list, addon); + } + } + + if (gs_app_list_length (addons_list) > 0 && addons_flags != 0) { + data->n_pending_recursions++; + run_refine_internal_async (self, plugin_loader, + addons_list, addons_flags, + cancellable, recursive_internal_refine_cb, + g_object_ref (task)); + } + } + + /* also do runtime */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME) { + g_autoptr(GsAppList) runtimes_list = gs_app_list_new (); + GsPluginRefineFlags runtimes_flags = flags; + + runtimes_flags &= ~GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsApp *runtime = gs_app_get_runtime (app); + + if (runtime != NULL) + gs_app_list_add (runtimes_list, runtime); + } + + if (gs_app_list_length (runtimes_list) > 0 && runtimes_flags != 0) { + data->n_pending_recursions++; + run_refine_internal_async (self, plugin_loader, + runtimes_list, runtimes_flags, + cancellable, recursive_internal_refine_cb, + g_object_ref (task)); + } + } + + /* also do related packages one layer deep */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED) { + g_autoptr(GsAppList) related_list = gs_app_list_new (); + GsPluginRefineFlags related_flags = flags; + + related_flags &= ~GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsAppList *related = gs_app_get_related (app); + for (guint j = 0; j < gs_app_list_length (related); j++) { + GsApp *app2 = gs_app_list_index (related, j); + g_debug ("refining related: %s[%s]", + gs_app_get_id (app2), + gs_app_get_source_default (app2)); + gs_app_list_add (related_list, app2); + } + } + + if (gs_app_list_length (related_list) > 0 && related_flags != 0) { + data->n_pending_recursions++; + run_refine_internal_async (self, plugin_loader, + related_list, related_flags, + cancellable, recursive_internal_refine_cb, + g_object_ref (task)); + } + } + + finish_refine_internal_recursion (task, NULL); +} + +static void +recursive_internal_refine_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + run_refine_internal_finish (self, result, &local_error); + finish_refine_internal_recursion (task, g_steal_pointer (&local_error)); +} + +/* @error is (transfer full) if non-NULL */ +static void +finish_refine_internal_recursion (GTask *task, + GError *error) +{ + g_autoptr(GError) error_owned = g_steal_pointer (&error); + RefineInternalData *data = g_task_get_task_data (task); + + if (data->error == NULL && error_owned != NULL) { + data->error = g_steal_pointer (&error_owned); + } else if (error_owned != NULL) { + g_debug ("Additional error while refining: %s", error_owned->message); + } + + g_assert (data->n_pending_recursions > 0); + data->n_pending_recursions--; + + if (data->n_pending_recursions > 0) + return; + + /* The entire refine operation (and all its sub-operations and + * recursions) is complete. */ + if (data->error != NULL) + g_task_return_error (task, g_steal_pointer (&data->error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +run_refine_internal_finish (GsPluginJobRefine *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +app_thaw_notify_idle (gpointer data) +{ + GsApp *app = GS_APP (data); + g_object_thaw_notify (G_OBJECT (app)); + g_object_unref (app); + return G_SOURCE_REMOVE; +} + +static void run_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_run (GTask *task, + GsAppList *result_list); + +static void +gs_plugin_job_refine_run_async (GsPluginJob *job, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (job); + g_autoptr(GTask) task = NULL; + g_autoptr(GsAppList) result_list = NULL; + + /* check required args */ + task = g_task_new (job, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_job_refine_run_async); + + /* Operate on a copy of the input list so we don’t modify it when + * resolving wildcards. */ + result_list = gs_app_list_copy (self->app_list); + g_task_set_task_data (task, g_object_ref (result_list), (GDestroyNotify) g_object_unref); + + /* nothing to do */ + if (self->flags == 0 || + gs_app_list_length (result_list) == 0) { + g_debug ("no refine flags set for transaction or app list is empty"); + finish_run (task, result_list); + return; + } + + /* freeze all apps */ + for (guint i = 0; i < gs_app_list_length (self->app_list); i++) { + GsApp *app = gs_app_list_index (self->app_list, i); + g_object_freeze_notify (G_OBJECT (app)); + } + + /* Start refining the apps. */ + run_refine_internal_async (self, plugin_loader, result_list, + self->flags, cancellable, + run_cb, g_steal_pointer (&task)); +} + +static void +run_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginJobRefine *self = GS_PLUGIN_JOB_REFINE (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsAppList *result_list = g_task_get_task_data (task); + g_autoptr(GError) local_error = NULL; + + if (run_refine_internal_finish (self, result, &local_error)) { + /* remove any addons that have the same source as the parent app */ + for (guint i = 0; i < gs_app_list_length (result_list); i++) { + g_autoptr(GPtrArray) to_remove = g_ptr_array_new (); + GsApp *app = gs_app_list_index (result_list, i); + g_autoptr(GsAppList) addons = gs_app_dup_addons (app); + + /* find any apps with the same source */ + const gchar *pkgname_parent = gs_app_get_source_default (app); + if (pkgname_parent == NULL) + continue; + for (guint j = 0; addons != NULL && j < gs_app_list_length (addons); j++) { + GsApp *addon = gs_app_list_index (addons, j); + if (g_strcmp0 (gs_app_get_source_default (addon), + pkgname_parent) == 0) { + g_debug ("%s has the same pkgname of %s as %s", + gs_app_get_unique_id (app), + pkgname_parent, + gs_app_get_unique_id (addon)); + g_ptr_array_add (to_remove, addon); + } + } + + /* remove any addons with the same source */ + for (guint j = 0; j < to_remove->len; j++) { + GsApp *addon = g_ptr_array_index (to_remove, j); + gs_app_remove_addon (app, addon); + } + } + } + + /* now emit all the changed signals */ + for (guint i = 0; i < gs_app_list_length (self->app_list); i++) { + GsApp *app = gs_app_list_index (self->app_list, i); + g_idle_add (app_thaw_notify_idle, g_object_ref (app)); + } + + /* Delayed error handling. */ + if (local_error != NULL) { + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + finish_run (task, result_list); +} + +static void +finish_run (GTask *task, + GsAppList *result_list) +{ + GsPluginJobRefine *self = g_task_get_source_object (task); + g_autofree gchar *job_debug = NULL; + + /* Internal calls to #GsPluginJobRefine may want to do their own + * filtering, typically if the refine is being done as part of another + * plugin job. If so, only filter to remove wildcards. Wildcards should + * always be removed, as they should have been resolved as part of the + * refine; any remaining wildcards will never be resolved. + * + * If the flag is not specified, filter by a variety of indicators of + * what a ‘valid’ app is. */ + if (self->flags & GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING) + gs_app_list_filter (result_list, app_is_non_wildcard, NULL); + else + gs_app_list_filter (result_list, app_is_valid_filter, self); + + /* show elapsed time */ + job_debug = gs_plugin_job_to_string (GS_PLUGIN_JOB (self)); + g_debug ("%s", job_debug); + + /* success */ + g_set_object (&self->result_list, result_list); + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_job_refine_run_finish (GsPluginJob *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_job_refine_class_init (GsPluginJobRefineClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginJobClass *job_class = GS_PLUGIN_JOB_CLASS (klass); + + object_class->dispose = gs_plugin_job_refine_dispose; + object_class->constructed = gs_plugin_job_refine_constructed; + object_class->get_property = gs_plugin_job_refine_get_property; + object_class->set_property = gs_plugin_job_refine_set_property; + + job_class->run_async = gs_plugin_job_refine_run_async; + job_class->run_finish = gs_plugin_job_refine_run_finish; + + /** + * GsPluginJobRefine:app-list: + * + * List of #GsApps to refine. + * + * This will not change during the course of the operation. + * + * Since: 42 + */ + props[PROP_APP_LIST] = + g_param_spec_object ("app-list", "App List", + "List of GsApps to refine.", + GS_TYPE_APP_LIST, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginJobRefine:flags: + * + * Flags to control what to refine. + * + * Since: 42 + */ + props[PROP_FLAGS] = + g_param_spec_flags ("flags", "Flags", + "Flags to control what to refine.", + GS_TYPE_PLUGIN_REFINE_FLAGS, GS_PLUGIN_REFINE_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static void +gs_plugin_job_refine_init (GsPluginJobRefine *self) +{ +} + +/** + * gs_plugin_job_refine_new: + * @app_list: the list of #GsApps to refine + * @flags: flags to affect what is refined + * + * Create a new #GsPluginJobRefine for refining the given @app_list. + * + * Returns: (transfer full): a new #GsPluginJobRefine + * Since: 42 + */ +GsPluginJob * +gs_plugin_job_refine_new (GsAppList *app_list, + GsPluginRefineFlags flags) +{ + return g_object_new (GS_TYPE_PLUGIN_JOB_REFINE, + "app-list", app_list, + "flags", flags, + NULL); +} + +/** + * gs_plugin_job_refine_new_for_app: + * @app: the #GsApp to refine + * @flags: flags to affect what is refined + * + * Create a new #GsPluginJobRefine for refining the given @app. + * + * Returns: (transfer full): a new #GsPluginJobRefine + * Since: 42 + */ +GsPluginJob * +gs_plugin_job_refine_new_for_app (GsApp *app, + GsPluginRefineFlags flags) +{ + g_autoptr(GsAppList) list = gs_app_list_new (); + gs_app_list_add (list, app); + + return gs_plugin_job_refine_new (list, flags); +} + +/** + * gs_plugin_job_refine_get_result_list: + * @self: a #GsPluginJobRefine + * + * Get the full list of refined #GsApps. This includes apps created in place of + * wildcards, if wildcards were provided in the #GsAppList passed to + * gs_plugin_job_refine_new(). + * + * If this is called before the job is complete, %NULL will be returned. + * + * Returns: (transfer none) (nullable): the job results, or %NULL on error + * or if called before the job has completed + * Since: 42 + */ +GsAppList * +gs_plugin_job_refine_get_result_list (GsPluginJobRefine *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB_REFINE (self), NULL); + + return self->result_list; +} diff --git a/lib/gs-plugin-job-refine.h b/lib/gs-plugin-job-refine.h new file mode 100644 index 0000000..ed207bc --- /dev/null +++ b/lib/gs-plugin-job-refine.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB_REFINE (gs_plugin_job_refine_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginJobRefine, gs_plugin_job_refine, GS, PLUGIN_JOB_REFINE, GsPluginJob) + +GsPluginJob *gs_plugin_job_refine_new_for_app (GsApp *app, + GsPluginRefineFlags flags); +GsPluginJob *gs_plugin_job_refine_new (GsAppList *app_list, + GsPluginRefineFlags flags); + +GsAppList *gs_plugin_job_refine_get_result_list (GsPluginJobRefine *self); + +G_END_DECLS diff --git a/lib/gs-plugin-job-refresh-metadata.c b/lib/gs-plugin-job-refresh-metadata.c new file mode 100644 index 0000000..01c65d1 --- /dev/null +++ b/lib/gs-plugin-job-refresh-metadata.c @@ -0,0 +1,531 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-job-refresh-metadata + * @short_description: A plugin job to refresh metadata + * + * #GsPluginJobRefreshMetadata is a #GsPluginJob representing an operation to + * refresh metadata inside plugins and about applications. + * + * For example, the metadata could be the list of applications available, or + * the list of updates, or a new set of popular applications to highlight. + * + * The maximum cache age should be set using + * #GsPluginJobRefreshMetadata:cache-age-secs. If this is not a low value, this + * job is not expected to do much work. Set it to zero to force all caches to be + * refreshed. + * + * This class is a wrapper around #GsPluginClass.refresh_metadata_async(), + * calling it for all loaded plugins. In addition it will refresh ODRS data on + * the #GsOdrsProvider set on the #GsPluginLoader. + * + * Once the refresh is complete, signals may be asynchronously emitted on + * plugins, apps and the #GsPluginLoader to indicate what metadata or sets of + * apps have changed. + * + * See also: #GsPluginClass.refresh_metadata_async + * Since: 42 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> +#include <glib/gi18n.h> + +#include "gs-enums.h" +#include "gs-external-appstream-utils.h" +#include "gs-plugin-job-private.h" +#include "gs-plugin-job-refresh-metadata.h" +#include "gs-plugin-types.h" +#include "gs-odrs-provider.h" +#include "gs-utils.h" + +/* A tuple to store the last-received progress data for a single download. + * See progress_cb() for more details. */ +typedef struct { + gsize bytes_downloaded; + gsize total_download_size; +} ProgressTuple; + +struct _GsPluginJobRefreshMetadata +{ + GsPluginJob parent; + + /* Input arguments. */ + guint64 cache_age_secs; + GsPluginRefreshMetadataFlags flags; + + /* In-progress data. */ + GError *saved_error; /* (owned) (nullable) */ + guint n_pending_ops; +#ifdef ENABLE_EXTERNAL_APPSTREAM + ProgressTuple external_appstream_progress; +#endif + ProgressTuple odrs_progress; + struct { + guint n_plugins; + guint n_plugins_complete; + } plugins_progress; + GSource *progress_source; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPluginJobRefreshMetadata, gs_plugin_job_refresh_metadata, GS_TYPE_PLUGIN_JOB) + +typedef enum { + PROP_CACHE_AGE_SECS = 1, + PROP_FLAGS, +} GsPluginJobRefreshMetadataProperty; + +static GParamSpec *props[PROP_FLAGS + 1] = { NULL, }; + +typedef enum { + SIGNAL_PROGRESS, +} GsPluginJobRefreshMetadataSignal; + +static guint signals[SIGNAL_PROGRESS + 1] = { 0, }; + +static void +gs_plugin_job_refresh_metadata_dispose (GObject *object) +{ + GsPluginJobRefreshMetadata *self = GS_PLUGIN_JOB_REFRESH_METADATA (object); + + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + /* Progress reporting should have been stopped by now. */ + if (self->progress_source != NULL) { + g_assert (g_source_is_destroyed (self->progress_source)); + g_clear_pointer (&self->progress_source, g_source_unref); + } + + G_OBJECT_CLASS (gs_plugin_job_refresh_metadata_parent_class)->dispose (object); +} + +static void +gs_plugin_job_refresh_metadata_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsPluginJobRefreshMetadata *self = GS_PLUGIN_JOB_REFRESH_METADATA (object); + + switch ((GsPluginJobRefreshMetadataProperty) prop_id) { + case PROP_CACHE_AGE_SECS: + g_value_set_uint64 (value, self->cache_age_secs); + break; + case PROP_FLAGS: + g_value_set_flags (value, self->flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_refresh_metadata_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsPluginJobRefreshMetadata *self = GS_PLUGIN_JOB_REFRESH_METADATA (object); + + switch ((GsPluginJobRefreshMetadataProperty) prop_id) { + case PROP_CACHE_AGE_SECS: + /* Construct only. */ + g_assert (self->cache_age_secs == 0); + self->cache_age_secs = g_value_get_uint64 (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + case PROP_FLAGS: + /* Construct only. */ + g_assert (self->flags == 0); + self->flags = g_value_get_flags (value); + g_object_notify_by_pspec (object, props[prop_id]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void refresh_progress_tuple_cb (gsize bytes_downloaded, + gsize total_download_size, + gpointer user_data); +static gboolean progress_cb (gpointer user_data); +#ifdef ENABLE_EXTERNAL_APPSTREAM +static void external_appstream_refresh_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +#endif +static void odrs_provider_refresh_ratings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void plugin_refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_op (GTask *task, + GError *error); + +static void +gs_plugin_job_refresh_metadata_run_async (GsPluginJob *job, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginJobRefreshMetadata *self = GS_PLUGIN_JOB_REFRESH_METADATA (job); + g_autoptr(GTask) task = NULL; + GPtrArray *plugins; /* (element-type GsPlugin) */ + gboolean any_plugins_ran = FALSE; + GsOdrsProvider *odrs_provider; + + /* Chosen to allow a few UI updates per second without updating the + * progress label so often it’s unreadable. */ + const guint progress_update_period_ms = 300; + + /* check required args */ + task = g_task_new (job, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_job_refresh_metadata_run_async); + g_task_set_task_data (task, g_object_ref (plugin_loader), (GDestroyNotify) g_object_unref); + + /* Set up the progress timeout. This periodically sums up the progress + * tuples in `self->*_progress` and reports them to the calling + * function via the #GsPluginJobRefreshMetadata::progress signal, giving + * an overall progress for all the parallel operations. */ + self->progress_source = g_timeout_source_new (progress_update_period_ms); + g_source_set_callback (self->progress_source, progress_cb, g_object_ref (self), g_object_unref); + g_source_attach (self->progress_source, g_main_context_get_thread_default ()); + + /* run each plugin, keeping a counter of pending operations which is + * initialised to 1 until all the operations are started */ + self->n_pending_ops = 1; + plugins = gs_plugin_loader_get_plugins (plugin_loader); + odrs_provider = gs_plugin_loader_get_odrs_provider (plugin_loader); + + /* Start downloading updated external appstream before anything else */ +#ifdef ENABLE_EXTERNAL_APPSTREAM + self->n_pending_ops++; + gs_external_appstream_refresh_async (self->cache_age_secs, + refresh_progress_tuple_cb, + &self->external_appstream_progress, + cancellable, + external_appstream_refresh_cb, + g_object_ref (task)); +#endif + + for (guint i = 0; i < plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugins, i); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + + if (!gs_plugin_get_enabled (plugin)) + continue; + if (plugin_class->refresh_metadata_async == NULL) + continue; + + /* at least one plugin supports this vfunc */ + any_plugins_ran = TRUE; + + /* Set up progress reporting for this plugin. */ + self->plugins_progress.n_plugins++; + + /* run the plugin */ + self->n_pending_ops++; + plugin_class->refresh_metadata_async (plugin, + self->cache_age_secs, + self->flags, + cancellable, + plugin_refresh_metadata_cb, + g_object_ref (task)); + } + + if (odrs_provider != NULL) { + self->n_pending_ops++; + gs_odrs_provider_refresh_ratings_async (odrs_provider, + self->cache_age_secs, + refresh_progress_tuple_cb, + &self->odrs_progress, + cancellable, + odrs_provider_refresh_ratings_cb, + g_object_ref (task)); + } + + /* some functions are really required for proper operation */ + if (!any_plugins_ran) { + g_autoptr(GError) local_error = NULL; + g_set_error_literal (&local_error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no plugin could handle refreshing"); + finish_op (task, g_steal_pointer (&local_error)); + } else { + finish_op (task, NULL); + } +} + +static void +refresh_progress_tuple_cb (gsize bytes_downloaded, + gsize total_download_size, + gpointer user_data) +{ + ProgressTuple *tuple = user_data; + + tuple->bytes_downloaded = bytes_downloaded; + tuple->total_download_size = total_download_size; + + /* The timeout callback in progress_cb() periodically sums these. No + * need to notify of progress from here. */ +} + +static gboolean +progress_cb (gpointer user_data) +{ + GsPluginJobRefreshMetadata *self = GS_PLUGIN_JOB_REFRESH_METADATA (user_data); +#ifdef ENABLE_EXTERNAL_APPSTREAM + gdouble external_appstream_completion = 0.0; +#endif + gdouble odrs_completion = 0.0; + gdouble progress; + guint n_portions; + + /* Sum up the progress for all parallel operations. This is complicated + * by the fact that external-appstream and ODRS operations report their + * progress in terms of bytes downloaded, but the other operations are + * just a counter. + * + * There is further complication from the fact that external-appstream + * support can be compiled out. + * + * Allocate each operation an equal portion of 100 percentage points. In + * this context, an operation is either a call to a plugin’s + * refresh_metadata_async() vfunc, or an external-appstream or ODRS + * refresh. */ + n_portions = self->plugins_progress.n_plugins; + +#ifdef ENABLE_EXTERNAL_APPSTREAM + if (self->external_appstream_progress.total_download_size > 0) + external_appstream_completion = (self->external_appstream_progress.bytes_downloaded / + self->external_appstream_progress.total_download_size); + n_portions++; +#endif + + if (self->odrs_progress.total_download_size > 0) + odrs_completion = (self->odrs_progress.bytes_downloaded / + self->odrs_progress.total_download_size); + n_portions++; + + /* Report progress via signal emission. */ + progress = (100.0 / n_portions) * (self->plugins_progress.n_plugins_complete + odrs_completion); +#ifdef ENABLE_EXTERNAL_APPSTREAM + progress += (100.0 / n_portions) * external_appstream_completion; +#endif + + g_signal_emit (self, signals[SIGNAL_PROGRESS], 0, (guint) progress); + + return G_SOURCE_CONTINUE; +} + +#ifdef ENABLE_EXTERNAL_APPSTREAM +static void +external_appstream_refresh_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GError) local_error = NULL; + + if (!gs_external_appstream_refresh_finish (result, &local_error)) + g_debug ("Failed to refresh external appstream: %s", local_error->message); + /* Intentionally ignore errors, to not block other plugins */ + finish_op (task, NULL); +} +#endif /* ENABLE_EXTERNAL_APPSTREAM */ + +static void +odrs_provider_refresh_ratings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsOdrsProvider *odrs_provider = GS_ODRS_PROVIDER (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GError) local_error = NULL; + + if (!gs_odrs_provider_refresh_ratings_finish (odrs_provider, result, &local_error)) + g_debug ("Failed to refresh ratings: %s", local_error->message); + /* Intentionally ignore errors, to not block other plugins */ + finish_op (task, NULL); +} + +static void +plugin_refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginJobRefreshMetadata *self = g_task_get_source_object (task); + g_autoptr(GError) local_error = NULL; + + if (!plugin_class->refresh_metadata_finish (plugin, result, &local_error)) + g_debug ("Failed to refresh plugin '%s': %s", gs_plugin_get_name (plugin), local_error->message); + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + + /* Update progress reporting. */ + self->plugins_progress.n_plugins_complete++; + + /* Intentionally ignore errors, to not block other plugins */ + finish_op (task, NULL); +} + +/* @error is (transfer full) if non-%NULL */ +static void +finish_op (GTask *task, + GError *error) +{ + GsPluginJobRefreshMetadata *self = g_task_get_source_object (task); + g_autoptr(GError) error_owned = g_steal_pointer (&error); + g_autofree gchar *job_debug = NULL; + + if (error_owned != NULL && self->saved_error == NULL) + self->saved_error = g_steal_pointer (&error_owned); + else if (error_owned != NULL) + g_debug ("Additional error while refreshing metadata: %s", error_owned->message); + + g_assert (self->n_pending_ops > 0); + self->n_pending_ops--; + + if (self->n_pending_ops > 0) + return; + + /* Emit one final progress update, then stop any further ones. + * Ensure the emission is in the right #GMainContext. */ + g_assert (g_main_context_is_owner (g_task_get_context (task))); + progress_cb (self); + g_source_destroy (self->progress_source); + + /* Get the results of the parallel ops. */ + if (self->saved_error != NULL) { + g_task_return_error (task, g_steal_pointer (&self->saved_error)); + return; + } + + /* show elapsed time */ + job_debug = gs_plugin_job_to_string (GS_PLUGIN_JOB (self)); + g_debug ("%s", job_debug); + + /* Check the intermediate working values are all cleared. */ + g_assert (self->saved_error == NULL); + g_assert (self->n_pending_ops == 0); + + /* success */ + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_job_refresh_metadata_run_finish (GsPluginJob *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_job_refresh_metadata_class_init (GsPluginJobRefreshMetadataClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginJobClass *job_class = GS_PLUGIN_JOB_CLASS (klass); + + object_class->dispose = gs_plugin_job_refresh_metadata_dispose; + object_class->get_property = gs_plugin_job_refresh_metadata_get_property; + object_class->set_property = gs_plugin_job_refresh_metadata_set_property; + + job_class->run_async = gs_plugin_job_refresh_metadata_run_async; + job_class->run_finish = gs_plugin_job_refresh_metadata_run_finish; + + /** + * GsPluginJobRefreshMetadata:cache-age-secs: + * + * Maximum age of caches before they are refreshed. + * + * Since: 42 + */ + props[PROP_CACHE_AGE_SECS] = + g_param_spec_uint64 ("cache-age-secs", "Cache Age", + "Maximum age of caches before they are refreshed.", + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginJobRefreshMetadata:flags: + * + * Flags to specify how the refresh job should behave. + * + * Since: 42 + */ + props[PROP_FLAGS] = + g_param_spec_flags ("flags", "Flags", + "Flags to specify how the refresh job should behave.", + GS_TYPE_PLUGIN_REFRESH_METADATA_FLAGS, GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); + + /** + * GsPluginJobRefreshMetadata::progress: + * @progress_percent: percentage completion of the job, [0, 100], or + * %G_MAXUINT to indicate that progress is unknown + * + * Emitted during #GsPluginJob.run_async() when progress is made. + * + * It’s emitted in the thread which is running the #GMainContext which + * was the thread-default context when #GsPluginJob.run_async() was + * called. + * + * Since: 42 + */ + signals[SIGNAL_PROGRESS] = + g_signal_new ("progress", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); +} + +static void +gs_plugin_job_refresh_metadata_init (GsPluginJobRefreshMetadata *self) +{ +} + +/** + * gs_plugin_job_refresh_metadata_new: + * @cache_age_secs: maximum allowed cache age, in seconds + * @flags: flags to affect the refresh + * + * Create a new #GsPluginJobRefreshMetadata for refreshing metadata about + * available applications. + * + * Caches will be refreshed if they are older than @cache_age_secs. + * + * Returns: (transfer full): a new #GsPluginJobRefreshMetadata + * Since: 42 + */ +GsPluginJob * +gs_plugin_job_refresh_metadata_new (guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags) +{ + return g_object_new (GS_TYPE_PLUGIN_JOB_REFRESH_METADATA, + "cache-age-secs", cache_age_secs, + "flags", flags, + NULL); +} diff --git a/lib/gs-plugin-job-refresh-metadata.h b/lib/gs-plugin-job-refresh-metadata.h new file mode 100644 index 0000000..94b69c7 --- /dev/null +++ b/lib/gs-plugin-job-refresh-metadata.h @@ -0,0 +1,28 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2022 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB_REFRESH_METADATA (gs_plugin_job_refresh_metadata_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginJobRefreshMetadata, gs_plugin_job_refresh_metadata, GS, PLUGIN_JOB_REFRESH_METADATA, GsPluginJob) + +GsPluginJob *gs_plugin_job_refresh_metadata_new (guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags); + +G_END_DECLS diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c new file mode 100644 index 0000000..a40980f --- /dev/null +++ b/lib/gs-plugin-job.c @@ -0,0 +1,502 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-enums.h" +#include "gs-plugin-private.h" +#include "gs-plugin-job-private.h" + +typedef struct +{ + GsPluginRefineFlags refine_flags; + GsAppListFilterFlags dedupe_flags; + gboolean interactive; + gboolean propagate_error; + guint max_results; + GsPlugin *plugin; + GsPluginAction action; + GsAppListSortFunc sort_func; + gpointer sort_func_data; + gchar *search; + GsApp *app; + GsAppList *list; + GFile *file; + gint64 time_created; +} GsPluginJobPrivate; + +enum { + PROP_0, + PROP_ACTION, + PROP_SEARCH, + PROP_REFINE_FLAGS, + PROP_DEDUPE_FLAGS, + PROP_INTERACTIVE, + PROP_APP, + PROP_LIST, + PROP_FILE, + PROP_MAX_RESULTS, + PROP_PROPAGATE_ERROR, + PROP_LAST +}; + +G_DEFINE_TYPE_WITH_PRIVATE (GsPluginJob, gs_plugin_job, G_TYPE_OBJECT) + +gchar * +gs_plugin_job_to_string (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + GString *str = g_string_new (NULL); + gint64 time_now = g_get_monotonic_time (); + g_string_append_printf (str, "running %s", + gs_plugin_action_to_string (priv->action)); + if (priv->plugin != NULL) { + g_string_append_printf (str, " on plugin=%s", + gs_plugin_get_name (priv->plugin)); + } + if (priv->dedupe_flags > 0) + g_string_append_printf (str, " with dedupe-flags=%" G_GUINT64_FORMAT, priv->dedupe_flags); + if (priv->refine_flags > 0) { + g_autofree gchar *tmp = gs_plugin_refine_flags_to_string (priv->refine_flags); + g_string_append_printf (str, " with refine-flags=%s", tmp); + } + if (priv->interactive) + g_string_append_printf (str, " with interactive=True"); + if (priv->propagate_error) + g_string_append_printf (str, " with propagate-error=True"); + + if (priv->max_results > 0) + g_string_append_printf (str, " with max-results=%u", priv->max_results); + if (priv->search != NULL) { + g_string_append_printf (str, " with search=%s", + priv->search); + } + if (priv->file != NULL) { + g_autofree gchar *path = g_file_get_path (priv->file); + g_string_append_printf (str, " with file=%s", path); + } + if (priv->list != NULL && gs_app_list_length (priv->list) > 0) { + g_autofree const gchar **unique_ids = NULL; + g_autofree gchar *unique_ids_str = NULL; + unique_ids = g_new0 (const gchar *, gs_app_list_length (priv->list) + 1); + for (guint i = 0; i < gs_app_list_length (priv->list); i++) { + GsApp *app = gs_app_list_index (priv->list, i); + unique_ids[i] = gs_app_get_unique_id (app); + } + unique_ids_str = g_strjoinv (",", (gchar**) unique_ids); + g_string_append_printf (str, " on apps %s", unique_ids_str); + } + if (time_now - priv->time_created > 1000) { + g_string_append_printf (str, ", elapsed time since creation %" G_GINT64_FORMAT "ms", + (time_now - priv->time_created) / 1000); + } + return g_string_free (str, FALSE); +} + +void +gs_plugin_job_set_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->refine_flags = refine_flags; +} + +void +gs_plugin_job_set_dedupe_flags (GsPluginJob *self, GsAppListFilterFlags dedupe_flags) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->dedupe_flags = dedupe_flags; +} + +GsPluginRefineFlags +gs_plugin_job_get_refine_flags (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), GS_PLUGIN_REFINE_FLAGS_NONE); + return priv->refine_flags; +} + +GsAppListFilterFlags +gs_plugin_job_get_dedupe_flags (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), GS_APP_LIST_FILTER_FLAG_NONE); + return priv->dedupe_flags; +} + +gboolean +gs_plugin_job_has_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), FALSE); + return (priv->refine_flags & refine_flags) > 0; +} + +void +gs_plugin_job_add_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->refine_flags |= refine_flags; +} + +void +gs_plugin_job_remove_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->refine_flags &= ~refine_flags; +} + +void +gs_plugin_job_set_interactive (GsPluginJob *self, gboolean interactive) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->interactive = interactive; +} + +gboolean +gs_plugin_job_get_interactive (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), FALSE); + return priv->interactive; +} + +void +gs_plugin_job_set_propagate_error (GsPluginJob *self, gboolean propagate_error) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->propagate_error = propagate_error; +} + +gboolean +gs_plugin_job_get_propagate_error (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), FALSE); + return priv->propagate_error; +} + +void +gs_plugin_job_set_max_results (GsPluginJob *self, guint max_results) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->max_results = max_results; +} + +guint +gs_plugin_job_get_max_results (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return priv->max_results; +} + +void +gs_plugin_job_set_action (GsPluginJob *self, GsPluginAction action) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->action = action; +} + +GsPluginAction +gs_plugin_job_get_action (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), GS_PLUGIN_ACTION_UNKNOWN); + return priv->action; +} + +void +gs_plugin_job_set_sort_func (GsPluginJob *self, GsAppListSortFunc sort_func, gpointer user_data) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + priv->sort_func = sort_func; + priv->sort_func_data = user_data; +} + +GsAppListSortFunc +gs_plugin_job_get_sort_func (GsPluginJob *self, gpointer *user_data_out) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + if (user_data_out != NULL) + *user_data_out = priv->sort_func_data; + return priv->sort_func; +} + +void +gs_plugin_job_set_search (GsPluginJob *self, const gchar *search) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_free (priv->search); + priv->search = g_strdup (search); +} + +const gchar * +gs_plugin_job_get_search (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return priv->search; +} + +void +gs_plugin_job_set_app (GsPluginJob *self, GsApp *app) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&priv->app, app); + + /* ensure we can always operate on a list object */ + if (priv->list != NULL && app != NULL && gs_app_list_length (priv->list) == 0) + gs_app_list_add (priv->list, priv->app); +} + +GsApp * +gs_plugin_job_get_app (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return priv->app; +} + +void +gs_plugin_job_set_list (GsPluginJob *self, GsAppList *list) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + if (list == NULL) + g_warning ("trying to set list to NULL, not a good idea"); + g_set_object (&priv->list, list); +} + +GsAppList * +gs_plugin_job_get_list (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return priv->list; +} + +void +gs_plugin_job_set_file (GsPluginJob *self, GFile *file) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&priv->file, file); +} + +GFile * +gs_plugin_job_get_file (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return priv->file; +} + +void +gs_plugin_job_set_plugin (GsPluginJob *self, GsPlugin *plugin) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&priv->plugin, plugin); +} + +GsPlugin * +gs_plugin_job_get_plugin (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return priv->plugin; +} + +static void +gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsPluginJob *self = GS_PLUGIN_JOB (obj); + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + + switch (prop_id) { + case PROP_ACTION: + g_value_set_enum (value, priv->action); + break; + case PROP_REFINE_FLAGS: + g_value_set_flags (value, priv->refine_flags); + break; + case PROP_DEDUPE_FLAGS: + g_value_set_flags (value, priv->dedupe_flags); + break; + case PROP_INTERACTIVE: + g_value_set_boolean (value, priv->interactive); + break; + case PROP_SEARCH: + g_value_set_string (value, priv->search); + break; + case PROP_APP: + g_value_set_object (value, priv->app); + break; + case PROP_LIST: + g_value_set_object (value, priv->list); + break; + case PROP_FILE: + g_value_set_object (value, priv->file); + break; + case PROP_MAX_RESULTS: + g_value_set_uint (value, priv->max_results); + break; + case PROP_PROPAGATE_ERROR: + g_value_set_boolean (value, priv->propagate_error); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsPluginJob *self = GS_PLUGIN_JOB (obj); + + switch (prop_id) { + case PROP_ACTION: + gs_plugin_job_set_action (self, g_value_get_enum (value)); + break; + case PROP_REFINE_FLAGS: + gs_plugin_job_set_refine_flags (self, g_value_get_flags (value)); + break; + case PROP_DEDUPE_FLAGS: + gs_plugin_job_set_dedupe_flags (self, g_value_get_flags (value)); + break; + case PROP_INTERACTIVE: + gs_plugin_job_set_interactive (self, g_value_get_boolean (value)); + break; + case PROP_SEARCH: + gs_plugin_job_set_search (self, g_value_get_string (value)); + break; + case PROP_APP: + gs_plugin_job_set_app (self, g_value_get_object (value)); + break; + case PROP_LIST: + gs_plugin_job_set_list (self, g_value_get_object (value)); + break; + case PROP_FILE: + gs_plugin_job_set_file (self, g_value_get_object (value)); + break; + case PROP_MAX_RESULTS: + gs_plugin_job_set_max_results (self, g_value_get_uint (value)); + break; + case PROP_PROPAGATE_ERROR: + gs_plugin_job_set_propagate_error (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_finalize (GObject *obj) +{ + GsPluginJob *self = GS_PLUGIN_JOB (obj); + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + + g_free (priv->search); + g_clear_object (&priv->app); + g_clear_object (&priv->list); + g_clear_object (&priv->file); + g_clear_object (&priv->plugin); + + G_OBJECT_CLASS (gs_plugin_job_parent_class)->finalize (obj); +} + +static void +gs_plugin_job_class_init (GsPluginJobClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_plugin_job_finalize; + object_class->get_property = gs_plugin_job_get_property; + object_class->set_property = gs_plugin_job_set_property; + + pspec = g_param_spec_enum ("action", NULL, NULL, + GS_TYPE_PLUGIN_ACTION, GS_PLUGIN_ACTION_UNKNOWN, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_ACTION, pspec); + + pspec = g_param_spec_flags ("refine-flags", NULL, NULL, + GS_TYPE_PLUGIN_REFINE_FLAGS, GS_PLUGIN_REFINE_FLAGS_NONE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_REFINE_FLAGS, pspec); + + pspec = g_param_spec_flags ("dedupe-flags", NULL, NULL, + GS_TYPE_APP_LIST_FILTER_FLAGS, GS_APP_LIST_FILTER_FLAG_NONE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_DEDUPE_FLAGS, pspec); + + pspec = g_param_spec_boolean ("interactive", NULL, NULL, + FALSE, + G_PARAM_READWRITE); + + g_object_class_install_property (object_class, PROP_INTERACTIVE, pspec); + + pspec = g_param_spec_string ("search", NULL, NULL, + NULL, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_SEARCH, pspec); + + pspec = g_param_spec_object ("app", NULL, NULL, + GS_TYPE_APP, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_APP, pspec); + + pspec = g_param_spec_object ("list", NULL, NULL, + GS_TYPE_APP_LIST, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_LIST, pspec); + + pspec = g_param_spec_object ("file", NULL, NULL, + G_TYPE_FILE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_FILE, pspec); + + pspec = g_param_spec_uint ("max-results", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_MAX_RESULTS, pspec); + + pspec = g_param_spec_boolean ("propagate-error", NULL, NULL, + FALSE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PROPAGATE_ERROR, pspec); +} + +static void +gs_plugin_job_init (GsPluginJob *self) +{ + GsPluginJobPrivate *priv = gs_plugin_job_get_instance_private (self); + + priv->refine_flags = GS_PLUGIN_REFINE_FLAGS_NONE; + priv->dedupe_flags = GS_APP_LIST_FILTER_FLAG_KEY_ID | + GS_APP_LIST_FILTER_FLAG_KEY_SOURCE | + GS_APP_LIST_FILTER_FLAG_KEY_VERSION; + priv->list = gs_app_list_new (); + priv->time_created = g_get_monotonic_time (); +} diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h new file mode 100644 index 0000000..dcdbc85 --- /dev/null +++ b/lib/gs-plugin-job.h @@ -0,0 +1,68 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app-list.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB (gs_plugin_job_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsPluginJob, gs_plugin_job, GS, PLUGIN_JOB, GObject) + +#include "gs-plugin-loader.h" + +struct _GsPluginJobClass +{ + GObjectClass parent_class; + + void (*run_async) (GsPluginJob *self, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*run_finish) (GsPluginJob *self, + GAsyncResult *result, + GError **error); +}; + +void gs_plugin_job_set_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +void gs_plugin_job_set_dedupe_flags (GsPluginJob *self, + GsAppListFilterFlags dedupe_flags); +void gs_plugin_job_set_interactive (GsPluginJob *self, + gboolean interactive); +void gs_plugin_job_set_propagate_error (GsPluginJob *self, + gboolean propagate_error); +void gs_plugin_job_set_max_results (GsPluginJob *self, + guint max_results); +void gs_plugin_job_set_sort_func (GsPluginJob *self, + GsAppListSortFunc sort_func, + gpointer user_data); +void gs_plugin_job_set_search (GsPluginJob *self, + const gchar *search); +void gs_plugin_job_set_app (GsPluginJob *self, + GsApp *app); +void gs_plugin_job_set_list (GsPluginJob *self, + GsAppList *list); +void gs_plugin_job_set_file (GsPluginJob *self, + GFile *file); +void gs_plugin_job_set_plugin (GsPluginJob *self, + GsPlugin *plugin); + +#define gs_plugin_job_newv(a,...) GS_PLUGIN_JOB(g_object_new(GS_TYPE_PLUGIN_JOB, "action", a, __VA_ARGS__)) + +#define GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT (GS_APP_LIST_FILTER_FLAG_KEY_ID | \ + GS_APP_LIST_FILTER_FLAG_KEY_SOURCE | \ + GS_APP_LIST_FILTER_FLAG_KEY_VERSION) + +G_END_DECLS diff --git a/lib/gs-plugin-loader-sync.c b/lib/gs-plugin-loader-sync.c new file mode 100644 index 0000000..2b8b39a --- /dev/null +++ b/lib/gs-plugin-loader-sync.c @@ -0,0 +1,254 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-plugin-loader-sync.h" + +/* tiny helper to help us do the async operation */ +typedef struct { + GAsyncResult *res; + GMainContext *context; + GMainLoop *loop; +} GsPluginLoaderHelper; + +static void +_helper_finish_sync (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoaderHelper *helper = user_data; + helper->res = g_object_ref (res); + g_main_loop_quit (helper->loop); +} + +GsAppList * +gs_plugin_loader_job_process (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + GsAppList *list; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job, + cancellable, + _helper_finish_sync, + &helper); + g_main_loop_run (helper.loop); + list = gs_plugin_loader_job_process_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return list; +} + +gboolean +gs_plugin_loader_job_action (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + gboolean ret; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job, + cancellable, + _helper_finish_sync, + &helper); + g_main_loop_run (helper.loop); + ret = gs_plugin_loader_job_action_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return ret; +} + +GsApp * +gs_plugin_loader_job_process_app (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + g_autoptr(GsAppList) list = NULL; + GsApp *app = NULL; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job, + cancellable, + _helper_finish_sync, + &helper); + g_main_loop_run (helper.loop); + list = gs_plugin_loader_job_process_finish (plugin_loader, + helper.res, + error); + if (list != NULL) + app = g_object_ref (gs_app_list_index (list, 0)); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return app; +} + +GsApp * +gs_plugin_loader_app_create (GsPluginLoader *plugin_loader, + const gchar *unique_id, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + GsApp *app; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_app_create_async (plugin_loader, + unique_id, + cancellable, + _helper_finish_sync, + &helper); + g_main_loop_run (helper.loop); + app = gs_plugin_loader_app_create_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return app; +} + +GsApp * +gs_plugin_loader_get_system_app (GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + GsApp *app; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_get_system_app_async (plugin_loader, + cancellable, + _helper_finish_sync, + &helper); + g_main_loop_run (helper.loop); + app = gs_plugin_loader_get_system_app_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return app; +} + +gboolean +gs_plugin_loader_setup (GsPluginLoader *plugin_loader, + const gchar * const *allowlist, + const gchar * const *blocklist, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + gboolean retval; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_setup_async (plugin_loader, + allowlist, + blocklist, + cancellable, + _helper_finish_sync, + &helper); + g_main_loop_run (helper.loop); + retval = gs_plugin_loader_setup_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return retval; +} diff --git a/lib/gs-plugin-loader-sync.h b/lib/gs-plugin-loader-sync.h new file mode 100644 index 0000000..844e2aa --- /dev/null +++ b/lib/gs-plugin-loader-sync.h @@ -0,0 +1,42 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2007-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-plugin-loader.h" + +G_BEGIN_DECLS + +GsAppList *gs_plugin_loader_job_process (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error); +GsApp *gs_plugin_loader_job_process_app (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error); +gboolean gs_plugin_loader_job_action (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error); +GsApp *gs_plugin_loader_app_create (GsPluginLoader *plugin_loader, + const gchar *unique_id, + GCancellable *cancellable, + GError **error); +GsApp *gs_plugin_loader_get_system_app (GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GError **error); +gboolean gs_plugin_loader_setup (GsPluginLoader *plugin_loader, + const gchar * const *allowlist, + const gchar * const *blocklist, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c new file mode 100644 index 0000000..d10b1be --- /dev/null +++ b/lib/gs-plugin-loader.c @@ -0,0 +1,4172 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2007-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <locale.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <appstream.h> +#include <math.h> + +#ifdef HAVE_SYSPROF +#include <sysprof-capture.h> +#endif + +#include "gs-app-collation.h" +#include "gs-app-private.h" +#include "gs-app-list-private.h" +#include "gs-category-manager.h" +#include "gs-category-private.h" +#include "gs-external-appstream-utils.h" +#include "gs-ioprio.h" +#include "gs-os-release.h" +#include "gs-plugin-loader.h" +#include "gs-plugin.h" +#include "gs-plugin-event.h" +#include "gs-plugin-job-private.h" +#include "gs-plugin-private.h" +#include "gs-utils.h" + +#define GS_PLUGIN_LOADER_UPDATES_CHANGED_DELAY 3 /* s */ +#define GS_PLUGIN_LOADER_RELOAD_DELAY 5 /* s */ + +struct _GsPluginLoader +{ + GObject parent; + + gboolean setup_complete; + GCancellable *setup_complete_cancellable; /* (nullable) (owned) */ + + GPtrArray *plugins; + GPtrArray *locations; + gchar *language; + gboolean plugin_dir_dirty; + GPtrArray *file_monitors; + GsPluginStatus global_status_last; + + GMutex pending_apps_mutex; + GsAppList *pending_apps; /* (nullable) (owned) */ + GCancellable *pending_apps_cancellable; /* (nullable) (owned) */ + + GThreadPool *queued_ops_pool; + gint active_jobs; + + GSettings *settings; + + GMutex events_by_id_mutex; + GHashTable *events_by_id; /* unique-id : GsPluginEvent */ + + gchar **compatible_projects; + guint scale; + + guint updates_changed_id; + guint updates_changed_cnt; + guint reload_id; + GHashTable *disallow_updates; /* GsPlugin : const char *name */ + + GNetworkMonitor *network_monitor; + gulong network_changed_handler; + gulong network_available_notify_handler; + gulong network_metered_notify_handler; + + GsCategoryManager *category_manager; + GsOdrsProvider *odrs_provider; /* (owned) (nullable) */ + +#ifdef HAVE_SYSPROF + SysprofCaptureWriter *sysprof_writer; /* (owned) (nullable) */ +#endif + + GDBusConnection *session_bus_connection; /* (owned); (not nullable) after setup */ + GDBusConnection *system_bus_connection; /* (owned); (not nullable) after setup */ +}; + +static void gs_plugin_loader_monitor_network (GsPluginLoader *plugin_loader); +static void add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app); +static gboolean remove_app_from_install_queue (GsPluginLoader *plugin_loader, GsApp *app); +static void gs_plugin_loader_process_in_thread_pool_cb (gpointer data, gpointer user_data); +static void gs_plugin_loader_status_changed_cb (GsPlugin *plugin, + GsApp *app, + GsPluginStatus status, + GsPluginLoader *plugin_loader); +static void async_result_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +G_DEFINE_TYPE (GsPluginLoader, gs_plugin_loader, G_TYPE_OBJECT) + +enum { + SIGNAL_STATUS_CHANGED, + SIGNAL_PENDING_APPS_CHANGED, + SIGNAL_UPDATES_CHANGED, + SIGNAL_RELOAD, + SIGNAL_BASIC_AUTH_START, + SIGNAL_ASK_UNTRUSTED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +typedef enum { + PROP_EVENTS = 1, + PROP_ALLOW_UPDATES, + PROP_NETWORK_AVAILABLE, + PROP_NETWORK_METERED, + PROP_SESSION_BUS_CONNECTION, + PROP_SYSTEM_BUS_CONNECTION, +} GsPluginLoaderProperty; + +static GParamSpec *obj_props[PROP_SYSTEM_BUS_CONNECTION + 1] = { NULL, }; + +typedef void (*GsPluginFunc) (GsPlugin *plugin); +typedef gboolean (*GsPluginSetupFunc) (GsPlugin *plugin, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginSearchFunc) (GsPlugin *plugin, + gchar **value, + GsAppList *list, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginAlternatesFunc) (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginCategoryFunc) (GsPlugin *plugin, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginGetRecentFunc) (GsPlugin *plugin, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginResultsFunc) (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginCategoriesFunc) (GsPlugin *plugin, + GPtrArray *list, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginActionFunc) (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginRefreshFunc) (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginFileToAppFunc) (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginUrlToAppFunc) (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error); +typedef gboolean (*GsPluginUpdateFunc) (GsPlugin *plugin, + GsAppList *apps, + GCancellable *cancellable, + GError **error); +typedef void (*GsPluginAdoptAppFunc) (GsPlugin *plugin, + GsApp *app); +typedef gboolean (*GsPluginGetLangPacksFunc) (GsPlugin *plugin, + GsAppList *list, + const gchar *locale, + GCancellable *cancellable, + GError **error); + + +/* async helper */ +typedef struct { + GsPluginLoader *plugin_loader; + const gchar *function_name; + const gchar *function_name_parent; + GPtrArray *catlist; + GsPluginJob *plugin_job; + gboolean anything_ran; + gchar **tokens; +} GsPluginLoaderHelper; + +static GsPluginLoaderHelper * +gs_plugin_loader_helper_new (GsPluginLoader *plugin_loader, GsPluginJob *plugin_job) +{ + GsPluginLoaderHelper *helper = g_slice_new0 (GsPluginLoaderHelper); + GsPluginAction action = gs_plugin_job_get_action (plugin_job); + helper->plugin_loader = g_object_ref (plugin_loader); + helper->plugin_job = g_object_ref (plugin_job); + helper->function_name = gs_plugin_action_to_function_name (action); + return helper; +} + +static void +reset_app_progress (GsApp *app) +{ + g_autoptr(GsAppList) addons = gs_app_dup_addons (app); + GsAppList *related = gs_app_get_related (app); + + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + + for (guint i = 0; addons != NULL && i < gs_app_list_length (addons); i++) { + GsApp *app_addons = gs_app_list_index (addons, i); + gs_app_set_progress (app_addons, GS_APP_PROGRESS_UNKNOWN); + } + for (guint i = 0; i < gs_app_list_length (related); i++) { + GsApp *app_related = gs_app_list_index (related, i); + gs_app_set_progress (app_related, GS_APP_PROGRESS_UNKNOWN); + } +} + +static void +gs_plugin_loader_helper_free (GsPluginLoaderHelper *helper) +{ + /* reset progress */ + switch (gs_plugin_job_get_action (helper->plugin_job)) { + case GS_PLUGIN_ACTION_INSTALL: + case GS_PLUGIN_ACTION_REMOVE: + case GS_PLUGIN_ACTION_UPDATE: + case GS_PLUGIN_ACTION_DOWNLOAD: + { + GsApp *app; + GsAppList *list; + + app = gs_plugin_job_get_app (helper->plugin_job); + if (app != NULL) + reset_app_progress (app); + + list = gs_plugin_job_get_list (helper->plugin_job); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app_tmp = gs_app_list_index (list, i); + reset_app_progress (app_tmp); + } + } + break; + default: + break; + } + + g_object_unref (helper->plugin_loader); + if (helper->plugin_job != NULL) + g_object_unref (helper->plugin_job); + if (helper->catlist != NULL) + g_ptr_array_unref (helper->catlist); + g_strfreev (helper->tokens); + g_slice_free (GsPluginLoaderHelper, helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPluginLoaderHelper, gs_plugin_loader_helper_free) + +GsPlugin * +gs_plugin_loader_find_plugin (GsPluginLoader *plugin_loader, + const gchar *plugin_name) +{ + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + if (g_strcmp0 (gs_plugin_get_name (plugin), plugin_name) == 0) + return plugin; + } + return NULL; +} + +static gboolean +gs_plugin_loader_notify_idle_cb (gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data); + g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_EVENTS]); + return FALSE; +} + +void +gs_plugin_loader_add_event (GsPluginLoader *plugin_loader, GsPluginEvent *event) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->events_by_id_mutex); + + /* events should always have a unique ID, either constructed from the + * app they are processing or preferably from the GError message */ + if (gs_plugin_event_get_unique_id (event) == NULL) { + g_warning ("failed to add event from action %s", + gs_plugin_action_to_string (gs_plugin_event_get_action (event))); + return; + } + + g_hash_table_insert (plugin_loader->events_by_id, + g_strdup (gs_plugin_event_get_unique_id (event)), + g_object_ref (event)); + g_idle_add (gs_plugin_loader_notify_idle_cb, plugin_loader); +} + +/** + * gs_plugin_loader_claim_error: + * @plugin_loader: a #GsPluginLoader + * @plugin: (nullable): a #GsPlugin to get an application from, or %NULL + * @action: a #GsPluginAction associated with the @error + * @app: (nullable): a #GsApp for the event, or %NULL + * @interactive: whether to set interactive flag + * @error: a #GError to claim + * + * Convert the @error into a plugin event and add it to the queue. + * + * The @plugin is used only if the @error contains a reference + * to a concrete application, in which case any cached application + * overrides the passed in @app. + * + * The %GS_PLUGIN_ERROR_CANCELLED and %G_IO_ERROR_CANCELLED errors + * are automatically ignored. + * + * Since: 41 + **/ +void +gs_plugin_loader_claim_error (GsPluginLoader *plugin_loader, + GsPlugin *plugin, + GsPluginAction action, + GsApp *app, + gboolean interactive, + const GError *error) +{ + g_autoptr(GError) error_copy = NULL; + g_autofree gchar *app_id = NULL; + g_autofree gchar *origin_id = NULL; + g_autoptr(GsPluginEvent) event = NULL; + g_autoptr(GsApp) event_app = NULL; + g_autoptr(GsApp) event_origin = NULL; + + g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader)); + g_return_if_fail (error != NULL); + + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + /* find and strip any unique IDs from the error message */ + error_copy = g_error_copy (error); + + for (guint i = 0; i < 2; i++) { + if (app_id == NULL) + app_id = gs_utils_error_strip_app_id (error_copy); + if (origin_id == NULL) + origin_id = gs_utils_error_strip_origin_id (error_copy); + } + + /* invalid */ + if (error_copy->domain != GS_PLUGIN_ERROR) { + g_warning ("not GsPlugin error %s:%i: %s", + g_quark_to_string (error_copy->domain), + error_copy->code, + error_copy->message); + error_copy->domain = GS_PLUGIN_ERROR; + error_copy->code = GS_PLUGIN_ERROR_FAILED; + } + + /* set the app and origin IDs if we managed to scrape them from the error above */ + if (app != NULL) + event_app = g_object_ref (app); + event_origin = NULL; + + if (plugin != NULL && as_utils_data_id_valid (app_id)) { + g_autoptr(GsApp) cached_app = gs_plugin_cache_lookup (plugin, app_id); + if (cached_app != NULL) { + g_debug ("found app %s in error", app_id); + g_set_object (&event_app, cached_app); + } else { + g_debug ("no unique ID found for app %s", app_id); + } + } + if (plugin != NULL && as_utils_data_id_valid (origin_id)) { + g_autoptr(GsApp) origin = gs_plugin_cache_lookup (plugin, origin_id); + if (origin != NULL) { + g_debug ("found origin %s in error", origin_id); + g_set_object (&event_origin, origin); + } else { + g_debug ("no unique ID found for origin %s", origin_id); + } + } + + /* create event which is handled by the GsShell */ + event = gs_plugin_event_new ("error", error_copy, + "action", action, + "app", event_app, + "origin", event_origin, + NULL); + if (interactive) + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + + /* add event to the queue */ + gs_plugin_loader_add_event (plugin_loader, event); +} + +/** + * gs_plugin_loader_claim_job_error: + * @plugin_loader: a #GsPluginLoader + * @plugin: (nullable): a #GsPlugin to get an application from, or %NULL + * @job: a #GsPluginJob for the @error + * @error: a #GError to claim + * + * The same as gs_plugin_loader_claim_error(), only reads the information + * from the @job. + * + * Since: 41 + **/ +void +gs_plugin_loader_claim_job_error (GsPluginLoader *plugin_loader, + GsPlugin *plugin, + GsPluginJob *job, + const GError *error) +{ + g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader)); + g_return_if_fail (GS_IS_PLUGIN_JOB (job)); + g_return_if_fail (error != NULL); + + gs_plugin_loader_claim_error (plugin_loader, plugin, + gs_plugin_job_get_action (job), + gs_plugin_job_get_app (job), + gs_plugin_job_get_interactive (job), + error); +} + +static gboolean +gs_plugin_loader_is_error_fatal (const GError *err) +{ + if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT)) + return TRUE; + if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED)) + return TRUE; + if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID)) + return TRUE; + return FALSE; +} + +static gboolean +gs_plugin_error_handle_failure (GsPluginLoaderHelper *helper, + GsPlugin *plugin, + const GError *error_local, + GError **error) +{ + g_autoptr(GError) error_local_copy = NULL; + g_autofree gchar *app_id = NULL; + g_autofree gchar *origin_id = NULL; + + /* badly behaved plugin */ + if (error_local == NULL) { + g_critical ("%s did not set error for %s", + gs_plugin_get_name (plugin), + helper->function_name); + return TRUE; + } + + if (gs_plugin_job_get_propagate_error (helper->plugin_job)) { + g_propagate_error (error, g_error_copy (error_local)); + return FALSE; + } + + /* this is only ever informational */ + if (g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) || + g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_debug ("ignoring error cancelled: %s", error_local->message); + return TRUE; + } + + /* find and strip any unique IDs from the error message */ + error_local_copy = g_error_copy (error_local); + + for (guint i = 0; i < 2; i++) { + if (app_id == NULL) + app_id = gs_utils_error_strip_app_id (error_local_copy); + if (origin_id == NULL) + origin_id = gs_utils_error_strip_origin_id (error_local_copy); + } + + /* fatal error */ + if (gs_plugin_loader_is_error_fatal (error_local_copy) || + g_getenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD") != NULL) { + if (error != NULL) + *error = g_steal_pointer (&error_local_copy); + return FALSE; + } + + gs_plugin_loader_claim_job_error (helper->plugin_loader, plugin, helper->plugin_job, error_local); + + return TRUE; +} + +/** + * gs_plugin_loader_run_adopt: + * @plugin_loader: a #GsPluginLoader + * @list: list of apps to try and adopt + * + * Call the gs_plugin_adopt_app() function on each plugin on each app in @list + * to try and find the plugin which should manage each app. + * + * This function is intended to be used by internal gnome-software code. + * + * Since: 42 + */ +void +gs_plugin_loader_run_adopt (GsPluginLoader *plugin_loader, GsAppList *list) +{ + guint i; + guint j; + + /* go through each plugin in order */ + for (i = 0; i < plugin_loader->plugins->len; i++) { + GsPluginAdoptAppFunc adopt_app_func = NULL; + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + adopt_app_func = gs_plugin_get_symbol (plugin, "gs_plugin_adopt_app"); + if (adopt_app_func == NULL) + continue; + for (j = 0; j < gs_app_list_length (list); j++) { + GsApp *app = gs_app_list_index (list, j); + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (!gs_app_has_management_plugin (app, NULL)) + continue; + + adopt_app_func (plugin, app); + + if (!gs_app_has_management_plugin (app, NULL)) { + g_debug ("%s adopted %s", + gs_plugin_get_name (plugin), + gs_app_get_unique_id (app)); + } + } + } + for (j = 0; j < gs_app_list_length (list); j++) { + GsApp *app = gs_app_list_index (list, j); + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (!gs_app_has_management_plugin (app, NULL)) + continue; + + g_debug ("nothing adopted %s", gs_app_get_unique_id (app)); + } +} + +static gboolean +gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper, + GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags refine_flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoader *plugin_loader = helper->plugin_loader; + GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job); + gboolean ret = TRUE; + gpointer func = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GTimer) timer = g_timer_new (); +#ifdef HAVE_SYSPROF + gint64 begin_time_nsec = SYSPROF_CAPTURE_CURRENT_TIME; +#endif + + /* load the possible symbol */ + func = gs_plugin_get_symbol (plugin, helper->function_name); + if (func == NULL) + return TRUE; + + /* at least one plugin supports this vfunc */ + helper->anything_ran = TRUE; + + /* fallback if unset */ + if (app == NULL) + app = gs_plugin_job_get_app (helper->plugin_job); + if (list == NULL) + list = gs_plugin_job_get_list (helper->plugin_job); + if (refine_flags == GS_PLUGIN_REFINE_FLAGS_NONE) + refine_flags = gs_plugin_job_get_refine_flags (helper->plugin_job); + + /* set what plugin is running on the job */ + gs_plugin_job_set_plugin (helper->plugin_job, plugin); + + /* run the correct vfunc */ + if (gs_plugin_job_get_interactive (helper->plugin_job)) + gs_plugin_interactive_inc (plugin); + switch (action) { + case GS_PLUGIN_ACTION_UPDATE: + if (g_strcmp0 (helper->function_name, "gs_plugin_update_app") == 0) { + GsPluginActionFunc plugin_func = func; + ret = plugin_func (plugin, app, cancellable, &error_local); + } else if (g_strcmp0 (helper->function_name, "gs_plugin_update") == 0) { + GsPluginUpdateFunc plugin_func = func; + ret = plugin_func (plugin, list, cancellable, &error_local); + } else { + g_critical ("function_name %s invalid for %s", + helper->function_name, + gs_plugin_action_to_string (action)); + } + break; + case GS_PLUGIN_ACTION_DOWNLOAD: + if (g_strcmp0 (helper->function_name, "gs_plugin_download_app") == 0) { + GsPluginActionFunc plugin_func = func; + ret = plugin_func (plugin, app, cancellable, &error_local); + } else if (g_strcmp0 (helper->function_name, "gs_plugin_download") == 0) { + GsPluginUpdateFunc plugin_func = func; + ret = plugin_func (plugin, list, cancellable, &error_local); + } else { + g_critical ("function_name %s invalid for %s", + helper->function_name, + gs_plugin_action_to_string (action)); + } + break; + case GS_PLUGIN_ACTION_INSTALL: + case GS_PLUGIN_ACTION_REMOVE: + case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD: + case GS_PLUGIN_ACTION_UPGRADE_TRIGGER: + case GS_PLUGIN_ACTION_LAUNCH: + case GS_PLUGIN_ACTION_UPDATE_CANCEL: + { + GsPluginActionFunc plugin_func = func; + ret = plugin_func (plugin, app, cancellable, &error_local); + } + break; + case GS_PLUGIN_ACTION_GET_UPDATES: + case GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL: + case GS_PLUGIN_ACTION_GET_SOURCES: + { + GsPluginResultsFunc plugin_func = func; + ret = plugin_func (plugin, list, cancellable, &error_local); + } + break; + case GS_PLUGIN_ACTION_FILE_TO_APP: + { + GsPluginFileToAppFunc plugin_func = func; + ret = plugin_func (plugin, list, + gs_plugin_job_get_file (helper->plugin_job), + cancellable, &error_local); + } + break; + case GS_PLUGIN_ACTION_URL_TO_APP: + { + GsPluginUrlToAppFunc plugin_func = func; + ret = plugin_func (plugin, list, + gs_plugin_job_get_search (helper->plugin_job), + cancellable, &error_local); + } + break; + case GS_PLUGIN_ACTION_GET_LANGPACKS: + { + GsPluginGetLangPacksFunc plugin_func = func; + ret = plugin_func (plugin, list, + gs_plugin_job_get_search (helper->plugin_job), + cancellable, &error_local); + } + break; + default: + g_critical ("no handler for %s", helper->function_name); + break; + } + if (gs_plugin_job_get_interactive (helper->plugin_job)) + gs_plugin_interactive_dec (plugin); + + /* plugin did not return error on cancellable abort */ + if (ret && g_cancellable_set_error_if_cancelled (cancellable, &error_local)) { + g_debug ("plugin %s did not return error with cancellable set", + gs_plugin_get_name (plugin)); + gs_utils_error_convert_gio (&error_local); + ret = FALSE; + } + + /* failed */ + if (!ret) { + return gs_plugin_error_handle_failure (helper, + plugin, + error_local, + error); + } + + /* add app to the pending installation queue if necessary */ + if (action == GS_PLUGIN_ACTION_INSTALL && + app != NULL && gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL) { + add_app_to_install_queue (plugin_loader, app); + } + +#ifdef HAVE_SYSPROF + if (plugin_loader->sysprof_writer != NULL) { + g_autofree gchar *sysprof_name = NULL; + g_autofree gchar *sysprof_message = NULL; + + sysprof_name = g_strconcat ("vfunc:", gs_plugin_action_to_string (action), NULL); + sysprof_message = gs_plugin_job_to_string (helper->plugin_job); + sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer, + begin_time_nsec, + sched_getcpu (), + getpid (), + SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec, + "gnome-software", + sysprof_name, + sysprof_message); + } +#endif /* HAVE_SYSPROF */ + + /* check the plugin didn't take too long */ + if (g_timer_elapsed (timer, NULL) > 1.0f) { + g_log_structured_standard (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, + __FILE__, G_STRINGIFY (__LINE__), + G_STRFUNC, + "plugin %s took %.1f seconds to do %s", + gs_plugin_get_name (plugin), + g_timer_elapsed (timer, NULL), + gs_plugin_action_to_string (action)); + } + + return TRUE; +} + +static void +gs_plugin_loader_job_sorted_truncation_again (GsPluginJob *plugin_job, + GsAppList *list) +{ + GsAppListSortFunc sort_func; + gpointer sort_func_data; + + /* not valid */ + if (list == NULL) + return; + + /* unset */ + sort_func = gs_plugin_job_get_sort_func (plugin_job, &sort_func_data); + if (sort_func == NULL) + return; + gs_app_list_sort (list, sort_func, sort_func_data); +} + +static void +gs_plugin_loader_job_sorted_truncation (GsPluginJob *plugin_job, + GsAppList *list) +{ + GsAppListSortFunc sort_func; + gpointer sort_func_data; + guint max_results; + + /* not valid */ + if (list == NULL) + return; + + /* unset */ + max_results = gs_plugin_job_get_max_results (plugin_job); + if (max_results == 0) + return; + + /* already small enough */ + if (gs_app_list_length (list) <= max_results) + return; + + /* nothing set */ + g_debug ("truncating results to %u from %u", + max_results, gs_app_list_length (list)); + sort_func = gs_plugin_job_get_sort_func (plugin_job, &sort_func_data); + if (sort_func == NULL) { + GsPluginAction action = gs_plugin_job_get_action (plugin_job); + g_debug ("no ->sort_func() set for %s, using random!", + gs_plugin_action_to_string (action)); + gs_app_list_randomize (list); + } else { + gs_app_list_sort (list, sort_func, sort_func_data); + } + gs_app_list_truncate (list, max_results); +} + +static gboolean +gs_plugin_loader_run_results (GsPluginLoaderHelper *helper, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoader *plugin_loader = helper->plugin_loader; +#ifdef HAVE_SYSPROF + gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME; +#endif + + /* Refining is done separately as it’s a special action */ + g_assert (!GS_IS_PLUGIN_JOB_REFINE (helper->plugin_job)); + + /* run each plugin */ + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, NULL, + GS_PLUGIN_REFINE_FLAGS_NONE, + cancellable, error)) { + return FALSE; + } + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + } + +#ifdef HAVE_SYSPROF + if (plugin_loader->sysprof_writer != NULL) { + g_autofree gchar *sysprof_name = NULL; + g_autofree gchar *sysprof_message = NULL; + + sysprof_name = g_strconcat ("run-results:", + gs_plugin_action_to_string (gs_plugin_job_get_action (helper->plugin_job)), + NULL); + sysprof_message = gs_plugin_job_to_string (helper->plugin_job); + sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer, + begin_time_nsec, + sched_getcpu (), + getpid (), + SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec, + "gnome-software", + sysprof_name, + sysprof_message); + } +#endif /* HAVE_SYSPROF */ + + return TRUE; +} + +static const gchar * +gs_plugin_loader_get_app_str (GsApp *app) +{ + const gchar *id; + + /* first try the actual id */ + id = gs_app_get_unique_id (app); + if (id != NULL) + return id; + + /* then try the source */ + id = gs_app_get_source_default (app); + if (id != NULL) + return id; + + /* lastly try the source id */ + id = gs_app_get_source_id_default (app); + if (id != NULL) + return id; + + /* urmmm */ + return "<invalid>"; +} + +gboolean +gs_plugin_loader_app_is_valid (GsApp *app, + GsPluginRefineFlags refine_flags) +{ + /* never show addons */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_ADDON) { + g_debug ("app invalid as addon %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* never show CLI apps */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_CONSOLE_APP) { + g_debug ("app invalid as console %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show unknown state */ + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) { + g_debug ("app invalid as state unknown %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show unconverted unavailables */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN && + gs_app_get_state (app) == GS_APP_STATE_UNAVAILABLE) { + g_debug ("app invalid as unconverted unavailable %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show blocklisted apps */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE)) { + g_debug ("app invalid as blocklisted %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* Don’t show parentally filtered apps unless they’re already + * installed. See the comments in gs-details-page.c for details. */ + if (!gs_app_is_installed (app) && + gs_app_has_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER)) { + g_debug ("app invalid as parentally filtered %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show apps with hide-from-search quirk, unless they are already installed */ + if (!gs_app_is_installed (app) && + gs_app_has_quirk (app, GS_APP_QUIRK_HIDE_FROM_SEARCH)) { + g_debug ("app invalid as hide-from-search quirk set %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show sources */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) { + g_debug ("app invalid as source %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show unknown kind */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN) { + g_debug ("app invalid as kind unknown %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show unconverted packages in the application view */ + if (!(refine_flags & GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES) && + gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC && + gs_app_get_special_kind (app) == GS_APP_SPECIAL_KIND_NONE) { + g_debug ("app invalid as only a %s: %s", + as_component_kind_to_string (gs_app_get_kind (app)), + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* don't show apps that do not have the required details */ + if (gs_app_get_name (app) == NULL) { + g_debug ("app invalid as no name %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + if (gs_app_get_summary (app) == NULL) { + g_debug ("app invalid as no summary %s", + gs_plugin_loader_get_app_str (app)); + return FALSE; + } + + /* ignore this crazy application */ + if (g_strcmp0 (gs_app_get_id (app), "gnome-system-monitor-kde.desktop") == 0) { + g_debug ("Ignoring KDE version of %s", gs_app_get_id (app)); + return FALSE; + } + return TRUE; +} + +static gboolean +gs_plugin_loader_app_is_valid_filter (GsApp *app, + gpointer user_data) +{ + GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data; + + return gs_plugin_loader_app_is_valid (app, gs_plugin_job_get_refine_flags (helper->plugin_job)); +} + +static gboolean +gs_plugin_loader_app_is_valid_updatable (GsApp *app, gpointer user_data) +{ + return gs_plugin_loader_app_is_valid_filter (app, user_data) && + (gs_app_is_updatable (app) || gs_app_get_state (app) == GS_APP_STATE_INSTALLING); +} + +gboolean +gs_plugin_loader_app_is_compatible (GsPluginLoader *plugin_loader, + GsApp *app) +{ + const gchar *tmp; + guint i; + + /* search for any compatible projects */ + tmp = gs_app_get_project_group (app); + if (tmp == NULL) + return TRUE; + for (i = 0; plugin_loader->compatible_projects[i] != NULL; i++) { + if (g_strcmp0 (tmp, plugin_loader->compatible_projects[i]) == 0) + return TRUE; + } + g_debug ("removing incompatible %s from project group %s", + gs_app_get_id (app), gs_app_get_project_group (app)); + return FALSE; +} + +/******************************************************************************/ + +/** + * gs_plugin_loader_job_process_finish: + * @plugin_loader: A #GsPluginLoader + * @res: a #GAsyncResult + * @error: A #GError, or %NULL + * + * Return value: (element-type GsApp) (transfer full): A list of applications + **/ +GsAppList * +gs_plugin_loader_job_process_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error) +{ + GTask *task; + GsAppList *list = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL); + g_return_val_if_fail (G_IS_TASK (res), NULL); + g_return_val_if_fail (g_task_is_valid (res, plugin_loader), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + task = G_TASK (res); + + /* Return cancelled if the task was cancelled and there is no other error set. + * + * This is needed because we set the task `check_cancellable` to FALSE, + * to be able to catch other errors such as timeout, but that means + * g_task_propagate_pointer() will ignore if the task was cancelled and only + * check if there was an error (i.e. g_task_return_*error*). + * + * We only do this if there is no error already set in the task (e.g. + * timeout) because in that case we want to return the existing error. + */ + if (!g_task_had_error (task)) { + GCancellable *cancellable = g_task_get_cancellable (task); + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_utils_error_convert_gio (error); + return NULL; + } + } + list = g_task_propagate_pointer (task, error); + gs_utils_error_convert_gio (error); + return list; +} + +/** + * gs_plugin_loader_job_action_finish: + * @plugin_loader: A #GsPluginLoader + * @res: a #GAsyncResult + * @error: A #GError, or %NULL + * + * Return value: success + **/ +gboolean +gs_plugin_loader_job_action_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error) +{ + g_autoptr(GsAppList) list = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), FALSE); + g_return_val_if_fail (G_IS_TASK (res), FALSE); + g_return_val_if_fail (g_task_is_valid (res, plugin_loader), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + list = g_task_propagate_pointer (G_TASK (res), error); + return list != NULL; +} + +/******************************************************************************/ + +static gboolean +emit_pending_apps_idle (gpointer loader) +{ + g_signal_emit (loader, signals[SIGNAL_PENDING_APPS_CHANGED], 0); + g_object_unref (loader); + + return G_SOURCE_REMOVE; +} + +static void +gs_plugin_loader_pending_apps_add (GsPluginLoader *plugin_loader, + GsPluginLoaderHelper *helper) +{ + GsAppList *list = gs_plugin_job_get_list (helper->plugin_job); + + g_assert (gs_app_list_length (list) > 0); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + switch (gs_plugin_job_get_action (helper->plugin_job)) { + case GS_PLUGIN_ACTION_INSTALL: + if (gs_app_get_state (app) != GS_APP_STATE_AVAILABLE_LOCAL) + add_app_to_install_queue (plugin_loader, app); + /* make sure the progress is properly initialized */ + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + break; + case GS_PLUGIN_ACTION_REMOVE: + remove_app_from_install_queue (plugin_loader, app); + break; + default: + break; + } + } + g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader)); +} + +static void +gs_plugin_loader_pending_apps_remove (GsPluginLoader *plugin_loader, + GsPluginLoaderHelper *helper) +{ + GsAppList *list = gs_plugin_job_get_list (helper->plugin_job); + + g_assert (gs_app_list_length (list) > 0); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + remove_app_from_install_queue (plugin_loader, app); + + /* check the app is not still in an action helper */ + switch (gs_app_get_state (app)) { + case GS_APP_STATE_INSTALLING: + case GS_APP_STATE_REMOVING: + g_warning ("application %s left in %s helper", + gs_app_get_unique_id (app), + gs_app_state_to_string (gs_app_get_state (app))); + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + break; + default: + break; + } + + } + g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader)); +} + +static void +async_result_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GAsyncResult **result_out = user_data; + + *result_out = g_object_ref (result); + g_main_context_wakeup (g_main_context_get_thread_default ()); +} + +/* This will load the install queue and add it to #GsPluginLoader.pending_apps, + * but it won’t refine the loaded apps. */ +static GsAppList * +load_install_queue (GsPluginLoader *plugin_loader, + GError **error) +{ + g_autofree gchar *contents = NULL; + g_autofree gchar *file = NULL; + g_auto(GStrv) names = NULL; + g_autoptr(GsAppList) list = NULL; + + /* load from file */ + file = g_build_filename (g_get_user_data_dir (), + "gnome-software", + "install-queue", + NULL); + if (!g_file_test (file, G_FILE_TEST_EXISTS)) + return gs_app_list_new (); + + g_debug ("loading install queue from %s", file); + if (!g_file_get_contents (file, &contents, NULL, error)) + return NULL; + + /* add to GsAppList, deduplicating if required */ + list = gs_app_list_new (); + names = g_strsplit (contents, "\n", 0); + for (guint i = 0; names[i] != NULL; i++) { + g_autoptr(GsApp) app = NULL; + g_auto(GStrv) split = g_strsplit (names[i], "\t", -1); + if (split[0] == NULL || split[1] == NULL) + continue; + app = gs_app_new (NULL); + gs_app_set_from_unique_id (app, split[0], as_component_kind_from_string (split[1])); + gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + } + + /* add to pending list */ + g_mutex_lock (&plugin_loader->pending_apps_mutex); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + g_debug ("adding pending app %s", gs_app_get_unique_id (app)); + if (plugin_loader->pending_apps == NULL) + plugin_loader->pending_apps = gs_app_list_new (); + gs_app_list_add (plugin_loader->pending_apps, app); + } + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + + return g_steal_pointer (&list); +} + +static void +save_install_queue (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GString) s = NULL; + g_autofree gchar *file = NULL; + + s = g_string_new (""); + g_mutex_lock (&plugin_loader->pending_apps_mutex); + for (guint i = 0; plugin_loader->pending_apps != NULL && i < gs_app_list_length (plugin_loader->pending_apps); i++) { + GsApp *app = gs_app_list_index (plugin_loader->pending_apps, i); + if (gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL) { + g_string_append (s, gs_app_get_unique_id (app)); + g_string_append_c (s, '\t'); + g_string_append (s, as_component_kind_to_string (gs_app_get_kind (app))); + g_string_append_c (s, '\n'); + } + } + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + + /* save file */ + file = g_build_filename (g_get_user_data_dir (), + "gnome-software", + "install-queue", + NULL); + if (s->len == 0) { + if (g_unlink (file) == -1 && errno != ENOENT) { + gint errn = errno; + g_warning ("Failed to unlink '%s': %s", file, g_strerror (errn)); + } + return; + } + + if (!gs_mkdir_parent (file, &error)) { + g_warning ("failed to create dir for %s: %s", + file, error->message); + return; + } + g_debug ("saving install queue to %s", file); + ret = g_file_set_contents (file, s->str, (gssize) s->len, &error); + if (!ret) + g_warning ("failed to save install queue: %s", error->message); +} + +static void +add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app) +{ + g_autoptr(GsAppList) addons = NULL; + g_autoptr(GSource) source = NULL; + guint i; + + /* queue the app itself */ + g_mutex_lock (&plugin_loader->pending_apps_mutex); + if (plugin_loader->pending_apps == NULL) + plugin_loader->pending_apps = gs_app_list_new (); + gs_app_list_add (plugin_loader->pending_apps, app); + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + + gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL); + + source = g_idle_source_new (); + g_source_set_callback (source, emit_pending_apps_idle, g_object_ref (plugin_loader), NULL); + g_source_set_name (source, "[gnome-software] emit_pending_apps_idle"); + g_source_attach (source, NULL); + + save_install_queue (plugin_loader); + + /* recursively queue any addons */ + addons = gs_app_dup_addons (app); + for (i = 0; addons != NULL && i < gs_app_list_length (addons); i++) { + GsApp *addon = gs_app_list_index (addons, i); + if (gs_app_get_to_be_installed (addon)) + add_app_to_install_queue (plugin_loader, addon); + } +} + +static gboolean +remove_app_from_install_queue (GsPluginLoader *plugin_loader, GsApp *app) +{ + g_autoptr(GsAppList) addons = NULL; + gboolean ret; + guint i; + + g_mutex_lock (&plugin_loader->pending_apps_mutex); + ret = plugin_loader->pending_apps != NULL && gs_app_list_remove (plugin_loader->pending_apps, app); + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + + if (ret) { + g_autoptr(GSource) source = NULL; + + if (gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL) + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + + source = g_idle_source_new (); + g_source_set_callback (source, emit_pending_apps_idle, g_object_ref (plugin_loader), NULL); + g_source_set_name (source, "[gnome-software] emit_pending_apps_idle"); + g_source_attach (source, NULL); + + save_install_queue (plugin_loader); + + /* recursively remove any queued addons */ + addons = gs_app_dup_addons (app); + for (i = 0; addons != NULL && i < gs_app_list_length (addons); i++) { + GsApp *addon = gs_app_list_index (addons, i); + remove_app_from_install_queue (plugin_loader, addon); + } + } + + return ret; +} + +/******************************************************************************/ + +gboolean +gs_plugin_loader_get_allow_updates (GsPluginLoader *plugin_loader) +{ + GHashTableIter iter; + gpointer value; + + /* nothing */ + if (g_hash_table_size (plugin_loader->disallow_updates) == 0) + return TRUE; + + /* list */ + g_hash_table_iter_init (&iter, plugin_loader->disallow_updates); + while (g_hash_table_iter_next (&iter, NULL, &value)) { + const gchar *reason = value; + g_debug ("managed updates inhibited by %s", reason); + } + return FALSE; +} + +GsAppList * +gs_plugin_loader_get_pending (GsPluginLoader *plugin_loader) +{ + GsAppList *array; + + array = gs_app_list_new (); + g_mutex_lock (&plugin_loader->pending_apps_mutex); + if (plugin_loader->pending_apps != NULL) + gs_app_list_add_list (array, plugin_loader->pending_apps); + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + + return array; +} + +gboolean +gs_plugin_loader_get_enabled (GsPluginLoader *plugin_loader, + const gchar *plugin_name) +{ + GsPlugin *plugin; + plugin = gs_plugin_loader_find_plugin (plugin_loader, plugin_name); + if (plugin == NULL) + return FALSE; + return gs_plugin_get_enabled (plugin); +} + +/** + * gs_plugin_loader_get_events: + * @plugin_loader: A #GsPluginLoader + * + * Gets all plugin events, even ones that are not active or visible anymore. + * + * Returns: (transfer container) (element-type GsPluginEvent): events + **/ +GPtrArray * +gs_plugin_loader_get_events (GsPluginLoader *plugin_loader) +{ + GPtrArray *events = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->events_by_id_mutex); + GHashTableIter iter; + gpointer key, value; + + /* just add everything */ + g_hash_table_iter_init (&iter, plugin_loader->events_by_id); + while (g_hash_table_iter_next (&iter, &key, &value)) { + const gchar *id = key; + GsPluginEvent *event = value; + if (event == NULL) { + g_warning ("failed to get event for '%s'", id); + continue; + } + g_ptr_array_add (events, g_object_ref (event)); + } + return events; +} + +/** + * gs_plugin_loader_get_event_default: + * @plugin_loader: A #GsPluginLoader + * + * Gets an active plugin event where active means that it was not been + * already dismissed by the user. + * + * Returns: (transfer full): a #GsPluginEvent, or %NULL for none + **/ +GsPluginEvent * +gs_plugin_loader_get_event_default (GsPluginLoader *plugin_loader) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->events_by_id_mutex); + GHashTableIter iter; + gpointer key, value; + + /* just add everything */ + g_hash_table_iter_init (&iter, plugin_loader->events_by_id); + while (g_hash_table_iter_next (&iter, &key, &value)) { + const gchar *id = key; + GsPluginEvent *event = value; + if (event == NULL) { + g_warning ("failed to get event for '%s'", id); + continue; + } + if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID)) + return g_object_ref (event); + } + return NULL; +} + +/** + * gs_plugin_loader_remove_events: + * @plugin_loader: A #GsPluginLoader + * + * Removes all plugin events from the loader. This function should only be + * called from the self tests. + **/ +void +gs_plugin_loader_remove_events (GsPluginLoader *plugin_loader) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->events_by_id_mutex); + g_hash_table_remove_all (plugin_loader->events_by_id); +} + +static void +gs_plugin_loader_report_event_cb (GsPlugin *plugin, + GsPluginEvent *event, + GsPluginLoader *plugin_loader) +{ + if (gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE); + gs_plugin_loader_add_event (plugin_loader, event); +} + +static void +gs_plugin_loader_allow_updates_cb (GsPlugin *plugin, + gboolean allow_updates, + GsPluginLoader *plugin_loader) +{ + gboolean changed = FALSE; + + /* plugin now allowing gnome-software to show updates panel */ + if (allow_updates) { + if (g_hash_table_remove (plugin_loader->disallow_updates, plugin)) { + g_debug ("plugin %s no longer inhibited managed updates", + gs_plugin_get_name (plugin)); + changed = TRUE; + } + + /* plugin preventing the updates panel from being shown */ + } else { + if (g_hash_table_replace (plugin_loader->disallow_updates, + (gpointer) plugin, + (gpointer) gs_plugin_get_name (plugin))) { + g_debug ("plugin %s inhibited managed updates", + gs_plugin_get_name (plugin)); + changed = TRUE; + } + } + + /* notify display layer */ + if (changed) + g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_ALLOW_UPDATES]); +} + +static void +gs_plugin_loader_status_changed_cb (GsPlugin *plugin, + GsApp *app, + GsPluginStatus status, + GsPluginLoader *plugin_loader) +{ + /* nothing specific */ + if (app == NULL || gs_app_get_id (app) == NULL) { + if (plugin_loader->global_status_last != status) { + g_debug ("emitting global %s", + gs_plugin_status_to_string (status)); + g_signal_emit (plugin_loader, + signals[SIGNAL_STATUS_CHANGED], + 0, app, status); + plugin_loader->global_status_last = status; + } + return; + } + + /* a specific app */ + g_debug ("emitting %s(%s)", + gs_plugin_status_to_string (status), + gs_app_get_id (app)); + g_signal_emit (plugin_loader, + signals[SIGNAL_STATUS_CHANGED], + 0, app, status); +} + +static void +gs_plugin_loader_basic_auth_start_cb (GsPlugin *plugin, + const gchar *remote, + const gchar *realm, + GCallback callback, + gpointer user_data, + GsPluginLoader *plugin_loader) +{ + g_debug ("emitting basic-auth-start %s", realm); + g_signal_emit (plugin_loader, + signals[SIGNAL_BASIC_AUTH_START], 0, + remote, + realm, + callback, + user_data); +} + +static gboolean +gs_plugin_loader_ask_untrusted_cb (GsPlugin *plugin, + const gchar *title, + const gchar *msg, + const gchar *details, + const gchar *accept_label, + GsPluginLoader *plugin_loader) +{ + gboolean accepts = FALSE; + g_debug ("emitting ask-untrusted title:'%s', msg:'%s' details:'%s' accept-label:'%s'", title, msg, details, accept_label); + g_signal_emit (plugin_loader, + signals[SIGNAL_ASK_UNTRUSTED], 0, + title, msg, details, accept_label, &accepts); + return accepts; +} + +static gboolean +gs_plugin_loader_job_updates_changed_delay_cb (gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data); + + /* notify shells */ + g_debug ("updates-changed"); + g_signal_emit (plugin_loader, signals[SIGNAL_UPDATES_CHANGED], 0); + plugin_loader->updates_changed_id = 0; + plugin_loader->updates_changed_cnt = 0; + + return FALSE; +} + +static void +gs_plugin_loader_updates_changed (GsPluginLoader *plugin_loader) +{ + if (plugin_loader->updates_changed_id != 0) + return; + plugin_loader->updates_changed_id = + g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, + GS_PLUGIN_LOADER_UPDATES_CHANGED_DELAY, + gs_plugin_loader_job_updates_changed_delay_cb, + g_object_ref (plugin_loader), + g_object_unref); +} + +static void +gs_plugin_loader_job_updates_changed_cb (GsPlugin *plugin, + GsPluginLoader *plugin_loader) +{ + plugin_loader->updates_changed_cnt++; + + /* Schedule emit of updates changed when no job is active. + This helps to avoid a race condition when a plugin calls + updates-changed at the end of the job, but the job is + finished before the callback gets called in the main thread. */ + if (!g_atomic_int_get (&plugin_loader->active_jobs)) + gs_plugin_loader_updates_changed (plugin_loader); +} + +static gboolean +gs_plugin_loader_reload_delay_cb (gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data); + + /* notify shells */ + g_debug ("emitting ::reload"); + g_signal_emit (plugin_loader, signals[SIGNAL_RELOAD], 0); + plugin_loader->reload_id = 0; + + g_object_unref (plugin_loader); + return FALSE; +} + +static void +gs_plugin_loader_reload_cb (GsPlugin *plugin, + GsPluginLoader *plugin_loader) +{ + if (plugin_loader->reload_id != 0) + return; + plugin_loader->reload_id = + g_timeout_add_seconds (GS_PLUGIN_LOADER_RELOAD_DELAY, + gs_plugin_loader_reload_delay_cb, + g_object_ref (plugin_loader)); +} + +static void +gs_plugin_loader_repository_changed_cb (GsPlugin *plugin, + GsApp *repository, + GsPluginLoader *plugin_loader) +{ + GApplication *application = g_application_get_default (); + + /* Can be NULL when running the self tests */ + if (application) { + g_signal_emit_by_name (application, + "repository-changed", + repository); + } +} + +static void +gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader, + const gchar *filename) +{ + GsPlugin *plugin; + g_autoptr(GError) error = NULL; + + /* create plugin from file */ + plugin = gs_plugin_create (filename, + plugin_loader->session_bus_connection, + plugin_loader->system_bus_connection, + &error); + if (plugin == NULL) { + g_warning ("Failed to load %s: %s", filename, error->message); + return; + } + g_signal_connect (plugin, "updates-changed", + G_CALLBACK (gs_plugin_loader_job_updates_changed_cb), + plugin_loader); + g_signal_connect (plugin, "reload", + G_CALLBACK (gs_plugin_loader_reload_cb), + plugin_loader); + g_signal_connect (plugin, "status-changed", + G_CALLBACK (gs_plugin_loader_status_changed_cb), + plugin_loader); + g_signal_connect (plugin, "basic-auth-start", + G_CALLBACK (gs_plugin_loader_basic_auth_start_cb), + plugin_loader); + g_signal_connect (plugin, "report-event", + G_CALLBACK (gs_plugin_loader_report_event_cb), + plugin_loader); + g_signal_connect (plugin, "allow-updates", + G_CALLBACK (gs_plugin_loader_allow_updates_cb), + plugin_loader); + g_signal_connect (plugin, "repository-changed", + G_CALLBACK (gs_plugin_loader_repository_changed_cb), + plugin_loader); + g_signal_connect (plugin, "ask-untrusted", + G_CALLBACK (gs_plugin_loader_ask_untrusted_cb), + plugin_loader); + gs_plugin_set_language (plugin, plugin_loader->language); + gs_plugin_set_scale (plugin, gs_plugin_loader_get_scale (plugin_loader)); + gs_plugin_set_network_monitor (plugin, plugin_loader->network_monitor); + g_debug ("opened plugin %s: %s", filename, gs_plugin_get_name (plugin)); + + /* add to array */ + g_ptr_array_add (plugin_loader->plugins, plugin); +} + +static void +gs_plugin_loader_remove_all_plugins (GsPluginLoader *plugin_loader) +{ + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = GS_PLUGIN (plugin_loader->plugins->pdata[i]); + g_signal_handlers_disconnect_by_data (plugin, plugin_loader); + } + + g_ptr_array_set_size (plugin_loader->plugins, 0); +} + +void +gs_plugin_loader_set_scale (GsPluginLoader *plugin_loader, guint scale) +{ + /* save globally, and update each plugin */ + plugin_loader->scale = scale; + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + gs_plugin_set_scale (plugin, scale); + } +} + +guint +gs_plugin_loader_get_scale (GsPluginLoader *plugin_loader) +{ + return plugin_loader->scale; +} + +void +gs_plugin_loader_add_location (GsPluginLoader *plugin_loader, const gchar *location) +{ + for (guint i = 0; i < plugin_loader->locations->len; i++) { + const gchar *location_tmp = g_ptr_array_index (plugin_loader->locations, i); + if (g_strcmp0 (location_tmp, location) == 0) + return; + } + g_info ("adding plugin location %s", location); + g_ptr_array_add (plugin_loader->locations, g_strdup (location)); +} + +static gint +gs_plugin_loader_plugin_sort_fn (gconstpointer a, gconstpointer b) +{ + GsPlugin *pa = *((GsPlugin **) a); + GsPlugin *pb = *((GsPlugin **) b); + if (gs_plugin_get_order (pa) < gs_plugin_get_order (pb)) + return -1; + if (gs_plugin_get_order (pa) > gs_plugin_get_order (pb)) + return 1; + return g_strcmp0 (gs_plugin_get_name (pa), gs_plugin_get_name (pb)); +} + +static void +gs_plugin_loader_software_app_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginEvent) event = NULL; + g_autoptr(GError) error = NULL; + + app = gs_plugin_loader_app_create_finish (plugin_loader, result, NULL); + + g_set_error_literal (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_RESTART_REQUIRED, + "A restart is required"); + event = gs_plugin_event_new ("action", GS_PLUGIN_ACTION_UNKNOWN, + "app", app, + "error", error, + NULL); + + gs_plugin_loader_add_event (plugin_loader, event); +} + +static void +gs_plugin_loader_plugin_dir_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + GsPluginLoader *plugin_loader) +{ + /* already triggered */ + if (plugin_loader->plugin_dir_dirty) + return; + + gs_plugin_loader_app_create_async (plugin_loader, "system/*/*/org.gnome.Software.desktop/*", + NULL, gs_plugin_loader_software_app_created_cb, NULL); + + plugin_loader->plugin_dir_dirty = TRUE; +} + +void +gs_plugin_loader_clear_caches (GsPluginLoader *plugin_loader) +{ + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + gs_plugin_cache_invalidate (plugin); + } +} + +static void +gs_plugin_loader_remove_all_file_monitors (GsPluginLoader *plugin_loader) +{ + for (guint i = 0; i < plugin_loader->file_monitors->len; i++) { + GFileMonitor *file_monitor = G_FILE_MONITOR (plugin_loader->file_monitors->pdata[i]); + + g_signal_handlers_disconnect_by_data (file_monitor, plugin_loader); + g_file_monitor_cancel (file_monitor); + } + + g_ptr_array_set_size (plugin_loader->file_monitors, 0); +} + +typedef struct { + GsPluginLoader *plugin_loader; /* (unowned) */ + GMainContext *context; /* (owned) */ + guint n_pending; +} ShutdownData; + +static void plugin_shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/** + * gs_plugin_loader_shutdown: + * @plugin_loader: a #GsPluginLoader + * @cancellable: a #GCancellable, or %NULL + * + * Shut down the plugins. + * + * This blocks until the operation is complete. It may be refactored in future + * to be asynchronous. + * + * Since: 42 + */ +void +gs_plugin_loader_shutdown (GsPluginLoader *plugin_loader, + GCancellable *cancellable) +{ + ShutdownData shutdown_data; + + shutdown_data.plugin_loader = plugin_loader; + shutdown_data.n_pending = 1; /* incremented until all operations have been started */ + shutdown_data.context = g_main_context_new (); + + g_main_context_push_thread_default (shutdown_data.context); + + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = GS_PLUGIN (plugin_loader->plugins->pdata[i]); + + if (!gs_plugin_get_enabled (plugin)) + continue; + + if (GS_PLUGIN_GET_CLASS (plugin)->shutdown_async != NULL) { + GS_PLUGIN_GET_CLASS (plugin)->shutdown_async (plugin, cancellable, + plugin_shutdown_cb, &shutdown_data); + shutdown_data.n_pending++; + } + } + + /* Wait for shutdown to complete in all plugins. */ + shutdown_data.n_pending--; + + while (shutdown_data.n_pending > 0) + g_main_context_iteration (shutdown_data.context, TRUE); + + g_main_context_pop_thread_default (shutdown_data.context); + g_clear_pointer (&shutdown_data.context, g_main_context_unref); + + /* Clear some internal data structures. */ + gs_plugin_loader_remove_all_plugins (plugin_loader); + gs_plugin_loader_remove_all_file_monitors (plugin_loader); + plugin_loader->setup_complete = FALSE; + g_clear_object (&plugin_loader->setup_complete_cancellable); + plugin_loader->setup_complete_cancellable = g_cancellable_new (); +} + +static void +plugin_shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + ShutdownData *data = user_data; + g_autoptr(GError) local_error = NULL; + + g_assert (GS_PLUGIN_GET_CLASS (plugin)->shutdown_finish != NULL); + + if (!GS_PLUGIN_GET_CLASS (plugin)->shutdown_finish (plugin, result, &local_error)) { + g_debug ("disabling %s as shutdown failed: %s", + gs_plugin_get_name (plugin), + local_error->message); + gs_plugin_set_enabled (plugin, FALSE); + } + + /* Indicate this plugin has finished shutting down. */ + data->n_pending--; + g_main_context_wakeup (data->context); +} + +static gint +gs_plugin_loader_path_sort_fn (gconstpointer a, gconstpointer b) +{ + const gchar *sa = *((const gchar **) a); + const gchar *sb = *((const gchar **) b); + return g_strcmp0 (sa, sb); +} + +static GPtrArray * +gs_plugin_loader_find_plugins (const gchar *path, GError **error) +{ + const gchar *fn_tmp; + g_autoptr(GPtrArray) fns = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GDir) dir = g_dir_open (path, 0, error); + if (dir == NULL) + return NULL; + while ((fn_tmp = g_dir_read_name (dir)) != NULL) { + if (!g_str_has_suffix (fn_tmp, ".so")) + continue; + g_ptr_array_add (fns, g_build_filename (path, fn_tmp, NULL)); + } + g_ptr_array_sort (fns, gs_plugin_loader_path_sort_fn); + return g_steal_pointer (&fns); +} + +typedef struct { + guint n_pending; + gchar **allowlist; + gchar **blocklist; +#ifdef HAVE_SYSPROF + gint64 setup_begin_time_nsec; + gint64 plugins_begin_time_nsec; +#endif +} SetupData; + +static void +setup_data_free (SetupData *data) +{ + g_clear_pointer (&data->allowlist, g_strfreev); + g_clear_pointer (&data->blocklist, g_strfreev); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SetupData, setup_data_free) + +static void get_session_bus_cb (GObject *object, + GAsyncResult *result, + gpointer user_data); +static void get_system_bus_cb (GObject *object, + GAsyncResult *result, + gpointer user_data); +static void finish_setup_get_bus (GTask *task); +static void plugin_setup_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_setup_op (GTask *task); +static void finish_setup_install_queue_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/* Mark the asynchronous setup operation as complete. This will notify any + * waiting tasks by cancelling the #GCancellable. It’s safe to clear the + * #GCancellable as each waiting task holds its own reference. */ +static void +notify_setup_complete (GsPluginLoader *plugin_loader) +{ + plugin_loader->setup_complete = TRUE; + g_cancellable_cancel (plugin_loader->setup_complete_cancellable); + g_clear_object (&plugin_loader->setup_complete_cancellable); +} + +/** + * gs_plugin_loader_setup_async: + * @plugin_loader: a #GsPluginLoader + * @allowlist: list of plugin names, or %NULL + * @blocklist: list of plugin names, or %NULL + * @cancellable: A #GCancellable, or %NULL + * @callback: callback to indicate completion of the asynchronous operation + * @user_data: data to pass to @callback + * + * Sets up the plugin loader ready for use. + * + * Since: 42 + */ +void +gs_plugin_loader_setup_async (GsPluginLoader *plugin_loader, + const gchar * const *allowlist, + const gchar * const *blocklist, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SetupData *setup_data; + g_autoptr(SetupData) setup_data_owned = NULL; + g_autoptr(GTask) task = NULL; +#ifdef HAVE_SYSPROF + gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME; +#endif + + task = g_task_new (plugin_loader, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_loader_setup_async); + + /* If setup is already complete, return immediately. */ + if (plugin_loader->setup_complete) { + g_task_return_boolean (task, TRUE); + return; + } + + /* Setup data closure. */ + setup_data = setup_data_owned = g_new0 (SetupData, 1); + setup_data->allowlist = g_strdupv ((gchar **) allowlist); + setup_data->blocklist = g_strdupv ((gchar **) blocklist); +#ifdef HAVE_SYSPROF + setup_data->setup_begin_time_nsec = begin_time_nsec; +#endif + + g_task_set_task_data (task, g_steal_pointer (&setup_data_owned), (GDestroyNotify) setup_data_free); + + /* Connect to D-Bus if connections haven’t been provided at construction + * time. */ + if (plugin_loader->session_bus_connection == NULL) + g_bus_get (G_BUS_TYPE_SESSION, cancellable, get_session_bus_cb, g_object_ref (task)); + if (plugin_loader->system_bus_connection == NULL) + g_bus_get (G_BUS_TYPE_SYSTEM, cancellable, get_system_bus_cb, g_object_ref (task)); + + finish_setup_get_bus (task); +} + +static void +get_session_bus_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginLoader *plugin_loader = g_task_get_source_object (task); + g_autoptr(GError) local_error = NULL; + + plugin_loader->session_bus_connection = g_bus_get_finish (result, &local_error); + if (plugin_loader->session_bus_connection == NULL) { + notify_setup_complete (plugin_loader); + g_prefix_error_literal (&local_error, "Error getting session bus: "); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_SESSION_BUS_CONNECTION]); + + finish_setup_get_bus (task); +} + +static void +get_system_bus_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginLoader *plugin_loader = g_task_get_source_object (task); + g_autoptr(GError) local_error = NULL; + + plugin_loader->system_bus_connection = g_bus_get_finish (result, &local_error); + if (plugin_loader->system_bus_connection == NULL) { + notify_setup_complete (plugin_loader); + g_prefix_error_literal (&local_error, "Error getting system bus: "); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_SYSTEM_BUS_CONNECTION]); + + finish_setup_get_bus (task); +} + +static void +finish_setup_get_bus (GTask *task) +{ + SetupData *data = g_task_get_task_data (task); + GsPluginLoader *plugin_loader = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + const gchar *plugin_name; + gboolean changes; + GPtrArray *deps; + GsPlugin *dep; + GsPlugin *plugin; + guint dep_loop_check = 0; + guint i; + guint j; + g_autoptr(GPtrArray) locations = NULL; + g_autoptr(GError) local_error = NULL; + + /* Wait until we’ve got all the buses we need. */ + if (plugin_loader->session_bus_connection == NULL || + plugin_loader->system_bus_connection == NULL) + return; + + /* use the default, but this requires a 'make install' */ + if (plugin_loader->locations->len == 0) { + g_autofree gchar *filename = NULL; + filename = g_strdup_printf ("plugins-%s", GS_PLUGIN_API_VERSION); + locations = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (locations, g_build_filename (LIBDIR, "gnome-software", filename, NULL)); + } else { + locations = g_ptr_array_ref (plugin_loader->locations); + } + + for (i = 0; i < locations->len; i++) { + GFileMonitor *monitor; + const gchar *location = g_ptr_array_index (locations, i); + g_autoptr(GFile) plugin_dir = g_file_new_for_path (location); + g_debug ("monitoring plugin location %s", location); + monitor = g_file_monitor_directory (plugin_dir, + G_FILE_MONITOR_NONE, + cancellable, + &local_error); + if (monitor == NULL) { + notify_setup_complete (plugin_loader); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_signal_connect (monitor, "changed", + G_CALLBACK (gs_plugin_loader_plugin_dir_changed_cb), plugin_loader); + g_ptr_array_add (plugin_loader->file_monitors, monitor); + } + + /* search for plugins */ + for (i = 0; i < locations->len; i++) { + const gchar *location = g_ptr_array_index (locations, i); + g_autoptr(GPtrArray) fns = NULL; + + /* search in the plugin directory for plugins */ + g_debug ("searching for plugins in %s", location); + fns = gs_plugin_loader_find_plugins (location, &local_error); + if (fns == NULL) { + notify_setup_complete (plugin_loader); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + for (j = 0; j < fns->len; j++) { + const gchar *fn = g_ptr_array_index (fns, j); + gs_plugin_loader_open_plugin (plugin_loader, fn); + } + } + + /* optional allowlist */ + if (data->allowlist != NULL) { + for (i = 0; i < plugin_loader->plugins->len; i++) { + gboolean ret; + plugin = g_ptr_array_index (plugin_loader->plugins, i); + if (!gs_plugin_get_enabled (plugin)) + continue; + ret = g_strv_contains ((const gchar * const *) data->allowlist, + gs_plugin_get_name (plugin)); + if (!ret) { + g_debug ("%s not in allowlist, disabling", + gs_plugin_get_name (plugin)); + } + gs_plugin_set_enabled (plugin, ret); + } + } + + /* optional blocklist */ + if (data->blocklist != NULL) { + for (i = 0; i < plugin_loader->plugins->len; i++) { + gboolean ret; + plugin = g_ptr_array_index (plugin_loader->plugins, i); + if (!gs_plugin_get_enabled (plugin)) + continue; + ret = g_strv_contains ((const gchar * const *) data->blocklist, + gs_plugin_get_name (plugin)); + if (ret) + gs_plugin_set_enabled (plugin, FALSE); + } + } + + /* order by deps */ + do { + changes = FALSE; + for (i = 0; i < plugin_loader->plugins->len; i++) { + plugin = g_ptr_array_index (plugin_loader->plugins, i); + deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_RUN_AFTER); + for (j = 0; j < deps->len && !changes; j++) { + plugin_name = g_ptr_array_index (deps, j); + dep = gs_plugin_loader_find_plugin (plugin_loader, + plugin_name); + if (dep == NULL) { + g_debug ("cannot find plugin '%s' " + "requested by '%s'", + plugin_name, + gs_plugin_get_name (plugin)); + continue; + } + if (!gs_plugin_get_enabled (dep)) + continue; + if (gs_plugin_get_order (plugin) <= gs_plugin_get_order (dep)) { + gs_plugin_set_order (plugin, gs_plugin_get_order (dep) + 1); + changes = TRUE; + } + } + } + for (i = 0; i < plugin_loader->plugins->len; i++) { + plugin = g_ptr_array_index (plugin_loader->plugins, i); + deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_RUN_BEFORE); + for (j = 0; j < deps->len && !changes; j++) { + plugin_name = g_ptr_array_index (deps, j); + dep = gs_plugin_loader_find_plugin (plugin_loader, + plugin_name); + if (dep == NULL) { + g_debug ("cannot find plugin '%s' " + "requested by '%s'", + plugin_name, + gs_plugin_get_name (plugin)); + continue; + } + if (!gs_plugin_get_enabled (dep)) + continue; + if (gs_plugin_get_order (plugin) >= gs_plugin_get_order (dep)) { + gs_plugin_set_order (dep, gs_plugin_get_order (plugin) + 1); + changes = TRUE; + } + } + } + + /* check we're not stuck */ + if (dep_loop_check++ > 100) { + notify_setup_complete (plugin_loader); + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED, + "got stuck in dep loop"); + return; + } + } while (changes); + + /* check for conflicts */ + for (i = 0; i < plugin_loader->plugins->len; i++) { + plugin = g_ptr_array_index (plugin_loader->plugins, i); + if (!gs_plugin_get_enabled (plugin)) + continue; + deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_CONFLICTS); + for (j = 0; j < deps->len && !changes; j++) { + plugin_name = g_ptr_array_index (deps, j); + dep = gs_plugin_loader_find_plugin (plugin_loader, + plugin_name); + if (dep == NULL) + continue; + if (!gs_plugin_get_enabled (dep)) + continue; + g_debug ("disabling %s as conflicts with %s", + gs_plugin_get_name (dep), + gs_plugin_get_name (plugin)); + gs_plugin_set_enabled (dep, FALSE); + } + } + + /* sort by order */ + g_ptr_array_sort (plugin_loader->plugins, + gs_plugin_loader_plugin_sort_fn); + + /* assign priority values */ + do { + changes = FALSE; + for (i = 0; i < plugin_loader->plugins->len; i++) { + plugin = g_ptr_array_index (plugin_loader->plugins, i); + deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_BETTER_THAN); + for (j = 0; j < deps->len && !changes; j++) { + plugin_name = g_ptr_array_index (deps, j); + dep = gs_plugin_loader_find_plugin (plugin_loader, + plugin_name); + if (dep == NULL) { + g_debug ("cannot find plugin '%s' " + "requested by '%s'", + plugin_name, + gs_plugin_get_name (plugin)); + continue; + } + if (!gs_plugin_get_enabled (dep)) + continue; + if (gs_plugin_get_priority (plugin) <= gs_plugin_get_priority (dep)) { + gs_plugin_set_priority (plugin, gs_plugin_get_priority (dep) + 1); + changes = TRUE; + } + } + } + + /* check we're not stuck */ + if (dep_loop_check++ > 100) { + notify_setup_complete (plugin_loader); + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED, + "got stuck in priority loop"); + return; + } + } while (changes); + + /* run setup */ + data->n_pending = 1; /* incremented until all operations have been started */ +#ifdef HAVE_SYSPROF + data->plugins_begin_time_nsec = SYSPROF_CAPTURE_CURRENT_TIME; +#endif + + for (i = 0; i < plugin_loader->plugins->len; i++) { + plugin = GS_PLUGIN (plugin_loader->plugins->pdata[i]); + + if (!gs_plugin_get_enabled (plugin)) + continue; + + if (GS_PLUGIN_GET_CLASS (plugin)->setup_async != NULL) { + data->n_pending++; + GS_PLUGIN_GET_CLASS (plugin)->setup_async (plugin, cancellable, + plugin_setup_cb, g_object_ref (task)); + } + } + + finish_setup_op (task); +} + +static void +plugin_setup_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; +#ifdef HAVE_SYSPROF + GsPluginLoader *plugin_loader = g_task_get_source_object (task); + SetupData *data = g_task_get_task_data (task); +#endif /* HAVE_SYSPROF */ + + g_assert (GS_PLUGIN_GET_CLASS (plugin)->setup_finish != NULL); + + if (!GS_PLUGIN_GET_CLASS (plugin)->setup_finish (plugin, result, &local_error)) { + g_debug ("disabling %s as setup failed: %s", + gs_plugin_get_name (plugin), + local_error->message); + gs_plugin_set_enabled (plugin, FALSE); + } + +#ifdef HAVE_SYSPROF + if (plugin_loader->sysprof_writer != NULL) { + sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer, + data->plugins_begin_time_nsec, + sched_getcpu (), + getpid (), + SYSPROF_CAPTURE_CURRENT_TIME - data->plugins_begin_time_nsec, + "gnome-software", + "setup-plugin", + NULL); + } +#endif /* HAVE_SYSPROF */ + + /* Indicate this plugin has finished setting up. */ + finish_setup_op (task); +} + +static void +finish_setup_op (GTask *task) +{ + SetupData *data = g_task_get_task_data (task); + GsPluginLoader *plugin_loader = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GsAppList) install_queue = NULL; + g_autoptr(GError) local_error = NULL; + + g_assert (data->n_pending > 0); + data->n_pending--; + + if (data->n_pending > 0) + return; + + /* now we can load the install-queue */ + install_queue = load_install_queue (plugin_loader, &local_error); + if (install_queue == NULL) { + notify_setup_complete (plugin_loader); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Mark setup as complete as it’s now safe for other jobs to be + * processed. Indeed, the final step in setup is to refine the install + * queue apps, which requires @setup_complete to be %TRUE. */ + notify_setup_complete (plugin_loader); + +#ifdef HAVE_SYSPROF + if (plugin_loader->sysprof_writer != NULL) { + sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer, + data->setup_begin_time_nsec, + sched_getcpu (), + getpid (), + SYSPROF_CAPTURE_CURRENT_TIME - data->setup_begin_time_nsec, + "gnome-software", + "setup", + NULL); + } +#endif /* HAVE_SYSPROF */ + + /* Refine the install queue. */ + if (gs_app_list_length (install_queue) > 0) { + g_autoptr(GsPluginJob) refine_job = NULL; + + /* Require ID and Origin to get complete unique IDs */ + refine_job = gs_plugin_job_refine_new (install_queue, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN | + GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING); + gs_plugin_loader_job_process_async (plugin_loader, refine_job, + cancellable, + finish_setup_install_queue_cb, + g_object_ref (task)); + } else { + g_task_return_boolean (task, TRUE); + } +} + +static void gs_plugin_loader_maybe_flush_pending_install_queue (GsPluginLoader *plugin_loader); + +static void +finish_setup_install_queue_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GsAppList) new_list = NULL; + g_autoptr(GError) local_error = NULL; + + new_list = gs_plugin_loader_job_process_finish (plugin_loader, result, &local_error); + if (new_list == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + } else { + g_autoptr(GsAppList) old_pending_apps = NULL; + gboolean has_pending_apps = FALSE; + gboolean changed; + g_mutex_lock (&plugin_loader->pending_apps_mutex); + changed = plugin_loader->pending_apps != NULL; + /* Merge the existing and newly-loaded lists, in case pending apps were added + while the install-queue file was being loaded */ + old_pending_apps = g_steal_pointer (&plugin_loader->pending_apps); + if (old_pending_apps != NULL && gs_app_list_length (new_list) > 0) { + g_autoptr(GHashTable) expected_unique_ids = g_hash_table_new (g_str_hash, g_str_equal); + for (guint i = 0; i < gs_app_list_length (old_pending_apps); i++) { + GsApp *app = gs_app_list_index (old_pending_apps, i); + if (gs_app_get_unique_id (app) != NULL) + g_hash_table_add (expected_unique_ids, (gpointer) gs_app_get_unique_id (app)); + } + for (guint i = 0; i < gs_app_list_length (new_list); i++) { + GsApp *app = gs_app_list_index (new_list, i); + if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE && + gs_app_get_unique_id (app) != NULL && + g_hash_table_contains (expected_unique_ids, gs_app_get_unique_id (app))) { + if (plugin_loader->pending_apps == NULL) + plugin_loader->pending_apps = gs_app_list_new (); + gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL); + gs_app_set_pending_action (app, GS_PLUGIN_ACTION_INSTALL); + gs_app_list_add (plugin_loader->pending_apps, app); + } + } + has_pending_apps = plugin_loader->pending_apps != NULL; + changed = TRUE; + } + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + g_task_return_boolean (task, TRUE); + + if (changed) + save_install_queue (plugin_loader); + if (has_pending_apps) + gs_plugin_loader_maybe_flush_pending_install_queue (plugin_loader); + } +} + +/** + * gs_plugin_loader_setup_finish: + * @plugin_loader: a #GsPluginLoader + * @result: result of the asynchronous operation + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous setup operation started with + * gs_plugin_loader_setup_async(). + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_plugin_loader_setup_finish (GsPluginLoader *plugin_loader, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), FALSE); + g_return_val_if_fail (g_task_is_valid (result, plugin_loader), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, gs_plugin_loader_setup_async), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +gs_plugin_loader_dump_state (GsPluginLoader *plugin_loader) +{ + g_autoptr(GString) str_enabled = g_string_new (NULL); + g_autoptr(GString) str_disabled = g_string_new (NULL); + + /* print what the priorities are if verbose */ + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + GString *str = gs_plugin_get_enabled (plugin) ? str_enabled : str_disabled; + g_string_append_printf (str, "%s, ", gs_plugin_get_name (plugin)); + g_debug ("[%s]\t%u\t->\t%s", + gs_plugin_get_enabled (plugin) ? "enabled" : "disabld", + gs_plugin_get_order (plugin), + gs_plugin_get_name (plugin)); + } + if (str_enabled->len > 2) + g_string_truncate (str_enabled, str_enabled->len - 2); + if (str_disabled->len > 2) + g_string_truncate (str_disabled, str_disabled->len - 2); + g_info ("enabled plugins: %s", str_enabled->str); + g_info ("disabled plugins: %s", str_disabled->str); +} + +static void +gs_plugin_loader_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object); + + switch ((GsPluginLoaderProperty) prop_id) { + case PROP_EVENTS: + g_value_set_pointer (value, plugin_loader->events_by_id); + break; + case PROP_ALLOW_UPDATES: + g_value_set_boolean (value, gs_plugin_loader_get_allow_updates (plugin_loader)); + break; + case PROP_NETWORK_AVAILABLE: + g_value_set_boolean (value, gs_plugin_loader_get_network_available (plugin_loader)); + break; + case PROP_NETWORK_METERED: + g_value_set_boolean (value, gs_plugin_loader_get_network_metered (plugin_loader)); + break; + case PROP_SESSION_BUS_CONNECTION: + g_value_set_object (value, plugin_loader->session_bus_connection); + break; + case PROP_SYSTEM_BUS_CONNECTION: + g_value_set_object (value, plugin_loader->system_bus_connection); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_loader_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object); + + switch ((GsPluginLoaderProperty) prop_id) { + case PROP_EVENTS: + case PROP_ALLOW_UPDATES: + case PROP_NETWORK_AVAILABLE: + case PROP_NETWORK_METERED: + /* Read only */ + g_assert_not_reached (); + break; + case PROP_SESSION_BUS_CONNECTION: + /* Construct only */ + g_assert (plugin_loader->session_bus_connection == NULL); + plugin_loader->session_bus_connection = g_value_dup_object (value); + break; + case PROP_SYSTEM_BUS_CONNECTION: + /* Construct only */ + g_assert (plugin_loader->system_bus_connection == NULL); + plugin_loader->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_loader_dispose (GObject *object) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object); + + g_cancellable_cancel (plugin_loader->pending_apps_cancellable); + + if (plugin_loader->plugins != NULL) { + /* Shut down all the plugins first. */ + gs_plugin_loader_shutdown (plugin_loader, NULL); + + g_clear_pointer (&plugin_loader->plugins, g_ptr_array_unref); + } + if (plugin_loader->updates_changed_id != 0) { + g_source_remove (plugin_loader->updates_changed_id); + plugin_loader->updates_changed_id = 0; + } + if (plugin_loader->network_changed_handler != 0) { + g_signal_handler_disconnect (plugin_loader->network_monitor, + plugin_loader->network_changed_handler); + plugin_loader->network_changed_handler = 0; + } + if (plugin_loader->network_available_notify_handler != 0) { + g_signal_handler_disconnect (plugin_loader->network_monitor, + plugin_loader->network_available_notify_handler); + plugin_loader->network_available_notify_handler = 0; + } + if (plugin_loader->network_metered_notify_handler != 0) { + g_signal_handler_disconnect (plugin_loader->network_monitor, + plugin_loader->network_metered_notify_handler); + plugin_loader->network_metered_notify_handler = 0; + } + if (plugin_loader->queued_ops_pool != NULL) { + /* stop accepting more requests and wait until any currently + * running ones are finished */ + g_thread_pool_free (plugin_loader->queued_ops_pool, TRUE, TRUE); + plugin_loader->queued_ops_pool = NULL; + } + g_clear_object (&plugin_loader->network_monitor); + g_clear_object (&plugin_loader->settings); + g_clear_object (&plugin_loader->pending_apps); + g_clear_object (&plugin_loader->category_manager); + g_clear_object (&plugin_loader->odrs_provider); + g_clear_object (&plugin_loader->setup_complete_cancellable); + g_clear_object (&plugin_loader->pending_apps_cancellable); + +#ifdef HAVE_SYSPROF + g_clear_pointer (&plugin_loader->sysprof_writer, sysprof_capture_writer_unref); +#endif + + g_clear_object (&plugin_loader->session_bus_connection); + g_clear_object (&plugin_loader->system_bus_connection); + + G_OBJECT_CLASS (gs_plugin_loader_parent_class)->dispose (object); +} + +static void +gs_plugin_loader_finalize (GObject *object) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object); + + g_strfreev (plugin_loader->compatible_projects); + g_ptr_array_unref (plugin_loader->locations); + g_free (plugin_loader->language); + g_ptr_array_unref (plugin_loader->file_monitors); + g_hash_table_unref (plugin_loader->events_by_id); + g_hash_table_unref (plugin_loader->disallow_updates); + + g_mutex_clear (&plugin_loader->pending_apps_mutex); + g_mutex_clear (&plugin_loader->events_by_id_mutex); + + G_OBJECT_CLASS (gs_plugin_loader_parent_class)->finalize (object); +} + +static void +gs_plugin_loader_class_init (GsPluginLoaderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gs_plugin_loader_get_property; + object_class->set_property = gs_plugin_loader_set_property; + object_class->dispose = gs_plugin_loader_dispose; + object_class->finalize = gs_plugin_loader_finalize; + + /** + * GsPluginLoader:events: + * + * Events added on the plugin loader using gs_plugin_loader_add_event(). + */ + obj_props[PROP_EVENTS] = + g_param_spec_string ("events", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginLoader:allow-updates: + * + * Whether updates and upgrades are managed by gnome-software. + * + * If not, the updates UI should be hidden and no automatic updates + * performed. + */ + obj_props[PROP_ALLOW_UPDATES] = + g_param_spec_boolean ("allow-updates", NULL, NULL, + TRUE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginLoader:network-available: + * + * Whether the network is considered available. + * + * This has the same semantics as #GNetworkMonitor:network-available. + */ + obj_props[PROP_NETWORK_AVAILABLE] = + g_param_spec_boolean ("network-available", NULL, NULL, + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginLoader:network-metered: + * + * Whether the network is considered metered. + * + * This has the same semantics as #GNetworkMonitor:network-metered. + */ + obj_props[PROP_NETWORK_METERED] = + g_param_spec_boolean ("network-metered", NULL, NULL, + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsPluginLoader:session-bus-connection: (nullable) + * + * A connection to the D-Bus session bus. + * + * This may be %NULL at construction time. If so, the default session + * bus connection will be used (and returned as the value of this + * property) after gs_plugin_loader_setup_async() is called. + * + * 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); + + /** + * GsPluginLoader:system-bus-connection: (not nullable) + * + * A connection to the D-Bus system bus. + * + * This may be %NULL at construction time. If so, the default system + * bus connection will be used (and returned as the value of this + * property) after gs_plugin_loader_setup_async() is called. + * + * 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_STATUS_CHANGED] = + g_signal_new ("status-changed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_UINT); + signals [SIGNAL_PENDING_APPS_CHANGED] = + g_signal_new ("pending-apps-changed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals [SIGNAL_UPDATES_CHANGED] = + g_signal_new ("updates-changed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals [SIGNAL_RELOAD] = + g_signal_new ("reload", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals [SIGNAL_BASIC_AUTH_START] = + g_signal_new ("basic-auth-start", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER); + signals [SIGNAL_ASK_UNTRUSTED] = + g_signal_new ("ask-untrusted", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, 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_loader_allow_updates_recheck (GsPluginLoader *plugin_loader) +{ + if (g_settings_get_boolean (plugin_loader->settings, "allow-updates")) { + g_hash_table_remove (plugin_loader->disallow_updates, plugin_loader); + } else { + g_hash_table_insert (plugin_loader->disallow_updates, + (gpointer) plugin_loader, + (gpointer) "GSettings"); + } +} + +static void +gs_plugin_loader_settings_changed_cb (GSettings *settings, + const gchar *key, + GsPluginLoader *plugin_loader) +{ + if (g_strcmp0 (key, "allow-updates") == 0) + gs_plugin_loader_allow_updates_recheck (plugin_loader); +} + +static gint +get_max_parallel_ops (void) +{ + guint mem_total = gs_utils_get_memory_total (); + if (mem_total == 0) + return 8; + /* allow 1 op per GB of memory */ + return (gint) MAX (round((gdouble) mem_total / 1024), 1.0); +} + +static void +gs_plugin_loader_init (GsPluginLoader *plugin_loader) +{ + const gchar *tmp; + gchar *match; + gchar **projects; + guint i; + g_autofree gchar *review_server = NULL; + g_autofree gchar *user_hash = NULL; + g_autoptr(GError) local_error = NULL; + const guint64 odrs_review_max_cache_age_secs = 237000; /* 1 week */ + const guint odrs_review_n_results_max = 20; + const gchar *locale; + +#ifdef HAVE_SYSPROF + plugin_loader->sysprof_writer = sysprof_capture_writer_new_from_env (0); +#endif /* HAVE_SYSPROF */ + + plugin_loader->setup_complete_cancellable = g_cancellable_new (); + plugin_loader->scale = 1; + plugin_loader->plugins = g_ptr_array_new_with_free_func (g_object_unref); + plugin_loader->pending_apps = NULL; + plugin_loader->queued_ops_pool = g_thread_pool_new (gs_plugin_loader_process_in_thread_pool_cb, + NULL, + get_max_parallel_ops (), + FALSE, + NULL); + plugin_loader->file_monitors = g_ptr_array_new_with_free_func (g_object_unref); + plugin_loader->locations = g_ptr_array_new_with_free_func (g_free); + plugin_loader->settings = g_settings_new ("org.gnome.software"); + g_signal_connect (plugin_loader->settings, "changed", + G_CALLBACK (gs_plugin_loader_settings_changed_cb), plugin_loader); + plugin_loader->events_by_id = g_hash_table_new_full ((GHashFunc) as_utils_data_id_hash, + (GEqualFunc) as_utils_data_id_equal, + g_free, + (GDestroyNotify) g_object_unref); + + /* get the category manager */ + plugin_loader->category_manager = gs_category_manager_new (); + + /* set up the ODRS provider */ + + /* get the machine+user ID hash value */ + user_hash = gs_utils_get_user_hash (&local_error); + if (user_hash == NULL) { + g_warning ("Failed to get machine+user hash: %s", local_error->message); + plugin_loader->odrs_provider = NULL; + } else { + review_server = g_settings_get_string (plugin_loader->settings, "review-server"); + + if (review_server != NULL && *review_server != '\0') { + const gchar *distro = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + g_autoptr(SoupSession) odrs_soup_session = NULL; + + /* get the distro name (e.g. 'Fedora') but allow a fallback */ + os_release = gs_os_release_new (&local_error); + if (os_release != NULL) { + distro = gs_os_release_get_name (os_release); + if (distro == NULL) + g_warning ("no distro name specified"); + } else { + g_warning ("failed to get distro name: %s", local_error->message); + } + + /* Fallback */ + if (distro == NULL) + distro = C_("Distribution name", "Unknown"); + + odrs_soup_session = gs_build_soup_session (); + plugin_loader->odrs_provider = gs_odrs_provider_new (review_server, + user_hash, + distro, + odrs_review_max_cache_age_secs, + odrs_review_n_results_max, + odrs_soup_session); + } + } + + /* the settings key sets the initial override */ + plugin_loader->disallow_updates = g_hash_table_new (g_direct_hash, g_direct_equal); + gs_plugin_loader_allow_updates_recheck (plugin_loader); + + /* get the language from the locale (i.e. strip the territory, codeset + * and modifier) */ + locale = setlocale (LC_MESSAGES, NULL); + plugin_loader->language = g_strdup (locale); + match = strpbrk (plugin_loader->language, "._@"); + if (match != NULL) + *match = '\0'; + + g_debug ("Using locale = %s, language = %s", locale, plugin_loader->language); + + g_mutex_init (&plugin_loader->pending_apps_mutex); + g_mutex_init (&plugin_loader->events_by_id_mutex); + + /* monitor the network as the many UI operations need the network */ + gs_plugin_loader_monitor_network (plugin_loader); + + /* by default we only show project-less apps or compatible projects */ + tmp = g_getenv ("GNOME_SOFTWARE_COMPATIBLE_PROJECTS"); + if (tmp == NULL) { + projects = g_settings_get_strv (plugin_loader->settings, + "compatible-projects"); + } else { + projects = g_strsplit (tmp, ",", -1); + } + for (i = 0; projects[i] != NULL; i++) + g_debug ("compatible-project: %s", projects[i]); + plugin_loader->compatible_projects = projects; +} + +/** + * gs_plugin_loader_new: + * @session_bus_connection: (nullable) (transfer none): a D-Bus session bus + * connection to use, or %NULL to use the default + * @system_bus_connection: (nullable) (transfer none): a D-Bus system bus + * connection to use, or %NULL to use the default + * + * Create a new #GsPluginLoader. + * + * The D-Bus connection arguments should typically be %NULL, and only be + * non-%NULL when doing unit tests. + * + * Return value: (transfer full) (not nullable): a new #GsPluginLoader + * Since: 43 + **/ +GsPluginLoader * +gs_plugin_loader_new (GDBusConnection *session_bus_connection, + GDBusConnection *system_bus_connection) +{ + g_return_val_if_fail (session_bus_connection == NULL || G_IS_DBUS_CONNECTION (session_bus_connection), NULL); + g_return_val_if_fail (system_bus_connection == NULL || G_IS_DBUS_CONNECTION (system_bus_connection), NULL); + + return g_object_new (GS_TYPE_PLUGIN_LOADER, + "session-bus-connection", session_bus_connection, + "system-bus-connection", system_bus_connection, + NULL); +} + +static void +gs_plugin_loader_app_installed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = GS_APP (user_data); + + ret = gs_plugin_loader_job_action_finish (plugin_loader, + res, + &error); + remove_app_from_install_queue (plugin_loader, app); + if (!ret) { + gs_app_set_state_recover (app); + g_warning ("failed to install %s: %s", + gs_app_get_unique_id (app), error->message); + } +} + +gboolean +gs_plugin_loader_get_network_available (GsPluginLoader *plugin_loader) +{ + if (plugin_loader->network_monitor == NULL) { + g_debug ("no network monitor, so returning network-available=TRUE"); + return TRUE; + } + return g_network_monitor_get_network_available (plugin_loader->network_monitor); +} + +gboolean +gs_plugin_loader_get_network_metered (GsPluginLoader *plugin_loader) +{ + if (plugin_loader->network_monitor == NULL) { + g_debug ("no network monitor, so returning network-metered=FALSE"); + return FALSE; + } + return g_network_monitor_get_network_metered (plugin_loader->network_monitor); +} + +static void +gs_plugin_loader_pending_apps_refined_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GsAppList) old_queue = GS_APP_LIST (user_data); + g_autoptr(GsAppList) refined_queue = NULL; + g_autoptr(GError) error = NULL; + + refined_queue = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + + if (refined_queue == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) { + g_debug ("failed to refine pending apps: %s", error->message); + + g_mutex_lock (&plugin_loader->pending_apps_mutex); + g_clear_object (&plugin_loader->pending_apps); + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + + save_install_queue (plugin_loader); + } + return; + } + + for (guint i = 0; i < gs_app_list_length (old_queue); i++) { + GsApp *app = gs_app_list_index (old_queue, i); + + if (gs_app_list_lookup (refined_queue, gs_app_get_unique_id (app)) == NULL) + remove_app_from_install_queue (plugin_loader, app); + } + + for (guint i = 0; i < gs_app_list_length (refined_queue); i++) { + GsApp *app = gs_app_list_index (refined_queue, i); + g_autoptr(GsPluginJob) plugin_job = NULL; + + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) { + plugin_job = gs_plugin_job_manage_repository_new (app, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE | + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + } else { + /* The 'interactive' is needed for credentials prompt, otherwise it just fails */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + "interactive", TRUE, + NULL); + } + + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + gs_app_get_cancellable (app), + gs_plugin_loader_app_installed_cb, + g_object_ref (app)); + } + + g_clear_object (&plugin_loader->pending_apps_cancellable); +} + +static void +gs_plugin_loader_maybe_flush_pending_install_queue (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppList) obsolete = NULL; + g_autoptr(GsAppList) queue = NULL; + + if (!gs_plugin_loader_get_network_available (plugin_loader) || + gs_plugin_loader_get_network_metered (plugin_loader)) { + /* Print the debug message only when had anything to skip */ + g_mutex_lock (&plugin_loader->pending_apps_mutex); + if (plugin_loader->pending_apps != NULL) { + g_debug ("Cannot flush pending install queue, because is %sonline and is %smetered", + !gs_plugin_loader_get_network_available (plugin_loader) ? "not " : "", + gs_plugin_loader_get_network_metered (plugin_loader) ? "" : "not "); + } + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + return; + } + + /* Already flushing pending queue */ + if (plugin_loader->pending_apps_cancellable) + return; + + queue = gs_app_list_new (); + obsolete = gs_app_list_new (); + g_mutex_lock (&plugin_loader->pending_apps_mutex); + for (guint i = 0; plugin_loader->pending_apps != NULL && i < gs_app_list_length (plugin_loader->pending_apps); i++) { + GsApp *app = gs_app_list_index (plugin_loader->pending_apps, i); + if (gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL) { + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + gs_app_list_add (queue, app); + } else { + gs_app_list_add (obsolete, app); + } + } + g_mutex_unlock (&plugin_loader->pending_apps_mutex); + for (guint i = 0; i < gs_app_list_length (obsolete); i++) { + GsApp *app = gs_app_list_index (obsolete, i); + remove_app_from_install_queue (plugin_loader, app); + } + + plugin_loader->pending_apps_cancellable = g_cancellable_new (); + + plugin_job = gs_plugin_job_refine_new (queue, GS_PLUGIN_REFINE_FLAGS_NONE); + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + plugin_loader->pending_apps_cancellable, + gs_plugin_loader_pending_apps_refined_cb, + g_steal_pointer (&queue)); +} + +static void +gs_plugin_loader_network_changed_cb (GNetworkMonitor *monitor, + gboolean available, + GsPluginLoader *plugin_loader) +{ + gboolean metered = g_network_monitor_get_network_metered (plugin_loader->network_monitor); + + g_debug ("network status change: %s [%s]", + available ? "online" : "offline", + metered ? "metered" : "unmetered"); + + g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_NETWORK_AVAILABLE]); + g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_NETWORK_METERED]); + + gs_plugin_loader_maybe_flush_pending_install_queue (plugin_loader); +} + +static void +gs_plugin_loader_network_available_notify_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + GNetworkMonitor *monitor = G_NETWORK_MONITOR (obj); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data); + + gs_plugin_loader_network_changed_cb (monitor, g_network_monitor_get_network_available (monitor), plugin_loader); +} + +static void +gs_plugin_loader_network_metered_notify_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + GNetworkMonitor *monitor = G_NETWORK_MONITOR (obj); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data); + + gs_plugin_loader_network_changed_cb (monitor, g_network_monitor_get_network_available (monitor), plugin_loader); +} + +static void +gs_plugin_loader_monitor_network (GsPluginLoader *plugin_loader) +{ + GNetworkMonitor *network_monitor; + + network_monitor = g_network_monitor_get_default (); + if (network_monitor == NULL || plugin_loader->network_changed_handler != 0) + return; + plugin_loader->network_monitor = g_object_ref (network_monitor); + + plugin_loader->network_changed_handler = + g_signal_connect (plugin_loader->network_monitor, "network-changed", + G_CALLBACK (gs_plugin_loader_network_changed_cb), plugin_loader); + plugin_loader->network_available_notify_handler = + g_signal_connect (plugin_loader->network_monitor, "notify::network-available", + G_CALLBACK (gs_plugin_loader_network_available_notify_cb), plugin_loader); + plugin_loader->network_metered_notify_handler = + g_signal_connect (plugin_loader->network_monitor, "notify::network-metered", + G_CALLBACK (gs_plugin_loader_network_metered_notify_cb), plugin_loader); + + gs_plugin_loader_network_changed_cb (plugin_loader->network_monitor, + g_network_monitor_get_network_available (plugin_loader->network_monitor), + plugin_loader); +} + +/******************************************************************************/ + +static void +generic_update_cancelled_cb (GCancellable *cancellable, gpointer data) +{ + GCancellable *app_cancellable = G_CANCELLABLE (data); + g_cancellable_cancel (app_cancellable); +} + +static gboolean +gs_plugin_loader_generic_update (GsPluginLoader *plugin_loader, + GsPluginLoaderHelper *helper, + GCancellable *cancellable, + GError **error) +{ + guint cancel_handler_id = 0; + GsAppList *list; + + /* run each plugin, per-app version */ + list = gs_plugin_job_get_list (helper->plugin_job); + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPluginActionFunc plugin_app_func = NULL; + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + plugin_app_func = gs_plugin_get_symbol (plugin, helper->function_name); + if (plugin_app_func == NULL) + continue; + + /* for each app */ + for (guint j = 0; j < gs_app_list_length (list); j++) { + GCancellable *app_cancellable; + GsApp *app = gs_app_list_index (list, j); + gboolean ret; + g_autoptr(GError) error_local = NULL; + + /* if the whole operation should be cancelled */ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + /* already installed? */ + if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED) + continue; + + /* make sure that the app update is cancelled when the whole op is cancelled */ + app_cancellable = gs_app_get_cancellable (app); + cancel_handler_id = g_cancellable_connect (cancellable, + G_CALLBACK (generic_update_cancelled_cb), + g_object_ref (app_cancellable), + g_object_unref); + + gs_plugin_job_set_app (helper->plugin_job, app); + ret = plugin_app_func (plugin, app, app_cancellable, &error_local); + g_cancellable_disconnect (cancellable, cancel_handler_id); + + if (!ret) { + if (!gs_plugin_error_handle_failure (helper, + plugin, + error_local, + error)) { + return FALSE; + } + } + } + helper->anything_ran = TRUE; + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED); + } + + if (gs_plugin_job_get_action (helper->plugin_job) == GS_PLUGIN_ACTION_UPDATE) + gs_utils_set_online_updates_timestamp (plugin_loader->settings); + + return TRUE; +} + +static void +gs_plugin_loader_inherit_list_props (GsAppList *des_list, + GsAppList *src_list) +{ + if (gs_app_list_has_flag (src_list, GS_APP_LIST_FLAG_IS_TRUNCATED)) + gs_app_list_add_flag (des_list, GS_APP_LIST_FLAG_IS_TRUNCATED); + + gs_app_list_set_size_peak (des_list, gs_app_list_get_size_peak (src_list)); +} + +static void +gs_plugin_loader_process_thread_cb (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + GError *error = NULL; + GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) task_data; + GsAppListFilterFlags dedupe_flags; + g_autoptr(GsAppList) list = g_object_ref (gs_plugin_job_get_list (helper->plugin_job)); + GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object); + gboolean add_to_pending_array = FALSE; + g_autoptr(GMainContext) context = g_main_context_new (); + g_autoptr(GMainContextPusher) pusher = g_main_context_pusher_new (context); + g_autofree gchar *job_debug = NULL; +#ifdef HAVE_SYSPROF + gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME; +#endif + + /* these change the pending count on the installed panel */ + switch (action) { + case GS_PLUGIN_ACTION_INSTALL: + case GS_PLUGIN_ACTION_REMOVE: + add_to_pending_array = TRUE; + break; + default: + break; + } + + /* add to pending list */ + if (add_to_pending_array) + gs_plugin_loader_pending_apps_add (plugin_loader, helper); + + /* run each plugin */ + if (!GS_IS_PLUGIN_JOB_REFINE (helper->plugin_job)) { + if (!gs_plugin_loader_run_results (helper, cancellable, &error)) { + if (add_to_pending_array) { + gs_app_set_state_recover (gs_plugin_job_get_app (helper->plugin_job)); + gs_plugin_loader_pending_apps_remove (plugin_loader, helper); + } + gs_utils_error_convert_gio (&error); + g_task_return_error (task, error); + return; + } + + if (action == GS_PLUGIN_ACTION_URL_TO_APP) { + const gchar *search = gs_plugin_job_get_search (helper->plugin_job); + if (search && g_ascii_strncasecmp (search, "file://", 7) == 0 && ( + gs_plugin_job_get_list (helper->plugin_job) == NULL || + gs_app_list_length (gs_plugin_job_get_list (helper->plugin_job)) == 0)) { + g_autoptr(GError) local_error = NULL; + g_autoptr(GFile) file = NULL; + file = g_file_new_for_uri (search); + gs_plugin_job_set_action (helper->plugin_job, GS_PLUGIN_ACTION_FILE_TO_APP); + gs_plugin_job_set_file (helper->plugin_job, file); + helper->function_name = gs_plugin_action_to_function_name (GS_PLUGIN_ACTION_FILE_TO_APP); + if (gs_plugin_loader_run_results (helper, cancellable, &local_error)) { + for (guint j = 0; j < gs_app_list_length (list); j++) { + GsApp *app = gs_app_list_index (list, j); + if (gs_app_get_local_file (app) == NULL) + gs_app_set_local_file (app, gs_plugin_job_get_file (helper->plugin_job)); + } + } else { + g_debug ("Failed to convert file:// URI to app using file-to-app action: %s", local_error->message); + } + gs_plugin_job_set_action (helper->plugin_job, GS_PLUGIN_ACTION_URL_TO_APP); + gs_plugin_job_set_file (helper->plugin_job, NULL); + } + } + } + + /* run per-app version */ + if (action == GS_PLUGIN_ACTION_UPDATE) { + helper->function_name = "gs_plugin_update_app"; + if (!gs_plugin_loader_generic_update (plugin_loader, helper, + cancellable, &error)) { + gs_utils_error_convert_gio (&error); + g_task_return_error (task, error); + return; + } + } else if (action == GS_PLUGIN_ACTION_DOWNLOAD) { + helper->function_name = "gs_plugin_download_app"; + if (!gs_plugin_loader_generic_update (plugin_loader, helper, + cancellable, &error)) { + gs_utils_error_convert_gio (&error); + g_task_return_error (task, error); + return; + } + } + + if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER) + gs_utils_set_online_updates_timestamp (plugin_loader->settings); + + /* remove from pending list */ + if (add_to_pending_array) + gs_plugin_loader_pending_apps_remove (plugin_loader, helper); + + /* some functions are really required for proper operation */ + switch (action) { + case GS_PLUGIN_ACTION_GET_UPDATES: + case GS_PLUGIN_ACTION_INSTALL: + case GS_PLUGIN_ACTION_DOWNLOAD: + case GS_PLUGIN_ACTION_LAUNCH: + case GS_PLUGIN_ACTION_REMOVE: + case GS_PLUGIN_ACTION_UPDATE: + if (!helper->anything_ran) { + g_set_error (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no plugin could handle %s", + gs_plugin_action_to_string (action)); + g_task_return_error (task, error); + return; + } + break; + default: + if (!helper->anything_ran && !GS_IS_PLUGIN_JOB_REFINE (helper->plugin_job)) { + g_debug ("no plugin could handle %s", + gs_plugin_action_to_string (action)); + } + break; + } + + /* unstage addons */ + if (add_to_pending_array) { + g_autoptr(GsAppList) addons = gs_app_dup_addons (gs_plugin_job_get_app (helper->plugin_job)); + + for (guint i = 0; addons != NULL && i < gs_app_list_length (addons); i++) { + GsApp *addon = gs_app_list_index (addons, i); + if (gs_app_get_to_be_installed (addon)) + gs_app_set_to_be_installed (addon, FALSE); + } + } + + /* filter to reduce to a sane set */ + gs_plugin_loader_job_sorted_truncation (helper->plugin_job, list); + + /* set the local file on any of the returned results */ + switch (action) { + case GS_PLUGIN_ACTION_FILE_TO_APP: + for (guint j = 0; j < gs_app_list_length (list); j++) { + GsApp *app = gs_app_list_index (list, j); + if (gs_app_get_local_file (app) == NULL) + gs_app_set_local_file (app, gs_plugin_job_get_file (helper->plugin_job)); + } + default: + break; + } + + /* pick up new source id */ + switch (action) { + case GS_PLUGIN_ACTION_INSTALL: + case GS_PLUGIN_ACTION_REMOVE: + gs_plugin_job_add_refine_flags (helper->plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION); + break; + default: + break; + } + + /* run refine() on each one if required */ + if (gs_plugin_job_get_refine_flags (helper->plugin_job) != 0 && + list != NULL && + gs_app_list_length (list) > 0) { + g_autoptr(GsPluginJob) refine_job = NULL; + g_autoptr(GAsyncResult) refine_result = NULL; + g_autoptr(GsAppList) new_list = NULL; + + refine_job = gs_plugin_job_refine_new (list, gs_plugin_job_get_refine_flags (helper->plugin_job) | GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING); + gs_plugin_loader_job_process_async (plugin_loader, refine_job, + cancellable, + async_result_cb, + &refine_result); + + /* FIXME: Make this sync until the enclosing function is + * refactored to be async. */ + while (refine_result == NULL) + g_main_context_iteration (g_main_context_get_thread_default (), TRUE); + + new_list = gs_plugin_loader_job_process_finish (plugin_loader, refine_result, &error); + if (new_list == NULL) { + gs_utils_error_convert_gio (&error); + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + gs_plugin_loader_inherit_list_props (new_list, list); + + /* Update the app list in case the refine resolved any wildcards. */ + g_set_object (&list, new_list); + } else { + g_debug ("no refine flags set for transaction"); + } + + /* check the local files have an icon set */ + switch (action) { + case GS_PLUGIN_ACTION_URL_TO_APP: + case GS_PLUGIN_ACTION_FILE_TO_APP: { + g_autoptr(GsPluginJob) refine_job = NULL; + g_autoptr(GAsyncResult) refine_result = NULL; + g_autoptr(GsAppList) new_list = NULL; + + for (guint j = 0; j < gs_app_list_length (list); j++) { + GsApp *app = gs_app_list_index (list, j); + if (gs_app_get_icons (app) == NULL) { + g_autoptr(GIcon) ic = NULL; + const gchar *icon_name; + if (gs_app_has_quirk (app, GS_APP_QUIRK_HAS_SOURCE)) + icon_name = "x-package-repository"; + else + icon_name = "system-component-application"; + ic = g_themed_icon_new (icon_name); + gs_app_add_icon (app, ic); + } + } + + refine_job = gs_plugin_job_refine_new (list, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING); + gs_plugin_loader_job_process_async (plugin_loader, refine_job, + cancellable, + async_result_cb, + &refine_result); + + /* FIXME: Make this sync until the enclosing function is + * refactored to be async. */ + while (refine_result == NULL) + g_main_context_iteration (g_main_context_get_thread_default (), TRUE); + + new_list = gs_plugin_loader_job_process_finish (plugin_loader, refine_result, &error); + if (new_list == NULL) { + gs_utils_error_convert_gio (&error); + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + gs_plugin_loader_inherit_list_props (new_list, list); + + /* Update the app list in case the refine resolved any wildcards. */ + g_set_object (&list, new_list); + + break; + } + default: + break; + } + + /* filter package list */ + switch (action) { + case GS_PLUGIN_ACTION_URL_TO_APP: + gs_app_list_filter (list, gs_plugin_loader_app_is_valid_filter, helper); + break; + case GS_PLUGIN_ACTION_GET_UPDATES: + gs_app_list_filter (list, gs_plugin_loader_app_is_valid_updatable, helper); + break; + default: + break; + } + + /* only allow one result */ + if (action == GS_PLUGIN_ACTION_URL_TO_APP || + action == GS_PLUGIN_ACTION_FILE_TO_APP) { + if (gs_app_list_length (list) == 0) { + g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job); + g_autoptr(GError) error_local = NULL; + g_set_error (&error_local, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no application was created for %s", str); + if (!gs_plugin_job_get_propagate_error (helper->plugin_job)) + gs_plugin_loader_claim_job_error (plugin_loader, NULL, helper->plugin_job, error_local); + g_task_return_error (task, g_steal_pointer (&error_local)); + return; + } + if (gs_app_list_length (list) > 1) { + g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job); + g_debug ("more than one application was created for %s", str); + } + } + + /* filter duplicates with priority, taking into account the source name + * & version, so we combine available updates with the installed app */ + dedupe_flags = gs_plugin_job_get_dedupe_flags (helper->plugin_job); + if (dedupe_flags != GS_APP_LIST_FILTER_FLAG_NONE) + gs_app_list_filter_duplicates (list, dedupe_flags); + + /* sort these again as the refine may have added useful metadata */ + gs_plugin_loader_job_sorted_truncation_again (helper->plugin_job, list); + +#ifdef HAVE_SYSPROF + if (plugin_loader->sysprof_writer != NULL) { + g_autofree gchar *sysprof_name = g_strconcat ("process-thread:", gs_plugin_action_to_string (action), NULL); + g_autofree gchar *sysprof_message = gs_plugin_job_to_string (helper->plugin_job); + sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer, + begin_time_nsec, + sched_getcpu (), + getpid (), + SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec, + "gnome-software", + sysprof_name, + sysprof_message); + } +#endif /* HAVE_SYSPROF */ + + /* show elapsed time */ + job_debug = gs_plugin_job_to_string (helper->plugin_job); + g_debug ("%s", job_debug); + + /* success */ + g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref); +} + +static void +gs_plugin_loader_process_in_thread_pool_cb (gpointer data, + gpointer user_data) +{ + GTask *task = data; + gpointer source_object = g_task_get_source_object (task); + gpointer task_data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + GsPluginLoaderHelper *helper = g_task_get_task_data (task); + GsApp *app = gs_plugin_job_get_app (helper->plugin_job); + GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job); + + gs_ioprio_set (G_PRIORITY_LOW); + + gs_plugin_loader_process_thread_cb (task, source_object, task_data, cancellable); + + /* Clear any pending action set in gs_plugin_loader_schedule_task() */ + if (app != NULL && gs_app_get_pending_action (app) == action) + gs_app_set_pending_action (app, GS_PLUGIN_ACTION_UNKNOWN); + + g_object_unref (task); +} + +static void +gs_plugin_loader_cancelled_cb (GCancellable *cancellable, + gpointer user_data) +{ + GCancellable *child_cancellable = G_CANCELLABLE (user_data); + + /* just proxy this forward */ + g_debug ("Cancelling job with cancellable %p", child_cancellable); + g_cancellable_cancel (child_cancellable); +} + +static void +gs_plugin_loader_schedule_task (GsPluginLoader *plugin_loader, + GTask *task) +{ + GsPluginLoaderHelper *helper = g_task_get_task_data (task); + GsApp *app = gs_plugin_job_get_app (helper->plugin_job); + + if (app != NULL) { + /* set the pending-action to the app */ + GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job); + gs_app_set_pending_action (app, action); + + if (action == GS_PLUGIN_ACTION_INSTALL && + gs_app_get_state (app) != GS_APP_STATE_AVAILABLE_LOCAL) + add_app_to_install_queue (plugin_loader, app); + } + g_thread_pool_push (plugin_loader->queued_ops_pool, g_object_ref (task), NULL); +} + +static void +run_job_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginJob *plugin_job = GS_PLUGIN_JOB (source_object); + GsPluginJobClass *job_class; + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; +#ifdef HAVE_SYSPROF + GsPluginLoader *plugin_loader = g_task_get_source_object (task); + gint64 begin_time_nsec = GPOINTER_TO_SIZE (g_task_get_task_data (task)); + + if (plugin_loader->sysprof_writer != NULL) { + g_autofree gchar *sysprof_name = g_strconcat ("process-thread:", G_OBJECT_TYPE_NAME (plugin_job), NULL); + g_autofree gchar *sysprof_message = gs_plugin_job_to_string (plugin_job); + sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer, + begin_time_nsec, + sched_getcpu (), + getpid (), + SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec, + "gnome-software", + sysprof_name, + sysprof_message); + } +#endif /* HAVE_SYSPROF */ + + /* FIXME: This will eventually go away when + * gs_plugin_loader_job_process_finish() is removed. */ + job_class = GS_PLUGIN_JOB_GET_CLASS (plugin_job); + + g_assert (job_class->run_finish != NULL); + + if (!job_class->run_finish (plugin_job, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (GS_IS_PLUGIN_JOB_REFINE (plugin_job)) { + GsAppList *list = gs_plugin_job_refine_get_result_list (GS_PLUGIN_JOB_REFINE (plugin_job)); + g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref); + return; + } else if (GS_IS_PLUGIN_JOB_LIST_APPS (plugin_job)) { + GsAppList *list = gs_plugin_job_list_apps_get_result_list (GS_PLUGIN_JOB_LIST_APPS (plugin_job)); + g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref); + return; + } else if (GS_IS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (plugin_job)) { + GsAppList *list = gs_plugin_job_list_distro_upgrades_get_result_list (GS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (plugin_job)); + g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref); + return; + } else if (GS_IS_PLUGIN_JOB_REFRESH_METADATA (plugin_job)) { + /* FIXME: For some reason, existing callers of refresh jobs + * expect a #GsAppList instance back, even though it’s empty and + * they don’t use its contents. It’s just used to distinguish + * against returning an error. This will go away when + * job_process_async() does. */ + g_task_return_pointer (task, gs_app_list_new (), g_object_unref); + return; + } else if (GS_IS_PLUGIN_JOB_MANAGE_REPOSITORY (plugin_job) || + GS_IS_PLUGIN_JOB_LIST_CATEGORIES (plugin_job)) { + /* FIXME: The gs_plugin_loader_job_action_finish() expects a #GsAppList + * pointer on success, thus return it. */ + g_task_return_pointer (task, gs_app_list_new (), g_object_unref); + return; + } + + g_assert_not_reached (); +} + +typedef struct { + GWeakRef parent_cancellable_weak; + gulong handler_id; +} CancellableData; + +static void +cancellable_data_free (CancellableData *data) +{ + g_autoptr(GCancellable) parent_cancellable = g_weak_ref_get (&data->parent_cancellable_weak); + + if (parent_cancellable != NULL) + g_cancellable_disconnect (parent_cancellable, data->handler_id); + + g_weak_ref_clear (&data->parent_cancellable_weak); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (CancellableData, cancellable_data_free) + +static void +plugin_loader_task_freed_cb (gpointer user_data, + GObject *freed_object) +{ + g_autoptr(GsPluginLoader) plugin_loader = user_data; + if (g_atomic_int_dec_and_test (&plugin_loader->active_jobs)) { + /* if the plugin used updates-changed during its job, actually schedule + * the signal emission now */ + if (plugin_loader->updates_changed_cnt > 0) + gs_plugin_loader_updates_changed (plugin_loader); + } +} + +static gboolean job_process_setup_complete_cb (GCancellable *cancellable, + gpointer user_data); +static void job_process_cb (GTask *task); + +/** + * gs_plugin_loader_job_process_async: + * @plugin_loader: A #GsPluginLoader + * @plugin_job: job to process + * @cancellable: a #GCancellable, or %NULL + * @callback: function to call when complete + * @user_data: user data to pass to @callback + * + * This method calls all plugins. + * + * If the #GsPluginLoader is still being set up, this function will wait until + * setup is complete before running. + **/ +void +gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginJobClass *job_class; + GsPluginAction action; + g_autoptr(GTask) task = NULL; + g_autoptr(GCancellable) cancellable_job = NULL; + g_autofree gchar *task_name = NULL; + + g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader)); + g_return_if_fail (GS_IS_PLUGIN_JOB (plugin_job)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + job_class = GS_PLUGIN_JOB_GET_CLASS (plugin_job); + action = gs_plugin_job_get_action (plugin_job); + + if (job_class->run_async != NULL) { + task_name = g_strdup_printf ("%s %s", G_STRFUNC, G_OBJECT_TYPE_NAME (plugin_job)); + cancellable_job = (cancellable != NULL) ? g_object_ref (cancellable) : NULL; + } else { + task_name = g_strdup_printf ("%s %s", G_STRFUNC, gs_plugin_action_to_string (action)); + cancellable_job = g_cancellable_new (); + + /* Old-style jobs always have a valid cancellable, so proxy the caller */ + g_debug ("Chaining cancellation from %p to %p", cancellable, cancellable_job); + if (cancellable != NULL) { + g_autoptr(CancellableData) cancellable_data = NULL; + + cancellable_data = g_new0 (CancellableData, 1); + g_weak_ref_init (&cancellable_data->parent_cancellable_weak, cancellable); + cancellable_data->handler_id = g_cancellable_connect (cancellable, + G_CALLBACK (gs_plugin_loader_cancelled_cb), + cancellable_job, NULL); + + g_object_set_data_full (G_OBJECT (cancellable_job), + "gs-cancellable-chain", + g_steal_pointer (&cancellable_data), + (GDestroyNotify) cancellable_data_free); + } + } + + task = g_task_new (plugin_loader, cancellable_job, callback, user_data); + g_task_set_name (task, task_name); + g_task_set_task_data (task, g_object_ref (plugin_job), (GDestroyNotify) g_object_unref); + + g_atomic_int_inc (&plugin_loader->active_jobs); + g_object_weak_ref (G_OBJECT (task), + plugin_loader_task_freed_cb, g_object_ref (plugin_loader)); + + /* Wait until the plugin has finished setting up. + * + * Do this using a #GCancellable. While we’re not using the #GCancellable + * to cancel anything, it is a reliable way to signal between threads + * without polling, waking up all waiting #GMainContexts when it’s + * ‘cancelled’. */ + if (plugin_loader->setup_complete) { + job_process_cb (task); + } else { + g_autoptr(GSource) cancellable_source = g_cancellable_source_new (plugin_loader->setup_complete_cancellable); + g_task_attach_source (task, cancellable_source, G_SOURCE_FUNC (job_process_setup_complete_cb)); + } +} + +static gboolean +job_process_setup_complete_cb (GCancellable *cancellable, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + + job_process_cb (task); + + return G_SOURCE_REMOVE; +} + +static void +job_process_cb (GTask *task) +{ + g_autoptr(GsPluginJob) plugin_job = g_object_ref (g_task_get_task_data (task)); + GsPluginLoader *plugin_loader = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + GsPluginJobClass *job_class; + GsPluginAction action; + GsPluginLoaderHelper *helper; + + job_class = GS_PLUGIN_JOB_GET_CLASS (plugin_job); + action = gs_plugin_job_get_action (plugin_job); + + /* If the job provides a more specific async run function, use that. + * + * FIXME: This will eventually go away when + * gs_plugin_loader_job_process_async() is removed. */ + + if (job_class->run_async != NULL) { +#ifdef HAVE_SYSPROF + gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME; + + g_task_set_task_data (task, GSIZE_TO_POINTER (begin_time_nsec), NULL); +#endif + + job_class->run_async (plugin_job, plugin_loader, cancellable, + run_job_cb, g_object_ref (task)); + return; + } + + /* check job has valid action */ + if (action == GS_PLUGIN_ACTION_UNKNOWN) { + g_autofree gchar *job_str = gs_plugin_job_to_string (plugin_job); + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "job has no valid action: %s", job_str); + return; + } + + /* deal with the install queue */ + if (action == GS_PLUGIN_ACTION_REMOVE) { + if (remove_app_from_install_queue (plugin_loader, gs_plugin_job_get_app (plugin_job))) { + GsAppList *list = gs_plugin_job_get_list (plugin_job); + g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref); + return; + } + } + + /* FIXME: the plugins should specify this, rather than hardcoding */ + if (gs_plugin_job_has_refine_flags (plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI)) { + gs_plugin_job_add_refine_flags (plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN); + } + if (gs_plugin_job_has_refine_flags (plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME)) { + gs_plugin_job_add_refine_flags (plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN); + } + if (gs_plugin_job_has_refine_flags (plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE)) { + gs_plugin_job_add_refine_flags (plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME); + } + + /* FIXME: this is probably a bug */ + if (action == GS_PLUGIN_ACTION_GET_SOURCES) { + gs_plugin_job_add_refine_flags (plugin_job, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION); + } + + /* check required args */ + switch (action) { + case GS_PLUGIN_ACTION_URL_TO_APP: + if (gs_plugin_job_get_search (plugin_job) == NULL) { + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no valid search terms"); + return; + } + break; + default: + break; + } + + /* save helper */ + helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job); + g_task_set_task_data (task, helper, (GDestroyNotify) gs_plugin_loader_helper_free); + + /* let the task cancel itself */ + g_task_set_check_cancellable (task, FALSE); + g_task_set_return_on_cancel (task, FALSE); + + switch (action) { + case GS_PLUGIN_ACTION_INSTALL: + case GS_PLUGIN_ACTION_UPDATE: + case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD: + /* these actions must be performed by the thread pool because we + * want to limit the number of them running in parallel */ + gs_plugin_loader_schedule_task (plugin_loader, task); + return; + default: + break; + } + + /* run in a thread */ + g_task_run_in_thread (task, gs_plugin_loader_process_thread_cb); +} + +/******************************************************************************/ + +/** + * gs_plugin_loader_get_plugin_supported: + * @plugin_loader: A #GsPluginLoader + * @function_name: a function name + * + * This function returns TRUE if the symbol is found in any enabled plugin. + */ +gboolean +gs_plugin_loader_get_plugin_supported (GsPluginLoader *plugin_loader, + const gchar *function_name) +{ + for (guint i = 0; i < plugin_loader->plugins->len; i++) { + GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i); + if (gs_plugin_get_symbol (plugin, function_name) != NULL) + return TRUE; + } + return FALSE; +} + +/** + * gs_plugin_loader_get_plugins: + * @plugin_loader: a #GsPluginLoader + * + * Get the set of currently loaded plugins. + * + * This includes disabled plugins, which should be checked for using + * gs_plugin_get_enabled(). + * + * This is intended to be used by internal gnome-software code. Plugin and UI + * code should typically use #GsPluginJob to run operations. + * + * Returns: (transfer none) (element-type GsPlugin): list of #GsPlugins + * Since: 42 + */ +GPtrArray * +gs_plugin_loader_get_plugins (GsPluginLoader *plugin_loader) +{ + g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL); + + return plugin_loader->plugins; +} + +static void app_create_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/** + * gs_plugin_loader_app_create_async: + * @plugin_loader: a #GsPluginLoader + * @unique_id: a unique_id + * @cancellable: a #GCancellable, or %NULL + * @callback: function to call when complete + * @user_data: user data to pass to @callback + * + * Create a #GsApp identified by @unique_id asynchronously. + * Finish the call with gs_plugin_loader_app_create_finish(). + * + * If the #GsPluginLoader is still being set up, this function will wait until + * setup is complete before running. + * + * Since: 41 + **/ +void +gs_plugin_loader_app_create_async (GsPluginLoader *plugin_loader, + const gchar *unique_id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GsPluginJob) refine_job = NULL; + + g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader)); + g_return_if_fail (unique_id != NULL); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (plugin_loader, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_loader_app_create_async); + g_task_set_task_data (task, g_strdup (unique_id), g_free); + + /* use the plugin loader to convert a wildcard app */ + app = gs_app_new (NULL); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_from_unique_id (app, unique_id, AS_COMPONENT_KIND_UNKNOWN); + gs_app_list_add (list, app); + + /* Refine the wildcard app. */ + refine_job = gs_plugin_job_refine_new (list, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID | GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING); + gs_plugin_loader_job_process_async (plugin_loader, refine_job, + cancellable, + app_create_cb, + g_steal_pointer (&task)); +} + +static void +app_create_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (g_task_get_source_object (task)); + const gchar *unique_id = g_task_get_task_data (task); + g_autoptr(GsAppList) list = NULL; + g_autoptr(GError) local_error = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, result, &local_error); + if (list == NULL) { + g_prefix_error (&local_error, "Failed to refine '%s': ", unique_id); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* return the matching GsApp */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app_tmp = gs_app_list_index (list, i); + if (g_strcmp0 (unique_id, gs_app_get_unique_id (app_tmp)) == 0) { + g_task_return_pointer (task, g_object_ref (app_tmp), g_object_unref); + return; + } + } + + /* return the first returned app that's not a wildcard */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app_tmp = gs_app_list_index (list, i); + if (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD)) { + g_debug ("returning imperfect match: %s != %s", + unique_id, gs_app_get_unique_id (app_tmp)); + g_task_return_pointer (task, g_object_ref (app_tmp), g_object_unref); + return; + } + } + + /* does not exist */ + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Failed to create an app for '%s'", unique_id); +} + +/** + * gs_plugin_loader_app_create_finish: + * @plugin_loader: a #GsPluginLoader + * @res: a #GAsyncResult + * @error: A #GError, or %NULL + * + * Finishes call to gs_plugin_loader_app_create_async(). + * + * Returns: (transfer full): a #GsApp, or %NULL on error. + * + * Since: 41 + **/ +GsApp * +gs_plugin_loader_app_create_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error) +{ + GsApp *app; + + g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL); + g_return_val_if_fail (G_IS_TASK (res), NULL); + g_return_val_if_fail (g_task_is_valid (res, plugin_loader), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + app = g_task_propagate_pointer (G_TASK (res), error); + gs_utils_error_convert_gio (error); + return app; +} + +/** + * gs_plugin_loader_get_system_app_async: + * @plugin_loader: a #GsPluginLoader + * @cancellable: a #GCancellable, or %NULL + * @callback: function to call when complete + * @user_data: user data to pass to @callback + * + * Get the application that represents the currently installed OS + * asynchronously. Finish the call with gs_plugin_loader_get_system_app_finish(). + * + * Since: 41 + **/ +void +gs_plugin_loader_get_system_app_async (GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + gs_plugin_loader_app_create_async (plugin_loader, "*/*/*/system/*", cancellable, callback, user_data); +} + +/** + * gs_plugin_loader_get_system_app_finish: + * @plugin_loader: a #GsPluginLoader + * @res: a #GAsyncResult + * @error: A #GError, or %NULL + * + * Finishes call to gs_plugin_loader_get_system_app_async(). + * + * Returns: (transfer full): a #GsApp, which represents + * the currently installed OS, or %NULL on error. + * + * Since: 41 + **/ +GsApp * +gs_plugin_loader_get_system_app_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error) +{ + return gs_plugin_loader_app_create_finish (plugin_loader, res, error); +} + +/** + * gs_plugin_loader_get_odrs_provider: + * @plugin_loader: a #GsPluginLoader + * + * Get the singleton #GsOdrsProvider which provides access to ratings and + * reviews data from ODRS. + * + * Returns: (transfer none) (nullable): a #GsOdrsProvider, or %NULL if disabled + * Since: 41 + */ +GsOdrsProvider * +gs_plugin_loader_get_odrs_provider (GsPluginLoader *plugin_loader) +{ + g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL); + + return plugin_loader->odrs_provider; +} + +/** + * gs_plugin_loader_set_max_parallel_ops: + * @plugin_loader: a #GsPluginLoader + * @max_ops: the maximum number of parallel operations + * + * Sets the number of maximum number of queued operations (install/update/upgrade-download) + * to be processed at a time. If @max_ops is 0, then it will set the default maximum number. + */ +void +gs_plugin_loader_set_max_parallel_ops (GsPluginLoader *plugin_loader, + guint max_ops) +{ + g_autoptr(GError) error = NULL; + if (max_ops == 0) + max_ops = get_max_parallel_ops (); + if (!g_thread_pool_set_max_threads (plugin_loader->queued_ops_pool, max_ops, &error)) + g_warning ("Failed to set the maximum number of ops in parallel: %s", + error->message); +} + +/** + * gs_plugin_loader_get_category_manager: + * @plugin_loader: a #GsPluginLoader + * + * Get the category manager singleton. + * + * Returns: (transfer none): a category manager + * Since: 40 + */ +GsCategoryManager * +gs_plugin_loader_get_category_manager (GsPluginLoader *plugin_loader) +{ + g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL); + + return plugin_loader->category_manager; +} + +/** + * gs_plugin_loader_emit_updates_changed: + * @self: a #GsPluginLoader + * + * Emits the #GsPluginLoader:updates-changed signal in the nearest + * idle in the main thread. + * + * Since: 43 + **/ +void +gs_plugin_loader_emit_updates_changed (GsPluginLoader *self) +{ + g_return_if_fail (GS_IS_PLUGIN_LOADER (self)); + + if (self->updates_changed_id != 0) + g_source_remove (self->updates_changed_id); + + self->updates_changed_id = + g_idle_add_full (G_PRIORITY_HIGH_IDLE, + gs_plugin_loader_job_updates_changed_delay_cb, + g_object_ref (self), g_object_unref); +} diff --git a/lib/gs-plugin-loader.h b/lib/gs-plugin-loader.h new file mode 100644 index 0000000..93a44d6 --- /dev/null +++ b/lib/gs-plugin-loader.h @@ -0,0 +1,122 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2007-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" +#include "gs-category.h" +#include "gs-category-manager.h" +#include "gs-odrs-provider.h" +#include "gs-plugin-event.h" +#include "gs-plugin.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_LOADER (gs_plugin_loader_get_type ()) +G_DECLARE_FINAL_TYPE (GsPluginLoader, gs_plugin_loader, GS, PLUGIN_LOADER, GObject) + +#include "gs-plugin-job.h" + +GsPluginLoader *gs_plugin_loader_new (GDBusConnection *session_bus_connection, + GDBusConnection *system_bus_connection); +void gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GsAppList *gs_plugin_loader_job_process_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error); +gboolean gs_plugin_loader_job_action_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error); +void gs_plugin_loader_setup_async (GsPluginLoader *plugin_loader, + const gchar * const *allowlist, + const gchar * const *blocklist, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean gs_plugin_loader_setup_finish (GsPluginLoader *plugin_loader, + GAsyncResult *result, + GError **error); + +void gs_plugin_loader_shutdown (GsPluginLoader *plugin_loader, + GCancellable *cancellable); + +void gs_plugin_loader_dump_state (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_enabled (GsPluginLoader *plugin_loader, + const gchar *plugin_name); +void gs_plugin_loader_add_location (GsPluginLoader *plugin_loader, + const gchar *location); +guint gs_plugin_loader_get_scale (GsPluginLoader *plugin_loader); +void gs_plugin_loader_set_scale (GsPluginLoader *plugin_loader, + guint scale); +GsAppList *gs_plugin_loader_get_pending (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_allow_updates (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_network_available (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_network_metered (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_plugin_supported (GsPluginLoader *plugin_loader, + const gchar *function_name); + +GPtrArray *gs_plugin_loader_get_plugins (GsPluginLoader *plugin_loader); + +void gs_plugin_loader_add_event (GsPluginLoader *plugin_loader, + GsPluginEvent *event); +GPtrArray *gs_plugin_loader_get_events (GsPluginLoader *plugin_loader); +GsPluginEvent *gs_plugin_loader_get_event_default (GsPluginLoader *plugin_loader); +void gs_plugin_loader_remove_events (GsPluginLoader *plugin_loader); + +void gs_plugin_loader_app_create_async (GsPluginLoader *plugin_loader, + const gchar *unique_id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GsApp *gs_plugin_loader_app_create_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error); +void gs_plugin_loader_get_system_app_async (GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GsApp *gs_plugin_loader_get_system_app_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error); +GsOdrsProvider *gs_plugin_loader_get_odrs_provider (GsPluginLoader *plugin_loader); + +/* only useful from the self tests */ +void gs_plugin_loader_clear_caches (GsPluginLoader *plugin_loader); +GsPlugin *gs_plugin_loader_find_plugin (GsPluginLoader *plugin_loader, + const gchar *plugin_name); +void gs_plugin_loader_set_max_parallel_ops (GsPluginLoader *plugin_loader, + guint max_ops); + +GsCategoryManager *gs_plugin_loader_get_category_manager (GsPluginLoader *plugin_loader); +void gs_plugin_loader_claim_error (GsPluginLoader *plugin_loader, + GsPlugin *plugin, + GsPluginAction action, + GsApp *app, + gboolean interactive, + const GError *error); +void gs_plugin_loader_claim_job_error (GsPluginLoader *plugin_loader, + GsPlugin *plugin, + GsPluginJob *job, + const GError *error); + +gboolean gs_plugin_loader_app_is_valid (GsApp *app, + GsPluginRefineFlags flags); +gboolean gs_plugin_loader_app_is_compatible (GsPluginLoader *plugin_loader, + GsApp *app); + +void gs_plugin_loader_run_adopt (GsPluginLoader *plugin_loader, + GsAppList *list); +void gs_plugin_loader_emit_updates_changed (GsPluginLoader *self); + +G_END_DECLS diff --git a/lib/gs-plugin-private.h b/lib/gs-plugin-private.h new file mode 100644 index 0000000..3a44856 --- /dev/null +++ b/lib/gs-plugin-private.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gio/gio.h> +#include <glib-object.h> +#include <gmodule.h> + +#include "gs-plugin.h" + +G_BEGIN_DECLS + +GsPlugin *gs_plugin_new (GDBusConnection *session_bus_connection, + GDBusConnection *system_bus_connection); +GsPlugin *gs_plugin_create (const gchar *filename, + GDBusConnection *session_bus_connection, + GDBusConnection *system_bus_connection, + GError **error); +const gchar *gs_plugin_error_to_string (GsPluginError error); +const gchar *gs_plugin_action_to_string (GsPluginAction action); +GsPluginAction gs_plugin_action_from_string (const gchar *action); +const gchar *gs_plugin_action_to_function_name (GsPluginAction action); + +void gs_plugin_set_scale (GsPlugin *plugin, + guint scale); +guint gs_plugin_get_order (GsPlugin *plugin); +void gs_plugin_set_order (GsPlugin *plugin, + guint order); +guint gs_plugin_get_priority (GsPlugin *plugin); +void gs_plugin_set_priority (GsPlugin *plugin, + guint priority); +void gs_plugin_set_name (GsPlugin *plugin, + const gchar *name); +void gs_plugin_set_language (GsPlugin *plugin, + const gchar *language); +void gs_plugin_set_auth_array (GsPlugin *plugin, + GPtrArray *auth_array); +GPtrArray *gs_plugin_get_rules (GsPlugin *plugin, + GsPluginRule rule); +gpointer gs_plugin_get_symbol (GsPlugin *plugin, + const gchar *function_name); +void gs_plugin_interactive_inc (GsPlugin *plugin); +void gs_plugin_interactive_dec (GsPlugin *plugin); +gchar *gs_plugin_refine_flags_to_string (GsPluginRefineFlags refine_flags); +void gs_plugin_set_network_monitor (GsPlugin *plugin, + GNetworkMonitor *monitor); + +GDBusConnection *gs_plugin_get_session_bus_connection (GsPlugin *self); +GDBusConnection *gs_plugin_get_system_bus_connection (GsPlugin *self); + +G_END_DECLS diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h new file mode 100644 index 0000000..59504be --- /dev/null +++ b/lib/gs-plugin-types.h @@ -0,0 +1,319 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +/** + * GsPluginStatus: + * @GS_PLUGIN_STATUS_UNKNOWN: Unknown status + * @GS_PLUGIN_STATUS_WAITING: Waiting + * @GS_PLUGIN_STATUS_FINISHED: Finished + * @GS_PLUGIN_STATUS_SETUP: Setup in progress + * @GS_PLUGIN_STATUS_DOWNLOADING: Downloading in progress + * @GS_PLUGIN_STATUS_QUERYING: Querying in progress + * @GS_PLUGIN_STATUS_INSTALLING: Installing in progress + * @GS_PLUGIN_STATUS_REMOVING: Removing in progress + * + * The status of the plugin. + **/ +typedef enum { + GS_PLUGIN_STATUS_UNKNOWN, + GS_PLUGIN_STATUS_WAITING, + GS_PLUGIN_STATUS_FINISHED, + GS_PLUGIN_STATUS_SETUP, + GS_PLUGIN_STATUS_DOWNLOADING, + GS_PLUGIN_STATUS_QUERYING, + GS_PLUGIN_STATUS_INSTALLING, + GS_PLUGIN_STATUS_REMOVING, + GS_PLUGIN_STATUS_LAST /*< skip >*/ +} GsPluginStatus; + +/** + * GsPluginFlags: + * @GS_PLUGIN_FLAGS_NONE: No flags set + * @GS_PLUGIN_FLAGS_INTERACTIVE: User initiated the job + * + * The flags for the plugin at this point in time. + **/ +typedef enum { + GS_PLUGIN_FLAGS_NONE = 0, + GS_PLUGIN_FLAGS_INTERACTIVE = 1 << 4, +} GsPluginFlags; + +/** + * GsPluginError: + * @GS_PLUGIN_ERROR_FAILED: Generic failure + * @GS_PLUGIN_ERROR_NOT_SUPPORTED: Action not supported + * @GS_PLUGIN_ERROR_CANCELLED: Action was cancelled + * @GS_PLUGIN_ERROR_NO_NETWORK: No network connection available + * @GS_PLUGIN_ERROR_NO_SECURITY: Security policy forbid action + * @GS_PLUGIN_ERROR_NO_SPACE: No disk space to allow action + * @GS_PLUGIN_ERROR_AUTH_REQUIRED: Authentication was required + * @GS_PLUGIN_ERROR_AUTH_INVALID: Provided authentication was invalid + * @GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED: The plugins installed are incompatible + * @GS_PLUGIN_ERROR_DOWNLOAD_FAILED: The download action failed + * @GS_PLUGIN_ERROR_WRITE_FAILED: The save-to-disk failed + * @GS_PLUGIN_ERROR_INVALID_FORMAT: The data format is invalid + * @GS_PLUGIN_ERROR_DELETE_FAILED: The delete action failed + * @GS_PLUGIN_ERROR_RESTART_REQUIRED: A restart is required + * @GS_PLUGIN_ERROR_AC_POWER_REQUIRED: AC power is required + * @GS_PLUGIN_ERROR_TIMED_OUT: The job timed out + * @GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: The system battery level is too low + * + * The failure error types. + **/ +typedef enum { + GS_PLUGIN_ERROR_FAILED, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + GS_PLUGIN_ERROR_CANCELLED, + GS_PLUGIN_ERROR_NO_NETWORK, + GS_PLUGIN_ERROR_NO_SECURITY, + GS_PLUGIN_ERROR_NO_SPACE, + GS_PLUGIN_ERROR_AUTH_REQUIRED, + GS_PLUGIN_ERROR_AUTH_INVALID, + GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + GS_PLUGIN_ERROR_WRITE_FAILED, + GS_PLUGIN_ERROR_INVALID_FORMAT, + GS_PLUGIN_ERROR_DELETE_FAILED, + GS_PLUGIN_ERROR_RESTART_REQUIRED, + GS_PLUGIN_ERROR_AC_POWER_REQUIRED, + GS_PLUGIN_ERROR_TIMED_OUT, + GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW, + GS_PLUGIN_ERROR_LAST /*< skip >*/ +} GsPluginError; + +/** + * GsPluginRefineFlags: + * @GS_PLUGIN_REFINE_FLAGS_NONE: No explicit flags set + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID: Require the app’s ID; this is the minimum possible requirement + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE: Require the license + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL: Require the URL + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION: Require the long description + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE: Require the installed and download sizes + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING: Require the rating + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION: Require the version + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY: Require the history + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION: Require enough to install or remove the package + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS: Require update details + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN: Require the origin + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED: Require related packages + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS: Require available addons + * @GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES: Allow packages to be returned + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY: Require update severity + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED: Require distro upgrades + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE: Require the provenance + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS: Require user-reviews + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS: Require user-ratings + * @GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING: Normally the results of a refine are + * filtered to remove non-valid apps; if this flag is set, that won’t happen. + * This is intended to be used by internal #GsPluginLoader code. (Since: 42) + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON: Require the icon to be loaded + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS: Require the needed permissions + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME: Require the origin hostname + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI: Require the origin for UI + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME: Require the runtime + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS: Require screenshot information + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES: Require categories + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP: Require project group + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME: Require developer name + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS: Require kudos + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING: Require content rating + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE_DATA: Require user and cache data sizes (Since: 41) + * @GS_PLUGIN_REFINE_FLAGS_MASK: All flags (Since: 40) + * + * The refine flags. + **/ +typedef enum { + GS_PLUGIN_REFINE_FLAGS_NONE = 0, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID = 1 << 0, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE = 1 << 1, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL = 1 << 2, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION = 1 << 3, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE = 1 << 4, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING = 1 << 5, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION = 1 << 6, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY = 1 << 7, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION = 1 << 8, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS = 1 << 9, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN = 1 << 10, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED = 1 << 11, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE_DATA = 1 << 12, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS = 1 << 13, + GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES = 1 << 14, /* TODO: move to request */ + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY = 1 << 15, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED = 1 << 16, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE = 1 << 17, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS = 1 << 18, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS = 1 << 19, + GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING = 1 << 20, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON = 1 << 21, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS = 1 << 22, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME = 1 << 23, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI = 1 << 24, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME = 1 << 25, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS = 1 << 26, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES = 1 << 27, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP = 1 << 28, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME = 1 << 29, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS = 1 << 30, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING = 1 << 31, + GS_PLUGIN_REFINE_FLAGS_MASK = ~0, +} GsPluginRefineFlags; + +/** + * GsPluginListAppsFlags: + * @GS_PLUGIN_LIST_APPS_FLAGS_NONE: No flags set. + * @GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE: User initiated the job. + * + * Flags for an operation to list apps matching a given query. + * + * Since: 43 + */ +typedef enum { + GS_PLUGIN_LIST_APPS_FLAGS_NONE = 0, + GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE = 1 << 0, +} GsPluginListAppsFlags; + +/** + * GsPluginRefineCategoriesFlags: + * @GS_PLUGIN_REFINE_CATEGORIES_FLAGS_NONE: No flags set. + * @GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE: User initiated the job. + * @GS_PLUGIN_REFINE_CATEGORIES_FLAGS_SIZE: Work out the number of apps in each category. + * + * Flags for an operation to refine categories. + * + * Since: 43 + */ +typedef enum { + GS_PLUGIN_REFINE_CATEGORIES_FLAGS_NONE = 0, + GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE = 1 << 0, + GS_PLUGIN_REFINE_CATEGORIES_FLAGS_SIZE = 1 << 1, +} GsPluginRefineCategoriesFlags; + +/** + * GsPluginRefreshMetadataFlags: + * @GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE: No flags set. + * @GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE: User initiated the job. + * + * Flags for an operation to refresh metadata. + * + * Since: 42 + */ +typedef enum { + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE = 0, + GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE = 1 << 0, +} GsPluginRefreshMetadataFlags; + +/** + * GsPluginListDistroUpgradesFlags: + * @GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE: No flags set. + * @GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_INTERACTIVE: User initiated the job. + * + * Flags for an operation to list available distro upgrades. + * + * Since: 42 + */ +typedef enum { + GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE = 0, + GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_INTERACTIVE = 1 << 0, +} GsPluginListDistroUpgradesFlags; + +/** + * GsPluginManageRepositoryFlags: + * @GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_NONE: No flags set. + * @GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE: User initiated the job. + * @GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL: Install the repository. + * @GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE: Remove the repository. + * @GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE: Enable the repository. + * @GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE: Disable the repository. + * + * Flags for an operation on a repository. + * + * Since: 42 + */ +typedef enum { + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_NONE = 0, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE = 1 << 0, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL = 1 << 1, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE = 1 << 2, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE = 1 << 3, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE = 1 << 4, +} GsPluginManageRepositoryFlags; + +/** + * GsPluginRule: + * @GS_PLUGIN_RULE_CONFLICTS: The plugin conflicts with another + * @GS_PLUGIN_RULE_RUN_AFTER: Order the plugin after another + * @GS_PLUGIN_RULE_RUN_BEFORE: Order the plugin before another + * @GS_PLUGIN_RULE_BETTER_THAN: Results are better than another + * + * The rules used for ordering plugins. + * Plugins are expected to add rules in the init function for their #GsPlugin + * subclass. + **/ +typedef enum { + GS_PLUGIN_RULE_CONFLICTS, + GS_PLUGIN_RULE_RUN_AFTER, + GS_PLUGIN_RULE_RUN_BEFORE, + GS_PLUGIN_RULE_BETTER_THAN, + GS_PLUGIN_RULE_LAST /*< skip >*/ +} GsPluginRule; + +/** + * GsPluginAction: + * @GS_PLUGIN_ACTION_UNKNOWN: Action is unknown + * @GS_PLUGIN_ACTION_INSTALL: Install an application + * @GS_PLUGIN_ACTION_REMOVE: Remove an application + * @GS_PLUGIN_ACTION_UPDATE: Update an application + * @GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD: Download a distro upgrade + * @GS_PLUGIN_ACTION_UPGRADE_TRIGGER: Trigger a distro upgrade + * @GS_PLUGIN_ACTION_LAUNCH: Launch an application + * @GS_PLUGIN_ACTION_UPDATE_CANCEL: Cancel the update + * @GS_PLUGIN_ACTION_GET_UPDATES: Get the list of updates + * @GS_PLUGIN_ACTION_GET_SOURCES: Get the list of sources + * @GS_PLUGIN_ACTION_FILE_TO_APP: Convert the file to an application + * @GS_PLUGIN_ACTION_URL_TO_APP: Convert the URI to an application + * @GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL: Get the list of historical updates + * @GS_PLUGIN_ACTION_DOWNLOAD: Download an application + * @GS_PLUGIN_ACTION_GET_LANGPACKS: Get appropriate language pack + * @GS_PLUGIN_ACTION_INSTALL_REPO: Install a repository (Since: 41) + * @GS_PLUGIN_ACTION_REMOVE_REPO: Remove a repository (Since: 41) + * @GS_PLUGIN_ACTION_ENABLE_REPO: Enable a repository (Since: 41) + * @GS_PLUGIN_ACTION_DISABLE_REPO: Disable a repository (Since: 41) + * + * The plugin action. + **/ +typedef enum { + GS_PLUGIN_ACTION_UNKNOWN, + GS_PLUGIN_ACTION_INSTALL, + GS_PLUGIN_ACTION_REMOVE, + GS_PLUGIN_ACTION_UPDATE, + GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + GS_PLUGIN_ACTION_UPGRADE_TRIGGER, + GS_PLUGIN_ACTION_LAUNCH, + GS_PLUGIN_ACTION_UPDATE_CANCEL, + GS_PLUGIN_ACTION_GET_UPDATES, + GS_PLUGIN_ACTION_GET_SOURCES, + GS_PLUGIN_ACTION_FILE_TO_APP, + GS_PLUGIN_ACTION_URL_TO_APP, + GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL, + GS_PLUGIN_ACTION_DOWNLOAD, + GS_PLUGIN_ACTION_GET_LANGPACKS, + GS_PLUGIN_ACTION_INSTALL_REPO, + GS_PLUGIN_ACTION_REMOVE_REPO, + GS_PLUGIN_ACTION_ENABLE_REPO, + GS_PLUGIN_ACTION_DISABLE_REPO, + GS_PLUGIN_ACTION_LAST /*< skip >*/ +} GsPluginAction; + +G_END_DECLS diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h new file mode 100644 index 0000000..39093f5 --- /dev/null +++ b/lib/gs-plugin-vfuncs.h @@ -0,0 +1,429 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +/** + * SECTION:gs-plugin-vfuncs + * @title: GsPlugin Exports + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Vfuncs that plugins can implement + */ + +#include <appstream.h> +#include <glib-object.h> +#include <gmodule.h> +#include <gio/gio.h> +#include <libsoup/soup.h> + +#include "gs-app.h" +#include "gs-app-list.h" +#include "gs-category.h" + +G_BEGIN_DECLS + +/** + * gs_plugin_query_type: + * + * Returns the #GType for a subclass of #GsPlugin provided by this plugin + * module. It should not do any other computation. + * + * The init function for that type should initialize the plugin. If the plugin + * should not be run then gs_plugin_set_enabled() should be called from the + * init function. + * + * NOTE: Do not do any failable actions in the plugin class’ init function; use + * #GsPluginClass.setup_async instead. + * + * Since: 42 + */ +GType gs_plugin_query_type (void); + +/** + * gs_plugin_adopt_app: + * @plugin: a #GsPlugin + * @app: a #GsApp + * + * Called when an #GsApp has not been claimed (i.e. a management plugin has not + * been set). + * + * A claimed application means other plugins will not try to perform actions + * such as install, remove or update. Most applications are claimed when they + * are created. + * + * If a plugin can adopt this application then it should call + * gs_app_set_management_plugin() on @app. + **/ +void gs_plugin_adopt_app (GsPlugin *plugin, + GsApp *app); + +/** + * gs_plugin_add_updates: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of updates. + * + * NOTE: Actually downloading the updates can be done in gs_plugin_download_app() + * or in gs_plugin_download(). + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_sources: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of sources, for example the repos listed in /etc/yum.repos.d + * or the remotes configured in flatpak. + * + * Plugins are expected to add new apps using gs_app_list_add() of type + * %AS_COMPONENT_KIND_REPOSITORY. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_updates_historical + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of historical updates, i.e. the updates that have just been + * installed. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_updates_historical (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_launch: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Launch the specified application using a plugin-specific method. + * This is normally setting some environment or launching a specific binary. + * + * Plugins can simply use gs_plugin_app_launch() if no plugin-specific + * functionality is required. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_update_cancel: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Cancels the offline update of @app. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_update_cancel (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_install: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Install the application. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * NOTE: Once the action is complete, the plugin must set the new state of @app + * to %GS_APP_STATE_INSTALLED. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_remove: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Remove the application. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * NOTE: Once the action is complete, the plugin must set the new state of @app + * to %GS_APP_STATE_AVAILABLE or %GS_APP_STATE_UNKNOWN if not known. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_update_app: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Update the application live. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * NOTE: Once the action is complete, the plugin must set the new state of @app + * to %GS_APP_STATE_INSTALLED or %GS_APP_STATE_UNKNOWN if not known. + * + * If %GS_APP_QUIRK_IS_PROXY is set on the application then the actual #GsApp + * set in @app will be the related application of the parent. Plugins do not + * need to manually iterate on the related list of applications. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_download_app: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downloads the application and any dependencies ready to be installed or + * updated. + * + * Plugins are expected to schedule downloads using the system download + * scheduler if appropriate (if the download is not guaranteed to be under a few + * hundred kilobytes, for example), so that the user’s metered data preferences + * are honoured. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * If the @app is already downloaded, do not return an error and return %TRUE. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_download_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_download: + * @plugin: a #GsPlugin + * @apps: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downloads a list of applications ready to be installed or updated. + * + * Plugins are expected to schedule downloads using the system download + * scheduler if appropriate (if the download is not guaranteed to be under a few + * hundred kilobytes, for example), so that the user’s metered data preferences + * are honoured. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_download (GsPlugin *plugin, + GsAppList *apps, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_upgrade_download: + * @plugin: a #GsPlugin + * @app: a #GsApp, with kind %AS_COMPONENT_KIND_OPERATING_SYSTEM + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Starts downloading a distribution upgrade in the background. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_upgrade_trigger: + * @plugin: a #GsPlugin + * @app: a #GsApp, with kind %AS_COMPONENT_KIND_OPERATING_SYSTEM + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Triggers the distribution upgrade to be installed on next boot. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_upgrade_trigger (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_file_to_app: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @file: a #GFile + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Converts a local file to a #GsApp. It's expected that only one plugin will + * match the mimetype of @file and that a single #GsApp will be in the returned + * list. If no plugins can handle the file, the list will be empty. + * + * For example, the PackageKit plugin can turn a .rpm file into a application + * of kind %AS_COMPONENT_KIND_UNKNOWN but that in some cases it will be further refined + * into a %AS_COMPONENT_KIND_DESKTOP_APP (with all the extra metadata) by the appstream + * plugin. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_url_to_app: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @url: a #URL, e.g. "apt://gimp" + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Converts a URL to a #GsApp. It's expected that only one plugin will + * match the scheme of @url and that a single #GsApp will be in the returned + * list. If no plugins can handle the file, the list will be empty. + * + * For example, the apt plugin can turn apt://gimp into a application. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_update: + * @plugin: a #GsPlugin + * @apps: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Updates a list of applications, typically scheduling them for offline update. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_update (GsPlugin *plugin, + GsAppList *apps, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_langpacks: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @locale: a #LANGUAGE_CODE or #LOCALE, e.g. "ja" or "ja_JP" + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Returns a list of language packs, as per input language code or locale. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_langpacks (GsPlugin *plugin, + GsAppList *list, + const gchar *locale, + GCancellable *cancellable, + GError **error); + +G_END_DECLS 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; +} diff --git a/lib/gs-plugin.h b/lib/gs-plugin.h new file mode 100644 index 0000000..87dd858 --- /dev/null +++ b/lib/gs-plugin.h @@ -0,0 +1,331 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gmodule.h> +#include <gio/gio.h> + +#include "gs-app.h" +#include "gs-app-list.h" +#include "gs-app-query.h" +#include "gs-category.h" +#include "gs-plugin-event.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN (gs_plugin_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsPlugin, gs_plugin, GS, PLUGIN, GObject) + +/** + * GsPluginClass: + * @setup_async: (nullable): Setup method for the plugin. This is called after + * the #GsPlugin object is constructed, before it’s used for anything. It + * should do any long-running setup operations which the plugin needs, such as + * file or network access. It may be %NULL if the plugin doesn’t need to be + * explicitly shut down. It is not called if the plugin is disabled during + * construction. + * @setup_finish: (nullable): Finish method for @setup_async. Must be + * implemented if @setup_async is implemented. If this returns an error, the + * plugin will be disabled. + * @shutdown_async: (nullable): Shutdown method for the plugin. This is called + * by the #GsPluginLoader when the process is terminating or the + * #GsPluginLoader is being destroyed. It should be used to cancel or stop any + * ongoing operations or threads in the plugin. It may be %NULL if the plugin + * doesn’t need to be explicitly shut down. + * @shutdown_finish: (nullable): Finish method for @shutdown_async. Must be + * implemented if @shutdown_async is implemented. + * @refine_async: (nullable): Refining looks up and adds data to #GsApps. The + * apps to refine are provided in a list, and the flags specify what data to + * look up and add. Refining certain kinds of data can be very expensive (for + * example, requiring network requests), which is why it’s not all loaded by + * default. By refining multiple applications at once, data requests can be + * batched by the plugin where possible. (Since: 43) + * @refine_finish: (nullable): Finish method for @refine_async. Must be + * implemented if @refine_async is implemented. (Since: 43) + * @list_apps_async: (nullable): List apps matching a given query. (Since: 43) + * @list_apps_finish: (nullable): Finish method for @list_apps_async. Must be + * implemented if @list_apps_async is implemented. (Since: 43) + * @refresh_metadata_async: (nullable): Refresh plugin metadata. (Since: 43) + * @refresh_metadata_finish: (nullable): Finish method for + * @refresh_metadata_async. Must be implemented if @refresh_metadata_async is + * implemented. (Since: 43) + * @list_distro_upgrades_async: (nullable): List available distro upgrades. (Since: 43) + * @list_distro_upgrades_finish: (nullable): Finish method for + * @list_distro_upgrades_async. Must be implemented if + * @list_distro_upgrades_async is implemented. (Since: 43) + * @install_repository_async: (nullable): Install repository. (Since: 43) + * @install_repository_finish: (nullable): Finish method for + * @install_repository_async. Must be implemented if + * @install_repository_async is implemented. (Since: 43) + * @remove_repository_async: (nullable): Remove repository. (Since: 43) + * @remove_repository_finish: (nullable): Finish method for + * @remove_repository_async. Must be implemented if + * @remove_repository_async is implemented. (Since: 43) + * @enable_repository_async: (nullable): Enable repository. (Since: 43) + * @enable_repository_finish: (nullable): Finish method for + * @enable_repository_async. Must be implemented if + * @enable_repository_async is implemented. (Since: 43) + * @disable_repository_async: (nullable): Disable repository. (Since: 43) + * @disable_repository_finish: (nullable): Finish method for + * @disable_repository_async. Must be implemented if + * @disable_repository_async is implemented. (Since: 43) + * @refine_categories_async: (nullable): Refining looks up and adds data to + * #GsCategorys. The categories to refine are provided in a list, and the + * flags specify what data to look up and add. Refining certain kinds of data + * can be very expensive (for example, requiring network requests), which is + * why it’s not all loaded by default. By refining multiple categories at + * once, data requests can be batched by the plugin where possible. (Since: 43) + * @refine_categories_finish: (nullable): Finish method for + * @refine_categories_async. Must be implemented if @refine_categories_async + * is implemented. (Since: 43) + * + * The class structure for a #GsPlugin. Virtual methods here should be + * implemented by plugin implementations derived from #GsPlugin to provide their + * plugin-specific behaviour. + */ +struct _GsPluginClass +{ + GObjectClass parent_class; + void (*updates_changed) (GsPlugin *plugin); + void (*status_changed) (GsPlugin *plugin, + GsApp *app, + guint status); + void (*reload) (GsPlugin *plugin); + void (*report_event) (GsPlugin *plugin, + GsPluginEvent *event); + void (*allow_updates) (GsPlugin *plugin, + gboolean allow_updates); + void (*basic_auth_start) (GsPlugin *plugin, + const gchar *remote, + const gchar *realm, + GCallback callback, + gpointer user_data); + void (*repository_changed) (GsPlugin *plugin, + GsApp *repository); + gboolean (*ask_untrusted) (GsPlugin *plugin, + const gchar *title, + const gchar *msg, + const gchar *details, + const gchar *accept_label); + + void (*setup_async) (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*setup_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + void (*shutdown_async) (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*shutdown_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + void (*refine_async) (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*refine_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + void (*list_apps_async) (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + GsAppList * (*list_apps_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + void (*refresh_metadata_async) (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*refresh_metadata_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + void (*list_distro_upgrades_async) (GsPlugin *plugin, + GsPluginListDistroUpgradesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + GsAppList * (*list_distro_upgrades_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + void (*install_repository_async) (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*install_repository_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + void (*remove_repository_async) (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*remove_repository_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + void (*enable_repository_async) (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*enable_repository_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + void (*disable_repository_async) (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*disable_repository_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + void (*refine_categories_async) (GsPlugin *plugin, + GPtrArray *list, + GsPluginRefineCategoriesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (*refine_categories_finish) (GsPlugin *plugin, + GAsyncResult *result, + GError **error); + + gpointer padding[23]; +}; + +/* helpers */ +#define GS_PLUGIN_ERROR gs_plugin_error_quark () + +GQuark gs_plugin_error_quark (void); + +/* public getters and setters */ +const gchar *gs_plugin_get_name (GsPlugin *plugin); +const gchar *gs_plugin_get_appstream_id (GsPlugin *plugin); +void gs_plugin_set_appstream_id (GsPlugin *plugin, + const gchar *appstream_id); +gboolean gs_plugin_get_enabled (GsPlugin *plugin); +void gs_plugin_set_enabled (GsPlugin *plugin, + gboolean enabled); +gboolean gs_plugin_has_flags (GsPlugin *plugin, + GsPluginFlags flags); +void gs_plugin_add_flags (GsPlugin *plugin, + GsPluginFlags flags); +void gs_plugin_remove_flags (GsPlugin *plugin, + GsPluginFlags flags); +guint gs_plugin_get_scale (GsPlugin *plugin); +const gchar *gs_plugin_get_language (GsPlugin *plugin); +void gs_plugin_add_rule (GsPlugin *plugin, + GsPluginRule rule, + const gchar *name); + +/* helpers */ +gboolean gs_plugin_download_file (GsPlugin *plugin, + GsApp *app, + const gchar *uri, + const gchar *filename, + GCancellable *cancellable, + GError **error); +gchar *gs_plugin_download_rewrite_resource (GsPlugin *plugin, + GsApp *app, + const gchar *resource, + GCancellable *cancellable, + GError **error); + +gboolean gs_plugin_check_distro_id (GsPlugin *plugin, + const gchar *distro_id); +GsApp *gs_plugin_cache_lookup (GsPlugin *plugin, + const gchar *key); +void gs_plugin_cache_lookup_by_state (GsPlugin *plugin, + GsAppList *list, + GsAppState state); +void gs_plugin_cache_add (GsPlugin *plugin, + const gchar *key, + GsApp *app); +void gs_plugin_cache_remove (GsPlugin *plugin, + const gchar *key); +void gs_plugin_cache_invalidate (GsPlugin *plugin); +void gs_plugin_status_update (GsPlugin *plugin, + GsApp *app, + GsPluginStatus status); +gboolean gs_plugin_app_launch (GsPlugin *plugin, + GsApp *app, + GError **error); +typedef gboolean (* GsPluginPickDesktopFileCallback) (GsPlugin *plugin, + GsApp *app, + const gchar *filename, + GKeyFile *key_file); +/** + * GsPluginPickDesktopFileCallback: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @filename: a .desktop file name + * @key_file: a #GKeyFile with @filename loaded + * + * A callback used by gs_plugin_app_launch_filtered() to filter which + * of the candidate .desktop files should be used to launch the @app. + * + * Returns: %TRUE, when the @key_file should be used, %FALSE to continue + * searching. + * + * Since: 43 + **/ +gboolean gs_plugin_app_launch_filtered (GsPlugin *plugin, + GsApp *app, + GsPluginPickDesktopFileCallback cb, + gpointer user_data, + GError **error); +void gs_plugin_updates_changed (GsPlugin *plugin); +void gs_plugin_reload (GsPlugin *plugin); +const gchar *gs_plugin_status_to_string (GsPluginStatus status); +void gs_plugin_report_event (GsPlugin *plugin, + GsPluginEvent *event); +void gs_plugin_set_allow_updates (GsPlugin *plugin, + gboolean allow_updates); +gboolean gs_plugin_get_network_available (GsPlugin *plugin); +void gs_plugin_basic_auth_start (GsPlugin *plugin, + const gchar *remote, + const gchar *realm, + GCallback callback, + gpointer user_data); +void gs_plugin_repository_changed (GsPlugin *plugin, + GsApp *repository); +void gs_plugin_update_cache_state_for_repository + (GsPlugin *plugin, + GsApp *repository); +gboolean gs_plugin_ask_untrusted (GsPlugin *plugin, + const gchar *title, + const gchar *msg, + const gchar *details, + const gchar *accept_label); + +G_END_DECLS diff --git a/lib/gs-remote-icon.c b/lib/gs-remote-icon.c new file mode 100644 index 0000000..84b071b --- /dev/null +++ b/lib/gs-remote-icon.c @@ -0,0 +1,375 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation, Inc + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-remote-icon + * @short_description: A #GIcon implementation for remote icons + * + * #GsRemoteIcon is a #GIcon implementation which represents remote icons — + * icons which have an HTTP or HTTPS URI. It provides a well-known local filename + * for a cached copy of the icon, accessible as #GFileIcon:file, and a method + * to download the icon to the cache, gs_remote_icon_ensure_cached(). + * + * Constructing a #GsRemoteIcon does not guarantee that the icon is cached. Call + * gs_remote_icon_ensure_cached() for that. + * + * #GsRemoteIcon is immutable after construction and hence is entirely thread + * safe. + * + * FIXME: Currently does no cache invalidation. + * + * Since: 40 + */ + +#include "config.h" + +#include <gio/gio.h> +#include <glib.h> +#include <glib-object.h> +#include <glib/gstdio.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <libsoup/soup.h> + +#include "gs-remote-icon.h" +#include "gs-utils.h" + +/* FIXME: Work around the fact that GFileIcon is not derivable, by deriving from + * it anyway by copying its `struct GFileIcon` definition inline here. This will + * work as long as the size of `struct GFileIcon` doesn’t change within GIO. + * There’s no way of knowing if that’s the case. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2345 for why this is + * necessary. */ +struct _GsRemoteIcon +{ + /* struct GFileIcon { */ + GObject grandparent; + GFile *file; + /* } */ + + gchar *uri; /* (owned), immutable after construction */ +}; + +G_DEFINE_TYPE (GsRemoteIcon, gs_remote_icon, G_TYPE_FILE_ICON) + +typedef enum { + PROP_URI = 1, +} GsRemoteIconProperty; + +static GParamSpec *obj_props[PROP_URI + 1] = { NULL, }; + +static void +gs_remote_icon_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsRemoteIcon *self = GS_REMOTE_ICON (object); + + switch ((GsRemoteIconProperty) prop_id) { + case PROP_URI: + g_value_set_string (value, gs_remote_icon_get_uri (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_remote_icon_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsRemoteIcon *self = GS_REMOTE_ICON (object); + + switch ((GsRemoteIconProperty) prop_id) { + case PROP_URI: + /* Construct only */ + g_assert (self->uri == NULL); + self->uri = g_value_dup_string (value); + g_assert (g_str_has_prefix (self->uri, "http:") || + g_str_has_prefix (self->uri, "https:")); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_remote_icon_finalize (GObject *object) +{ + GsRemoteIcon *self = GS_REMOTE_ICON (object); + + g_free (self->uri); + + G_OBJECT_CLASS (gs_remote_icon_parent_class)->finalize (object); +} + +static void +gs_remote_icon_class_init (GsRemoteIconClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gs_remote_icon_get_property; + object_class->set_property = gs_remote_icon_set_property; + object_class->finalize = gs_remote_icon_finalize; + + /** + * GsRemoteIcon:uri: (not nullable) + * + * Remote URI of the icon. This must be an HTTP or HTTPS URI; it is a + * programmer error to provide other URI schemes. + * + * Since: 40 + */ + obj_props[PROP_URI] = + g_param_spec_string ("uri", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); +} + +static void +gs_remote_icon_init (GsRemoteIcon *self) +{ +} + +/* Use a hash-prefixed filename to avoid cache clashes. + * This can only fail if @create_directory is %TRUE. */ +static gchar * +gs_remote_icon_get_cache_filename (const gchar *uri, + gboolean create_directory, + GError **error) +{ + g_autofree gchar *uri_checksum = NULL; + g_autofree gchar *uri_basename = NULL; + g_autofree gchar *cache_basename = NULL; + GsUtilsCacheFlags flags; + + uri_checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA1, + uri, + -1); + uri_basename = g_path_get_basename (uri); + + /* convert filename from jpg to png, as we always convert to PNG on + * download */ + if (g_str_has_suffix (uri_basename, ".jpg")) + memcpy (uri_basename + strlen (uri_basename) - 4, ".png", 4); + + cache_basename = g_strdup_printf ("%s-%s", uri_checksum, uri_basename); + + flags = GS_UTILS_CACHE_FLAG_WRITEABLE; + if (create_directory) + flags |= GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY; + + return gs_utils_get_cache_filename ("icons", + cache_basename, + flags, + error); +} + +/** + * gs_remote_icon_new: + * @uri: remote URI of the icon + * + * Create a new #GsRemoteIcon representing @uri. The #GFileIcon:file of the + * resulting icon will represent the local cache location for the icon. + * + * Returns: (transfer full): a new remote icon + * Since: 40 + */ +GIcon * +gs_remote_icon_new (const gchar *uri) +{ + g_autofree gchar *cache_filename = NULL; + g_autoptr(GFile) file = NULL; + + g_return_val_if_fail (uri != NULL, NULL); + + /* The file is the expected cached location of the icon, once it’s + * downloaded. By setting it as the #GFileIcon:file property, existing + * code (particularly in GTK) which operates on #GFileIcons will work + * transparently with this. + * + * Ideally, #GFileIcon would be an interface rather than a class, which + * would make this implementation cleaner, but this is what we’re stuck + * with. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2345 */ + cache_filename = gs_remote_icon_get_cache_filename (uri, FALSE, NULL); + g_assert (cache_filename != NULL); + file = g_file_new_for_path (cache_filename); + + return g_object_new (GS_TYPE_REMOTE_ICON, + "file", file, + "uri", uri, + NULL); +} + +/** + * gs_remote_icon_get_uri: + * @self: a #GsRemoteIcon + * + * Gets the value of #GsRemoteIcon:uri. + * + * Returns: (not nullable): remote URI of the icon + * Since: 40 + */ +const gchar * +gs_remote_icon_get_uri (GsRemoteIcon *self) +{ + g_return_val_if_fail (GS_IS_REMOTE_ICON (self), NULL); + + return self->uri; +} + +static GdkPixbuf * +gs_icon_download (SoupSession *session, + const gchar *uri, + const gchar *destination_path, + guint max_size, + GCancellable *cancellable, + GError **error) +{ + guint status_code; + g_autoptr(SoupMessage) msg = NULL; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GdkPixbuf) scaled_pixbuf = NULL; + + /* Create the request */ + msg = soup_message_new (SOUP_METHOD_GET, uri); + if (msg == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Icon has an invalid URL"); + return NULL; + } + + /* Send request synchronously and start reading the response. */ + stream = soup_session_send (session, msg, cancellable, error); + +#if SOUP_CHECK_VERSION(3, 0, 0) + status_code = soup_message_get_status (msg); +#else + status_code = msg->status_code; +#endif + if (stream == NULL) { + return NULL; + } else if (status_code != SOUP_STATUS_OK) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to download icon %s: %s", + uri, soup_status_get_phrase (status_code)); + return NULL; + } + + /* Typically these icons are 64x64px PNG files. If not, resize down + * so it’s at most @max_size square, to minimise the size of the on-disk + * cache.*/ + pixbuf = gdk_pixbuf_new_from_stream (stream, cancellable, error); + if (pixbuf == NULL) + return NULL; + + if ((guint) gdk_pixbuf_get_height (pixbuf) <= max_size && + (guint) gdk_pixbuf_get_width (pixbuf) <= max_size) { + scaled_pixbuf = g_object_ref (pixbuf); + } else { + scaled_pixbuf = gdk_pixbuf_scale_simple (pixbuf, max_size, max_size, + GDK_INTERP_BILINEAR); + } + + /* write file */ + if (!gdk_pixbuf_save (scaled_pixbuf, destination_path, "png", error, NULL)) + return NULL; + + return g_steal_pointer (&scaled_pixbuf); +} + +/** + * gs_remote_icon_ensure_cached: + * @self: a #GsRemoteIcon + * @soup_session: a #SoupSession to use to download the icon + * @maximum_icon_size: maximum size (in device pixels) of the icon to save + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Ensure the given icon is present in the local cache, potentially downloading + * it from its remote server if needed. This will do network and disk I/O. + * + * @maximum_icon_size specifies the maximum size (in device pixels) of the icon + * which should be saved to the cache. This is the maximum size that the icon + * can ever be used at, as icons can be downscaled but never upscaled. Typically + * this will be 160px multiplied by the device scale + * (`gtk_widget_get_scale_factor()`). + * + * This can be called from any thread, as #GsRemoteIcon is immutable and hence + * thread-safe. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 40 + */ +gboolean +gs_remote_icon_ensure_cached (GsRemoteIcon *self, + SoupSession *soup_session, + guint maximum_icon_size, + GCancellable *cancellable, + GError **error) +{ + const gchar *uri; + g_autofree gchar *cache_filename = NULL; + g_autoptr(GdkPixbuf) cached_pixbuf = NULL; + GStatBuf stat_buf; + + g_return_val_if_fail (GS_IS_REMOTE_ICON (self), FALSE); + g_return_val_if_fail (SOUP_IS_SESSION (soup_session), FALSE); + g_return_val_if_fail (maximum_icon_size > 0, FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + uri = gs_remote_icon_get_uri (self); + + /* Work out cache filename. */ + cache_filename = gs_remote_icon_get_cache_filename (uri, TRUE, error); + if (cache_filename == NULL) + return FALSE; + + /* Already in cache and not older than 30 days */ + if (g_stat (cache_filename, &stat_buf) != -1 && + S_ISREG (stat_buf.st_mode) && + (g_get_real_time () / G_USEC_PER_SEC) - stat_buf.st_mtim.tv_sec < (60 * 60 * 24 * 30)) { + gint width = 0, height = 0; + /* Ensure the downloaded image dimensions are stored on the icon */ + if (!g_object_get_data (G_OBJECT (self), "width") && + gdk_pixbuf_get_file_info (cache_filename, &width, &height)) { + g_object_set_data (G_OBJECT (self), "width", GINT_TO_POINTER (width)); + g_object_set_data (G_OBJECT (self), "height", GINT_TO_POINTER (height)); + } + return TRUE; + } + + cached_pixbuf = gs_icon_download (soup_session, uri, cache_filename, maximum_icon_size, cancellable, error); + if (cached_pixbuf == NULL) + return FALSE; + + /* Ensure the dimensions are set correctly on the icon. */ + g_object_set_data (G_OBJECT (self), "width", GUINT_TO_POINTER (gdk_pixbuf_get_width (cached_pixbuf))); + g_object_set_data (G_OBJECT (self), "height", GUINT_TO_POINTER (gdk_pixbuf_get_height (cached_pixbuf))); + + return TRUE; +} diff --git a/lib/gs-remote-icon.h b/lib/gs-remote-icon.h new file mode 100644 index 0000000..82e246f --- /dev/null +++ b/lib/gs-remote-icon.h @@ -0,0 +1,37 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation, Inc + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gio/gio.h> +#include <glib.h> +#include <glib-object.h> +#include <libsoup/soup.h> + +G_BEGIN_DECLS + +#define GS_TYPE_REMOTE_ICON (gs_remote_icon_get_type ()) + +/* FIXME: This is actually derived from GFileIcon, but the GFileIconClass isn’t + * public, so we use GObjectClass instead (which is what GFileIconClass is — it + * doesn’t define any vfuncs). See the note in gs-remote-icon.c. */ +G_DECLARE_FINAL_TYPE (GsRemoteIcon, gs_remote_icon, GS, REMOTE_ICON, GObject) + +GIcon *gs_remote_icon_new (const gchar *uri); + +const gchar *gs_remote_icon_get_uri (GsRemoteIcon *self); + +gboolean gs_remote_icon_ensure_cached (GsRemoteIcon *self, + SoupSession *soup_session, + guint maximum_icon_size, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/lib/gs-self-test.c b/lib/gs-self-test.c new file mode 100644 index 0000000..c38f452 --- /dev/null +++ b/lib/gs-self-test.c @@ -0,0 +1,786 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-debug.h" +#include "gs-test.h" + +static gboolean +gs_app_list_filter_cb (GsApp *app, gpointer user_data) +{ + if (g_strcmp0 (gs_app_get_id (app), "a") == 0) + return FALSE; + if (g_strcmp0 (gs_app_get_id (app), "c") == 0) + return FALSE; + return TRUE; +} + +static void +gs_utils_url_func (void) +{ + g_autofree gchar *path1 = NULL; + g_autofree gchar *path2 = NULL; + g_autofree gchar *path3 = NULL; + g_autofree gchar *scheme1 = NULL; + g_autofree gchar *scheme2 = NULL; + + scheme1 = gs_utils_get_url_scheme ("appstream://gimp.desktop"); + g_assert_cmpstr (scheme1, ==, "appstream"); + scheme2 = gs_utils_get_url_scheme ("appstream:gimp.desktop"); + g_assert_cmpstr (scheme2, ==, "appstream"); + + path1 = gs_utils_get_url_path ("appstream://gimp.desktop"); + g_assert_cmpstr (path1, ==, "gimp.desktop"); + path2 = gs_utils_get_url_path ("appstream:gimp.desktop"); + g_assert_cmpstr (path2, ==, "gimp.desktop"); + path3 = gs_utils_get_url_path ("apt:/gimp"); + g_assert_cmpstr (path3, ==, "gimp"); +} + +static void +gs_utils_wilson_func (void) +{ + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 0), ==, -1); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 400), ==, 100); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (10, 0, 0, 0, 400), ==, 98); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 1), ==, 76); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (5, 4, 20, 100, 400), ==, 93); +} + +static void +gs_os_release_func (void) +{ + g_autofree gchar *fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + fn = gs_test_get_filename (TESTDATADIR, "tests/os-release"); + g_assert (fn != NULL); + g_setenv ("GS_SELF_TEST_OS_RELEASE_FILENAME", fn, TRUE); + + os_release = gs_os_release_new (&error); + g_assert_no_error (error); + g_assert (os_release != NULL); + g_assert_cmpstr (gs_os_release_get_id (os_release), ==, "fedora"); + g_assert_cmpstr (gs_os_release_get_name (os_release), ==, "Fedora"); + g_assert_cmpstr (gs_os_release_get_version (os_release), ==, "25 (Workstation Edition)"); + g_assert_cmpstr (gs_os_release_get_version_id (os_release), ==, "25"); + g_assert_cmpstr (gs_os_release_get_pretty_name (os_release), ==, "Fedora 25 (Workstation Edition)"); +} + +static void +gs_utils_append_kv_func (void) +{ + g_autoptr(GString) str = g_string_new (NULL); + + /* normal */ + gs_utils_append_key_value (str, 5, "key", "val"); + g_assert_cmpstr (str->str, ==, "key: val\n"); + + /* oversize */ + g_string_truncate (str, 0); + gs_utils_append_key_value (str, 5, "longkey", "val"); + g_assert_cmpstr (str->str, ==, "longkey: val\n"); + + /* null key */ + g_string_truncate (str, 0); + gs_utils_append_key_value (str, 5, NULL, "val"); + g_assert_cmpstr (str->str, ==, " val\n"); + + /* zero align key */ + g_string_truncate (str, 0); + gs_utils_append_key_value (str, 0, "key", "val"); + g_assert_cmpstr (str->str, ==, "key: val\n"); +} + +static void +gs_utils_cache_func (void) +{ + g_autofree gchar *fn1 = NULL; + g_autofree gchar *fn2 = NULL; + g_autoptr(GError) error = NULL; + + fn1 = gs_utils_get_cache_filename ("test", + "http://www.foo.bar/baz", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + &error); + g_assert_no_error (error); + g_assert_cmpstr (fn1, !=, NULL); + g_assert (g_str_has_prefix (fn1, g_get_user_cache_dir ())); + g_assert (g_str_has_suffix (fn1, "test/baz")); + + fn2 = gs_utils_get_cache_filename ("test", + "http://www.foo.bar/baz", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_USE_HASH | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + &error); + g_assert_no_error (error); + g_assert_cmpstr (fn2, !=, NULL); + g_assert (g_str_has_prefix (fn2, g_get_user_cache_dir ())); + g_assert (g_str_has_suffix (fn2, "test/295099f59d12b3eb0b955325fcb699cd23792a89-baz")); +} + +static void +gs_utils_error_func (void) +{ + g_autofree gchar *app_id = NULL; + g_autofree gchar *origin_id = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = gs_app_new ("gimp.desktop"); + g_autoptr(GsApp) origin = gs_app_new ("gimp-repo"); + + for (guint i = 0; i < GS_PLUGIN_ERROR_LAST; i++) + g_assert (gs_plugin_error_to_string (i) != NULL); + + /* noop */ + gs_utils_error_add_app_id (&error, app); + gs_utils_error_add_origin_id (&error, origin); + + g_set_error (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "failed"); + g_assert_cmpstr (error->message, ==, "failed"); + gs_utils_error_add_app_id (&error, app); + gs_utils_error_add_origin_id (&error, origin); + g_assert_cmpstr (error->message, ==, "[*/*/*/gimp-repo/*] {*/*/*/gimp.desktop/*} failed"); + + /* find and strip any unique IDs from the error message */ + for (guint i = 0; i < 2; i++) { + if (app_id == NULL) + app_id = gs_utils_error_strip_app_id (error); + if (origin_id == NULL) + origin_id = gs_utils_error_strip_origin_id (error); + } + + g_assert_cmpstr (app_id, ==, "*/*/*/gimp.desktop/*"); + g_assert_cmpstr (origin_id, ==, "*/*/*/gimp-repo/*"); + g_assert_cmpstr (error->message, ==, "failed"); +} + +static void +gs_plugin_download_rewrite_func (void) +{ + g_autofree gchar *css = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GDBusConnection) bus_connection = NULL; + g_autoptr(GsPlugin) plugin = NULL; + const gchar *resource = "background:\n" + " url('file://" DATADIR "/gnome-software/featured-maps.png')\n" + " url('file://" DATADIR "/gnome-software/featured-maps-bg.png')\n" + " bottom center / contain no-repeat;\n"; + + /* only when installed */ + if (!g_file_test (DATADIR "/gnome-software/featured-maps.png", G_FILE_TEST_EXISTS)) { + g_test_skip ("not installed"); + return; + } + + /* test rewrite */ + bus_connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + g_assert_no_error (error); + + plugin = gs_plugin_new (bus_connection, bus_connection); + gs_plugin_set_name (plugin, "self-test"); + css = gs_plugin_download_rewrite_resource (plugin, + NULL, /* app */ + resource, + NULL, + &error); + g_assert_no_error (error); + g_assert (css != NULL); +} + +static void +gs_plugin_func (void) +{ + GsAppList *list; + GsAppList *list_dup; + GsAppList *list_remove; + GsApp *app; + + /* check enums converted */ + for (guint i = 0; i < GS_PLUGIN_ACTION_LAST; i++) { + const gchar *tmp = gs_plugin_action_to_string (i); + if (tmp == NULL) + g_critical ("failed to convert %u", i); + g_assert_cmpint (gs_plugin_action_from_string (tmp), ==, i); + } + for (guint i = 1; i < GS_PLUGIN_ACTION_LAST; i++) { + const gchar *tmp = gs_plugin_action_to_function_name (i); + if (tmp == NULL) { + /* These do not have function, they exist only for better error messages. */ + if (i == GS_PLUGIN_ACTION_INSTALL_REPO || + i == GS_PLUGIN_ACTION_REMOVE_REPO || + i == GS_PLUGIN_ACTION_ENABLE_REPO || + i == GS_PLUGIN_ACTION_DISABLE_REPO) + continue; + g_critical ("failed to convert %u", i); + } + } + + /* add a couple of duplicate IDs */ + app = gs_app_new ("a"); + list = gs_app_list_new (); + gs_app_list_add (list, app); + g_object_unref (app); + + /* test refcounting */ + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list, 0)), ==, "a"); + list_dup = gs_app_list_copy (list); + g_object_unref (list); + g_assert_cmpint (gs_app_list_length (list_dup), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_dup, 0)), ==, "a"); + g_object_unref (list_dup); + + /* test removing objects */ + app = gs_app_new ("a"); + list_remove = gs_app_list_new (); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new ("c"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 3); + gs_app_list_filter (list_remove, gs_app_list_filter_cb, NULL); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_remove, 0)), ==, "b"); + + /* test removing duplicates at runtime */ + app = gs_app_new ("b"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_remove, 0)), ==, "b"); + g_object_unref (list_remove); + + /* test removing duplicates when lazy-loading */ + list_remove = gs_app_list_new (); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_set_id (app, "e"); + g_object_unref (app); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_set_id (app, "e"); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 2); + gs_app_list_filter_duplicates (list_remove, GS_APP_LIST_FILTER_FLAG_NONE); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 1); + g_object_unref (list_remove); + + /* test removing duplicates when some apps have no app ID */ + list_remove = gs_app_list_new (); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_set_id (app, "e"); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 3); + gs_app_list_filter_duplicates (list_remove, GS_APP_LIST_FILTER_FLAG_NONE); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 3); + g_object_unref (list_remove); + + /* remove lazy-loaded app */ + list_remove = gs_app_list_new (); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_list_remove (list_remove, app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 0); + g_object_unref (app); + g_object_unref (list_remove); + + /* respect priority when deduplicating */ + list = gs_app_list_new (); + app = gs_app_new ("e"); + gs_app_set_unique_id (app, "user/foo/*/e/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 0); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_set_unique_id (app, "user/bar/*/e/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 99); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_set_unique_id (app, "user/baz/*/e/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 50); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 3); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/bar/*/e/*"); + g_object_unref (list); + + /* respect priority (using name and version) when deduplicating */ + list = gs_app_list_new (); + app = gs_app_new ("e"); + gs_app_add_source (app, "foo"); + gs_app_set_version (app, "1.2.3"); + gs_app_set_unique_id (app, "user/foo/repo/*/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 0); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_add_source (app, "foo"); + gs_app_set_version (app, "1.2.3"); + gs_app_set_unique_id (app, "user/foo/repo-security/*/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 99); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_add_source (app, "foo"); + gs_app_set_version (app, "1.2.3"); + gs_app_set_unique_id (app, "user/foo/repo-universe/*/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 50); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 3); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID | + GS_APP_LIST_FILTER_FLAG_KEY_SOURCE | + GS_APP_LIST_FILTER_FLAG_KEY_VERSION); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/foo/repo-security/*/*"); + g_object_unref (list); + + /* prefer installed applications */ + list = gs_app_list_new (); + app = gs_app_new ("e"); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + gs_app_set_unique_id (app, "user/foo/*/e/*"); + gs_app_set_priority (app, 0); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + gs_app_set_unique_id (app, "user/bar/*/e/*"); + gs_app_set_priority (app, 100); + gs_app_list_add (list, app); + g_object_unref (app); + gs_app_list_filter_duplicates (list, + GS_APP_LIST_FILTER_FLAG_KEY_ID | + GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/foo/*/e/*"); + g_object_unref (list); + + /* use the provides ID to dedupe */ + list = gs_app_list_new (); + app = gs_app_new ("gimp.desktop"); + gs_app_set_unique_id (app, "user/fedora/*/gimp.desktop/*"); + gs_app_set_priority (app, 0); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("org.gimp.GIMP"); + gs_app_add_provided_item (app, + AS_PROVIDED_KIND_ID, + "gimp.desktop"); + gs_app_set_unique_id (app, "user/flathub/*/org.gimp.GIMP/*"); + gs_app_set_priority (app, 100); + gs_app_list_add (list, app); + g_object_unref (app); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, + "user/flathub/*/org.gimp.GIMP/*"); + g_object_unref (list); + + /* use globs when adding */ + list = gs_app_list_new (); + app = gs_app_new ("b"); + gs_app_set_unique_id (app, "a/b/c/d/e"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_set_unique_id (app, "a/b/c/*/e"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list, 0)), ==, "b"); + g_object_unref (list); + + /* lookup with a wildcard */ + list = gs_app_list_new (); + app = gs_app_new ("b"); + gs_app_set_unique_id (app, "a/b/c/d/e"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert (gs_app_list_lookup (list, "a/b/c/d/e") != NULL); + g_assert (gs_app_list_lookup (list, "a/b/c/d/*") != NULL); + g_assert (gs_app_list_lookup (list, "*/b/c/d/e") != NULL); + g_assert (gs_app_list_lookup (list, "x/x/x/x/x") == NULL); + g_object_unref (list); + + /* allow duplicating a wildcard */ + list = gs_app_list_new (); + app = gs_app_new ("gimp.desktop"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("gimp.desktop"); + gs_app_set_unique_id (app, "system/flatpak/*/gimp.desktop/stable"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 2); + g_object_unref (list); + + /* allow duplicating a wildcard */ + list = gs_app_list_new (); + app = gs_app_new ("gimp.desktop"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("gimp.desktop"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_object_unref (list); + + /* add a list to a list */ + list = gs_app_list_new (); + list_dup = gs_app_list_new (); + app = gs_app_new ("a"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list_dup, app); + g_object_unref (app); + gs_app_list_add_list (list, list_dup); + g_assert_cmpint (gs_app_list_length (list), ==, 2); + g_assert_cmpint (gs_app_list_length (list_dup), ==, 1); + g_object_unref (list); + g_object_unref (list_dup); + + /* remove apps from the list */ + list = gs_app_list_new (); + app = gs_app_new ("a"); + gs_app_list_add (list, app); + gs_app_list_remove (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 0); + g_object_unref (list); + + /* truncate list */ + list = gs_app_list_new (); + app = gs_app_new ("a"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("c"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert (!gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED)); + g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3); + gs_app_list_truncate (list, 3); + g_assert_cmpint (gs_app_list_length (list), ==, 3); + g_assert (gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED)); + g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3); + gs_app_list_truncate (list, 2); + g_assert_cmpint (gs_app_list_length (list), ==, 2); + gs_app_list_truncate (list, 1); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + gs_app_list_truncate (list, 0); + g_assert_cmpint (gs_app_list_length (list), ==, 0); + g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3); + g_object_unref (list); +} + +static gpointer +gs_app_thread_cb (gpointer data) +{ + GsApp *app = GS_APP (data); + for (guint i = 0; i < 10000; i++) { + g_assert_cmpstr (gs_app_get_unique_id (app), !=, NULL); + gs_app_set_branch (app, "master"); + g_assert_cmpstr (gs_app_get_unique_id (app), !=, NULL); + gs_app_set_branch (app, "stable"); + } + return NULL; +} + +static void +gs_app_thread_func (gconstpointer user_data) +{ + GsDebug *debug = GS_DEBUG ((void *)user_data); + GThread *thread1; + GThread *thread2; + g_autoptr(GsApp) app = gs_app_new ("gimp.desktop"); + + /* try really hard to cause a threading problem */ + gs_debug_set_verbose (debug, FALSE); + thread1 = g_thread_new ("thread1", gs_app_thread_cb, app); + thread2 = g_thread_new ("thread2", gs_app_thread_cb, app); + g_thread_join (thread1); /* consumes the reference */ + g_thread_join (thread2); + gs_debug_set_verbose (debug, TRUE); +} + +static void +gs_app_unique_id_func (void) +{ + g_autoptr(GsApp) app = gs_app_new (NULL); + g_autofree gchar *data_id = NULL; + const gchar *unique_id; + + unique_id = "system/flatpak/gnome/org.gnome.Software/master"; + gs_app_set_from_unique_id (app, unique_id, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert (GS_IS_APP (app)); + g_assert_cmpint (gs_app_get_scope (app), ==, AS_COMPONENT_SCOPE_SYSTEM); + g_assert_cmpint (gs_app_get_bundle_kind (app), ==, AS_BUNDLE_KIND_FLATPAK); + g_assert_cmpstr (gs_app_get_origin (app), ==, "gnome"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.gnome.Software"); + g_assert_cmpstr (gs_app_get_branch (app), ==, "master"); + + /* test conversions from 6-part IDs */ + data_id = gs_utils_unique_id_compat_convert (unique_id); + g_assert_cmpstr (data_id, ==, unique_id); + g_clear_pointer (&data_id, g_free); + + data_id = gs_utils_unique_id_compat_convert ("not a unique ID"); + g_assert_null (data_id); + + data_id = gs_utils_unique_id_compat_convert ("system/flatpak/gnome/desktop-app/org.gnome.Software/master"); + g_assert_cmpstr (data_id, ==, unique_id); + g_clear_pointer (&data_id, g_free); +} + +static void +gs_app_addons_func (void) +{ + g_autoptr(GsApp) app = gs_app_new ("test.desktop"); + g_autoptr(GsApp) addon = NULL; + g_autoptr(GsAppList) addons_list = NULL; + + /* create, add then drop ref, so @app has the only refcount of addon */ + addon = gs_app_new ("test.desktop"); + addons_list = gs_app_list_new (); + gs_app_list_add (addons_list, addon); + + gs_app_add_addons (app, addons_list); + + gs_app_remove_addon (app, addon); +} + +static void +gs_app_func (void) +{ + g_autoptr(GsApp) app = NULL; + + app = gs_app_new ("gnome-software.desktop"); + g_assert (GS_IS_APP (app)); + g_assert_cmpstr (gs_app_get_id (app), ==, "gnome-software.desktop"); + + /* check we clean up the version, but not at the expense of having + * the same string as the update version */ + gs_app_set_version (app, "2.8.6-3.fc20"); + gs_app_set_update_version (app, "2.8.6-4.fc20"); + g_assert_cmpstr (gs_app_get_version (app), ==, "2.8.6-3.fc20"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, "2.8.6-4.fc20"); + g_assert_cmpstr (gs_app_get_version_ui (app), ==, "2.8.6-3"); + g_assert_cmpstr (gs_app_get_update_version_ui (app), ==, "2.8.6-4"); + + /* check the quality stuff works */ + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "dave"); + g_assert_cmpstr (gs_app_get_name (app), ==, "dave"); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, "brian"); + g_assert_cmpstr (gs_app_get_name (app), ==, "dave"); + gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, "hugh"); + g_assert_cmpstr (gs_app_get_name (app), ==, "hugh"); + + /* check non-transient state saving */ + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + gs_app_set_state (app, GS_APP_STATE_REMOVING); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_REMOVING); + gs_app_set_state_recover (app); // simulate an error + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + + /* try again */ + gs_app_set_state (app, GS_APP_STATE_REMOVING); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_REMOVING); + gs_app_set_state_recover (app); // simulate an error + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + + /* correctly parse URL */ + gs_app_set_origin_hostname (app, "https://mirrors.fedoraproject.org/metalink"); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "fedoraproject.org"); + gs_app_set_origin_hostname (app, "file:///home/hughsie"); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost"); + + /* check setting the progress */ + gs_app_set_progress (app, 42); + g_assert_cmpuint (gs_app_get_progress (app), ==, 42); + gs_app_set_progress (app, 0); + g_assert_cmpuint (gs_app_get_progress (app), ==, 0); + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + g_assert_cmpuint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + g_assert_false ((gint) 0 <= (gint) GS_APP_PROGRESS_UNKNOWN && GS_APP_PROGRESS_UNKNOWN <= 100); + + /* check pending action */ + g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UNKNOWN); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_pending_action (app, GS_PLUGIN_ACTION_UPDATE); + g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UPDATE); + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UNKNOWN); + gs_app_set_state_recover (app); +} + +static void +gs_app_progress_clamping_func (void) +{ + g_autoptr(GsApp) app = NULL; + + if (g_test_subprocess ()) { + app = gs_app_new ("gnome-software.desktop"); + gs_app_set_progress (app, 142); + g_assert_cmpuint (gs_app_get_progress (app), ==, 100); + } else { + g_test_trap_subprocess (NULL, 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("*cannot set 142% for *, setting instead: 100%*"); + } +} + +static void +gs_app_list_wildcard_dedupe_func (void) +{ + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GsApp) app1 = gs_app_new ("app"); + g_autoptr(GsApp) app2 = gs_app_new ("app"); + + gs_app_add_quirk (app1, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app1); + gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app2); + g_assert_cmpint (gs_app_list_length (list), ==, 1); +} + +static void +gs_app_list_func (void) +{ + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GsApp) app1 = gs_app_new ("app1"); + g_autoptr(GsApp) app2 = gs_app_new ("app2"); + + /* turn on */ + gs_app_list_add_flag (list, GS_APP_LIST_FLAG_WATCH_APPS); + + g_assert_cmpint (gs_app_list_get_progress (list), ==, 0); + g_assert_cmpint (gs_app_list_get_state (list), ==, GS_APP_STATE_UNKNOWN); + gs_app_list_add (list, app1); + gs_app_set_progress (app1, 75); + gs_app_set_state (app1, GS_APP_STATE_AVAILABLE); + gs_app_set_state (app1, GS_APP_STATE_INSTALLING); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 75); + g_assert_cmpint (gs_app_list_get_state (list), ==, GS_APP_STATE_INSTALLING); + + gs_app_list_add (list, app2); + gs_app_set_progress (app2, 25); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 50); + g_assert_cmpint (gs_app_list_get_state (list), ==, GS_APP_STATE_INSTALLING); + + gs_app_list_remove (list, app1); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 25); + g_assert_cmpint (gs_app_list_get_state (list), ==, GS_APP_STATE_UNKNOWN); +} + +static void +gs_app_list_performance_func (void) +{ + g_autoptr(GPtrArray) apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GTimer) timer = NULL; + + /* create a few apps */ + for (guint i = 0; i < 500; i++) { + g_autofree gchar *id = g_strdup_printf ("%03u.desktop", i); + g_ptr_array_add (apps, gs_app_new (id)); + } + + /* add them to the list */ + timer = g_timer_new (); + for (guint i = 0; i < apps->len; i++) { + GsApp *app = g_ptr_array_index (apps, i); + gs_app_list_add (list, app); + } + g_print ("%.2fms ", g_timer_elapsed (timer, NULL) * 1000); +} + +static void +gs_app_list_related_func (void) +{ + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GsApp) app = gs_app_new ("app"); + g_autoptr(GsApp) related = gs_app_new ("related"); + + /* turn on */ + gs_app_list_add_flag (list, + GS_APP_LIST_FLAG_WATCH_APPS | + GS_APP_LIST_FLAG_WATCH_APPS_RELATED); + gs_app_add_related (app, related); + gs_app_list_add (list, app); + + gs_app_set_progress (app, 75); + gs_app_set_progress (related, 25); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 50); +} + +int +main (int argc, char **argv) +{ + g_autoptr(GsDebug) debug = gs_debug_new (NULL, TRUE, FALSE); + + gs_test_init (&argc, &argv); + + /* tests go here */ + g_test_add_func ("/gnome-software/lib/utils{url}", gs_utils_url_func); + g_test_add_func ("/gnome-software/lib/utils{wilson}", gs_utils_wilson_func); + g_test_add_func ("/gnome-software/lib/utils{error}", gs_utils_error_func); + g_test_add_func ("/gnome-software/lib/utils{cache}", gs_utils_cache_func); + g_test_add_func ("/gnome-software/lib/utils{append-kv}", gs_utils_append_kv_func); + g_test_add_func ("/gnome-software/lib/os-release", gs_os_release_func); + g_test_add_func ("/gnome-software/lib/app", gs_app_func); + g_test_add_func ("/gnome-software/lib/app/progress-clamping", gs_app_progress_clamping_func); + g_test_add_func ("/gnome-software/lib/app{addons}", gs_app_addons_func); + g_test_add_func ("/gnome-software/lib/app{unique-id}", gs_app_unique_id_func); + g_test_add_data_func ("/gnome-software/lib/app{thread}", debug, gs_app_thread_func); + g_test_add_func ("/gnome-software/lib/app{list}", gs_app_list_func); + g_test_add_func ("/gnome-software/lib/app{list-wildcard-dedupe}", gs_app_list_wildcard_dedupe_func); + g_test_add_func ("/gnome-software/lib/app{list-performance}", gs_app_list_performance_func); + g_test_add_func ("/gnome-software/lib/app{list-related}", gs_app_list_related_func); + g_test_add_func ("/gnome-software/lib/plugin", gs_plugin_func); + g_test_add_func ("/gnome-software/lib/plugin{download-rewrite}", gs_plugin_download_rewrite_func); + + return g_test_run (); +} diff --git a/lib/gs-test.c b/lib/gs-test.c new file mode 100644 index 0000000..2a2e107 --- /dev/null +++ b/lib/gs-test.c @@ -0,0 +1,147 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <stdlib.h> + +#include "gs-plugin-loader-sync.h" +#include "gs-test.h" + +/** + * gs_test_init: + * + * Initializes the environment with the common settings for the test, + * as a replacement for the g_test_init(), which is called as well. + * + * Since: 42 + **/ +void +gs_test_init (gint *pargc, + gchar ***pargv) +{ + g_autoptr(GSettings) settings = NULL; + + g_setenv ("GSETTINGS_BACKEND", "memory", FALSE); + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + /* To not download ODRS data during the test */ + settings = g_settings_new ("org.gnome.software"); + g_settings_set_string (settings, "review-server", ""); + + g_test_init (pargc, pargv, + G_TEST_OPTION_ISOLATE_DIRS, + NULL); + + /* only critical and error are fatal */ + g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); +} + +gchar * +gs_test_get_filename (const gchar *testdatadir, const gchar *filename) +{ + gchar *tmp; + char full_tmp[PATH_MAX]; + g_autofree gchar *path = NULL; + path = g_build_filename (testdatadir, filename, NULL); + g_debug ("looking in %s", path); + tmp = realpath (path, full_tmp); + if (tmp == NULL) + return NULL; + return g_strdup (full_tmp); +} + +void +gs_test_flush_main_context (void) +{ + guint cnt = 0; + while (g_main_context_iteration (NULL, FALSE)) { + if (cnt == 0) + g_debug ("clearing pending events..."); + cnt++; + } + if (cnt > 0) + g_debug ("cleared %u events", cnt); +} + +/** + * gs_test_expose_icon_theme_paths: + * + * Calculate and set the `GS_SELF_TEST_ICON_THEME_PATH` environment variable + * to include the current system icon theme paths. This is designed to be called + * before calling `gs_test_init()`, which will clear the system icon theme paths. + * + * As this function calls `g_setenv()`, it must not be called after threads have + * been spawned. + * + * Calling this function is an explicit acknowledgement that the code under test + * should be accessing the icon theme. + * + * Since: 3.38 + */ +void +gs_test_expose_icon_theme_paths (void) +{ + const gchar * const *data_dirs; + g_autoptr(GString) data_dirs_str = NULL; + g_autofree gchar *data_dirs_joined = NULL; + + data_dirs = g_get_system_data_dirs (); + data_dirs_str = g_string_new (""); + for (gsize i = 0; data_dirs[i] != NULL; i++) + g_string_append_printf (data_dirs_str, "%s%s/icons", + (data_dirs_str->len > 0) ? ":" : "", + data_dirs[i]); + data_dirs_joined = g_string_free (g_steal_pointer (&data_dirs_str), FALSE); + g_setenv ("GS_SELF_TEST_ICON_THEME_PATH", data_dirs_joined, TRUE); +} + +/** + * gs_test_reinitialise_plugin_loader: + * @plugin_loader: a #GsPluginLoader + * + * Calls setup on each plugin. This should only be used from the self tests + * and in a controlled way. + * + * Since: 42 + */ +void +gs_test_reinitialise_plugin_loader (GsPluginLoader *plugin_loader, + const gchar * const *allowlist, + const gchar * const *blocklist) +{ + g_autoptr(GError) local_error = NULL; +#ifdef HAVE_SYSPROF + gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME; +#endif + + /* Shut down */ + gs_plugin_loader_shutdown (plugin_loader, NULL); + + /* clear global cache */ + gs_plugin_loader_clear_caches (plugin_loader); + + /* remove any events */ + gs_plugin_loader_remove_events (plugin_loader); + + /* Start all the plugins setting up again in parallel. Use the blocking + * sync version of the function, just for the tests. */ + gs_plugin_loader_setup (plugin_loader, allowlist, blocklist, NULL, &local_error); + g_assert_no_error (local_error); + +#ifdef HAVE_SYSPROF + if (plugin_loader->sysprof_writer != NULL) { + sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer, + begin_time_nsec, + sched_getcpu (), + getpid (), + SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec, + "gnome-software", + "setup-again", + NULL); + } +#endif /* HAVE_SYSPROF */ +} diff --git a/lib/gs-test.h b/lib/gs-test.h new file mode 100644 index 0000000..8114c23 --- /dev/null +++ b/lib/gs-test.h @@ -0,0 +1,27 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app.h" +#include "gs-plugin-loader.h" + +G_BEGIN_DECLS + +void gs_test_init (gint *pargc, + gchar ***pargv); +void gs_test_flush_main_context (void); +gchar *gs_test_get_filename (const gchar *testdatadir, + const gchar *filename); +void gs_test_expose_icon_theme_paths (void); + +void gs_test_reinitialise_plugin_loader (GsPluginLoader *plugin_loader, + const gchar * const *allowlist, + const gchar * const *blocklist); + +G_END_DECLS diff --git a/lib/gs-utils.c b/lib/gs-utils.c new file mode 100644 index 0000000..2c3fb5b --- /dev/null +++ b/lib/gs-utils.c @@ -0,0 +1,1689 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-utils + * @title: GsUtils + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Utilities that plugins can use + * + * These functions provide useful functionality that makes it easy to + * add new plugin functions. + */ + +#include "config.h" + +#include <errno.h> +#include <fnmatch.h> +#include <math.h> +#include <string.h> +#include <glib/gstdio.h> +#include <json-glib/json-glib.h> + +#if defined(__linux__) +#include <sys/sysinfo.h> +#elif defined(__FreeBSD__) +#include <sys/types.h> +#include <sys/sysctl.h> +#endif + +#ifdef HAVE_POLKIT +#include <polkit/polkit.h> +#endif + +#include "gs-app.h" +#include "gs-app-private.h" +#include "gs-utils.h" +#include "gs-plugin.h" + +#define MB_IN_BYTES (1024 * 1024) + +/** + * gs_mkdir_parent: + * @path: A full pathname + * @error: A #GError, or %NULL + * + * Creates any required directories, including any parent directories. + * + * Returns: %TRUE for success + **/ +gboolean +gs_mkdir_parent (const gchar *path, GError **error) +{ + g_autofree gchar *parent = NULL; + + parent = g_path_get_dirname (path); + if (g_mkdir_with_parents (parent, 0755) == -1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Failed to create '%s': %s", + parent, g_strerror (errno)); + return FALSE; + } + return TRUE; +} + +/** + * gs_utils_get_file_age: + * @file: A #GFile + * + * Gets a file age. + * + * Returns: The time in seconds since the file was modified, or %G_MAXUINT64 for error + */ +guint64 +gs_utils_get_file_age (GFile *file) +{ + guint64 now; + guint64 mtime; + g_autoptr(GFileInfo) info = NULL; + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (info == NULL) + return G_MAXUINT64; + mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + now = (guint64) g_get_real_time () / G_USEC_PER_SEC; + if (mtime > now) + return G_MAXUINT64; + if (now - mtime > G_MAXUINT64) + return G_MAXUINT64; + return (guint) (now - mtime); +} + +static gchar * +gs_utils_filename_array_return_newest (GPtrArray *array) +{ + const gchar *filename_best = NULL; + guint age_lowest = G_MAXUINT; + guint i; + for (i = 0; i < array->len; i++) { + const gchar *fn = g_ptr_array_index (array, i); + g_autoptr(GFile) file = g_file_new_for_path (fn); + guint64 age_tmp = gs_utils_get_file_age (file); + if (age_tmp < age_lowest) { + age_lowest = age_tmp; + filename_best = fn; + } + } + return g_strdup (filename_best); +} + +/** + * gs_utils_get_cache_filename: + * @kind: A cache kind, e.g. "fwupd" or "screenshots/123x456" + * @resource: A resource, e.g. "system.bin" or "http://foo.bar/baz.bin" + * @flags: Some #GsUtilsCacheFlags, e.g. %GS_UTILS_CACHE_FLAG_WRITEABLE + * @error: A #GError, or %NULL + * + * Returns a filename that points into the cache. + * This may be per-system or per-user, the latter being more likely + * when %GS_UTILS_CACHE_FLAG_WRITEABLE is specified in @flags. + * + * If %GS_UTILS_CACHE_FLAG_USE_HASH is set in @flags then the returned filename + * will contain the hashed version of @resource. + * + * If there is more than one match, the file that has been modified last is + * returned. + * + * If a plugin requests a file to be saved in the cache it is the plugins + * responsibility to remove the file when it is no longer valid or is too old + * -- gnome-software will not ever clean the cache for the plugin. + * For this reason it is a good idea to use the plugin name as @kind. + * + * This function can only fail if %GS_UTILS_CACHE_FLAG_ENSURE_EMPTY or + * %GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY are passed in @flags. + * + * Returns: The full path and filename, which may or may not exist, or %NULL + **/ +gchar * +gs_utils_get_cache_filename (const gchar *kind, + const gchar *resource, + GsUtilsCacheFlags flags, + GError **error) +{ + const gchar *tmp; + g_autofree gchar *basename = NULL; + g_autofree gchar *cachedir = NULL; + g_autoptr(GFile) cachedir_file = NULL; + g_autoptr(GPtrArray) candidates = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GError) local_error = NULL; + + /* in the self tests */ + tmp = g_getenv ("GS_SELF_TEST_CACHEDIR"); + if (tmp != NULL) { + cachedir = g_build_filename (tmp, kind, NULL); + cachedir_file = g_file_new_for_path (cachedir); + + if ((flags & GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY) && + !g_file_make_directory_with_parents (cachedir_file, NULL, &local_error) && + !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + + return g_build_filename (cachedir, resource, NULL);; + } + + /* get basename */ + if (flags & GS_UTILS_CACHE_FLAG_USE_HASH) { + g_autofree gchar *basename_tmp = g_path_get_basename (resource); + g_autofree gchar *hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, + resource, -1); + basename = g_strdup_printf ("%s-%s", hash, basename_tmp); + } else { + basename = g_path_get_basename (resource); + } + + /* not writable, so try the system cache first */ + if (!(flags & GS_UTILS_CACHE_FLAG_WRITEABLE)) { + g_autofree gchar *cachefn = NULL; + cachefn = g_build_filename (LOCALSTATEDIR, + "cache", + "gnome-software", + kind, + basename, + NULL); + if (g_file_test (cachefn, G_FILE_TEST_EXISTS)) { + g_ptr_array_add (candidates, + g_steal_pointer (&cachefn)); + } + } + + /* create the cachedir in a per-release location, creating + * if it does not already exist */ + cachedir = g_build_filename (g_get_user_cache_dir (), + "gnome-software", + kind, + NULL); + cachedir_file = g_file_new_for_path (cachedir); + if (g_file_query_exists (cachedir_file, NULL) && + flags & GS_UTILS_CACHE_FLAG_ENSURE_EMPTY) { + if (!gs_utils_rmtree (cachedir, error)) + return NULL; + } + if ((flags & GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY) && + !g_file_query_exists (cachedir_file, NULL) && + !g_file_make_directory_with_parents (cachedir_file, NULL, error)) + return NULL; + g_ptr_array_add (candidates, g_build_filename (cachedir, basename, NULL)); + + /* common case: we only have one option */ + if (candidates->len == 1) + return g_strdup (g_ptr_array_index (candidates, 0)); + + /* return the newest (i.e. one with least age) */ + return gs_utils_filename_array_return_newest (candidates); +} + +/** + * gs_utils_get_user_hash: + * @error: A #GError, or %NULL + * + * This SHA1 hash is composed of the contents of machine-id and your + * username and is also salted with a hardcoded value. + * + * This provides an identifier that can be used to identify a specific + * user on a machine, allowing them to cast only one vote or perform + * one review on each application. + * + * There is no known way to calculate the machine ID or username from + * the machine hash and there should be no privacy issue. + * + * Returns: The user hash, or %NULL on error + */ +gchar * +gs_utils_get_user_hash (GError **error) +{ + g_autofree gchar *data = NULL; + g_autofree gchar *salted = NULL; + + if (!g_file_get_contents ("/etc/machine-id", &data, NULL, error)) + return NULL; + + salted = g_strdup_printf ("gnome-software[%s:%s]", + g_get_user_name (), data); + return g_compute_checksum_for_string (G_CHECKSUM_SHA1, salted, -1); +} + +/** + * gs_utils_get_permission: + * @id: A PolicyKit ID, e.g. "org.gnome.Desktop" + * @cancellable: A #GCancellable, or %NULL + * @error: A #GError, or %NULL + * + * Gets a permission object for an ID. + * + * Returns: a #GPermission, or %NULL if this if not possible. + **/ +GPermission * +gs_utils_get_permission (const gchar *id, GCancellable *cancellable, GError **error) +{ +#ifdef HAVE_POLKIT + g_autoptr(GPermission) permission = NULL; + permission = polkit_permission_new_sync (id, NULL, cancellable, error); + if (permission == NULL) { + g_prefix_error (error, "failed to create permission %s: ", id); + gs_utils_error_convert_gio (error); + return NULL; + } + return g_steal_pointer (&permission); +#else + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no PolicyKit, so can't return GPermission for %s", id); + return NULL; +#endif +} + +/** + * gs_utils_get_permission_async: + * @id: a polkit action ID, for example `org.freedesktop.packagekit.trigger-offline-update` + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: callback for when the asynchronous operation is complete + * @user_data: data to pass to @callback + * + * Asynchronously gets a #GPermission object representing the given polkit + * action @id. + * + * Since: 42 + */ +void +gs_utils_get_permission_async (const gchar *id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (id != NULL); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + +#ifdef HAVE_POLKIT + polkit_permission_new (id, NULL, cancellable, callback, user_data); +#else + g_task_report_new_error (NULL, callback, user_data, gs_utils_get_permission_async, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no PolicyKit, so can't return GPermission for %s", id); +#endif +} + +/** + * gs_utils_get_permission_finish: + * @result: result of the asynchronous operation + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous operation started with gs_utils_get_permission_async(). + * + * Returns: (transfer full): a #GPermission representing the given action ID + * Since: 42 + */ +GPermission * +gs_utils_get_permission_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + +#ifdef HAVE_POLKIT + return polkit_permission_new_finish (result, error); +#else + return g_task_propagate_pointer (G_TASK (result), error); +#endif +} + +/** + * gs_utils_get_content_type: + * @file: A GFile + * @cancellable: A #GCancellable, or %NULL + * @error: A #GError, or %NULL + * + * Gets the standard content type for a file. + * + * Returns: the content type, or %NULL, e.g. "text/plain" + */ +gchar * +gs_utils_get_content_type (GFile *file, + GCancellable *cancellable, + GError **error) +{ + const gchar *tmp; + g_autoptr(GFileInfo) info = NULL; + + /* get content type */ + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, + cancellable, + error); + if (info == NULL) + return NULL; + tmp = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); + if (tmp == NULL) + return NULL; + return g_strdup (tmp); +} + +/** + * gs_utils_strv_fnmatch: + * @strv: A NUL-terminated list of strings + * @str: A string + * + * Matches a string against a list of globs. + * + * Returns: %TRUE if the list matches + */ +gboolean +gs_utils_strv_fnmatch (gchar **strv, const gchar *str) +{ + guint i; + + /* empty */ + if (strv == NULL) + return FALSE; + + /* look at each one */ + for (i = 0; strv[i] != NULL; i++) { + if (fnmatch (strv[i], str, 0) == 0) + return TRUE; + } + return FALSE; +} + +/** + * gs_utils_sort_key: + * @str: A string to convert to a sort key + * + * Useful to sort strings in a locale-sensitive, presentational way. + * Case is ignored and utf8 collation is used (e.g. accents are ignored). + * + * Returns: a newly allocated string sort key + */ +gchar * +gs_utils_sort_key (const gchar *str) +{ + g_autofree gchar *casefolded = g_utf8_casefold (str, -1); + return g_utf8_collate_key (casefolded, -1); +} + +/** + * gs_utils_sort_strcmp: + * @str1: (nullable): A string to compare + * @str2: (nullable): A string to compare + * + * Compares two strings in a locale-sensitive, presentational way. + * Case is ignored and utf8 collation is used (e.g. accents are ignored). %NULL + * is sorted before all non-%NULL strings, and %NULLs compare equal. + * + * Returns: < 0 if str1 is before str2, 0 if equal, > 0 if str1 is after str2 + */ +gint +gs_utils_sort_strcmp (const gchar *str1, const gchar *str2) +{ + g_autofree gchar *key1 = (str1 != NULL) ? gs_utils_sort_key (str1) : NULL; + g_autofree gchar *key2 = (str2 != NULL) ? gs_utils_sort_key (str2) : NULL; + return g_strcmp0 (key1, key2); +} + +/** + * gs_utils_get_desktop_app_info: + * @id: A desktop ID, e.g. "gimp.desktop" + * + * Gets a a #GDesktopAppInfo taking into account the kde4- prefix. + * If the given @id doesn not have a ".desktop" suffix, it will add one to it + * for convenience. + * + * Returns: a #GDesktopAppInfo for a specific ID, or %NULL + */ +GDesktopAppInfo * +gs_utils_get_desktop_app_info (const gchar *id) +{ + GDesktopAppInfo *app_info; + g_autofree gchar *desktop_id = NULL; + + /* for convenience, if the given id doesn't have the required .desktop + * suffix, we add it here */ + if (!g_str_has_suffix (id, ".desktop")) { + desktop_id = g_strconcat (id, ".desktop", NULL); + id = desktop_id; + } + + /* try to get the standard app-id */ + app_info = g_desktop_app_info_new (id); + + /* KDE is a special project because it believes /usr/share/applications + * isn't KDE enough. For this reason we support falling back to the + * "kde4-" prefixed ID to avoid educating various self-righteous + * upstreams about the correct ID to use in the AppData file. */ + if (app_info == NULL) { + g_autofree gchar *kde_id = NULL; + kde_id = g_strdup_printf ("%s-%s", "kde4", id); + app_info = g_desktop_app_info_new (kde_id); + } + + return app_info; +} + +/** + * gs_utils_symlink: + * @target: the full path of the symlink to create + * @linkpath: where the symlink should point to + * @error: A #GError, or %NULL + * + * Creates a symlink that can cross filesystem boundaries. + * Any parent directories needed for target to exist are also created. + * + * Returns: %TRUE for success + **/ +gboolean +gs_utils_symlink (const gchar *target, const gchar *linkpath, GError **error) +{ + if (!gs_mkdir_parent (target, error)) + return FALSE; + if (symlink (target, linkpath) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_WRITE_FAILED, + "failed to create symlink from %s to %s", + linkpath, target); + return FALSE; + } + return TRUE; +} + +/** + * gs_utils_unlink: + * @filename: A full pathname to delete + * @error: A #GError, or %NULL + * + * Deletes a file from disk. + * + * Returns: %TRUE for success + **/ +gboolean +gs_utils_unlink (const gchar *filename, GError **error) +{ + if (g_unlink (filename) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DELETE_FAILED, + "failed to delete %s", + filename); + return FALSE; + } + return TRUE; +} + +static gboolean +gs_utils_rmtree_real (const gchar *directory, GError **error) +{ + const gchar *filename; + g_autoptr(GDir) dir = NULL; + + /* try to open */ + dir = g_dir_open (directory, 0, error); + if (dir == NULL) + return FALSE; + + /* find each */ + while ((filename = g_dir_read_name (dir))) { + g_autofree gchar *src = NULL; + src = g_build_filename (directory, filename, NULL); + if (g_file_test (src, G_FILE_TEST_IS_DIR) && + !g_file_test (src, G_FILE_TEST_IS_SYMLINK)) { + if (!gs_utils_rmtree_real (src, error)) + return FALSE; + } else { + if (g_unlink (src) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DELETE_FAILED, + "Failed to delete: %s", src); + return FALSE; + } + } + } + + if (g_rmdir (directory) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DELETE_FAILED, + "Failed to remove: %s", directory); + return FALSE; + } + return TRUE; +} + +/** + * gs_utils_rmtree: + * @directory: A full directory pathname to delete + * @error: A #GError, or %NULL + * + * Deletes a directory from disk and all its contents. + * + * Returns: %TRUE for success + **/ +gboolean +gs_utils_rmtree (const gchar *directory, GError **error) +{ + g_debug ("recursively removing directory '%s'", directory); + return gs_utils_rmtree_real (directory, error); +} + +static gdouble +pnormaldist (gdouble qn) +{ + static gdouble b[11] = { 1.570796288, 0.03706987906, -0.8364353589e-3, + -0.2250947176e-3, 0.6841218299e-5, 0.5824238515e-5, + -0.104527497e-5, 0.8360937017e-7, -0.3231081277e-8, + 0.3657763036e-10, 0.6936233982e-12 }; + gdouble w1, w3; + guint i; + + if (qn < 0 || qn > 1) + return 0; // This is an error case + if (qn == 0.5) + return 0; + + w1 = qn; + if (qn > 0.5) + w1 = 1.0 - w1; + w3 = -log (4.0 * w1 * (1.0 - w1)); + w1 = b[0]; + for (i = 1; i < 11; i++) + w1 = w1 + (b[i] * pow (w3, i)); + + if (qn > 0.5) + return sqrt (w1 * w3); + else + return -sqrt (w1 * w3); +} + +static gdouble +wilson_score (gdouble value, gdouble n, gdouble power) +{ + gdouble z, phat; + if (value == 0) + return 0; + z = pnormaldist (1 - power / 2); + phat = value / n; + return (phat + z * z / (2 * n) - + z * sqrt ((phat * (1 - phat) + z * z / (4 * n)) / n)) / + (1 + z * z / n); +} + +/** + * gs_utils_get_wilson_rating: + * @star1: The number of 1 star reviews + * @star2: The number of 2 star reviews + * @star3: The number of 3 star reviews + * @star4: The number of 4 star reviews + * @star5: The number of 5 star reviews + * + * Returns the lower bound of Wilson score confidence interval for a + * Bernoulli parameter. This ensures small numbers of ratings don't give overly + * high scores. + * See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval + * for details. + * + * Returns: Wilson rating percentage, or -1 for error + **/ +gint +gs_utils_get_wilson_rating (guint64 star1, + guint64 star2, + guint64 star3, + guint64 star4, + guint64 star5) +{ + gdouble val; + guint64 star_sum = star1 + star2 + star3 + star4 + star5; + if (star_sum == 0) + return -1; + + /* get score */ + val = (wilson_score ((gdouble) star1, (gdouble) star_sum, 0.2) * -2); + val += (wilson_score ((gdouble) star2, (gdouble) star_sum, 0.2) * -1); + val += (wilson_score ((gdouble) star4, (gdouble) star_sum, 0.2) * 1); + val += (wilson_score ((gdouble) star5, (gdouble) star_sum, 0.2) * 2); + + /* normalize from -2..+2 to 0..5 */ + val += 3; + + /* multiply to a percentage */ + val *= 20; + + /* return rounded up integer */ + return (gint) ceil (val); +} + +/** + * gs_utils_error_add_app_id: + * @error: a #GError + * @app: a #GsApp + * + * Adds app unique ID prefix to the error. + * + * Since: 3.30 + **/ +void +gs_utils_error_add_app_id (GError **error, GsApp *app) +{ + g_return_if_fail (GS_APP (app)); + if (error == NULL || *error == NULL) + return; + g_prefix_error (error, "{%s} ", gs_app_get_unique_id (app)); +} + +/** + * gs_utils_error_add_origin_id: + * @error: a #GError + * @origin: a #GsApp + * + * Adds origin unique ID prefix to the error. + * + * Since: 3.30 + **/ +void +gs_utils_error_add_origin_id (GError **error, GsApp *origin) +{ + g_return_if_fail (GS_APP (origin)); + if (error == NULL || *error == NULL) + return; + g_prefix_error (error, "[%s] ", gs_app_get_unique_id (origin)); +} + +/** + * gs_utils_error_strip_app_id: + * @error: a #GError + * + * Removes a possible app ID prefix from the error, and returns the removed + * app ID. + * + * Returns: A newly allocated string with the app ID + * + * Since: 3.30 + **/ +gchar * +gs_utils_error_strip_app_id (GError *error) +{ + g_autofree gchar *app_id = NULL; + g_autofree gchar *msg = NULL; + + if (error == NULL || error->message == NULL) + return FALSE; + + if (g_str_has_prefix (error->message, "{")) { + const gchar *endp = strstr (error->message + 1, "} "); + if (endp != NULL) { + app_id = g_strndup (error->message + 1, + endp - (error->message + 1)); + msg = g_strdup (endp + 2); + } + } + + if (msg != NULL) { + g_free (error->message); + error->message = g_steal_pointer (&msg); + } + + return g_steal_pointer (&app_id); +} + +/** + * gs_utils_error_strip_origin_id: + * @error: a #GError + * + * Removes a possible origin ID prefix from the error, and returns the removed + * origin ID. + * + * Returns: A newly allocated string with the origin ID + * + * Since: 3.30 + **/ +gchar * +gs_utils_error_strip_origin_id (GError *error) +{ + g_autofree gchar *origin_id = NULL; + g_autofree gchar *msg = NULL; + + if (error == NULL || error->message == NULL) + return FALSE; + + if (g_str_has_prefix (error->message, "[")) { + const gchar *endp = strstr (error->message + 1, "] "); + if (endp != NULL) { + origin_id = g_strndup (error->message + 1, + endp - (error->message + 1)); + msg = g_strdup (endp + 2); + } + } + + if (msg != NULL) { + g_free (error->message); + error->message = g_steal_pointer (&msg); + } + + return g_steal_pointer (&origin_id); +} + +/** + * gs_utils_error_convert_gdbus: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GDBusError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gdbus (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != G_DBUS_ERROR) + return FALSE; + switch (error->code) { + case G_DBUS_ERROR_FAILED: + case G_DBUS_ERROR_NO_REPLY: + case G_DBUS_ERROR_TIMEOUT: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case G_DBUS_ERROR_IO_ERROR: + case G_DBUS_ERROR_NAME_HAS_NO_OWNER: + case G_DBUS_ERROR_NOT_SUPPORTED: + case G_DBUS_ERROR_SERVICE_UNKNOWN: + case G_DBUS_ERROR_UNKNOWN_INTERFACE: + case G_DBUS_ERROR_UNKNOWN_METHOD: + case G_DBUS_ERROR_UNKNOWN_OBJECT: + case G_DBUS_ERROR_UNKNOWN_PROPERTY: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case G_DBUS_ERROR_NO_MEMORY: + error->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + case G_DBUS_ERROR_ACCESS_DENIED: + case G_DBUS_ERROR_AUTH_FAILED: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case G_DBUS_ERROR_NO_NETWORK: + error->code = GS_PLUGIN_ERROR_NO_NETWORK; + break; + case G_DBUS_ERROR_INVALID_FILE_CONTENT: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_gio: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GIOError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gio (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != G_IO_ERROR) + return FALSE; + switch (error->code) { + case G_IO_ERROR_FAILED: + case G_IO_ERROR_NOT_FOUND: + case G_IO_ERROR_EXISTS: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case G_IO_ERROR_TIMED_OUT: + error->code = GS_PLUGIN_ERROR_TIMED_OUT; + break; + case G_IO_ERROR_NOT_SUPPORTED: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case G_IO_ERROR_CANCELLED: + error->code = GS_PLUGIN_ERROR_CANCELLED; + break; + case G_IO_ERROR_NO_SPACE: + error->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + case G_IO_ERROR_PERMISSION_DENIED: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case G_IO_ERROR_HOST_NOT_FOUND: + case G_IO_ERROR_HOST_UNREACHABLE: + case G_IO_ERROR_CONNECTION_REFUSED: + case G_IO_ERROR_PROXY_FAILED: + case G_IO_ERROR_PROXY_AUTH_FAILED: + case G_IO_ERROR_PROXY_NOT_ALLOWED: + error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + break; + case G_IO_ERROR_NETWORK_UNREACHABLE: + error->code = GS_PLUGIN_ERROR_NO_NETWORK; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_gresolver: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GResolverError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gresolver (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != G_RESOLVER_ERROR) + return FALSE; + switch (error->code) { + case G_RESOLVER_ERROR_INTERNAL: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case G_RESOLVER_ERROR_NOT_FOUND: + case G_RESOLVER_ERROR_TEMPORARY_FAILURE: + error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_gdk_pixbuf: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GdkPixbufError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gdk_pixbuf (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != GDK_PIXBUF_ERROR) + return FALSE; + switch (error->code) { + case GDK_PIXBUF_ERROR_UNSUPPORTED_OPERATION: + case GDK_PIXBUF_ERROR_UNKNOWN_TYPE: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case GDK_PIXBUF_ERROR_FAILED: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case GDK_PIXBUF_ERROR_CORRUPT_IMAGE: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_appstream: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the various AppStream error types to an error with a GsPluginError + * domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_appstream (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + + /* custom to this plugin */ + if (error->domain == AS_METADATA_ERROR) { + switch (error->code) { + case AS_METADATA_ERROR_PARSE: + case AS_METADATA_ERROR_FORMAT_UNEXPECTED: + case AS_METADATA_ERROR_NO_COMPONENT: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + case AS_METADATA_ERROR_FAILED: + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else if (error->domain == AS_POOL_ERROR) { + switch (error->code) { + case AS_POOL_ERROR_FAILED: + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else if (error->domain == G_FILE_ERROR) { + switch (error->code) { + case G_FILE_ERROR_EXIST: + case G_FILE_ERROR_ACCES: + case G_FILE_ERROR_PERM: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case G_FILE_ERROR_NOSPC: + error->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else { + g_warning ("can't reliably fixup error from domain %s", + g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_get_url_scheme: + * @url: A URL, e.g. "appstream://gimp.desktop" + * + * Gets the scheme from the URL string. + * + * Returns: the URL scheme, e.g. "appstream" + */ +gchar * +gs_utils_get_url_scheme (const gchar *url) +{ + g_autoptr(GUri) uri = NULL; + + /* no data */ + if (url == NULL) + return NULL; + + /* create URI from URL */ + uri = g_uri_parse (url, SOUP_HTTP_URI_FLAGS, NULL); + if (!uri) + return NULL; + + /* success */ + return g_strdup (g_uri_get_scheme (uri)); +} + +/** + * gs_utils_get_url_path: + * @url: A URL, e.g. "appstream://gimp.desktop" + * + * Gets the path from the URL string, removing any leading slashes. + * + * Returns: the URL path, e.g. "gimp.desktop" + */ +gchar * +gs_utils_get_url_path (const gchar *url) +{ + g_autoptr(GUri) uri = NULL; + const gchar *host; + const gchar *path; + + uri = g_uri_parse (url, SOUP_HTTP_URI_FLAGS, NULL); + if (!uri) + return NULL; + + /* foo://bar -> scheme: foo, host: bar, path: / */ + /* foo:bar -> scheme: foo, host: (empty string), path: /bar */ + host = g_uri_get_host (uri); + path = g_uri_get_path (uri); + if (host != NULL && *host != '\0') + path = host; + + /* trim any leading slashes */ + while (*path == '/') + path++; + + /* success */ + return g_strdup (path); +} + +/** + * gs_user_agent: + * + * Gets the user agent to use for remote requests. + * + * Returns: the user-agent, e.g. "gnome-software/3.22.1" + */ +const gchar * +gs_user_agent (void) +{ + return PACKAGE_NAME "/" PACKAGE_VERSION; +} + +/** + * gs_utils_append_key_value: + * @str: A #GString + * @align_len: The alignment of the @value compared to the @key + * @key: The text to use as a title + * @value: The text to use as a value + * + * Adds a line to an existing string, padding the key to a set number of spaces. + * + * Since: 3.26 + */ +void +gs_utils_append_key_value (GString *str, gsize align_len, + const gchar *key, const gchar *value) +{ + gsize len = 0; + + g_return_if_fail (str != NULL); + g_return_if_fail (value != NULL); + + if (key != NULL) { + len = strlen (key) + 2; + g_string_append (str, key); + g_string_append (str, ": "); + } + for (gsize i = len; i < align_len + 1; i++) + g_string_append (str, " "); + g_string_append (str, value); + g_string_append (str, "\n"); +} + +guint +gs_utils_get_memory_total (void) +{ +#if defined(__linux__) + struct sysinfo si = { 0 }; + sysinfo (&si); + if (si.mem_unit > 0) + return si.totalram / MB_IN_BYTES / si.mem_unit; + return 0; +#elif defined(__FreeBSD__) + unsigned long physmem; + sysctl ((int[]){ CTL_HW, HW_PHYSMEM }, 2, &physmem, &(size_t){ sizeof (physmem) }, NULL, 0); + return physmem / MB_IN_BYTES; +#else +#error "Please implement gs_utils_get_memory_total for your system." +#endif +} + +/** + * gs_utils_set_online_updates_timestamp: + * + * Sets the value of online-updates-timestamp to current epoch. "online-updates-timestamp" represents + * the last time the system was online and got any updates. + **/ +void +gs_utils_set_online_updates_timestamp (GSettings *settings) +{ + g_autoptr(GDateTime) now = NULL; + + g_return_if_fail (settings != NULL); + + now = g_date_time_new_now_local (); + g_settings_set (settings, "online-updates-timestamp", "x", g_date_time_to_unix (now)); +} + +/** + * gs_utils_unique_id_compat_convert: + * @data_id: (nullable): A string that may be a unique component ID + * + * Converts the unique ID string from its legacy 6-part form into + * a new-style 5-part AppStream data-id. + * Does nothing if the string is already valid. + * + * See !583 for the history of this conversion. + * + * Returns: (nullable): A newly allocated string with the new-style data-id, or %NULL if input was no valid ID. + * + * Since: 40 + **/ +gchar* +gs_utils_unique_id_compat_convert (const gchar *data_id) +{ + g_auto(GStrv) parts = NULL; + if (data_id == NULL) + return NULL; + + /* check for the most common case first: data-id is already valid */ + if (as_utils_data_id_valid (data_id)) + return g_strdup (data_id); + + parts = g_strsplit (data_id, "/", -1); + if (g_strv_length (parts) != 6) + return NULL; + return g_strdup_printf ("%s/%s/%s/%s/%s", + parts[0], + parts[1], + parts[2], + parts[4], + parts[5]); +} + +static const gchar * +_fix_data_id_part (const gchar *value) +{ + if (!value || !*value) + return "*"; + + return value; +} + +/** + * gs_utils_build_unique_id: + * @scope: Scope of the metadata as #AsComponentScope e.g. %AS_COMPONENT_SCOPE_SYSTEM + * @bundle_kind: Bundling system providing this data, e.g. 'package' or 'flatpak' + * @origin: Origin string, e.g. 'os' or 'gnome-apps-nightly' + * @cid: AppStream component ID, e.g. 'org.freedesktop.appstream.cli' + * @branch: Branch, e.g. '3-20' or 'master' + * + * Builds an identifier string unique to the individual dataset using the supplied information. + * It's similar to as_utils_build_data_id(), except it respects the @origin for the packages. + * + * Returns: (transfer full): a unique ID, free with g_free(), when no longer needed. + * + * Since: 41 + */ +gchar * +gs_utils_build_unique_id (AsComponentScope scope, + AsBundleKind bundle_kind, + const gchar *origin, + const gchar *cid, + const gchar *branch) +{ + const gchar *scope_str = NULL; + const gchar *bundle_str = NULL; + + if (scope != AS_COMPONENT_SCOPE_UNKNOWN) + scope_str = as_component_scope_to_string (scope); + if (bundle_kind != AS_BUNDLE_KIND_UNKNOWN) + bundle_str = as_bundle_kind_to_string (bundle_kind); + + return g_strdup_printf ("%s/%s/%s/%s/%s", + _fix_data_id_part (scope_str), + _fix_data_id_part (bundle_str), + _fix_data_id_part (origin), + _fix_data_id_part (cid), + _fix_data_id_part (branch)); +} + +static void +gs_pixbuf_blur_private (GdkPixbuf *src, GdkPixbuf *dest, guint radius, guint8 *div_kernel_size) +{ + gint width, height, src_rowstride, dest_rowstride, n_channels; + guchar *p_src, *p_dest, *c1, *c2; + gint x, y, i, i1, i2, width_minus_1, height_minus_1, radius_plus_1; + gint r, g, b; + guchar *p_dest_row, *p_dest_col; + + width = gdk_pixbuf_get_width (src); + height = gdk_pixbuf_get_height (src); + n_channels = gdk_pixbuf_get_n_channels (src); + radius_plus_1 = radius + 1; + + /* horizontal blur */ + p_src = gdk_pixbuf_get_pixels (src); + p_dest = gdk_pixbuf_get_pixels (dest); + src_rowstride = gdk_pixbuf_get_rowstride (src); + dest_rowstride = gdk_pixbuf_get_rowstride (dest); + width_minus_1 = width - 1; + for (y = 0; y < height; y++) { + + /* calc the initial sums of the kernel */ + r = g = b = 0; + for (i = -radius; i <= (gint) radius; i++) { + c1 = p_src + (CLAMP (i, 0, width_minus_1) * n_channels); + r += c1[0]; + g += c1[1]; + b += c1[2]; + } + + p_dest_row = p_dest; + for (x = 0; x < width; x++) { + /* set as the mean of the kernel */ + p_dest_row[0] = div_kernel_size[r]; + p_dest_row[1] = div_kernel_size[g]; + p_dest_row[2] = div_kernel_size[b]; + p_dest_row += n_channels; + + /* the pixel to add to the kernel */ + i1 = x + radius_plus_1; + if (i1 > width_minus_1) + i1 = width_minus_1; + c1 = p_src + (i1 * n_channels); + + /* the pixel to remove from the kernel */ + i2 = x - radius; + if (i2 < 0) + i2 = 0; + c2 = p_src + (i2 * n_channels); + + /* calc the new sums of the kernel */ + r += c1[0] - c2[0]; + g += c1[1] - c2[1]; + b += c1[2] - c2[2]; + } + + p_src += src_rowstride; + p_dest += dest_rowstride; + } + + /* vertical blur */ + p_src = gdk_pixbuf_get_pixels (dest); + p_dest = gdk_pixbuf_get_pixels (src); + src_rowstride = gdk_pixbuf_get_rowstride (dest); + dest_rowstride = gdk_pixbuf_get_rowstride (src); + height_minus_1 = height - 1; + for (x = 0; x < width; x++) { + + /* calc the initial sums of the kernel */ + r = g = b = 0; + for (i = -radius; i <= (gint) radius; i++) { + c1 = p_src + (CLAMP (i, 0, height_minus_1) * src_rowstride); + r += c1[0]; + g += c1[1]; + b += c1[2]; + } + + p_dest_col = p_dest; + for (y = 0; y < height; y++) { + /* set as the mean of the kernel */ + + p_dest_col[0] = div_kernel_size[r]; + p_dest_col[1] = div_kernel_size[g]; + p_dest_col[2] = div_kernel_size[b]; + p_dest_col += dest_rowstride; + + /* the pixel to add to the kernel */ + i1 = y + radius_plus_1; + if (i1 > height_minus_1) + i1 = height_minus_1; + c1 = p_src + (i1 * src_rowstride); + + /* the pixel to remove from the kernel */ + i2 = y - radius; + if (i2 < 0) + i2 = 0; + c2 = p_src + (i2 * src_rowstride); + + /* calc the new sums of the kernel */ + r += c1[0] - c2[0]; + g += c1[1] - c2[1]; + b += c1[2] - c2[2]; + } + + p_src += n_channels; + p_dest += n_channels; + } +} + +/** + * gs_utils_pixbuf_blur: + * @src: the GdkPixbuf. + * @radius: the pixel radius for the gaussian blur, typical values are 1..3 + * @iterations: Amount to blur the image, typical values are 1..5 + * + * Blurs an image. Warning, this method is s..l..o..w... for large images. + **/ +void +gs_utils_pixbuf_blur (GdkPixbuf *src, guint radius, guint iterations) +{ + gint kernel_size; + gint i; + g_autofree guchar *div_kernel_size = NULL; + g_autoptr(GdkPixbuf) tmp = NULL; + + tmp = gdk_pixbuf_new (gdk_pixbuf_get_colorspace (src), + gdk_pixbuf_get_has_alpha (src), + gdk_pixbuf_get_bits_per_sample (src), + gdk_pixbuf_get_width (src), + gdk_pixbuf_get_height (src)); + kernel_size = 2 * radius + 1; + div_kernel_size = g_new (guchar, 256 * kernel_size); + for (i = 0; i < 256 * kernel_size; i++) + div_kernel_size[i] = (guchar) (i / kernel_size); + + while (iterations-- > 0) + gs_pixbuf_blur_private (src, tmp, radius, div_kernel_size); +} + +/** + * gs_utils_get_file_size: + * @filename: a file name to get the size of; it can be a file or a directory + * @include_func: (nullable) (scope call): optional callback to limit what files to count + * @user_data: user data passed to the @include_func + * @cancellable: (nullable): an optional #GCancellable or %NULL + * + * Gets the size of the file or a directory identified by @filename. + * + * When the @include_func is not %NULL, it can limit which files are included + * in the resulting size. When it's %NULL, all files and subdirectories are included. + * + * Returns: disk size of the @filename; or 0 when not found + * + * Since: 41 + **/ +guint64 +gs_utils_get_file_size (const gchar *filename, + GsFileSizeIncludeFunc include_func, + gpointer user_data, + GCancellable *cancellable) +{ + guint64 size = 0; + + g_return_val_if_fail (filename != NULL, 0); + + if (g_file_test (filename, G_FILE_TEST_IS_DIR)) { + GSList *dirs_to_do = NULL; + gsize base_len = strlen (filename); + + /* The `include_func()` expects a path relative to the `filename`, without + a leading dir separator. As the `dirs_to_do` contains the full path, + constructed with `g_build_filename()`, the added dir separator needs + to be skipped, when it's not part of the `filename` already. */ + if (!g_str_has_suffix (filename, G_DIR_SEPARATOR_S)) + base_len++; + + dirs_to_do = g_slist_prepend (dirs_to_do, g_strdup (filename)); + while (dirs_to_do != NULL && !g_cancellable_is_cancelled (cancellable)) { + g_autofree gchar *path = NULL; + g_autoptr(GDir) dir = NULL; + + /* Steal the top `path` out of the `dirs_to_do`. */ + path = dirs_to_do->data; + dirs_to_do = g_slist_remove (dirs_to_do, path); + + dir = g_dir_open (path, 0, NULL); + if (dir) { + const gchar *name; + while (name = g_dir_read_name (dir), name != NULL && !g_cancellable_is_cancelled (cancellable)) { + g_autofree gchar *full_path = g_build_filename (path, name, NULL); + GStatBuf st; + + if (g_stat (full_path, &st) == 0 && (include_func == NULL || + include_func (full_path + base_len, + g_file_test (full_path, G_FILE_TEST_IS_SYMLINK) ? G_FILE_TEST_IS_SYMLINK : + S_ISDIR (st.st_mode) ? G_FILE_TEST_IS_DIR : + G_FILE_TEST_IS_REGULAR, + user_data))) { + if (S_ISDIR (st.st_mode)) { + /* Skip symlinks, they can point to a shared storage */ + if (!g_file_test (full_path, G_FILE_TEST_IS_SYMLINK)) + dirs_to_do = g_slist_prepend (dirs_to_do, g_steal_pointer (&full_path)); + } else { + size += st.st_size; + } + } + } + } + } + g_slist_free_full (dirs_to_do, g_free); + } else { + GStatBuf st; + + if (g_stat (filename, &st) == 0) + size = st.st_size; + } + + return size; +} + +#define METADATA_ETAG_ATTRIBUTE "xattr::gnome-software::etag" + +/** + * gs_utils_get_file_etag: + * @file: a file to get the ETag for + * @last_modified_date_out: (out callee-allocates) (transfer full) (optional) (nullable): + * return location for the last modified date of the file (%NULL to ignore), + * or %NULL if unknown + * @cancellable: (nullable): an optional #GCancellable or %NULL + * + * Gets the ETag for the @file, previously stored by + * gs_utils_set_file_etag(). + * + * Returns: (nullable) (transfer full): The ETag stored for the @file, + * or %NULL, when the file does not exist, no ETag is stored for it + * or other error occurs. + * + * Since: 43 + **/ +gchar * +gs_utils_get_file_etag (GFile *file, + GDateTime **last_modified_date_out, + GCancellable *cancellable) +{ + g_autoptr(GFileInfo) info = NULL; + const gchar *attributes; + g_autoptr(GError) local_error = NULL; + + g_return_val_if_fail (G_IS_FILE (file), NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + + if (last_modified_date_out == NULL) + attributes = METADATA_ETAG_ATTRIBUTE; + else + attributes = METADATA_ETAG_ATTRIBUTE "," G_FILE_ATTRIBUTE_TIME_MODIFIED; + + info = g_file_query_info (file, attributes, G_FILE_QUERY_INFO_NONE, cancellable, &local_error); + + if (info == NULL) { + g_debug ("Error getting attribute ‘%s’ for file ‘%s’: %s", + METADATA_ETAG_ATTRIBUTE, g_file_peek_path (file), local_error->message); + + if (last_modified_date_out != NULL) + *last_modified_date_out = NULL; + + return NULL; + } + + if (last_modified_date_out != NULL) + *last_modified_date_out = g_file_info_get_modification_date_time (info); + + return g_strdup (g_file_info_get_attribute_string (info, METADATA_ETAG_ATTRIBUTE)); +} + +/** + * gs_utils_set_file_etag: + * @file: a file to get the ETag for + * @etag: (nullable): an ETag to set + * @cancellable: (nullable): an optional #GCancellable or %NULL + * + * Sets the ETag for the @file. When the @etag is %NULL or an empty + * string, then unsets the ETag for the @file. The ETag can be read + * back with gs_utils_get_file_etag(). + * + * The @file should exist, otherwise the function fails. + * + * Returns: whether succeeded. + * + * Since: 42 + **/ +gboolean +gs_utils_set_file_etag (GFile *file, + const gchar *etag, + GCancellable *cancellable) +{ + g_autoptr(GError) local_error = NULL; + + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + + if (etag == NULL || *etag == '\0') { + if (!g_file_set_attribute (file, METADATA_ETAG_ATTRIBUTE, G_FILE_ATTRIBUTE_TYPE_INVALID, + NULL, G_FILE_QUERY_INFO_NONE, cancellable, &local_error)) { + g_debug ("Error clearing attribute ‘%s’ on file ‘%s’: %s", + METADATA_ETAG_ATTRIBUTE, g_file_peek_path (file), local_error->message); + return FALSE; + } + + return TRUE; + } + + if (!g_file_set_attribute_string (file, METADATA_ETAG_ATTRIBUTE, etag, G_FILE_QUERY_INFO_NONE, cancellable, &local_error)) { + g_debug ("Error setting attribute ‘%s’ to ‘%s’ on file ‘%s’: %s", + METADATA_ETAG_ATTRIBUTE, etag, g_file_peek_path (file), local_error->message); + return FALSE; + } + + return TRUE; +} + +/** + * gs_utils_get_upgrade_background: + * @version: (nullable): version string of the upgrade (which must be non-empty + * if provided), or %NULL if unknown + * + * Get the path to a background image to display as the background for a banner + * advertising an upgrade to the given @version. + * + * If a path is returned, it’s guaranteed to exist on the file system. + * + * Vendors can drop their customised backgrounds in this directory for them to + * be used by gnome-software. See `doc/vendor-customisation.md`. + * + * Returns: (transfer full) (type filename) (nullable): path to an upgrade + * background image to use, or %NULL if a suitable one didn’t exist + * Since: 42 +*/ +gchar * +gs_utils_get_upgrade_background (const gchar *version) +{ + g_autofree gchar *filename = NULL; + g_autofree gchar *os_id = g_get_os_info (G_OS_INFO_KEY_ID); + + g_return_val_if_fail (version == NULL || *version != '\0', NULL); + + if (version != NULL) { + filename = g_strdup_printf (DATADIR "/gnome-software/backgrounds/%s-%s.png", os_id, version); + if (g_file_test (filename, G_FILE_TEST_EXISTS)) + return g_steal_pointer (&filename); + g_clear_pointer (&filename, g_free); + } + + filename = g_strdup_printf (DATADIR "/gnome-software/backgrounds/%s.png", os_id); + if (g_file_test (filename, G_FILE_TEST_EXISTS)) + return g_steal_pointer (&filename); + g_clear_pointer (&filename, g_free); + + return NULL; +} + +/** + * gs_utils_app_sort_name: + * @app1: a #GsApp + * @app2: another #GsApp + * @user_data: data passed to the sort function + * + * Comparison function to sort apps in increasing alphabetical order of name. + * + * This is suitable for passing to gs_app_list_sort(). + * + * Returns: a strcmp()-style sort value comparing @app1 to @app2 + * Since: 43 + */ +gint +gs_utils_app_sort_name (GsApp *app1, + GsApp *app2, + gpointer user_data) +{ + return gs_utils_sort_strcmp (gs_app_get_name (app1), gs_app_get_name (app2)); +} + +/** + * gs_utils_app_sort_match_value: + * @app1: a #GsApp + * @app2: another #GsApp + * @user_data: data passed to the sort function + * + * Comparison function to sort apps in decreasing order of match value + * (#GsApp:match-value). + * + * This is suitable for passing to gs_app_list_sort(). + * + * Returns: a strcmp()-style sort value comparing @app1 to @app2 + * Since: 43 + */ +gint +gs_utils_app_sort_match_value (GsApp *app1, + GsApp *app2, + gpointer user_data) +{ + return gs_app_get_match_value (app2) - gs_app_get_match_value (app1); +} + +/** + * gs_utils_app_sort_priority: + * @app1: a #GsApp + * @app2: another #GsApp + * @user_data: data passed to the sort function + * + * Comparison function to sort apps in increasing order of their priority + * (#GsApp:priority). + * + * This is suitable for passing to gs_app_list_sort(). + * + * Returns: a strcmp()-style sort value comparing @app1 to @app2 + * Since: 43 + */ +gint +gs_utils_app_sort_priority (GsApp *app1, + GsApp *app2, + gpointer user_data) +{ + return gs_app_compare_priority (app1, app2); +} diff --git a/lib/gs-utils.h b/lib/gs-utils.h new file mode 100644 index 0000000..855e984 --- /dev/null +++ b/lib/gs-utils.h @@ -0,0 +1,155 @@ +/* -*- 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) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gio/gdesktopappinfo.h> +#include <gtk/gtk.h> + +#include "gs-app.h" + +G_BEGIN_DECLS + +/** + * GsUtilsCacheFlags: + * @GS_UTILS_CACHE_FLAG_NONE: No flags set + * @GS_UTILS_CACHE_FLAG_WRITEABLE: A writable directory is required + * @GS_UTILS_CACHE_FLAG_USE_HASH: Prefix a hash to the filename + * @GS_UTILS_CACHE_FLAG_ENSURE_EMPTY: Clear existing cached items + * @GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY: Create the cache directory (Since: 40) + * + * The cache flags. + **/ +typedef enum { + GS_UTILS_CACHE_FLAG_NONE = 0, + GS_UTILS_CACHE_FLAG_WRITEABLE = 1 << 0, + GS_UTILS_CACHE_FLAG_USE_HASH = 1 << 1, + GS_UTILS_CACHE_FLAG_ENSURE_EMPTY = 1 << 2, + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY = 1 << 3, + GS_UTILS_CACHE_FLAG_LAST /*< skip >*/ +} GsUtilsCacheFlags; + +guint64 gs_utils_get_file_age (GFile *file); +gchar *gs_utils_get_content_type (GFile *file, + GCancellable *cancellable, + GError **error); +gboolean gs_utils_symlink (const gchar *target, + const gchar *linkpath, + GError **error); +gboolean gs_utils_unlink (const gchar *filename, + GError **error); +gboolean gs_mkdir_parent (const gchar *path, + GError **error); +gchar *gs_utils_get_cache_filename (const gchar *kind, + const gchar *resource, + GsUtilsCacheFlags flags, + GError **error); +gchar *gs_utils_get_user_hash (GError **error); +GPermission *gs_utils_get_permission (const gchar *id, + GCancellable *cancellable, + GError **error); +void gs_utils_get_permission_async (const gchar *id, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GPermission *gs_utils_get_permission_finish (GAsyncResult *result, + GError **error); +gboolean gs_utils_strv_fnmatch (gchar **strv, + const gchar *str); +gchar *gs_utils_sort_key (const gchar *str); +gint gs_utils_sort_strcmp (const gchar *str1, + const gchar *str2); +GDesktopAppInfo *gs_utils_get_desktop_app_info (const gchar *id); +gboolean gs_utils_rmtree (const gchar *directory, + GError **error); +gint gs_utils_get_wilson_rating (guint64 star1, + guint64 star2, + guint64 star3, + guint64 star4, + guint64 star5); +void gs_utils_error_add_app_id (GError **error, + GsApp *app); +void gs_utils_error_add_origin_id (GError **error, + GsApp *origin); +gchar *gs_utils_error_strip_app_id (GError *error); +gchar *gs_utils_error_strip_origin_id (GError *error); +gboolean gs_utils_error_convert_gio (GError **perror); +gboolean gs_utils_error_convert_gresolver (GError **perror); +gboolean gs_utils_error_convert_gdbus (GError **perror); +gboolean gs_utils_error_convert_gdk_pixbuf(GError **perror); +gboolean gs_utils_error_convert_appstream (GError **perror); + +gchar *gs_utils_get_url_scheme (const gchar *url); +gchar *gs_utils_get_url_path (const gchar *url); +const gchar *gs_user_agent (void); +void gs_utils_append_key_value (GString *str, + gsize align_len, + const gchar *key, + const gchar *value); +guint gs_utils_get_memory_total (void); +gboolean gs_utils_parse_evr (const gchar *evr, + gchar **out_epoch, + gchar **out_version, + gchar **out_release); +void gs_utils_set_online_updates_timestamp (GSettings *settings); + +gchar *gs_utils_unique_id_compat_convert (const gchar *data_id); + +gchar *gs_utils_build_unique_id (AsComponentScope scope, + AsBundleKind bundle_kind, + const gchar *origin, + const gchar *cid, + const gchar *branch); + +void gs_utils_pixbuf_blur (GdkPixbuf *src, + guint radius, + guint iterations); + +/** + * GsFileSizeIncludeFunc: + * @filename: file name to check + * @file_kind: the file kind, one of #GFileTest enums + * @user_data: a user data passed to the gs_utils_get_file_size() + * + * Check whether include the @filename in the size calculation. + * The @filename is a relative path to the file name passed to + * the #GsFileSizeIncludeFunc. + * + * Returns: Whether to include the @filename in the size calculation + * + * Since: 41 + **/ +typedef gboolean (*GsFileSizeIncludeFunc) (const gchar *filename, + GFileTest file_kind, + gpointer user_data); + +guint64 gs_utils_get_file_size (const gchar *filename, + GsFileSizeIncludeFunc include_func, + gpointer user_data, + GCancellable *cancellable); +gchar * gs_utils_get_file_etag (GFile *file, + GDateTime **last_modified_date_out, + GCancellable *cancellable); +gboolean gs_utils_set_file_etag (GFile *file, + const gchar *etag, + GCancellable *cancellable); + +gchar *gs_utils_get_upgrade_background (const gchar *version); + +gint gs_utils_app_sort_name (GsApp *app1, + GsApp *app2, + gpointer user_data); +gint gs_utils_app_sort_match_value (GsApp *app1, + GsApp *app2, + gpointer user_data); +gint gs_utils_app_sort_priority (GsApp *app1, + GsApp *app2, + gpointer user_data); + +G_END_DECLS diff --git a/lib/gs-worker-thread.c b/lib/gs-worker-thread.c new file mode 100644 index 0000000..6e6686d --- /dev/null +++ b/lib/gs-worker-thread.c @@ -0,0 +1,425 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-worker-thread + * @short_description: A worker thread which executes queued #GTasks until stopped + * + * #GsWorkerThread is a thread-safe wrapper around a #GTask queue and a single + * worker thread which executes tasks on that queue. + * + * Tasks can be added to the queue using gs_worker_thread_queue(). The worker + * thread (which is created when #GsWorkerThread is constructed) will execute + * them in (priority, queue order) order. Each #GTaskThreadFunc is responsible + * for calling `g_task_return_*()` on its #GTask to complete that task. + * + * The priority passed to gs_worker_thread_queue() will be used to adjust the + * worker thread’s I/O priority (using `ioprio_set()`) when executing that task. + * + * It is intended that gs_worker_thread_queue() is an alternative to using + * g_task_run_in_thread(). g_task_run_in_thread() queues tasks into a single + * process-wide thread pool, so they are mixed in with other tasks, and it can + * become hard to ensure the thread pool isn’t overwhelmed and that tasks are + * executed in the right order. + * + * The worker thread will continue executing tasks until + * gs_worker_thread_shutdown_async() is called. This must be called before the + * final reference to the #GsWorkerThread is dropped. + * + * Since: 42 + */ + +#include "config.h" + +#include <glib.h> +#include <glib-object.h> + +#include "gs-ioprio.h" +#include "gs-worker-thread.h" + +typedef enum { + GS_WORKER_THREAD_STATE_RUNNING = 0, + GS_WORKER_THREAD_STATE_SHUTTING_DOWN = 1, + GS_WORKER_THREAD_STATE_SHUT_DOWN = 2, +} GsWorkerThreadState; + +struct _GsWorkerThread +{ + GObject parent; + + gchar *name; /* (nullable) (owned) */ + + GsWorkerThreadState worker_state; /* (atomic) */ + GMainContext *worker_context; /* (owned); may be NULL before setup or after shutdown */ + GThread *worker_thread; /* (atomic); may be NULL before setup or after shutdown */ +}; + +typedef enum { + PROP_NAME = 1, +} GsWorkerThreadProperty; + +static GParamSpec *props[PROP_NAME + 1] = { NULL, }; + +G_DEFINE_TYPE (GsWorkerThread, gs_worker_thread, G_TYPE_OBJECT) + +static void +gs_worker_thread_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsWorkerThread *self = GS_WORKER_THREAD (object); + + switch ((GsWorkerThreadProperty) prop_id) { + case PROP_NAME: + g_value_set_string (value, self->name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_worker_thread_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsWorkerThread *self = GS_WORKER_THREAD (object); + + switch ((GsWorkerThreadProperty) prop_id) { + case PROP_NAME: + /* Construct only */ + g_assert (self->name == NULL); + self->name = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_worker_thread_dispose (GObject *object) +{ + GsWorkerThread *self = GS_WORKER_THREAD (object); + + /* Should have stopped by now. */ + g_assert (self->worker_thread == NULL); + + g_clear_pointer (&self->name, g_free); + g_clear_pointer (&self->worker_context, g_main_context_unref); + + G_OBJECT_CLASS (gs_worker_thread_parent_class)->dispose (object); +} + +static gpointer thread_cb (gpointer data); + +static void +gs_worker_thread_constructed (GObject *object) +{ + GsWorkerThread *self = GS_WORKER_THREAD (object); + + G_OBJECT_CLASS (gs_worker_thread_parent_class)->constructed (object); + + /* Start up a worker thread and its #GMainContext. The worker will run + * and process events on @worker_context until @worker_state changes + * from %GS_WORKER_THREAD_STATE_RUNNING. */ + self->worker_state = GS_WORKER_THREAD_STATE_RUNNING; + self->worker_context = g_main_context_new (); + self->worker_thread = g_thread_new (self->name, thread_cb, self); +} + +static void +gs_worker_thread_class_init (GsWorkerThreadClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = gs_worker_thread_constructed; + object_class->get_property = gs_worker_thread_get_property; + object_class->set_property = gs_worker_thread_set_property; + object_class->dispose = gs_worker_thread_dispose; + + /** + * GsWorkerThread:name: (not nullable): + * + * Name for the worker thread to use in debug output. This must be set. + * + * Since: 42 + */ + props[PROP_NAME] = + g_param_spec_string ("name", + "Name", + "Name for the worker thread to use in debug output.", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props); +} + +static gpointer +thread_cb (gpointer data) +{ + GsWorkerThread *self = GS_WORKER_THREAD (data); + g_autoptr(GMainContextPusher) pusher = g_main_context_pusher_new (self->worker_context); + + while (g_atomic_int_get (&self->worker_state) != GS_WORKER_THREAD_STATE_SHUT_DOWN) + g_main_context_iteration (self->worker_context, TRUE); + + return NULL; +} + +static void +gs_worker_thread_init (GsWorkerThread *self) +{ +} + +/** + * gs_worker_thread_new: + * @name: (not nullable): name for the worker thread + * + * Create and start a new #GsWorkerThread. + * + * @name will be used to set the thread name and in debug output. + * + * Returns: (transfer full): a new #GsWorkerThread + * Since: 42 + */ +GsWorkerThread * +gs_worker_thread_new (const gchar *name) +{ + g_return_val_if_fail (name != NULL, NULL); + + return g_object_new (GS_TYPE_WORKER_THREAD, + "name", name, + NULL); +} + +/* Essentially a wrapper around these elements to avoid the caller having to + * return `G_SOURCE_REMOVE` from their `work_func` every time. */ +typedef struct { + GTaskThreadFunc work_func; + GTask *task; /* (owned) */ + gint priority; +} WorkData; + +static void +work_data_free (WorkData *data) +{ + g_clear_object (&data->task); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (WorkData, work_data_free) + +static gboolean +work_run_cb (gpointer _data) +{ + WorkData *data = _data; + GTask *task = data->task; + gpointer source_object = g_task_get_source_object (task); + gpointer task_data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + + /* Set the I/O priority of the thread to match the priority of the + * task. */ + gs_ioprio_set (data->priority); + + data->work_func (task, source_object, task_data, cancellable); + + return G_SOURCE_REMOVE; +} + +/** + * gs_worker_thread_queue: + * @self: a #GsWorkerThread + * @priority: (default G_PRIORITY_DEFAULT): priority to queue the task at, + * typically #G_PRIORITY_DEFAULT + * @work_func: (not nullable): function to run the task + * @task: (transfer full) (not nullable): the #GTask containing context data to + * pass to @work_func + * + * Queue @task to be run in the worker thread at the given @priority. + * + * This function takes ownership of @task. + * + * @priority sets the order of the task in the queue, and also affects the I/O + * priority of the worker thread when the task is executed — high priorities + * result in a high I/O priority, low priorities result in an idle I/O priority, + * as per `ioprio_set()`. + * + * When the task is run, @work_func will be executed and passed @task and the + * source object, task data and cancellable set on @task. + * + * @work_func is responsible for calling `g_task_return_*()` on @task once the + * task is complete. + * + * If a task is cancelled using its #GCancellable after it’s queued to the + * #GsWorkerThread, @work_func will still be executed. @work_func is responsible + * for checking whether the #GCancellable has been cancelled. + * + * It is an error to call this function after gs_worker_thread_shutdown_async() + * has called. + * + * Since: 42 + */ +void +gs_worker_thread_queue (GsWorkerThread *self, + gint priority, + GTaskThreadFunc work_func, + GTask *task) +{ + g_autoptr(WorkData) data = NULL; + + g_return_if_fail (GS_IS_WORKER_THREAD (self)); + g_return_if_fail (work_func != NULL); + g_return_if_fail (G_IS_TASK (task)); + + g_assert (g_atomic_int_get (&self->worker_state) == GS_WORKER_THREAD_STATE_RUNNING || + g_task_get_source_tag (task) == gs_worker_thread_shutdown_async); + + data = g_new0 (WorkData, 1); + data->work_func = work_func; + data->task = g_steal_pointer (&task); + data->priority = priority; + + g_main_context_invoke_full (self->worker_context, priority, + work_run_cb, g_steal_pointer (&data), (GDestroyNotify) work_data_free); +} + +/** + * gs_worker_thread_is_in_worker_context: + * @self: a #GsWorkerThread + * + * Returns whether the calling thread is the worker thread. + * + * This is intended to be used as a precondition check to ensure that worker + * code is not accidentally run from the wrong thread. + * + * |[ + * static void + * do_work (MyPlugin *self) + * { + * g_assert (gs_worker_thread_is_in_worker_context (self->worker_thread)); + * + * // do some work + * } + * ]| + * + * Returns: %TRUE if running in the worker context, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_worker_thread_is_in_worker_context (GsWorkerThread *self) +{ + return g_main_context_is_owner (self->worker_context); +} + +static void shutdown_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +/** + * gs_worker_thread_shutdown_async: + * @self: a #GsWorkerThread + * @cancellable: (nullable): a #GCancellable, or %NULL + * @callback: callback for once the asynchronous operation is complete + * @user_data: data to pass to @callback + * + * Shut down the worker thread. + * + * The thread will finish processing whatever task it’s currently processing + * (if any), will return %G_IO_ERROR_CANCELLED for all remaining queued + * tasks, and will then join the main process. + * + * This is a no-op if called subsequently. + * + * Since: 42 + */ +void +gs_worker_thread_shutdown_async (GsWorkerThread *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (GS_IS_WORKER_THREAD (self)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_worker_thread_shutdown_async); + + /* Already called? */ + if (g_atomic_int_get (&self->worker_state) != GS_WORKER_THREAD_STATE_RUNNING) { + g_task_return_boolean (task, TRUE); + return; + } + + /* Signal the worker thread to stop processing tasks. */ + g_atomic_int_set (&self->worker_state, GS_WORKER_THREAD_STATE_SHUTTING_DOWN); + gs_worker_thread_queue (self, G_MAXINT /* lowest priority */, + shutdown_cb, g_steal_pointer (&task)); +} + +static void +shutdown_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsWorkerThread *self = GS_WORKER_THREAD (source_object); + gboolean updated_state; + + updated_state = g_atomic_int_compare_and_exchange (&self->worker_state, + GS_WORKER_THREAD_STATE_SHUTTING_DOWN, + GS_WORKER_THREAD_STATE_SHUT_DOWN); + g_assert (updated_state); + + /* Tidy up. We can’t join the thread here as this function is executing + * within the thread and that would deadlock. */ + g_clear_pointer (&self->worker_context, g_main_context_unref); + + g_task_return_boolean (task, TRUE); +} + +/** + * gs_worker_thread_shutdown_finish: + * @self: a #GsWorkerThread + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finish an asynchronous shutdown operation started with + * gs_worker_thread_shutdown_async(); + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 42 + */ +gboolean +gs_worker_thread_shutdown_finish (GsWorkerThread *self, + GAsyncResult *result, + GError **error) +{ + gboolean success; + + g_return_val_if_fail (GS_IS_WORKER_THREAD (self), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, gs_worker_thread_shutdown_async), FALSE); + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + success = g_task_propagate_boolean (G_TASK (result), error); + + if (success) + g_thread_join (g_steal_pointer (&self->worker_thread)); + + return success; +} diff --git a/lib/gs-worker-thread.h b/lib/gs-worker-thread.h new file mode 100644 index 0000000..36b28df --- /dev/null +++ b/lib/gs-worker-thread.h @@ -0,0 +1,41 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GS_TYPE_WORKER_THREAD (gs_worker_thread_get_type ()) + +G_DECLARE_FINAL_TYPE (GsWorkerThread, gs_worker_thread, GS, WORKER_THREAD, GObject) + +GsWorkerThread *gs_worker_thread_new (const gchar *name); + +void gs_worker_thread_queue (GsWorkerThread *self, + gint priority, + GTaskThreadFunc work_func, + GTask *task); + +gboolean gs_worker_thread_is_in_worker_context (GsWorkerThread *self); + +void gs_worker_thread_shutdown_async (GsWorkerThread *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean gs_worker_thread_shutdown_finish (GsWorkerThread *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/lib/meson.build b/lib/meson.build new file mode 100644 index 0000000..d591bca --- /dev/null +++ b/lib/meson.build @@ -0,0 +1,205 @@ +cargs = ['-DG_LOG_DOMAIN="Gs"'] +cargs += ['-DLOCALPLUGINDIR=""'] + +libgnomesoftware_public_headers = [ + 'gnome-software.h', + 'gs-app.h', + 'gs-app-collation.h', + 'gs-app-list.h', + 'gs-app-permissions.h', + 'gs-app-query.h', + 'gs-appstream.h', + 'gs-category.h', + 'gs-category-manager.h', + 'gs-desktop-data.h', + 'gs-download-utils.h', + 'gs-external-appstream-utils.h', + 'gs-icon.h', + 'gs-ioprio.h', + 'gs-key-colors.h', + 'gs-metered.h', + 'gs-odrs-provider.h', + 'gs-os-release.h', + 'gs-plugin.h', + 'gs-plugin-event.h', + 'gs-plugin-helpers.h', + 'gs-plugin-job.h', + 'gs-plugin-job-list-apps.h', + 'gs-plugin-job-list-categories.h', + 'gs-plugin-job-list-distro-upgrades.h', + 'gs-plugin-job-manage-repository.h', + 'gs-plugin-job-refine.h', + 'gs-plugin-job-refresh-metadata.h', + 'gs-plugin-loader.h', + 'gs-plugin-loader-sync.h', + 'gs-plugin-types.h', + 'gs-plugin-vfuncs.h', + 'gs-remote-icon.h', + 'gs-test.h', + 'gs-utils.h', + 'gs-worker-thread.h', +] + +install_headers(libgnomesoftware_public_headers, + subdir : 'gnome-software' +) + +librarydeps = [ + appstream, + gio_unix, + glib, + gmodule, + gtk, + json_glib, + libm, + libsoup, + libsysprof_capture_dep, + libxmlb, +] + +if get_option('mogwai') + librarydeps += mogwai_schedule_client +endif + +if get_option('polkit') + librarydeps += polkit +endif + +gs_build_ident_h = vcs_tag( + fallback: meson.project_version(), + input: 'gs-build-ident.h.in', + output: 'gs-build-ident.h', +) + +libgnomesoftware_enums = gnome.mkenums_simple('gs-enums', + sources : libgnomesoftware_public_headers, + install_header : true, + install_dir : join_paths(get_option('includedir'), 'gnome-software'), +) + +libgnomesoftware_include_directories = [ + include_directories('..'), +] + +libgnomesoftware = library( + 'gnomesoftware', + sources : [ + 'gs-app.c', + 'gs-app-list.c', + 'gs-app-permissions.c', + 'gs-app-query.c', + 'gs-appstream.c', + 'gs-category.c', + 'gs-category-manager.c', + 'gs-debug.c', + 'gs-desktop-data.c', + 'gs-download-utils.c', + 'gs-external-appstream-utils.c', + 'gs-fedora-third-party.c', + 'gs-icon.c', + 'gs-ioprio.c', + 'gs-ioprio.h', + 'gs-key-colors.c', + 'gs-metered.c', + 'gs-odrs-provider.c', + 'gs-os-release.c', + 'gs-plugin.c', + 'gs-plugin-event.c', + 'gs-plugin-helpers.c', + 'gs-plugin-job.c', + 'gs-plugin-job-list-apps.c', + 'gs-plugin-job-list-categories.c', + 'gs-plugin-job-list-distro-upgrades.c', + 'gs-plugin-job-manage-repository.c', + 'gs-plugin-job-refine.c', + 'gs-plugin-job-refresh-metadata.c', + 'gs-plugin-loader.c', + 'gs-plugin-loader-sync.c', + 'gs-remote-icon.c', + 'gs-test.c', + 'gs-utils.c', + 'gs-worker-thread.c', + ] + libgnomesoftware_enums + [gs_build_ident_h], + soversion: gs_plugin_api_version, + include_directories : libgnomesoftware_include_directories, + dependencies : librarydeps, + c_args : cargs, + install: true, + install_dir: gs_private_libdir, +) + +libgnomesoftware_dep = declare_dependency(link_with : libgnomesoftware, + sources : libgnomesoftware_enums, + include_directories : libgnomesoftware_include_directories, + dependencies: librarydeps, +) + +pkg = import('pkgconfig') + +pkg.generate( + libgnomesoftware, + description : 'GNOME Software is a software center for GNOME', + filebase : 'gnome-software', + name : 'gnome-software', + subdirs : 'gnome-software', + variables : [ + 'gs_private_libdir=${libdir}/gnome-software', + 'plugindir=${gs_private_libdir}/plugins-' + gs_plugin_api_version, + 'soupapiversion=' + libsoupapiversion, + ], + install_dir : join_paths(get_option('prefix'), get_option('libdir'), 'pkgconfig'), # or it defaults to gs_private_libdir, which is wrong +) + +executable( + 'gnome-software-cmd', + sources : [ + 'gs-cmd.c', + ], + include_directories : [ + include_directories('..'), + ], + dependencies : [ + appstream, + gio_unix, + glib, + gmodule, + gtk, + json_glib, + libgnomesoftware_dep, + libm, + libsoup, + ], + c_args : cargs, + install : true, + install_dir : get_option('libexecdir'), + install_rpath : gs_private_libdir, +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), '..', 'data') + '"'] + e = executable( + 'gs-self-test', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('..'), + ], + dependencies : [ + appstream, + gio_unix, + glib, + gmodule, + gtk, + json_glib, + libgnomesoftware_dep, + libm, + libsoup + ], + c_args : cargs + ) + test('gs-self-test-lib', e, suite: ['lib'], env: test_env, timeout : 120) +endif + +subdir('tools') diff --git a/lib/tools/meson.build b/lib/tools/meson.build new file mode 100644 index 0000000..6844775 --- /dev/null +++ b/lib/tools/meson.build @@ -0,0 +1,24 @@ +# Test program to profile performance of the key-colors functions +executable( + 'profile-key-colors', + sources : [ + 'profile-key-colors.c', + '../gs-key-colors.c', + '../gs-key-colors.h', + ], + include_directories : [ + include_directories('..'), + include_directories('../..'), + ], + dependencies : [ + glib, + gtk, + gdk_pixbuf, + libm, + ], + c_args : [ + '-Wall', + '-Wextra', + ], + install: false, +) diff --git a/lib/tools/profile-key-colors.c b/lib/tools/profile-key-colors.c new file mode 100644 index 0000000..44df6a4 --- /dev/null +++ b/lib/tools/profile-key-colors.c @@ -0,0 +1,175 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Authors: + * - Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <glib.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <gdk/gdk.h> +#include <locale.h> +#include <math.h> + +#include "gs-key-colors.h" + +/* Test program which can be used to check the output and performance of the + * gs_calculate_key_colors() function. It is linked against libgnomesoftware, so + * will use the function implementation from there. It outputs a HTML page which + * lists each icon from the flathub appstream data in your home directory, along + * with its extracted key colors and how long extraction took. */ + +static void +print_colours (GString *html_output, + GArray *colours) +{ + g_string_append_printf (html_output, "<table class='colour-swatch'><tr>"); + for (guint i = 0; i < colours->len; i++) { + GdkRGBA *rgba = &g_array_index (colours, GdkRGBA, i); + + g_string_append_printf (html_output, + "<td style='background-color: rgb(%u, %u, %u)'></td>", + (guint) (rgba->red * 255), + (guint) (rgba->green * 255), + (guint) (rgba->blue * 255)); + + if (i % 3 == 2) + g_string_append (html_output, "</tr><tr>"); + } + g_string_append_printf (html_output, "</tr></table>"); +} + +static void +print_summary_statistics (GString *html_output, + GArray *durations /* (element-type gint64) */) +{ + gint64 sum = 0, min = G_MAXINT64, max = G_MININT64; + guint n_measurements = durations->len; + gint64 mean, stddev; + gint64 sum_of_square_deviations = 0; + + for (guint i = 0; i < durations->len; i++) { + gint64 duration = g_array_index (durations, gint64, i); + sum += duration; + min = MIN (min, duration); + max = MAX (max, duration); + } + + mean = sum / n_measurements; + + for (guint i = 0; i < durations->len; i++) { + gint64 duration = g_array_index (durations, gint64, i); + gint64 diff = duration - mean; + sum_of_square_deviations += diff * diff; + } + + stddev = sqrt (sum_of_square_deviations / n_measurements); + + g_string_append_printf (html_output, + "[%" G_GINT64_FORMAT ", %" G_GINT64_FORMAT "]μs, mean %" G_GINT64_FORMAT "±%" G_GINT64_FORMAT "μs, n = %u", + min, max, mean, stddev, n_measurements); +} + +int +main (void) +{ + const gchar *icons_subdir = ".local/share/flatpak/appstream/flathub/x86_64/active/icons/128x128"; + g_autofree gchar *icons_dir = g_build_filename (g_get_home_dir (), icons_subdir, NULL); + g_autoptr(GDir) dir = NULL; + const gchar *entry; + g_autoptr(GPtrArray) filenames = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GPtrArray) pixbufs = g_ptr_array_new_with_free_func (g_object_unref); + g_autoptr(GString) html_output = g_string_new (""); + g_autoptr(GArray) durations = g_array_new (FALSE, FALSE, sizeof (gint64)); + + setlocale (LC_ALL, ""); + + /* Load pixbufs from the icons directory. */ + dir = g_dir_open (icons_dir, 0, NULL); + if (dir == NULL) + return 1; + + while ((entry = g_dir_read_name (dir)) != NULL) { + g_autofree gchar *filename = g_build_filename (icons_dir, entry, NULL); + g_autoptr(GdkPixbuf) pixbuf = gdk_pixbuf_new_from_file (filename, NULL); + + if (pixbuf == NULL) + continue; + + g_ptr_array_add (filenames, g_steal_pointer (&filename)); + g_ptr_array_add (pixbufs, g_steal_pointer (&pixbuf)); + } + + if (!pixbufs->len) + return 2; + + /* Set up an output page */ + g_string_append (html_output, + "<!DOCTYPE html>\n" + "<html>\n" + " <head>\n" + " <meta charset='UTF-8'>\n" + " <style>\n" + " #main-table, #main-table th, #main-table td { border: 1px solid black; border-collapse: collapse }\n" + " #main-table th, #main-table td { padding: 4px }\n" + " td.number { text-align: right }\n" + " table.colour-swatch td { width: 30px; height: 30px }\n" + " .faster { background-color: rgb(190, 236, 57) }\n" + " .slower { background-color: red }\n" + " </style>\n" + " </head>\n" + " <body>\n" + " <table id='main-table'>\n" + " <thead>\n" + " <tr>\n" + " <td>Filename</td>\n" + " <td>Icon</td>\n" + " <td>Code duration (μs)</td>\n" + " <td>Code colours</td>\n" + " </tr>\n" + " </thead>\n"); + + /* For each pixbuf, run both algorithms. */ + for (guint i = 0; i < pixbufs->len; i++) { + GdkPixbuf *pixbuf = pixbufs->pdata[i]; + const gchar *filename = filenames->pdata[i]; + g_autofree gchar *basename = g_path_get_basename (filename); + g_autoptr(GArray) colours = NULL; + gint64 start_time, duration; + + g_message ("Processing %u of %u, %s", i + 1, pixbufs->len, filename); + + start_time = g_get_real_time (); + colours = gs_calculate_key_colors (pixbuf); + duration = g_get_real_time () - start_time; + + g_string_append_printf (html_output, + "<tr>\n" + "<th>%s</th>\n" + "<td><img src='file:%s'></td>\n" + "<td class='number'>%" G_GINT64_FORMAT "</td>\n" + "<td>", + basename, filename, duration); + print_colours (html_output, colours); + g_string_append (html_output, + "</td>\n" + "</tr>\n"); + + g_array_append_val (durations, duration); + } + + /* Summary statistics for the timings. */ + g_string_append (html_output, "<tfoot><tr><td></td><td></td><td>"); + print_summary_statistics (html_output, durations); + g_string_append (html_output, "</td><td></td></tr></tfoot>"); + + g_string_append (html_output, "</table></body></html>"); + + g_print ("%s\n", html_output->str); + + return 0; +} |