summaryrefslogtreecommitdiffstats
path: root/plugins/fedora-pkgdb-collections
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c855
-rw-r--r--plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.h22
-rw-r--r--plugins/fedora-pkgdb-collections/meson.build14
3 files changed, 891 insertions, 0 deletions
diff --git a/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c b/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c
new file mode 100644
index 0000000..1f788ca
--- /dev/null
+++ b/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c
@@ -0,0 +1,855 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016-2018 Kalev Lember <klember@redhat.com>
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+#include <json-glib/json-glib.h>
+#include <gnome-software.h>
+
+#include "gs-plugin-fedora-pkgdb-collections.h"
+
+/*
+ * SECTION:
+ * Queries the list of Fedora package collections.
+ *
+ * This plugin downloads a file and performs some basic parsing on it. It
+ * executes entirely in the main thread, and therefore does not require any
+ * locking.
+ */
+
+#define FEDORA_PKGDB_COLLECTIONS_API_URI "https://admin.fedoraproject.org/pkgdb/api/collections/"
+
+struct _GsPluginFedoraPkgdbCollections {
+ GsPlugin parent;
+
+ /* Only set at setup time, then read only: */
+ gchar *cachefn; /* (owned) (not nullable) */
+ GFileMonitor *cachefn_monitor; /* (owned) (not nullable) */
+ gchar *os_name; /* (owned) (not nullable) */
+ guint64 os_version;
+ GsApp *cached_origin; /* (owned) (not nullable) */
+ GSettings *settings; /* (owned) (not nullable) */
+
+ /* Contents may vary throughout the plugin’s lifetime: */
+ gboolean is_valid;
+ GPtrArray *distros; /* (owned) (not nullable) (element-type PkgdbItem) */
+};
+
+G_DEFINE_TYPE (GsPluginFedoraPkgdbCollections, gs_plugin_fedora_pkgdb_collections, GS_TYPE_PLUGIN)
+
+typedef enum {
+ PKGDB_ITEM_STATUS_ACTIVE,
+ PKGDB_ITEM_STATUS_DEVEL,
+ PKGDB_ITEM_STATUS_EOL,
+ PKGDB_ITEM_STATUS_LAST
+} PkgdbItemStatus;
+
+typedef struct {
+ gchar *name;
+ PkgdbItemStatus status;
+ guint version;
+} PkgdbItem;
+
+static void
+_pkgdb_item_free (PkgdbItem *item)
+{
+ g_free (item->name);
+ g_slice_free (PkgdbItem, item);
+}
+
+static void
+gs_plugin_fedora_pkgdb_collections_init (GsPluginFedoraPkgdbCollections *self)
+{
+ GsPlugin *plugin = GS_PLUGIN (self);
+
+ /* check that we are running on Fedora */
+ if (!gs_plugin_check_distro_id (plugin, "fedora")) {
+ gs_plugin_set_enabled (plugin, FALSE);
+ g_debug ("disabling '%s' as we're not Fedora", gs_plugin_get_name (plugin));
+ return;
+ }
+ self->distros = g_ptr_array_new_with_free_func ((GDestroyNotify) _pkgdb_item_free);
+ self->settings = g_settings_new ("org.gnome.software");
+
+ /* require the GnomeSoftware::CpeName metadata */
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "os-release");
+
+ /* old name */
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "fedora-distro-upgrades");
+}
+
+static void
+gs_plugin_fedora_pkgdb_collections_dispose (GObject *object)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (object);
+
+ g_clear_object (&self->cachefn_monitor);
+ g_clear_object (&self->cached_origin);
+ g_clear_object (&self->settings);
+
+ G_OBJECT_CLASS (gs_plugin_fedora_pkgdb_collections_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_fedora_pkgdb_collections_finalize (GObject *object)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (object);
+
+ g_clear_pointer (&self->distros, g_ptr_array_unref);
+ g_clear_pointer (&self->os_name, g_free);
+ g_clear_pointer (&self->cachefn, g_free);
+
+ G_OBJECT_CLASS (gs_plugin_fedora_pkgdb_collections_parent_class)->finalize (object);
+}
+
+/* Runs in the main thread. */
+static void
+_file_changed_cb (GFileMonitor *monitor,
+ GFile *file, GFile *other_file,
+ GFileMonitorEvent event_type,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (user_data);
+
+ g_debug ("cache file changed, so reloading upgrades list");
+ gs_plugin_updates_changed (GS_PLUGIN (self));
+
+ self->is_valid = FALSE;
+}
+
+static void
+gs_plugin_fedora_pkgdb_collections_setup_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (plugin);
+ const gchar *verstr = NULL;
+ gchar *endptr = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GsOsRelease) os_release = NULL;
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_fedora_pkgdb_collections_setup_async);
+
+ /* get the file to cache */
+ self->cachefn = gs_utils_get_cache_filename ("fedora-pkgdb-collections",
+ "fedora.json",
+ GS_UTILS_CACHE_FLAG_WRITEABLE |
+ GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY,
+ &local_error);
+ if (self->cachefn == NULL) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* watch this in case it is changed by the user */
+ file = g_file_new_for_path (self->cachefn);
+ self->cachefn_monitor = g_file_monitor (file,
+ G_FILE_MONITOR_NONE,
+ cancellable,
+ &local_error);
+ if (self->cachefn_monitor == NULL) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_signal_connect (self->cachefn_monitor, "changed",
+ G_CALLBACK (_file_changed_cb), plugin);
+
+ /* read os-release for the current versions */
+ os_release = gs_os_release_new (&local_error);
+ if (os_release == NULL) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ self->os_name = g_strdup (gs_os_release_get_name (os_release));
+ if (self->os_name == NULL) {
+ g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "OS release had no name");
+ return;
+ }
+
+ verstr = gs_os_release_get_version_id (os_release);
+ if (verstr == NULL) {
+ g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "OS release had no version ID");
+ return;
+ }
+
+ /* parse the version */
+ self->os_version = g_ascii_strtoull (verstr, &endptr, 10);
+ if (endptr == verstr || self->os_version > G_MAXUINT) {
+ g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "Failed parse VERSION_ID: %s", verstr);
+ return;
+ }
+
+ /* add source */
+ self->cached_origin = gs_app_new (gs_plugin_get_name (plugin));
+ gs_app_set_kind (self->cached_origin, AS_COMPONENT_KIND_REPOSITORY);
+ gs_app_set_origin_hostname (self->cached_origin,
+ FEDORA_PKGDB_COLLECTIONS_API_URI);
+ gs_app_set_management_plugin (self->cached_origin, plugin);
+
+ /* add the source to the plugin cache which allows us to match the
+ * unique ID to a GsApp when creating an event */
+ gs_plugin_cache_add (plugin,
+ gs_app_get_unique_id (self->cached_origin),
+ self->cached_origin);
+
+ /* success */
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_fedora_pkgdb_collections_setup_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void download_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+_refresh_cache_async (GsPluginFedoraPkgdbCollections *self,
+ guint64 cache_age_secs,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (self);
+ g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin));
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GFile) output_file = g_file_new_for_path (self->cachefn);
+ g_autoptr(SoupSession) soup_session = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, _refresh_cache_async);
+
+ /* check cache age */
+ if (cache_age_secs > 0) {
+ guint64 tmp = gs_utils_get_file_age (output_file);
+ if (tmp < cache_age_secs) {
+ g_debug ("%s is only %" G_GUINT64_FORMAT " seconds old",
+ self->cachefn, tmp);
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+ }
+
+ /* download new file */
+ gs_app_set_summary_missing (app_dl,
+ /* TRANSLATORS: status text when downloading */
+ _("Downloading upgrade information…"));
+
+ soup_session = gs_build_soup_session ();
+
+ gs_download_file_async (soup_session,
+ FEDORA_PKGDB_COLLECTIONS_API_URI,
+ output_file,
+ G_PRIORITY_LOW,
+ NULL, NULL, /* FIXME: progress reporting */
+ cancellable,
+ download_cb,
+ g_steal_pointer (&task));
+}
+
+static void
+download_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);
+ GsPluginFedoraPkgdbCollections *self = g_task_get_source_object (task);
+ 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_autoptr(GError) wrapped_error = NULL;
+
+ /* Wrap in a GsPluginError. */
+ g_set_error_literal (&wrapped_error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ local_error->message);
+
+ gs_utils_error_add_origin_id (&wrapped_error, self->cached_origin);
+ g_task_return_error (task, g_steal_pointer (&wrapped_error));
+ return;
+ }
+
+ /* success */
+ self->is_valid = FALSE;
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+_refresh_cache_finish (GsPluginFedoraPkgdbCollections *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_fedora_pkgdb_collections_refresh_metadata_async (GsPlugin *plugin,
+ guint64 cache_age_secs,
+ GsPluginRefreshMetadataFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (plugin);
+ _refresh_cache_async (self, cache_age_secs, cancellable, callback, user_data);
+}
+
+static gboolean
+gs_plugin_fedora_pkgdb_collections_refresh_metadata_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return _refresh_cache_finish (GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (plugin),
+ result,
+ error);
+}
+
+static gchar *
+_get_upgrade_css_background (guint version)
+{
+ g_autofree gchar *version_str = g_strdup_printf ("%u", version);
+ g_autofree gchar *filename0 = NULL;
+ g_autofree gchar *filename1 = NULL;
+ g_autofree gchar *filename2 = NULL;
+
+ /* Check the standard location. */
+ filename0 = gs_utils_get_upgrade_background (version_str);
+ if (filename0 != NULL)
+ return g_strdup_printf ("url('file://%s')", filename0);
+
+ /* Fedora-specific locations. Deprecated. */
+ filename1 = g_strdup_printf ("/usr/share/backgrounds/f%u/default/standard/f%u.png", version, version);
+ if (g_file_test (filename1, G_FILE_TEST_EXISTS))
+ return g_strdup_printf ("url('file://%s')", filename1);
+
+ filename2 = g_strdup_printf ("/usr/share/gnome-software/backgrounds/f%u.png", version);
+ if (g_file_test (filename2, G_FILE_TEST_EXISTS))
+ return g_strdup_printf ("url('file://%s')", filename2);
+
+ return NULL;
+}
+
+static gint
+_sort_items_cb (gconstpointer a, gconstpointer b)
+{
+ PkgdbItem *item_a = *((PkgdbItem **) a);
+ PkgdbItem *item_b = *((PkgdbItem **) b);
+
+ if (item_a->version > item_b->version)
+ return 1;
+ if (item_a->version < item_b->version)
+ return -1;
+ return 0;
+}
+
+static GsApp *
+_create_upgrade_from_info (GsPluginFedoraPkgdbCollections *self,
+ PkgdbItem *item)
+{
+ GsApp *app;
+ g_autofree gchar *app_id = NULL;
+ g_autofree gchar *app_version = NULL;
+ g_autofree gchar *background = NULL;
+ g_autofree gchar *cache_key = NULL;
+ g_autofree gchar *css = NULL;
+ g_autofree gchar *url = NULL;
+ g_autoptr(GFile) icon_file = NULL;
+ g_autoptr(GIcon) ic = NULL;
+
+ /* search in the cache */
+ cache_key = g_strdup_printf ("release-%u", item->version);
+ app = gs_plugin_cache_lookup (GS_PLUGIN (self), cache_key);
+ if (app != NULL)
+ return app;
+
+ app_id = g_strdup_printf ("org.fedoraproject.fedora-%u", item->version);
+ app_version = g_strdup_printf ("%u", item->version);
+
+ /* icon from disk */
+ icon_file = g_file_new_for_path ("/usr/share/pixmaps/fedora-logo-sprite.png");
+ ic = g_file_icon_new (icon_file);
+
+ /* create */
+ app = gs_app_new (app_id);
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
+ gs_app_set_kind (app, AS_COMPONENT_KIND_OPERATING_SYSTEM);
+ gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST, item->name);
+ gs_app_set_summary (app, GS_APP_QUALITY_LOWEST,
+ /* TRANSLATORS: this is a title for Fedora distro upgrades */
+ _("Upgrade for the latest features, performance and stability improvements."));
+ gs_app_set_version (app, app_version);
+ gs_app_set_size_installed (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
+ gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0);
+ gs_app_set_license (app, GS_APP_QUALITY_LOWEST, "LicenseRef-free");
+ gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
+ gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE);
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_REVIEWABLE);
+ gs_app_add_icon (app, ic);
+
+ /* show a Fedora magazine article for the release */
+ url = g_strdup_printf ("https://fedoramagazine.org/whats-new-fedora-%u-workstation",
+ item->version);
+ gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);
+
+ /* use a fancy background if possible, and suppress the border which is
+ * shown by default; the background image is designed to be borderless */
+ background = _get_upgrade_css_background (item->version);
+ if (background != NULL) {
+ css = g_strdup_printf ("background: %s;"
+ "background-position: top;"
+ "background-size: 100%% 100%%;"
+ "color: white;"
+ "border-width: 0;",
+ background);
+ gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", css);
+ }
+
+ /* save in the cache */
+ gs_plugin_cache_add (GS_PLUGIN (self), cache_key, app);
+
+ /* success */
+ return app;
+}
+
+static gboolean
+_is_valid_upgrade (GsPluginFedoraPkgdbCollections *self,
+ PkgdbItem *item)
+{
+ /* only interested in upgrades to the same distro */
+ if (g_strcmp0 (item->name, self->os_name) != 0)
+ return FALSE;
+
+ /* only interested in newer versions, but not more than N+2 */
+ if (item->version <= self->os_version ||
+ item->version > self->os_version + 2)
+ return FALSE;
+
+ /* only interested in non-devel distros */
+ if (!g_settings_get_boolean (self->settings, "show-upgrade-prerelease")) {
+ if (item->status == PKGDB_ITEM_STATUS_DEVEL)
+ return FALSE;
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static GPtrArray *
+load_json (GsPluginFedoraPkgdbCollections *self,
+ GError **error)
+{
+ JsonArray *collections;
+ JsonNode *root_node;
+ JsonObject *root = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ g_autoptr(GPtrArray) new_distros = NULL;
+
+ new_distros = g_ptr_array_new_with_free_func ((GDestroyNotify) _pkgdb_item_free);
+ parser = json_parser_new_immutable ();
+
+ if (!json_parser_load_from_mapped_file (parser, self->cachefn, error))
+ return NULL;
+
+ root_node = json_parser_get_root (parser);
+ if (root_node != NULL && JSON_NODE_HOLDS_OBJECT (root_node))
+ root = json_node_get_object (root_node);
+ if (root == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "no root object");
+ return NULL;
+ }
+
+ collections = json_object_get_array_member (root, "collections");
+ if (collections == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "no collections object");
+ return NULL;
+ }
+
+ for (guint i = 0; i < json_array_get_length (collections); i++) {
+ PkgdbItem *item;
+ JsonObject *collection;
+ PkgdbItemStatus status;
+ const gchar *name;
+ const gchar *status_str;
+ const gchar *version_str;
+ gchar *endptr = NULL;
+ guint64 version;
+
+ collection = json_array_get_object_element (collections, i);
+ if (collection == NULL)
+ continue;
+
+ name = json_object_get_string_member (collection, "name");
+ if (name == NULL)
+ continue;
+
+ status_str = json_object_get_string_member (collection, "status");
+ if (status_str == NULL)
+ continue;
+
+ if (g_strcmp0 (status_str, "Active") == 0)
+ status = PKGDB_ITEM_STATUS_ACTIVE;
+ else if (g_strcmp0 (status_str, "Under Development") == 0)
+ status = PKGDB_ITEM_STATUS_DEVEL;
+ else if (g_strcmp0 (status_str, "EOL") == 0)
+ status = PKGDB_ITEM_STATUS_EOL;
+ else
+ continue;
+
+ version_str = json_object_get_string_member (collection, "version");
+ if (version_str == NULL)
+ continue;
+
+ version = g_ascii_strtoull (version_str, &endptr, 10);
+ if (endptr == version_str || version > G_MAXUINT)
+ continue;
+
+ /* add item */
+ item = g_slice_new0 (PkgdbItem);
+ item->name = g_strdup (name);
+ item->status = status;
+ item->version = (guint) version;
+ g_ptr_array_add (new_distros, item);
+ }
+
+ /* ensure in correct order */
+ g_ptr_array_sort (new_distros, _sort_items_cb);
+
+ /* success */
+ g_clear_pointer (&self->distros, g_ptr_array_unref);
+ self->distros = g_ptr_array_ref (new_distros);
+ self->is_valid = TRUE;
+
+ return g_steal_pointer (&new_distros);
+}
+
+static void ensure_refresh_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+/* This will return a strong reference to the latest distros
+ * #GPtrArray. The caller should use this in their computation. */
+static void
+_ensure_cache_async (GsPluginFedoraPkgdbCollections *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, _ensure_cache_async);
+
+ /* already done */
+ if (self->is_valid) {
+ g_task_return_pointer (task, g_ptr_array_ref (self->distros), (GDestroyNotify) g_ptr_array_unref);
+ return;
+ }
+
+ /* Ensure there is any data, no matter how old. This can download from
+ * the network if needed. */
+ _refresh_cache_async (self, G_MAXUINT, cancellable,
+ ensure_refresh_cb, g_steal_pointer (&task));
+}
+
+static void
+ensure_refresh_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (source_object);
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ g_autoptr(GPtrArray) distros = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ if (!_refresh_cache_finish (self, result, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ distros = load_json (self, &local_error);
+ if (distros == NULL) {
+ g_autoptr(GFile) cache_file = g_file_new_for_path (self->cachefn);
+
+ g_debug ("Failed to load cache file ‘%s’, deleting it", self->cachefn);
+ g_file_delete (cache_file, NULL, NULL);
+
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_task_return_pointer (task, g_steal_pointer (&distros), (GDestroyNotify) g_ptr_array_unref);
+}
+
+static GPtrArray *
+_ensure_cache_finish (GsPluginFedoraPkgdbCollections *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static PkgdbItem *
+_get_item_by_cpe_name (GPtrArray *distros,
+ const gchar *cpe_name)
+{
+ guint64 version;
+ g_auto(GStrv) split = NULL;
+
+ /* split up 'cpe:/o:fedoraproject:fedora:26' to sections */
+ split = g_strsplit (cpe_name, ":", -1);
+ if (g_strv_length (split) < 5) {
+ g_warning ("CPE invalid format: %s", cpe_name);
+ return NULL;
+ }
+
+ /* find the correct collection */
+ version = g_ascii_strtoull (split[4], NULL, 10);
+ if (version == 0) {
+ g_warning ("failed to parse CPE version: %s", split[4]);
+ return NULL;
+ }
+ for (guint i = 0; i < distros->len; i++) {
+ PkgdbItem *item = g_ptr_array_index (distros, i);
+ if (g_ascii_strcasecmp (item->name, split[3]) == 0 &&
+ item->version == version)
+ return item;
+ }
+ return NULL;
+}
+
+static void list_distro_upgrades_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+gs_plugin_fedora_pkgdb_collections_list_distro_upgrades_async (GsPlugin *plugin,
+ GsPluginListDistroUpgradesFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_fedora_pkgdb_collections_list_distro_upgrades_async);
+
+ /* Ensure valid data is loaded. */
+ _ensure_cache_async (self, cancellable, list_distro_upgrades_cb, g_steal_pointer (&task));
+}
+
+static void
+list_distro_upgrades_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (source_object);
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ g_autoptr(GPtrArray) distros = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ distros = _ensure_cache_finish (self, result, &local_error);
+ if (distros == NULL) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* are any distros upgradable */
+ list = gs_app_list_new ();
+
+ for (guint i = 0; i < distros->len; i++) {
+ PkgdbItem *item = g_ptr_array_index (distros, i);
+ if (_is_valid_upgrade (self, item)) {
+ g_autoptr(GsApp) app = NULL;
+ app = _create_upgrade_from_info (self, item);
+ gs_app_list_add (list, app);
+ }
+ }
+
+ g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
+}
+
+static GsAppList *
+gs_plugin_fedora_pkgdb_collections_list_distro_upgrades_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static gboolean
+refine_app (GPtrArray *distros,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PkgdbItem *item;
+ const gchar *cpe_name;
+
+ /* not for us */
+ if (gs_app_get_kind (app) != AS_COMPONENT_KIND_OPERATING_SYSTEM)
+ return TRUE;
+
+ /* not enough metadata */
+ cpe_name = gs_app_get_metadata_item (app, "GnomeSoftware::CpeName");
+ if (cpe_name == NULL)
+ return TRUE;
+
+ /* find item */
+ item = _get_item_by_cpe_name (distros, cpe_name);
+ if (item == NULL) {
+ g_warning ("did not find %s", cpe_name);
+ return TRUE;
+ }
+
+ /* fix the state */
+ switch (item->status) {
+ case PKGDB_ITEM_STATUS_ACTIVE:
+ case PKGDB_ITEM_STATUS_DEVEL:
+ gs_app_set_state (app, GS_APP_STATE_UPDATABLE);
+ break;
+ case PKGDB_ITEM_STATUS_EOL:
+ gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE);
+ break;
+ default:
+ break;
+ }
+
+ return TRUE;
+}
+
+static void refine_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+gs_plugin_fedora_pkgdb_collections_refine_async (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (plugin);
+ g_autoptr(GTask) task = NULL;
+ gboolean refine_needed = FALSE;
+ g_autoptr(GError) local_error = NULL;
+
+ task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_fedora_pkgdb_collections_refine_async);
+
+ /* Check if any of the apps actually need to be refined by this plugin,
+ * before potentially updating the collections file from the internet. */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_OPERATING_SYSTEM) {
+ refine_needed = TRUE;
+ break;
+ }
+ }
+
+ if (!refine_needed) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* ensure valid data is loaded */
+ _ensure_cache_async (self, cancellable, refine_cb, g_steal_pointer (&task));
+}
+
+static void
+refine_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPluginFedoraPkgdbCollections *self = GS_PLUGIN_FEDORA_PKGDB_COLLECTIONS (source_object);
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ GsPluginRefineData *data = g_task_get_task_data (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ g_autoptr(GPtrArray) distros = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ distros = _ensure_cache_finish (self, result, &local_error);
+ if (distros == NULL) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ for (guint i = 0; i < gs_app_list_length (data->list); i++) {
+ GsApp *app = gs_app_list_index (data->list, i);
+ if (!refine_app (distros, app, data->flags, cancellable, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_fedora_pkgdb_collections_refine_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_fedora_pkgdb_collections_class_init (GsPluginFedoraPkgdbCollectionsClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_fedora_pkgdb_collections_dispose;
+ object_class->finalize = gs_plugin_fedora_pkgdb_collections_finalize;
+
+ plugin_class->setup_async = gs_plugin_fedora_pkgdb_collections_setup_async;
+ plugin_class->setup_finish = gs_plugin_fedora_pkgdb_collections_setup_finish;
+ plugin_class->refine_async = gs_plugin_fedora_pkgdb_collections_refine_async;
+ plugin_class->refine_finish = gs_plugin_fedora_pkgdb_collections_refine_finish;
+ plugin_class->refresh_metadata_async = gs_plugin_fedora_pkgdb_collections_refresh_metadata_async;
+ plugin_class->refresh_metadata_finish = gs_plugin_fedora_pkgdb_collections_refresh_metadata_finish;
+ plugin_class->list_distro_upgrades_async = gs_plugin_fedora_pkgdb_collections_list_distro_upgrades_async;
+ plugin_class->list_distro_upgrades_finish = gs_plugin_fedora_pkgdb_collections_list_distro_upgrades_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_FEDORA_PKGDB_COLLECTIONS;
+}
diff --git a/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.h b/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.h
new file mode 100644
index 0000000..058dc2f
--- /dev/null
+++ b/plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.h
@@ -0,0 +1,22 @@
+/* -*- 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>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_FEDORA_PKGDB_COLLECTIONS (gs_plugin_fedora_pkgdb_collections_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginFedoraPkgdbCollections, gs_plugin_fedora_pkgdb_collections, GS, PLUGIN_FEDORA_PKGDB_COLLECTIONS, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/fedora-pkgdb-collections/meson.build b/plugins/fedora-pkgdb-collections/meson.build
new file mode 100644
index 0000000..9058572
--- /dev/null
+++ b/plugins/fedora-pkgdb-collections/meson.build
@@ -0,0 +1,14 @@
+cargs = ['-DG_LOG_DOMAIN="GsPluginFedoraPkgdbCollections"']
+
+shared_module(
+ 'gs_plugin_fedora-pkgdb-collections',
+ sources : 'gs-plugin-fedora-pkgdb-collections.c',
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : plugin_libs,
+)