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 /plugins | |
parent | Initial commit. (diff) | |
download | gnome-software-upstream.tar.xz gnome-software-upstream.zip |
Adding upstream version 43.5.upstream/43.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
158 files changed, 38179 insertions, 0 deletions
diff --git a/plugins/core/gs-plugin-appstream.c b/plugins/core/gs-plugin-appstream.c new file mode 100644 index 0000000..098e24a --- /dev/null +++ b/plugins/core/gs-plugin-appstream.c @@ -0,0 +1,1653 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2014 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <errno.h> +#include <gnome-software.h> +#include <xmlb.h> + +#include "gs-appstream.h" +#include "gs-external-appstream-utils.h" +#include "gs-plugin-appstream.h" + +/* + * SECTION: + * Uses offline AppStream data to populate and refine package results. + * + * This plugin calls UpdatesChanged() if any of the AppStream stores are + * changed in any way. + * + * Methods: | AddCategory + * Refines: | [source]->[name,summary,pixbuf,id,kind] + */ + +struct _GsPluginAppstream +{ + GsPlugin parent; + + GsWorkerThread *worker; /* (owned) */ + + XbSilo *silo; + GRWLock silo_lock; + GSettings *settings; +}; + +G_DEFINE_TYPE (GsPluginAppstream, gs_plugin_appstream, GS_TYPE_PLUGIN) + +#define assert_in_worker(self) \ + g_assert (gs_worker_thread_is_in_worker_context (self->worker)) + +static void +gs_plugin_appstream_dispose (GObject *object) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (object); + + g_clear_object (&self->silo); + g_clear_object (&self->settings); + g_rw_lock_clear (&self->silo_lock); + g_clear_object (&self->worker); + + G_OBJECT_CLASS (gs_plugin_appstream_parent_class)->dispose (object); +} + +static void +gs_plugin_appstream_init (GsPluginAppstream *self) +{ + GApplication *application = g_application_get_default (); + + /* XbSilo needs external locking as we destroy the silo and build a new + * one when something changes */ + g_rw_lock_init (&self->silo_lock); + + /* need package name */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "dpkg"); + + /* require settings */ + self->settings = g_settings_new ("org.gnome.software"); + + /* Can be NULL when running the self tests */ + if (application) { + g_signal_connect_object (application, "repository-changed", + G_CALLBACK (gs_plugin_update_cache_state_for_repository), self, G_CONNECT_SWAPPED); + } +} + +static const gchar * +gs_plugin_appstream_convert_component_kind (const gchar *kind) +{ + if (g_strcmp0 (kind, "webapp") == 0) + return "web-application"; + if (g_strcmp0 (kind, "desktop") == 0) + return "desktop-application"; + return kind; +} + +static gboolean +gs_plugin_appstream_upgrade_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "application") == 0) { + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + g_autofree gchar *kind = NULL; + if (id != NULL) { + kind = g_strdup (xb_builder_node_get_attr (id, "type")); + xb_builder_node_remove_attr (id, "type"); + } + if (kind != NULL) + xb_builder_node_set_attr (bn, "type", kind); + xb_builder_node_set_element (bn, "component"); + } else if (g_strcmp0 (xb_builder_node_get_element (bn), "metadata") == 0) { + xb_builder_node_set_element (bn, "custom"); + } else if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + const gchar *type_old = xb_builder_node_get_attr (bn, "type"); + const gchar *type_new = gs_plugin_appstream_convert_component_kind (type_old); + if (type_old != type_new) + xb_builder_node_set_attr (bn, "type", type_new); + } + return TRUE; +} + +static gboolean +gs_plugin_appstream_add_icons_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") != 0) + return TRUE; + gs_appstream_component_add_extra_info (bn); + return TRUE; +} + +static gboolean +gs_plugin_appstream_add_origin_keyword_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) { + const gchar *origin = xb_builder_node_get_attr (bn, "origin"); + GPtrArray *components = xb_builder_node_get_children (bn); + if (origin == NULL || origin[0] == '\0') + return TRUE; + g_debug ("origin %s has %u components", origin, components->len); + if (components->len < 200) { + for (guint i = 0; i < components->len; i++) { + XbBuilderNode *component = g_ptr_array_index (components, i); + gs_appstream_component_add_keyword (component, origin); + } + } + } + return TRUE; +} + +static void +gs_plugin_appstream_media_baseurl_free (gpointer user_data) +{ + g_string_free ((GString *) user_data, TRUE); +} + +static gboolean +gs_plugin_appstream_media_baseurl_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + GString *baseurl = user_data; + if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) { + const gchar *url = xb_builder_node_get_attr (bn, "media_baseurl"); + if (url == NULL) { + g_string_truncate (baseurl, 0); + return TRUE; + } + g_string_assign (baseurl, url); + return TRUE; + } + + if (baseurl->len == 0) + return TRUE; + + if (g_strcmp0 (xb_builder_node_get_element (bn), "icon") == 0) { + const gchar *type = xb_builder_node_get_attr (bn, "type"); + if (g_strcmp0 (type, "remote") != 0) + return TRUE; + gs_appstream_component_fix_url (bn, baseurl->str); + } else if (g_strcmp0 (xb_builder_node_get_element (bn), "screenshots") == 0) { + GPtrArray *screenshots = xb_builder_node_get_children (bn); + for (guint i = 0; i < screenshots->len; i++) { + XbBuilderNode *screenshot = g_ptr_array_index (screenshots, i); + GPtrArray *children = NULL; + /* Type-check for security */ + if (g_strcmp0 (xb_builder_node_get_element (screenshot), "screenshot") != 0) { + continue; + } + children = xb_builder_node_get_children (screenshot); + for (guint j = 0; j < children->len; j++) { + XbBuilderNode *child = g_ptr_array_index (children, j); + const gchar *element = xb_builder_node_get_element (child); + if (g_strcmp0 (element, "image") != 0 && + g_strcmp0 (element, "video") != 0) + continue; + gs_appstream_component_fix_url (child, baseurl->str); + } + } + } + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_appdata_fn (GsPluginAppstream *self, + XbBuilder *builder, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderFixup) fixup = NULL; + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* fix up any legacy installed files */ + fixup = xb_builder_fixup_new ("AppStreamUpgrade2", + gs_plugin_appstream_upgrade_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup, 3); + xb_builder_source_add_fixup (source, fixup); + + /* add metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "filename", filename, NULL); + xb_builder_source_set_info (source, info); + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_appdata (GsPluginAppstream *self, + XbBuilder *builder, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GDir) dir = NULL; + g_autoptr(GFile) parent = g_file_new_for_path (path); + if (!g_file_query_exists (parent, cancellable)) { + g_debug ("appstream: Skipping appdata path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist"); + return TRUE; + } + + g_debug ("appstream: Loading appdata path '%s'", path); + + dir = g_dir_open (path, 0, error); + if (dir == NULL) + return FALSE; + + while ((fn = g_dir_read_name (dir)) != NULL) { + if (g_str_has_suffix (fn, ".appdata.xml") || + g_str_has_suffix (fn, ".metainfo.xml")) { + g_autofree gchar *filename = g_build_filename (path, fn, NULL); + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_appstream_load_appdata_fn (self, + builder, + filename, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } + } + + /* success */ + return TRUE; +} + +static GInputStream * +gs_plugin_appstream_load_desktop_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *xml = NULL; + g_autoptr(AsComponent) cpt = as_component_new (); + g_autoptr(AsContext) actx = as_context_new (); + g_autoptr(GBytes) bytes = NULL; + gboolean ret; + + bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error); + if (bytes == NULL) + return NULL; + + as_component_set_id (cpt, xb_builder_source_ctx_get_filename (ctx)); + ret = as_component_load_from_bytes (cpt, + actx, + AS_FORMAT_KIND_DESKTOP_ENTRY, + bytes, + error); + if (!ret) + return NULL; + xml = as_component_to_xml_data (cpt, actx, error); + if (xml == NULL) + return NULL; + return g_memory_input_stream_new_from_data (g_steal_pointer (&xml), (gssize) -1, g_free); +} + +static gboolean +gs_plugin_appstream_load_desktop_fn (GsPluginAppstream *self, + XbBuilder *builder, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + /* add support for desktop files */ + xb_builder_source_add_adapter (source, "application/x-desktop", + gs_plugin_appstream_load_desktop_cb, NULL, NULL); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* add metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "filename", filename, NULL); + xb_builder_source_set_info (source, info); + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_desktop (GsPluginAppstream *self, + XbBuilder *builder, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GDir) dir = NULL; + g_autoptr(GFile) parent = g_file_new_for_path (path); + if (!g_file_query_exists (parent, cancellable)) { + g_debug ("appstream: Skipping desktop path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist"); + return TRUE; + } + + g_debug ("appstream: Loading desktop path '%s'", path); + + dir = g_dir_open (path, 0, error); + if (dir == NULL) + return FALSE; + + while ((fn = g_dir_read_name (dir)) != NULL) { + if (g_str_has_suffix (fn, ".desktop")) { + g_autofree gchar *filename = g_build_filename (path, fn, NULL); + g_autoptr(GError) error_local = NULL; + if (g_strcmp0 (fn, "mimeinfo.cache") == 0) + continue; + if (!gs_plugin_appstream_load_desktop_fn (self, + builder, + filename, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } + } + + /* success */ + return TRUE; +} + +static GInputStream * +gs_plugin_appstream_load_dep11_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(AsMetadata) mdata = as_metadata_new (); + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GError) tmp_error = NULL; + g_autofree gchar *xml = NULL; + + bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error); + if (bytes == NULL) + return NULL; + + as_metadata_set_format_style (mdata, AS_FORMAT_STYLE_COLLECTION); + as_metadata_parse_bytes (mdata, + bytes, + AS_FORMAT_KIND_YAML, + &tmp_error); + if (tmp_error != NULL) { + g_propagate_error (error, g_steal_pointer (&tmp_error)); + return NULL; + } + + xml = as_metadata_components_to_collection (mdata, AS_FORMAT_KIND_XML, &tmp_error); + if (xml == NULL) { + // This API currently returns NULL if there is nothing to serialize, so we + // have to test if this is an error or not. + // See https://gitlab.gnome.org/GNOME/gnome-software/-/merge_requests/763 + // for discussion about changing this API. + if (tmp_error != NULL) { + g_propagate_error (error, g_steal_pointer (&tmp_error)); + return NULL; + } + + xml = g_strdup(""); + } + + return g_memory_input_stream_new_from_data (g_steal_pointer (&xml), (gssize) -1, g_free); +} + +#if LIBXMLB_CHECK_VERSION(0,3,1) +static gboolean +gs_plugin_appstream_tokenize_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const gchar * const elements_to_tokenize[] = { + "id", + "keyword", + "launchable", + "mimetype", + "name", + "pkgname", + "summary", + NULL }; + if (xb_builder_node_get_element (bn) != NULL && + g_strv_contains (elements_to_tokenize, xb_builder_node_get_element (bn))) + xb_builder_node_tokenize_text (bn); + return TRUE; +} +#endif + +static gboolean +gs_plugin_appstream_load_appstream_fn (GsPluginAppstream *self, + XbBuilder *builder, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderFixup) fixup1 = NULL; + g_autoptr(XbBuilderFixup) fixup2 = NULL; + g_autoptr(XbBuilderFixup) fixup3 = NULL; +#if LIBXMLB_CHECK_VERSION(0,3,1) + g_autoptr(XbBuilderFixup) fixup4 = NULL; +#endif + g_autoptr(XbBuilderFixup) fixup5 = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + /* add support for DEP-11 files */ + xb_builder_source_add_adapter (source, + "application/x-yaml", + gs_plugin_appstream_load_dep11_cb, + NULL, NULL); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* add metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", "system", NULL); + xb_builder_node_insert_text (info, "filename", filename, NULL); + xb_builder_source_set_info (source, info); + + /* add missing icons as required */ + fixup1 = xb_builder_fixup_new ("AddIcons", + gs_plugin_appstream_add_icons_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup1, 2); + xb_builder_source_add_fixup (source, fixup1); + + /* fix up any legacy installed files */ + fixup2 = xb_builder_fixup_new ("AppStreamUpgrade2", + gs_plugin_appstream_upgrade_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup2, 3); + xb_builder_source_add_fixup (source, fixup2); + + /* add the origin as a search keyword for small repos */ + fixup3 = xb_builder_fixup_new ("AddOriginKeyword", + gs_plugin_appstream_add_origin_keyword_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup3, 1); + xb_builder_source_add_fixup (source, fixup3); + +#if LIBXMLB_CHECK_VERSION(0,3,1) + fixup4 = xb_builder_fixup_new ("TextTokenize", + gs_plugin_appstream_tokenize_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup4, 2); + xb_builder_source_add_fixup (source, fixup4); +#endif + + /* prepend media_baseurl to remote relative URLs */ + fixup5 = xb_builder_fixup_new ("MediaBaseUrl", + gs_plugin_appstream_media_baseurl_cb, + g_string_new (NULL), + gs_plugin_appstream_media_baseurl_free); + xb_builder_fixup_set_max_depth (fixup5, 3); + xb_builder_source_add_fixup (source, fixup5); + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_appstream (GsPluginAppstream *self, + XbBuilder *builder, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GDir) dir = NULL; + g_autoptr(GFile) parent = g_file_new_for_path (path); + + /* parent path does not exist */ + if (!g_file_query_exists (parent, cancellable)) { + g_debug ("appstream: Skipping appstream path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist"); + return TRUE; + } + g_debug ("appstream: Loading appstream path '%s'", path); + dir = g_dir_open (path, 0, error); + if (dir == NULL) + return FALSE; + while ((fn = g_dir_read_name (dir)) != NULL) { +#ifdef ENABLE_EXTERNAL_APPSTREAM + /* Ignore our own system-installed files when + external-appstream-system-wide is FALSE */ + if (!g_settings_get_boolean (self->settings, "external-appstream-system-wide") && + g_strcmp0 (path, gs_external_appstream_utils_get_system_dir ()) == 0 && + g_str_has_prefix (fn, EXTERNAL_APPSTREAM_PREFIX)) + continue; +#endif + if (g_str_has_suffix (fn, ".xml") || + g_str_has_suffix (fn, ".yml") || + g_str_has_suffix (fn, ".yml.gz") || + g_str_has_suffix (fn, ".xml.gz")) { + g_autofree gchar *filename = g_build_filename (path, fn, NULL); + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_appstream_load_appstream_fn (self, + builder, + filename, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } + } + + /* success */ + return TRUE; +} + +static void +gs_add_appstream_catalog_location (GPtrArray *locations, const gchar *root) +{ + g_autofree gchar *catalog_path = NULL; + g_autofree gchar *catalog_legacy_path = NULL; + gboolean ignore_legacy_path = FALSE; + + catalog_path = g_build_filename (root, "swcatalog", NULL); + catalog_legacy_path = g_build_filename (root, "app-info", NULL); + + /* ignore compatibility symlink if one exists, so we don't scan the same location twice */ + if (g_file_test (catalog_legacy_path, G_FILE_TEST_IS_SYMLINK)) { + g_autofree gchar *link_target = g_file_read_link (catalog_legacy_path, NULL); + if (link_target != NULL) { + if (g_strcmp0 (link_target, catalog_path) == 0) { + ignore_legacy_path = TRUE; + g_debug ("Ignoring legacy AppStream catalog location '%s'.", catalog_legacy_path); + } + } + } + + g_ptr_array_add (locations, + g_build_filename (catalog_path, "xml", NULL)); + g_ptr_array_add (locations, + g_build_filename (catalog_path, "yaml", NULL)); + + if (!ignore_legacy_path) { + g_ptr_array_add (locations, + g_build_filename (catalog_legacy_path, "xml", NULL)); + g_ptr_array_add (locations, + g_build_filename (catalog_legacy_path, "xmls", NULL)); + g_ptr_array_add (locations, + g_build_filename (catalog_legacy_path, "yaml", NULL)); + } +} + +static void +gs_add_appstream_metainfo_location (GPtrArray *locations, const gchar *root) +{ + g_ptr_array_add (locations, + g_build_filename (root, "metainfo", NULL)); + g_ptr_array_add (locations, + g_build_filename (root, "appdata", NULL)); +} + +static gboolean +gs_plugin_appstream_check_silo (GsPluginAppstream *self, + GCancellable *cancellable, + GError **error) +{ + const gchar *test_xml; + g_autofree gchar *blobfn = NULL; + g_autoptr(XbBuilder) builder = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GRWLockReaderLocker) reader_locker = NULL; + g_autoptr(GRWLockWriterLocker) writer_locker = NULL; + g_autoptr(GPtrArray) parent_appdata = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GPtrArray) parent_appstream = g_ptr_array_new_with_free_func (g_free); + const gchar *const *locales = g_get_language_names (); + g_autoptr(GMainContext) old_thread_default = NULL; + + reader_locker = g_rw_lock_reader_locker_new (&self->silo_lock); + /* everything is okay */ + if (self->silo != NULL && xb_silo_is_valid (self->silo)) + return TRUE; + g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free); + + /* drat! silo needs regenerating */ + writer_locker = g_rw_lock_writer_locker_new (&self->silo_lock); + g_clear_object (&self->silo); + + /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */ + old_thread_default = g_main_context_ref_thread_default (); + if (old_thread_default == g_main_context_default ()) + g_clear_pointer (&old_thread_default, g_main_context_unref); + if (old_thread_default != NULL) + g_main_context_pop_thread_default (old_thread_default); + builder = xb_builder_new (); + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + g_clear_pointer (&old_thread_default, g_main_context_unref); + + /* verbose profiling */ + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + xb_builder_set_profile_flags (builder, + XB_SILO_PROFILE_FLAG_XPATH | + XB_SILO_PROFILE_FLAG_DEBUG); + } + + /* add current locales */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + /* only when in self test */ + test_xml = g_getenv ("GS_SELF_TEST_APPSTREAM_XML"); + if (test_xml != NULL) { + g_autoptr(XbBuilderFixup) fixup1 = NULL; + g_autoptr(XbBuilderFixup) fixup2 = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + if (!xb_builder_source_load_xml (source, test_xml, + XB_BUILDER_SOURCE_FLAG_NONE, + error)) + return FALSE; + fixup1 = xb_builder_fixup_new ("AddOriginKeywords", + gs_plugin_appstream_add_origin_keyword_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup1, 1); + xb_builder_source_add_fixup (source, fixup1); + fixup2 = xb_builder_fixup_new ("AddIcons", + gs_plugin_appstream_add_icons_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup2, 2); + xb_builder_source_add_fixup (source, fixup2); + xb_builder_import_source (builder, source); + } else { + g_autofree gchar *state_cache_dir = NULL; + g_autofree gchar *state_lib_dir = NULL; + + /* add search paths */ + gs_add_appstream_catalog_location (parent_appstream, DATADIR); + gs_add_appstream_metainfo_location (parent_appdata, DATADIR); + + state_cache_dir = g_build_filename (LOCALSTATEDIR, "cache", NULL); + gs_add_appstream_catalog_location (parent_appstream, state_cache_dir); + state_lib_dir = g_build_filename (LOCALSTATEDIR, "lib", NULL); + gs_add_appstream_catalog_location (parent_appstream, state_lib_dir); + +#ifdef ENABLE_EXTERNAL_APPSTREAM + /* check for the corresponding setting */ + if (!g_settings_get_boolean (self->settings, "external-appstream-system-wide")) { + g_autofree gchar *user_catalog_path = NULL; + g_autofree gchar *user_catalog_old_path = NULL; + + /* migrate data paths */ + user_catalog_path = g_build_filename (g_get_user_data_dir (), "swcatalog", NULL); + user_catalog_old_path = g_build_filename (g_get_user_data_dir (), "app-info", NULL); + if (g_file_test (user_catalog_old_path, G_FILE_TEST_IS_DIR) && + !g_file_test (user_catalog_path, G_FILE_TEST_IS_DIR)) { + g_debug ("Migrating external AppStream user location."); + if (g_rename (user_catalog_old_path, user_catalog_path) == 0) { + g_autofree gchar *user_catalog_xml_path = NULL; + g_autofree gchar *user_catalog_xml_old_path = NULL; + + user_catalog_xml_path = g_build_filename (user_catalog_path, "xml", NULL); + user_catalog_xml_old_path = g_build_filename (user_catalog_path, "xmls", NULL); + if (g_file_test (user_catalog_xml_old_path, G_FILE_TEST_IS_DIR)) { + if (g_rename (user_catalog_xml_old_path, user_catalog_xml_path) != 0) + g_warning ("Unable to migrate external XML data location from '%s' to '%s': %s", + user_catalog_xml_old_path, user_catalog_xml_path, g_strerror (errno)); + } + } else { + g_warning ("Unable to migrate external data location from '%s' to '%s': %s", + user_catalog_old_path, user_catalog_path, g_strerror (errno)); + } + + } + + /* add modern locations only */ + g_ptr_array_add (parent_appstream, + g_build_filename (user_catalog_path, "xml", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename (user_catalog_path, "yaml", NULL)); + } +#endif + + /* Add the normal system directories if the installation prefix + * is different from normal — typically this happens when doing + * development builds. It’s useful to still list the system apps + * during development. */ + if (g_strcmp0 (DATADIR, "/usr/share") != 0) { + gs_add_appstream_catalog_location (parent_appstream, "/usr/share"); + gs_add_appstream_metainfo_location (parent_appdata, "/usr/share"); + } + if (g_strcmp0 (LOCALSTATEDIR, "/var") != 0) { + gs_add_appstream_catalog_location (parent_appstream, "/var/cache"); + gs_add_appstream_catalog_location (parent_appstream, "/var/lib"); + } + + /* import all files */ + for (guint i = 0; i < parent_appstream->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appstream, i); + if (!gs_plugin_appstream_load_appstream (self, builder, fn, + cancellable, error)) + return FALSE; + } + for (guint i = 0; i < parent_appdata->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appdata, i); + if (!gs_plugin_appstream_load_appdata (self, builder, fn, + cancellable, error)) + return FALSE; + } + if (!gs_plugin_appstream_load_desktop (self, builder, + DATADIR "/applications", + cancellable, error)) { + return FALSE; + } + if (g_strcmp0 (DATADIR, "/usr/share") != 0 && + !gs_plugin_appstream_load_desktop (self, builder, + "/usr/share/applications", + cancellable, error)) { + return FALSE; + } + } + + /* regenerate with each minor release */ + xb_builder_append_guid (builder, PACKAGE_VERSION); + + /* create per-user cache */ + blobfn = gs_utils_get_cache_filename ("appstream", "components.xmlb", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + error); + if (blobfn == NULL) + return FALSE; + file = g_file_new_for_path (blobfn); + g_debug ("ensuring %s", blobfn); + + /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */ + old_thread_default = g_main_context_ref_thread_default (); + if (old_thread_default == g_main_context_default ()) + g_clear_pointer (&old_thread_default, g_main_context_unref); + if (old_thread_default != NULL) + g_main_context_pop_thread_default (old_thread_default); + + self->silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID | + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + NULL, error); + if (self->silo == NULL) { + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + return FALSE; + } + + /* watch all directories too */ + for (guint i = 0; i < parent_appstream->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appstream, i); + g_autoptr(GFile) file_tmp = g_file_new_for_path (fn); + if (!xb_silo_watch_file (self->silo, file_tmp, cancellable, error)) { + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + return FALSE; + } + } + for (guint i = 0; i < parent_appdata->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appdata, i); + g_autoptr(GFile) file_tmp = g_file_new_for_path (fn); + if (!xb_silo_watch_file (self->silo, file_tmp, cancellable, error)) { + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + return FALSE; + } + } + + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + + /* test we found something */ + n = xb_silo_query_first (self->silo, "components/component", NULL); + if (n == NULL) { + g_warning ("No AppStream data, try 'make install-sample-data' in data/"); + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "No AppStream data found"); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gint +get_priority_for_interactivity (gboolean interactive) +{ + return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW; +} + +static void setup_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_appstream_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_appstream_setup_async); + + /* Start up a worker thread to process all the plugin’s function calls. */ + self->worker = gs_worker_thread_new ("gs-plugin-appstream"); + + /* Queue a job to check the silo, which will cause it to be loaded. */ + gs_worker_thread_queue (self->worker, G_PRIORITY_DEFAULT, + setup_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +setup_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_appstream_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_appstream_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_appstream_shutdown_async); + + /* Stop the worker thread. */ + gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task)); +} + +static void +shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginAppstream *self = g_task_get_source_object (task); + g_autoptr(GsWorkerThread) worker = NULL; + g_autoptr(GError) local_error = NULL; + + worker = g_steal_pointer (&self->worker); + + if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_appstream_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (self, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + return gs_appstream_url_to_app (plugin, self->silo, list, url, cancellable, error); +} + +static void +gs_plugin_appstream_set_compulsory_quirk (GsApp *app, XbNode *component) +{ + g_autoptr(GPtrArray) array = NULL; + const gchar *current_desktop; + + /* + * Set the core applications for the current desktop that cannot be + * removed. + * + * If XDG_CURRENT_DESKTOP contains ":", indicating that it is made up + * of multiple components per the Desktop Entry Specification, an app + * is compulsory if any of the components in XDG_CURRENT_DESKTOP match + * any value in <compulsory_for_desktops />. In that way, + * "GNOME-Classic:GNOME" shares compulsory apps with GNOME. + * + * As a special case, if the <compulsory_for_desktop /> value contains + * a ":", we match the entire XDG_CURRENT_DESKTOP. This lets people set + * compulsory apps for such compound desktops if they want. + * + */ + array = xb_node_query (component, "compulsory_for_desktop", 0, NULL); + if (array == NULL) + return; + current_desktop = g_getenv ("XDG_CURRENT_DESKTOP"); + if (current_desktop != NULL) { + g_auto(GStrv) xdg_current_desktops = g_strsplit (current_desktop, ":", 0); + for (guint i = 0; i < array->len; i++) { + XbNode *n = g_ptr_array_index (array, i); + const gchar *tmp = xb_node_get_text (n); + /* if the value has a :, check the whole string */ + if (g_strstr_len (tmp, -1, ":")) { + if (g_strcmp0 (current_desktop, tmp) == 0) { + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); + break; + } + /* otherwise check if any element matches this one */ + } else if (g_strv_contains ((const gchar * const *) xdg_current_desktops, tmp)) { + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); + break; + } + } + } +} + +static gboolean +gs_plugin_appstream_refine_state (GsPluginAppstream *self, + GsApp *app, + GError **error) +{ + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(XbNode) component = NULL; + + /* Ignore apps with no ID */ + if (gs_app_get_id (app) == NULL) + return TRUE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + xpath = g_strdup_printf ("component/id[text()='%s']", gs_app_get_id (app)); + component = xb_silo_query_first (self->silo, xpath, &error_local); + if (component == 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; + } + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + return TRUE; +} + +static gboolean +gs_plugin_refine_from_id (GsPluginAppstream *self, + GsApp *app, + GsPluginRefineFlags flags, + gboolean *found, + GError **error) +{ + const gchar *id, *origin; + g_autoptr(GError) error_local = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GString) xpath = g_string_new (NULL); + g_autoptr(GPtrArray) components = NULL; + + /* not enough info to find */ + id = gs_app_get_id (app); + if (id == NULL) + return TRUE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + origin = gs_app_get_origin_appstream (app); + + /* look in AppStream then fall back to AppData */ + if (origin && *origin) { + xb_string_append_union (xpath, "components[@origin='%s']/component/id[text()='%s']/../pkgname/..", origin, id); + xb_string_append_union (xpath, "components[@origin='%s']/component[@type='web-application']/id[text()='%s']/..", origin, id); + } else { + xb_string_append_union (xpath, "components/component/id[text()='%s']/../pkgname/..", id); + xb_string_append_union (xpath, "components/component[@type='web-application']/id[text()='%s']/..", id); + } + xb_string_append_union (xpath, "component/id[text()='%s']/..", id); + components = xb_silo_query (self->silo, xpath->str, 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); + if (!gs_appstream_refine_app (GS_PLUGIN (self), app, self->silo, + component, flags, error)) + return FALSE; + gs_plugin_appstream_set_compulsory_quirk (app, component); + } + + /* if an installed desktop or appdata file exists set to installed */ + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) { + if (!gs_plugin_appstream_refine_state (self, app, error)) + return FALSE; + } + + /* success */ + *found = TRUE; + return TRUE; +} + +static gboolean +gs_plugin_refine_from_pkgname (GsPluginAppstream *self, + GsApp *app, + GsPluginRefineFlags flags, + GError **error) +{ + GPtrArray *sources = gs_app_get_sources (app); + g_autoptr(GError) error_local = NULL; + + /* not enough info to find */ + if (sources->len == 0) + return TRUE; + + /* find all apps when matching any prefixes */ + for (guint j = 0; j < sources->len; j++) { + const gchar *pkgname = g_ptr_array_index (sources, j); + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GString) xpath = g_string_new (NULL); + g_autoptr(XbNode) component = NULL; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + /* prefer actual apps and then fallback to anything else */ + xb_string_append_union (xpath, "components/component[@type='desktop-application']/pkgname[text()='%s']/..", pkgname); + xb_string_append_union (xpath, "components/component[@type='console-application']/pkgname[text()='%s']/..", pkgname); + xb_string_append_union (xpath, "components/component[@type='web-application']/pkgname[text()='%s']/..", pkgname); + xb_string_append_union (xpath, "components/component/pkgname[text()='%s']/..", pkgname); + component = xb_silo_query_first (self->silo, xpath->str, &error_local); + if (component == 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; + } + if (!gs_appstream_refine_app (GS_PLUGIN (self), app, self->silo, component, flags, error)) + return FALSE; + gs_plugin_appstream_set_compulsory_quirk (app, component); + } + + /* if an installed desktop or appdata file exists set to installed */ + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) { + if (!gs_plugin_appstream_refine_state (self, app, error)) + return FALSE; + } + + /* success */ + return TRUE; +} + +static void refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_appstream_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_appstream_refine_async); + + /* Queue a job for the refine. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refine_thread_cb, g_steal_pointer (&task)); +} + +static gboolean refine_wildcard (GsPluginAppstream *self, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags refine_flags, + GCancellable *cancellable, + GError **error); + +/* Run in @worker. */ +static void +refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object); + GsPluginRefineData *data = task_data; + GsAppList *list = data->list; + GsPluginRefineFlags flags = data->flags; + gboolean found = FALSE; + g_autoptr(GsAppList) app_list = NULL; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* not us */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE && + gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_UNKNOWN) + continue; + + /* find by ID then fall back to package name */ + if (!gs_plugin_refine_from_id (self, app, flags, &found, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + if (!found) { + if (!gs_plugin_refine_from_pkgname (self, app, flags, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + } + + /* Refine wildcards. + * + * Use a copy of the list for the loop because a function called + * on the plugin may affect the list which can lead to problems + * (e.g. inserting an app in the list on every call results in + * an infinite loop) */ + app_list = gs_app_list_copy (list); + + for (guint j = 0; j < gs_app_list_length (app_list); j++) { + GsApp *app = gs_app_list_index (app_list, j); + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD) && + !refine_wildcard (self, app, list, flags, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + + /* success */ + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_appstream_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +/* Run in @worker. Silo must be valid */ +static gboolean +refine_wildcard (GsPluginAppstream *self, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags refine_flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *id; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GPtrArray) components = NULL; + + /* not enough info to find */ + id = gs_app_get_id (app); + if (id == NULL) + return TRUE; + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + /* find all app with package names when matching any prefixes */ + xpath = g_strdup_printf ("components/component/id[text()='%s']/../pkgname/..", id); + components = xb_silo_query (self->silo, xpath, 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); + g_autoptr(GsApp) new = NULL; + + /* new app */ + new = gs_appstream_create_app (GS_PLUGIN (self), self->silo, component, error); + if (new == NULL) + return FALSE; + gs_app_set_scope (new, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_subsume_metadata (new, app); + if (!gs_appstream_refine_app (GS_PLUGIN (self), new, self->silo, component, + refine_flags, error)) + return FALSE; + gs_plugin_appstream_set_compulsory_quirk (new, component); + + /* if an installed desktop or appdata file exists set to installed */ + if (gs_app_get_state (new) == GS_APP_STATE_UNKNOWN) { + if (!gs_plugin_appstream_refine_state (self, new, error)) + return FALSE; + } + + gs_app_list_add (list, new); + } + + /* success */ + return TRUE; +} + +static void refine_categories_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_appstream_refine_categories_async (GsPlugin *plugin, + GPtrArray *list, + GsPluginRefineCategoriesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_categories_data_new_task (plugin, list, flags, + cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_appstream_refine_categories_async); + + /* All we actually do is add the sizes of each category. If that’s + * not been requested, avoid queueing a worker job. */ + if (!(flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_SIZE)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* Queue a job to get the apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refine_categories_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refine_categories_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object); + g_autoptr(GRWLockReaderLocker) locker = NULL; + GsPluginRefineCategoriesData *data = task_data; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + if (!gs_appstream_refine_category_sizes (self->silo, data->list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_appstream_refine_categories_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_appstream_list_apps_async (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE); + + task = gs_plugin_list_apps_data_new_task (plugin, query, flags, + cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_appstream_list_apps_async); + + /* Queue a job to get the apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + list_apps_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object); + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GsAppList) list = gs_app_list_new (); + GsPluginListAppsData *data = task_data; + GDateTime *released_since = NULL; + GsAppQueryTristate is_curated = GS_APP_QUERY_TRISTATE_UNSET; + GsAppQueryTristate is_featured = GS_APP_QUERY_TRISTATE_UNSET; + GsCategory *category = NULL; + GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET; + guint64 age_secs = 0; + const gchar * const *deployment_featured = NULL; + const gchar * const *developers = NULL; + const gchar * const *keywords = NULL; + GsApp *alternate_of = NULL; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (data->query != NULL) { + released_since = gs_app_query_get_released_since (data->query); + is_curated = gs_app_query_get_is_curated (data->query); + is_featured = gs_app_query_get_is_featured (data->query); + category = gs_app_query_get_category (data->query); + is_installed = gs_app_query_get_is_installed (data->query); + deployment_featured = gs_app_query_get_deployment_featured (data->query); + developers = gs_app_query_get_developers (data->query); + keywords = gs_app_query_get_keywords (data->query); + alternate_of = gs_app_query_get_alternate_of (data->query); + } + if (released_since != NULL) { + g_autoptr(GDateTime) now = g_date_time_new_now_utc (); + age_secs = g_date_time_difference (now, released_since) / G_TIME_SPAN_SECOND; + } + + /* Currently only support a subset of query properties, and only one set at once. + * Also don’t currently support GS_APP_QUERY_TRISTATE_FALSE. */ + if ((released_since == NULL && + is_curated == GS_APP_QUERY_TRISTATE_UNSET && + is_featured == GS_APP_QUERY_TRISTATE_UNSET && + category == NULL && + is_installed == GS_APP_QUERY_TRISTATE_UNSET && + deployment_featured == NULL && + developers == NULL && + keywords == NULL && + alternate_of == NULL) || + is_curated == GS_APP_QUERY_TRISTATE_FALSE || + is_featured == GS_APP_QUERY_TRISTATE_FALSE || + is_installed == GS_APP_QUERY_TRISTATE_FALSE || + gs_app_query_get_n_properties_set (data->query) != 1) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported query"); + return; + } + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + if (released_since != NULL && + !gs_appstream_add_recent (GS_PLUGIN (self), self->silo, list, age_secs, + cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (is_curated != GS_APP_QUERY_TRISTATE_UNSET && + !gs_appstream_add_popular (self->silo, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (is_featured != GS_APP_QUERY_TRISTATE_UNSET && + !gs_appstream_add_featured (self->silo, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (category != NULL && + !gs_appstream_add_category_apps (GS_PLUGIN (self), self->silo, category, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (is_installed == GS_APP_QUERY_TRISTATE_TRUE && + !gs_appstream_add_installed (GS_PLUGIN (self), self->silo, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (deployment_featured != NULL && + !gs_appstream_add_deployment_featured (self->silo, deployment_featured, list, + cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (developers != NULL && + !gs_appstream_search_developer_apps (GS_PLUGIN (self), self->silo, developers, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (keywords != NULL && + !gs_appstream_search (GS_PLUGIN (self), self->silo, keywords, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (alternate_of != NULL && + !gs_appstream_add_alternates (self->silo, alternate_of, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); +} + +static GsAppList * +gs_plugin_appstream_list_apps_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void refresh_metadata_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_appstream_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_appstream_refresh_metadata_async); + + /* Queue a job to check the silo, which will cause it to be refreshed if needed. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refresh_metadata_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refresh_metadata_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + /* Checking the silo will refresh it if needed. */ + if (!gs_plugin_appstream_check_silo (self, cancellable, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_appstream_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_appstream_class_init (GsPluginAppstreamClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_appstream_dispose; + + plugin_class->setup_async = gs_plugin_appstream_setup_async; + plugin_class->setup_finish = gs_plugin_appstream_setup_finish; + plugin_class->shutdown_async = gs_plugin_appstream_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_appstream_shutdown_finish; + plugin_class->refine_async = gs_plugin_appstream_refine_async; + plugin_class->refine_finish = gs_plugin_appstream_refine_finish; + plugin_class->list_apps_async = gs_plugin_appstream_list_apps_async; + plugin_class->list_apps_finish = gs_plugin_appstream_list_apps_finish; + plugin_class->refresh_metadata_async = gs_plugin_appstream_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_appstream_refresh_metadata_finish; + plugin_class->refine_categories_async = gs_plugin_appstream_refine_categories_async; + plugin_class->refine_categories_finish = gs_plugin_appstream_refine_categories_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_APPSTREAM; +} diff --git a/plugins/core/gs-plugin-appstream.h b/plugins/core/gs-plugin-appstream.h new file mode 100644 index 0000000..3063ada --- /dev/null +++ b/plugins/core/gs-plugin-appstream.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_APPSTREAM (gs_plugin_appstream_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginAppstream, gs_plugin_appstream, GS, PLUGIN_APPSTREAM, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-plugin-generic-updates.c b/plugins/core/gs-plugin-generic-updates.c new file mode 100644 index 0000000..06a16f1 --- /dev/null +++ b/plugins/core/gs-plugin-generic-updates.c @@ -0,0 +1,148 @@ +/* -*- 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+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> + +#include "gs-plugin-generic-updates.h" + +struct _GsPluginGenericUpdates +{ + GsPlugin parent; +}; + +G_DEFINE_TYPE (GsPluginGenericUpdates, gs_plugin_generic_updates, GS_TYPE_PLUGIN) + +static void +gs_plugin_generic_updates_init (GsPluginGenericUpdates *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "rpm-ostree"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); +} + +static gboolean +gs_plugin_generic_updates_merge_os_update (GsApp *app) +{ + /* this is only for grouping system-installed packages */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE || + gs_app_get_scope (app) != AS_COMPONENT_SCOPE_SYSTEM) + return FALSE; + + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC) + return TRUE; + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) + return TRUE; + + return FALSE; +} + +static GsApp * +gs_plugin_generic_updates_get_os_update (GsPlugin *plugin) +{ + GsApp *app; + const gchar *id = "org.gnome.Software.OsUpdate"; + g_autoptr(GIcon) ic = NULL; + + /* create new */ + app = gs_app_new (id); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_PROXY); + gs_app_set_management_plugin (app, plugin); + gs_app_set_special_kind (app, GS_APP_SPECIAL_KIND_OS_UPDATE); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_name (app, + GS_APP_QUALITY_NORMAL, + /* TRANSLATORS: this is a group of updates that are not + * packages and are not shown in the main list */ + _("System Updates")); + gs_app_set_summary (app, + GS_APP_QUALITY_NORMAL, + /* TRANSLATORS: this is a longer description of the + * "System Updates" string */ + _("General system updates, such as security or bug fixes, and performance improvements.")); + gs_app_set_description (app, + GS_APP_QUALITY_NORMAL, + gs_app_get_summary (app)); + ic = g_themed_icon_new ("system-component-os-updates"); + gs_app_add_icon (app, ic); + return app; +} + +static void +gs_plugin_generic_updates_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) os_updates = gs_app_list_new (); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_generic_updates_refine_async); + + /* not from get_updates() */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + /* do we have any packages left that are not apps? */ + 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)) + continue; + if (gs_plugin_generic_updates_merge_os_update (app_tmp)) + gs_app_list_add (os_updates, app_tmp); + } + if (gs_app_list_length (os_updates) == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + /* create new meta object */ + app = gs_plugin_generic_updates_get_os_update (plugin); + for (guint i = 0; i < gs_app_list_length (os_updates); i++) { + GsApp *app_tmp = gs_app_list_index (os_updates, i); + gs_app_add_related (app, app_tmp); + gs_app_list_remove (list, app_tmp); + } + gs_app_list_add (list, app); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_generic_updates_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_generic_updates_class_init (GsPluginGenericUpdatesClass *klass) +{ + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + plugin_class->refine_async = gs_plugin_generic_updates_refine_async; + plugin_class->refine_finish = gs_plugin_generic_updates_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_GENERIC_UPDATES; +} diff --git a/plugins/core/gs-plugin-generic-updates.h b/plugins/core/gs-plugin-generic-updates.h new file mode 100644 index 0000000..1b2448c --- /dev/null +++ b/plugins/core/gs-plugin-generic-updates.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_GENERIC_UPDATES (gs_plugin_generic_updates_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginGenericUpdates, gs_plugin_generic_updates, GS, PLUGIN_GENERIC_UPDATES, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-plugin-hardcoded-blocklist.c b/plugins/core/gs-plugin-hardcoded-blocklist.c new file mode 100644 index 0000000..f943496 --- /dev/null +++ b/plugins/core/gs-plugin-hardcoded-blocklist.c @@ -0,0 +1,121 @@ +/* -*- 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+ + */ + +#include <config.h> + +#include <fnmatch.h> +#include <gnome-software.h> + +#include "gs-plugin-hardcoded-blocklist.h" + +/* + * SECTION: + * Blocklists some applications based on a hardcoded list. + * + * This plugin executes entirely in the main thread. + */ + +struct _GsPluginHardcodedBlocklist +{ + GsPlugin parent; +}; + +G_DEFINE_TYPE (GsPluginHardcodedBlocklist, gs_plugin_hardcoded_blocklist, GS_TYPE_PLUGIN) + +static void +gs_plugin_hardcoded_blocklist_init (GsPluginHardcodedBlocklist *self) +{ + /* need ID */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + guint i; + const gchar *app_globs[] = { + "freeciv-server.desktop", + "links.desktop", + "nm-connection-editor.desktop", + "plank.desktop", + "*release-notes*.desktop", + "*Release_Notes*.desktop", + "Rodent-*.desktop", + "rygel-preferences.desktop", + "system-config-keyboard.desktop", + "tracker-preferences.desktop", + "Uninstall*.desktop", + "wine-*.desktop", + NULL }; + + /* not set yet */ + if (gs_app_get_id (app) == NULL) + return TRUE; + + /* search */ + for (i = 0; app_globs[i] != NULL; i++) { + if (fnmatch (app_globs[i], gs_app_get_id (app), 0) == 0) { + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + break; + } + } + + return TRUE; +} + +static void +gs_plugin_hardcoded_blocklist_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + 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_hardcoded_blocklist_refine_async); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, 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_hardcoded_blocklist_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_hardcoded_blocklist_class_init (GsPluginHardcodedBlocklistClass *klass) +{ + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + plugin_class->refine_async = gs_plugin_hardcoded_blocklist_refine_async; + plugin_class->refine_finish = gs_plugin_hardcoded_blocklist_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_HARDCODED_BLOCKLIST; +} diff --git a/plugins/core/gs-plugin-hardcoded-blocklist.h b/plugins/core/gs-plugin-hardcoded-blocklist.h new file mode 100644 index 0000000..fceef62 --- /dev/null +++ b/plugins/core/gs-plugin-hardcoded-blocklist.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_HARDCODED_BLOCKLIST (gs_plugin_hardcoded_blocklist_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginHardcodedBlocklist, gs_plugin_hardcoded_blocklist, GS, PLUGIN_HARDCODED_BLOCKLIST, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-plugin-icons.c b/plugins/core/gs-plugin-icons.c new file mode 100644 index 0000000..0dbb8ba --- /dev/null +++ b/plugins/core/gs-plugin-icons.c @@ -0,0 +1,249 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2014 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <libsoup/soup.h> +#include <string.h> + +#include <gnome-software.h> + +#include "gs-plugin-icons.h" + +/* + * SECTION: + * Loads remote icons and converts them into local cached ones. + * + * It is provided so that each plugin handling icons does not + * have to handle the download and caching functionality. + * + * FIXME: This plugin will eventually go away. Currently it only exists as the + * plugin threading code is a convenient way of ensuring that loading the remote + * icons happens in a worker thread. + */ + +struct _GsPluginIcons +{ + GsPlugin parent; + + SoupSession *soup_session; /* (owned) */ + GsWorkerThread *worker; /* (owned) */ +}; + +G_DEFINE_TYPE (GsPluginIcons, gs_plugin_icons, GS_TYPE_PLUGIN) + +#define assert_in_worker(self) \ + g_assert (gs_worker_thread_is_in_worker_context (self->worker)) + +static void +gs_plugin_icons_init (GsPluginIcons *self) +{ + /* needs remote icons downloaded */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "epiphany"); +} + +static void +gs_plugin_icons_dispose (GObject *object) +{ + GsPluginIcons *self = GS_PLUGIN_ICONS (object); + + g_clear_object (&self->soup_session); + g_clear_object (&self->worker); + + G_OBJECT_CLASS (gs_plugin_icons_parent_class)->dispose (object); +} + +static void +gs_plugin_icons_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginIcons *self = GS_PLUGIN_ICONS (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_icons_setup_async); + + self->soup_session = gs_build_soup_session (); + + /* Start up a worker thread to process all the plugin’s function calls. */ + self->worker = gs_worker_thread_new ("gs-plugin-icons"); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_icons_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_icons_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginIcons *self = GS_PLUGIN_ICONS (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_icons_shutdown_async); + + /* Stop the worker thread. */ + gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task)); +} + +static void +shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginIcons *self = g_task_get_source_object (task); + g_autoptr(GsWorkerThread) worker = NULL; + g_autoptr(GError) local_error = NULL; + + g_clear_object (&self->soup_session); + worker = g_steal_pointer (&self->worker); + + if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_icons_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +refine_app (GsPluginIcons *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + guint maximum_icon_size; + + assert_in_worker (self); + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) == 0) + return TRUE; + + /* Currently a 160px icon is needed for #GsFeatureTile, at most. */ + maximum_icon_size = 160 * gs_plugin_get_scale (GS_PLUGIN (self)); + + gs_app_ensure_icons_downloaded (app, self->soup_session, maximum_icon_size, cancellable); + + return TRUE; +} + +static void refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_icons_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginIcons *self = GS_PLUGIN_ICONS (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_icons_refine_async); + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + /* Queue a job for the refine. */ + gs_worker_thread_queue (self->worker, interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW, + refine_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginIcons *self = GS_PLUGIN_ICONS (source_object); + GsPluginRefineData *data = task_data; + GsAppList *list = data->list; + GsPluginRefineFlags flags = data->flags; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + if (!refine_app (self, app, 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_icons_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_icons_class_init (GsPluginIconsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_icons_dispose; + + plugin_class->setup_async = gs_plugin_icons_setup_async; + plugin_class->setup_finish = gs_plugin_icons_setup_finish; + plugin_class->shutdown_async = gs_plugin_icons_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_icons_shutdown_finish; + plugin_class->refine_async = gs_plugin_icons_refine_async; + plugin_class->refine_finish = gs_plugin_icons_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_ICONS; +} diff --git a/plugins/core/gs-plugin-icons.h b/plugins/core/gs-plugin-icons.h new file mode 100644 index 0000000..e092d0d --- /dev/null +++ b/plugins/core/gs-plugin-icons.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_ICONS (gs_plugin_icons_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginIcons, gs_plugin_icons, GS, PLUGIN_ICONS, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-plugin-os-release.c b/plugins/core/gs-plugin-os-release.c new file mode 100644 index 0000000..685af27 --- /dev/null +++ b/plugins/core/gs-plugin-os-release.c @@ -0,0 +1,180 @@ +/* -*- 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+ + */ + +#include <config.h> + +#include <gnome-software.h> + +#include "gs-plugin-os-release.h" + +struct _GsPluginOsRelease +{ + GsPlugin parent; + + GsApp *app_system; +}; + +G_DEFINE_TYPE (GsPluginOsRelease, gs_plugin_os_release, GS_TYPE_PLUGIN) + +static void +gs_plugin_os_release_dispose (GObject *object) +{ + GsPluginOsRelease *self = GS_PLUGIN_OS_RELEASE (object); + + g_clear_object (&self->app_system); + + G_OBJECT_CLASS (gs_plugin_os_release_parent_class)->dispose (object); +} + +static void +gs_plugin_os_release_init (GsPluginOsRelease *self) +{ + self->app_system = gs_app_new ("system"); + gs_app_set_kind (self->app_system, AS_COMPONENT_KIND_OPERATING_SYSTEM); + gs_app_set_state (self->app_system, GS_APP_STATE_INSTALLED); +} + +static void +gs_plugin_os_release_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginOsRelease *self = GS_PLUGIN_OS_RELEASE (plugin); + g_autoptr(GTask) task = NULL; + const gchar *cpe_name; + const gchar *home_url; + const gchar *name; + const gchar *version; + const gchar *os_id; + g_autoptr(GsOsRelease) os_release = NULL; + g_autoptr(GError) local_error = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_os_release_setup_async); + + /* parse os-release, wherever it may be */ + os_release = gs_os_release_new (&local_error); + if (os_release == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + cpe_name = gs_os_release_get_cpe_name (os_release); + if (cpe_name != NULL) + gs_app_set_metadata (self->app_system, "GnomeSoftware::CpeName", cpe_name); + name = gs_os_release_get_name (os_release); + if (name != NULL) + gs_app_set_name (self->app_system, GS_APP_QUALITY_LOWEST, name); + version = gs_os_release_get_version_id (os_release); + if (version != NULL) + gs_app_set_version (self->app_system, version); + + os_id = gs_os_release_get_id (os_release); + + /* use libsoup to convert a URL */ + home_url = gs_os_release_get_home_url (os_release); + if (home_url != NULL) { + g_autoptr(GUri) uri = NULL; + + /* homepage */ + gs_app_set_url (self->app_system, AS_URL_KIND_HOMEPAGE, home_url); + + /* Build ID from the reverse-DNS URL and the ID and version. */ + uri = g_uri_parse (home_url, SOUP_HTTP_URI_FLAGS, NULL); + if (uri != NULL) { + g_auto(GStrv) split = NULL; + const gchar *home_host = g_uri_get_host (uri); + split = g_strsplit_set (home_host, ".", -1); + if (g_strv_length (split) >= 2) { + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s.%s.%s-%s", + split[1], + split[0], + (os_id != NULL) ? os_id : "unnamed", + (version != NULL) ? version : "unversioned"); + gs_app_set_id (self->app_system, id); + } + } + } + + /* success */ + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_os_release_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_os_release_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginOsRelease *self = GS_PLUGIN_OS_RELEASE (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_os_release_refine_async); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* match meta-id */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD) && + g_strcmp0 (gs_app_get_id (app), "system") == 0) { + /* copy over interesting metadata */ + if (gs_app_get_install_date (app) != 0 && + gs_app_get_install_date (self->app_system) == 0) { + gs_app_set_install_date (self->app_system, + gs_app_get_install_date (app)); + } + + gs_app_list_add (list, self->app_system); + break; + } + } + + /* success */ + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_os_release_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_os_release_class_init (GsPluginOsReleaseClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_os_release_dispose; + + plugin_class->setup_async = gs_plugin_os_release_setup_async; + plugin_class->setup_finish = gs_plugin_os_release_setup_finish; + plugin_class->refine_async = gs_plugin_os_release_refine_async; + plugin_class->refine_finish = gs_plugin_os_release_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_OS_RELEASE; +} diff --git a/plugins/core/gs-plugin-os-release.h b/plugins/core/gs-plugin-os-release.h new file mode 100644 index 0000000..9ab9d0d --- /dev/null +++ b/plugins/core/gs-plugin-os-release.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_OS_RELEASE (gs_plugin_os_release_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginOsRelease, gs_plugin_os_release, GS, PLUGIN_OS_RELEASE, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-plugin-provenance-license.c b/plugins/core/gs-plugin-provenance-license.c new file mode 100644 index 0000000..8ddd071 --- /dev/null +++ b/plugins/core/gs-plugin-provenance-license.c @@ -0,0 +1,199 @@ +/* -*- 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 Matthias Klumpp <mak@debian.org> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +#include "gs-plugin-provenance-license.h" + +/* + * SECTION: + * Marks the application as Free Software if it comes from an origin + * that is recognized as being DFSGish-free. + * + * This plugin executes entirely in the main thread. + */ + +struct _GsPluginProvenanceLicense { + GsPlugin parent; + + GSettings *settings; + gchar **sources; + gchar *license_id; +}; + +G_DEFINE_TYPE (GsPluginProvenanceLicense, gs_plugin_provenance_license, GS_TYPE_PLUGIN) + +static gchar ** +gs_plugin_provenance_license_get_sources (GsPluginProvenanceLicense *self) +{ + const gchar *tmp; + + tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_LICENSE_SOURCES"); + if (tmp != NULL) { + g_debug ("using custom provenance_license sources of %s", tmp); + return g_strsplit (tmp, ",", -1); + } + return g_settings_get_strv (self->settings, "free-repos"); +} + +static gchar * +gs_plugin_provenance_license_get_id (GsPluginProvenanceLicense *self) +{ + const gchar *tmp; + g_autofree gchar *url = NULL; + + tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_LICENSE_URL"); + if (tmp != NULL) { + g_debug ("using custom license generic sources of %s", tmp); + url = g_strdup (tmp); + } else { + url = g_settings_get_string (self->settings, "free-repos-url"); + if (url == NULL) + return g_strdup ("LicenseRef-free"); + } + return g_strdup_printf ("LicenseRef-free=%s", url); +} + +static void +gs_plugin_provenance_license_changed_cb (GSettings *settings, + const gchar *key, + gpointer user_data) +{ + GsPluginProvenanceLicense *self = GS_PLUGIN_PROVENANCE_LICENSE (user_data); + + if (g_strcmp0 (key, "free-repos") == 0) { + g_strfreev (self->sources); + self->sources = gs_plugin_provenance_license_get_sources (self); + } + if (g_strcmp0 (key, "free-repos-url") == 0) { + g_free (self->license_id); + self->license_id = gs_plugin_provenance_license_get_id (self); + } +} + +static void +gs_plugin_provenance_license_init (GsPluginProvenanceLicense *self) +{ + self->settings = g_settings_new ("org.gnome.software"); + g_signal_connect (self->settings, "changed", + G_CALLBACK (gs_plugin_provenance_license_changed_cb), self); + self->sources = gs_plugin_provenance_license_get_sources (self); + self->license_id = gs_plugin_provenance_license_get_id (self); + + /* need this set */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "provenance"); +} + +static void +gs_plugin_provenance_license_dispose (GObject *object) +{ + GsPluginProvenanceLicense *self = GS_PLUGIN_PROVENANCE_LICENSE (object); + + g_clear_pointer (&self->sources, g_strfreev); + g_clear_pointer (&self->license_id, g_free); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (gs_plugin_provenance_license_parent_class)->dispose (object); +} + +static gboolean +refine_app (GsPluginProvenanceLicense *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *origin; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0) + return TRUE; + + /* no provenance */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) + return TRUE; + + /* nothing to search */ + if (self->sources == NULL || self->sources[0] == NULL) + return TRUE; + + /* simple case */ + origin = gs_app_get_origin (app); + if (origin != NULL && gs_utils_strv_fnmatch (self->sources, origin)) + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, self->license_id); + + return TRUE; +} + +static void +gs_plugin_provenance_license_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginProvenanceLicense *self = GS_PLUGIN_PROVENANCE_LICENSE (plugin); + 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_provenance_license_refine_async); + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + /* nothing to search */ + if (self->sources == NULL || self->sources[0] == NULL) { + g_task_return_boolean (task, TRUE); + return; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (self, app, 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_provenance_license_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_provenance_license_class_init (GsPluginProvenanceLicenseClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_provenance_license_dispose; + + plugin_class->refine_async = gs_plugin_provenance_license_refine_async; + plugin_class->refine_finish = gs_plugin_provenance_license_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_PROVENANCE_LICENSE; +} diff --git a/plugins/core/gs-plugin-provenance-license.h b/plugins/core/gs-plugin-provenance-license.h new file mode 100644 index 0000000..f793d61 --- /dev/null +++ b/plugins/core/gs-plugin-provenance-license.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_PROVENANCE_LICENSE (gs_plugin_provenance_license_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginProvenanceLicense, gs_plugin_provenance_license, GS, PLUGIN_PROVENANCE_LICENSE, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-plugin-provenance.c b/plugins/core/gs-plugin-provenance.c new file mode 100644 index 0000000..95e940b --- /dev/null +++ b/plugins/core/gs-plugin-provenance.c @@ -0,0 +1,292 @@ +/* -*- 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> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +#include "gs-plugin-provenance.h" + +/* + * SECTION: + * Sets the package provenance to TRUE if installed by an official + * software source. Also sets compulsory quirk when a required repository. + * + * This plugin executes entirely in the main thread. + */ + +struct _GsPluginProvenance { + GsPlugin parent; + + GSettings *settings; + GHashTable *repos; /* gchar *name ~> guint flags */ + GPtrArray *provenance_wildcards; /* non-NULL, when have names with wildcards */ + GPtrArray *compulsory_wildcards; /* non-NULL, when have names with wildcards */ +}; + +G_DEFINE_TYPE (GsPluginProvenance, gs_plugin_provenance, GS_TYPE_PLUGIN) + +static GHashTable * +gs_plugin_provenance_remove_by_flag (GHashTable *old_repos, + GsAppQuirk quirk) +{ + GHashTable *new_repos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, old_repos); + while (g_hash_table_iter_next (&iter, &key, &value)) { + guint flags = GPOINTER_TO_UINT (value); + flags = flags & (~quirk); + if (flags != 0) + g_hash_table_insert (new_repos, g_strdup (key), GUINT_TO_POINTER (flags)); + } + return new_repos; +} + +static void +gs_plugin_provenance_add_quirks (GsApp *app, + guint quirks) +{ + if ((quirks & GS_APP_QUIRK_PROVENANCE) != 0) + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + if ((quirks & GS_APP_QUIRK_COMPULSORY) != 0 && + gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); +} + +static gchar ** +gs_plugin_provenance_get_sources (GsPluginProvenance *self, + const gchar *key) +{ + const gchar *tmp; + tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_SOURCES"); + if (tmp != NULL) { + if (g_strcmp0 (key, "required-repos") == 0) + return NULL; + g_debug ("using custom provenance sources of %s", tmp); + return g_strsplit (tmp, ",", -1); + } + return g_settings_get_strv (self->settings, key); +} + +static void +gs_plugin_provenance_settings_changed_cb (GSettings *settings, + const gchar *key, + gpointer user_data) +{ + GsPluginProvenance *self = GS_PLUGIN_PROVENANCE (user_data); + GsAppQuirk quirk = GS_APP_QUIRK_NONE; + GPtrArray **pwildcards = NULL; + + if (g_strcmp0 (key, "official-repos") == 0) { + quirk = GS_APP_QUIRK_PROVENANCE; + pwildcards = &self->provenance_wildcards; + } else if (g_strcmp0 (key, "required-repos") == 0) { + quirk = GS_APP_QUIRK_COMPULSORY; + pwildcards = &self->compulsory_wildcards; + } + + if (quirk != GS_APP_QUIRK_NONE) { + /* The keys are stolen by the hash table, thus free only the array */ + g_autofree gchar **repos = NULL; + g_autoptr(GHashTable) old_repos = self->repos; + g_autoptr(GPtrArray) old_wildcards = *pwildcards; + GHashTable *new_repos = gs_plugin_provenance_remove_by_flag (old_repos, quirk); + GPtrArray *new_wildcards = NULL; + repos = gs_plugin_provenance_get_sources (self, key); + for (guint ii = 0; repos && repos[ii]; ii++) { + gchar *repo = g_steal_pointer (&(repos[ii])); + if (strchr (repo, '*') || + strchr (repo, '?') || + strchr (repo, '[')) { + if (new_wildcards == NULL) + new_wildcards = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (new_wildcards, repo); + } else { + g_hash_table_insert (new_repos, repo, + GUINT_TO_POINTER (quirk | + GPOINTER_TO_UINT (g_hash_table_lookup (new_repos, repo)))); + } + } + if (new_wildcards != NULL) + g_ptr_array_add (new_wildcards, NULL); + self->repos = new_repos; + *pwildcards = new_wildcards; + } +} + +static void +gs_plugin_provenance_init (GsPluginProvenance *self) +{ + self->settings = g_settings_new ("org.gnome.software"); + self->repos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_signal_connect (self->settings, "changed", + G_CALLBACK (gs_plugin_provenance_settings_changed_cb), self); + gs_plugin_provenance_settings_changed_cb (self->settings, "official-repos", self); + gs_plugin_provenance_settings_changed_cb (self->settings, "required-repos", self); + + /* after the package source is set */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "dummy"); + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "packagekit"); + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "rpm-ostree"); +} + +static void +gs_plugin_provenance_dispose (GObject *object) +{ + GsPluginProvenance *self = GS_PLUGIN_PROVENANCE (object); + + g_clear_pointer (&self->repos, g_hash_table_unref); + g_clear_pointer (&self->provenance_wildcards, g_ptr_array_unref); + g_clear_pointer (&self->compulsory_wildcards, g_ptr_array_unref); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (gs_plugin_provenance_parent_class)->dispose (object); +} + +static gboolean +gs_plugin_provenance_find_repo_flags (GHashTable *repos, + GPtrArray *provenance_wildcards, + GPtrArray *compulsory_wildcards, + const gchar *repo, + guint *out_flags) +{ + if (repo == NULL || *repo == '\0') + return FALSE; + *out_flags = GPOINTER_TO_UINT (g_hash_table_lookup (repos, repo)); + if (provenance_wildcards != NULL && + gs_utils_strv_fnmatch ((gchar **) provenance_wildcards->pdata, repo)) + *out_flags |= GS_APP_QUIRK_PROVENANCE; + if (compulsory_wildcards != NULL && + gs_utils_strv_fnmatch ((gchar **) compulsory_wildcards->pdata, repo)) + *out_flags |= GS_APP_QUIRK_COMPULSORY; + return *out_flags != 0; +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GHashTable *repos, + GPtrArray *provenance_wildcards, + GPtrArray *compulsory_wildcards, + GCancellable *cancellable, + GError **error) +{ + const gchar *origin; + guint quirks; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) == 0) + return TRUE; + if (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) + return TRUE; + + /* Software sources/repositories are represented as #GsApps too. Add the + * provenance quirk to the system-configured repositories (but not + * user-configured ones). */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) { + if (gs_plugin_provenance_find_repo_flags (repos, provenance_wildcards, compulsory_wildcards, gs_app_get_id (app), &quirks) && + gs_app_get_scope (app) != AS_COMPONENT_SCOPE_USER) + gs_plugin_provenance_add_quirks (app, quirks); + return TRUE; + } + + /* simple case */ + origin = gs_app_get_origin (app); + if (gs_plugin_provenance_find_repo_flags (repos, provenance_wildcards, compulsory_wildcards, origin, &quirks)) { + gs_plugin_provenance_add_quirks (app, quirks); + return TRUE; + } + + /* this only works for packages */ + origin = gs_app_get_source_id_default (app); + if (origin == NULL) + return TRUE; + origin = g_strrstr (origin, ";"); + if (origin == NULL) + return TRUE; + if (g_str_has_prefix (origin + 1, "installed:")) + origin += 10; + if (gs_plugin_provenance_find_repo_flags (repos, provenance_wildcards, compulsory_wildcards, origin + 1, &quirks)) + gs_plugin_provenance_add_quirks (app, quirks); + + return TRUE; +} + +static void +gs_plugin_provenance_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginProvenance *self = GS_PLUGIN_PROVENANCE (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GHashTable) repos = NULL; + g_autoptr(GPtrArray) provenance_wildcards = NULL; + g_autoptr(GPtrArray) compulsory_wildcards = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_provenance_refine_async); + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + repos = g_hash_table_ref (self->repos); + provenance_wildcards = self->provenance_wildcards != NULL ? g_ptr_array_ref (self->provenance_wildcards) : NULL; + compulsory_wildcards = self->compulsory_wildcards != NULL ? g_ptr_array_ref (self->compulsory_wildcards) : NULL; + + /* nothing to search */ + if (g_hash_table_size (repos) == 0 && provenance_wildcards == NULL && compulsory_wildcards == NULL) { + g_task_return_boolean (task, TRUE); + return; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, repos, provenance_wildcards, compulsory_wildcards, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_provenance_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_provenance_class_init (GsPluginProvenanceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_provenance_dispose; + + plugin_class->refine_async = gs_plugin_provenance_refine_async; + plugin_class->refine_finish = gs_plugin_provenance_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_PROVENANCE; +} diff --git a/plugins/core/gs-plugin-provenance.h b/plugins/core/gs-plugin-provenance.h new file mode 100644 index 0000000..e3e2d34 --- /dev/null +++ b/plugins/core/gs-plugin-provenance.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_PROVENANCE (gs_plugin_provenance_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginProvenance, gs_plugin_provenance, GS, PLUGIN_PROVENANCE, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-plugin-rewrite-resource.c b/plugins/core/gs-plugin-rewrite-resource.c new file mode 100644 index 0000000..44348c5 --- /dev/null +++ b/plugins/core/gs-plugin-rewrite-resource.c @@ -0,0 +1,252 @@ +/* -*- 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+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> + +#include "gs-plugin-rewrite-resource.h" + +/* + * SECTION: + * Rewrites CSS metadata for apps to refer to locally downloaded resources. + * + * This plugin rewrites the CSS of apps to refer to locally cached resources, + * rather than HTTP/HTTPS URIs for images (for example). + * + * It uses a worker thread to download the resources. + * + * FIXME: Eventually this should move into the refine plugin job, as it needs + * to execute after all other refine jobs (in order to see all the URIs which + * they produce). + */ + +struct _GsPluginRewriteResource +{ + GsPlugin parent; + + GsWorkerThread *worker; /* (owned) */ +}; + +G_DEFINE_TYPE (GsPluginRewriteResource, gs_plugin_rewrite_resource, GS_TYPE_PLUGIN) + +#define assert_in_worker(self) \ + g_assert (gs_worker_thread_is_in_worker_context (self->worker)) + +static void +gs_plugin_rewrite_resource_init (GsPluginRewriteResource *self) +{ + /* let appstream add metadata first */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +static void +gs_plugin_rewrite_resource_dispose (GObject *object) +{ + GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (object); + + g_clear_object (&self->worker); + + G_OBJECT_CLASS (gs_plugin_rewrite_resource_parent_class)->dispose (object); +} + +static void +gs_plugin_rewrite_resource_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rewrite_resource_setup_async); + + /* Start up a worker thread to process all the plugin’s function calls. */ + self->worker = gs_worker_thread_new ("gs-plugin-rewrite-resource"); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_rewrite_resource_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_rewrite_resource_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rewrite_resource_shutdown_async); + + /* Stop the worker thread. */ + gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task)); +} + +static void +shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginRewriteResource *self = g_task_get_source_object (task); + g_autoptr(GsWorkerThread) worker = NULL; + g_autoptr(GError) local_error = NULL; + + worker = g_steal_pointer (&self->worker); + + if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_rewrite_resource_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +refine_app (GsPluginRewriteResource *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *keys[] = { + "GnomeSoftware::FeatureTile-css", + "GnomeSoftware::UpgradeBanner-css", + NULL }; + + assert_in_worker (self); + + /* rewrite URIs */ + for (guint i = 0; keys[i] != NULL; i++) { + const gchar *css = gs_app_get_metadata_item (app, keys[i]); + if (css != NULL) { + g_autofree gchar *css_new = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (GS_PLUGIN (self))); + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading featured images…")); + css_new = gs_plugin_download_rewrite_resource (GS_PLUGIN (self), + app, + css, + cancellable, + error); + if (css_new == NULL) + return FALSE; + if (g_strcmp0 (css, css_new) != 0) { + gs_app_set_metadata (app, keys[i], NULL); + gs_app_set_metadata (app, keys[i], css_new); + } + } + } + return TRUE; +} + +static void refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_rewrite_resource_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rewrite_resource_refine_async); + + /* Queue a job for the refine. */ + gs_worker_thread_queue (self->worker, interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW, + refine_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginRewriteResource *self = GS_PLUGIN_REWRITE_RESOURCE (source_object); + GsPluginRefineData *data = task_data; + GsAppList *list = data->list; + GsPluginRefineFlags flags = data->flags; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + if (!refine_app (self, app, 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_rewrite_resource_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_rewrite_resource_class_init (GsPluginRewriteResourceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_rewrite_resource_dispose; + + plugin_class->setup_async = gs_plugin_rewrite_resource_setup_async; + plugin_class->setup_finish = gs_plugin_rewrite_resource_setup_finish; + plugin_class->shutdown_async = gs_plugin_rewrite_resource_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_rewrite_resource_shutdown_finish; + plugin_class->refine_async = gs_plugin_rewrite_resource_refine_async; + plugin_class->refine_finish = gs_plugin_rewrite_resource_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_REWRITE_RESOURCE; +} diff --git a/plugins/core/gs-plugin-rewrite-resource.h b/plugins/core/gs-plugin-rewrite-resource.h new file mode 100644 index 0000000..0af8c88 --- /dev/null +++ b/plugins/core/gs-plugin-rewrite-resource.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_REWRITE_RESOURCE (gs_plugin_rewrite_resource_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginRewriteResource, gs_plugin_rewrite_resource, GS, PLUGIN_REWRITE_RESOURCE, GsPlugin) + +G_END_DECLS diff --git a/plugins/core/gs-self-test.c b/plugins/core/gs-self-test.c new file mode 100644 index 0000000..8266a4c --- /dev/null +++ b/plugins/core/gs-self-test.c @@ -0,0 +1,277 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Joaquim Rocha <jrocha@endlessm.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> + +#include "gnome-software-private.h" + +#include "gs-appstream.h" +#include "gs-test.h" + +const gchar * const allowlist[] = { + "appstream", + "generic-updates", + "icons", + "os-release", + NULL +}; + +static void +gs_plugins_core_search_repo_name_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_tmp = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* force this app to be installed */ + app_tmp = gs_plugin_loader_app_create (plugin_loader, "*/*/yellow/arachne.desktop/*", NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (app_tmp); + gs_app_set_state (app_tmp, GS_APP_STATE_INSTALLED); + + /* get search result based on addon keyword */ + keywords[0] = "yellow"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (list); + + /* make sure there is at least one entry, the parent app */ + g_assert_cmpint (gs_app_list_length (list), >=, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "arachne.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); +} + +static void +gs_plugins_core_os_release_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app3 = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* refine system application */ + app = gs_plugin_loader_get_system_app (plugin_loader, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (app); + plugin_job = gs_plugin_job_refine_new_for_app (app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* make sure there is valid content */ + g_assert_cmpstr (gs_app_get_id (app), ==, "org.fedoraproject.fedora-25"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_OPERATING_SYSTEM); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_name (app), ==, "Fedora"); + g_assert_cmpstr (gs_app_get_version (app), ==, "25"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, + "https://fedoraproject.org/"); + g_assert_cmpstr (gs_app_get_metadata_item (app, "GnomeSoftware::CpeName"), ==, + "cpe:/o:fedoraproject:fedora:25"); + + /* this comes from appstream */ + g_assert_cmpstr (gs_app_get_summary (app), ==, "Fedora Workstation"); + + /* check we can get this by the old name too */ + app3 = gs_plugin_loader_get_system_app (plugin_loader, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (app3); + g_assert_true (app3 == app); +} + +static void +gs_plugins_core_generic_updates_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + GsApp *os_update; + GsAppList *related; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsPluginJob) plugin_job2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app1 = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app_wildcard = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_wildcard = NULL; + GsAppList *result_list; + GsAppList *result_list_wildcard; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* create a list with generic apps */ + list = gs_app_list_new (); + app1 = gs_app_new ("package1"); + app2 = gs_app_new ("package2"); + gs_app_set_kind (app1, AS_COMPONENT_KIND_GENERIC); + gs_app_set_kind (app2, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app1, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_bundle_kind (app2, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app1, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_scope (app2, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_state (app1, GS_APP_STATE_UPDATABLE); + gs_app_set_state (app2, GS_APP_STATE_UPDATABLE); + gs_app_add_source (app1, "package1"); + gs_app_add_source (app2, "package2"); + gs_app_list_add (list, app1); + gs_app_list_add (list, app2); + + /* refine to make the generic-updates plugin merge them into a single OsUpdate item */ + plugin_job = gs_plugin_job_refine_new (list, GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* make sure there is one entry, the os update */ + result_list = gs_plugin_job_refine_get_result_list (GS_PLUGIN_JOB_REFINE (plugin_job)); + g_assert_cmpint (gs_app_list_length (result_list), ==, 1); + os_update = gs_app_list_index (result_list, 0); + + /* make sure the os update is valid */ + g_assert_cmpstr (gs_app_get_id (os_update), ==, "org.gnome.Software.OsUpdate"); + g_assert_cmpint (gs_app_get_kind (os_update), ==, AS_COMPONENT_KIND_GENERIC); + g_assert_cmpint (gs_app_get_special_kind (os_update), ==, GS_APP_SPECIAL_KIND_OS_UPDATE); + g_assert_true (gs_app_has_quirk (os_update, GS_APP_QUIRK_IS_PROXY)); + + /* must have two related apps, the ones we added earlier */ + related = gs_app_get_related (os_update); + g_assert_cmpint (gs_app_list_length (related), ==, 2); + + /* another test to make sure that we don't get an OsUpdate item created for wildcard apps */ + list_wildcard = gs_app_list_new (); + app_wildcard = gs_app_new ("nosuchapp.desktop"); + gs_app_add_quirk (app_wildcard, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_kind (app_wildcard, AS_COMPONENT_KIND_GENERIC); + gs_app_list_add (list_wildcard, app_wildcard); + plugin_job2 = gs_plugin_job_refine_new (list_wildcard, GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job2, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + result_list_wildcard = gs_plugin_job_refine_get_result_list (GS_PLUGIN_JOB_REFINE (plugin_job2)); + + /* no OsUpdate item created */ + for (guint i = 0; i < gs_app_list_length (result_list_wildcard); i++) { + GsApp *app_tmp = gs_app_list_index (result_list_wildcard, i); + g_assert_cmpint (gs_app_get_kind (app_tmp), !=, AS_COMPONENT_KIND_GENERIC); + g_assert_cmpint (gs_app_get_special_kind (app_tmp), !=, GS_APP_SPECIAL_KIND_OS_UPDATE); + g_assert_false (gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_PROXY)); + } +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *os_release_filename = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *xml; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ + g_content_type_set_mime_dirs (NULL); + + /* Similarly, add the system-wide icon theme path before it’s + * overwritten by %G_TEST_OPTION_ISOLATE_DIRS. */ + gs_test_expose_icon_theme_paths (); + + gs_test_init (&argc, &argv); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-core-test-XXXXXX", NULL); + g_assert_nonnull (tmp_root); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + + os_release_filename = gs_test_get_filename (TESTDATADIR, "os-release"); + g_assert_nonnull (os_release_filename); + g_setenv ("GS_SELF_TEST_OS_RELEASE_FILENAME", os_release_filename, TRUE); + + /* fake some data */ + xml = "<?xml version=\"1.0\"?>\n" + "<components origin=\"yellow\" version=\"0.9\">\n" + " <component type=\"desktop\">\n" + " <id>arachne.desktop</id>\n" + " <name>test</name>\n" + " <summary>Test</summary>\n" + " <icon type=\"stock\">system-file-manager</icon>\n" + " <pkgname>arachne</pkgname>\n" + " </component>\n" + " <component type=\"os-upgrade\">\n" + " <id>org.fedoraproject.fedora-25</id>\n" + " <name>Fedora</name>\n" + " <summary>Fedora Workstation</summary>\n" + " <pkgname>fedora-release</pkgname>\n" + " </component>\n" + " <info>\n" + " <scope>user</scope>\n" + " </info>\n" + "</components>\n"; + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/core/search-repo-name", + plugin_loader, + (GTestDataFunc) gs_plugins_core_search_repo_name_func); + g_test_add_data_func ("/gnome-software/plugins/core/os-release", + plugin_loader, + (GTestDataFunc) gs_plugins_core_os_release_func); + g_test_add_data_func ("/gnome-software/plugins/core/generic-updates", + plugin_loader, + (GTestDataFunc) gs_plugins_core_generic_updates_func); + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/core/meson.build b/plugins/core/meson.build new file mode 100644 index 0000000..ca11ade --- /dev/null +++ b/plugins/core/meson.build @@ -0,0 +1,133 @@ +cargs = ['-DG_LOG_DOMAIN="GsPlugin"'] + +shared_module( + 'gs_plugin_generic-updates', + sources : 'gs-plugin-generic-updates.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +shared_module( + 'gs_plugin_provenance', + sources : 'gs-plugin-provenance.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +shared_module( + 'gs_plugin_provenance-license', + sources : 'gs-plugin-provenance-license.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + + +shared_module( + 'gs_plugin_icons', + sources : 'gs-plugin-icons.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +shared_module( + 'gs_plugin_appstream', + sources : [ + 'gs-plugin-appstream.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ + plugin_libs, + libxmlb, + ], +) + +shared_module( + 'gs_plugin_hardcoded-blocklist', + sources : 'gs-plugin-hardcoded-blocklist.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +shared_module( + 'gs_plugin_rewrite-resource', + sources : 'gs-plugin-rewrite-resource.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +shared_module( + 'gs_plugin_os-release', + sources : 'gs-plugin-os-release.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +if get_option('tests') + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-core', + compiled_schemas, + sources : [ + 'gs-self-test.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + libxmlb, + ], + c_args : cargs, + ) + test('gs-self-test-core', e, suite: ['plugins', 'core'], env: test_env) +endif diff --git a/plugins/core/tests/os-release b/plugins/core/tests/os-release new file mode 120000 index 0000000..1efe264 --- /dev/null +++ b/plugins/core/tests/os-release @@ -0,0 +1 @@ +../../../data/tests/os-release
\ No newline at end of file diff --git a/plugins/dpkg/gs-plugin-dpkg.c b/plugins/dpkg/gs-plugin-dpkg.c new file mode 100644 index 0000000..3f886e0 --- /dev/null +++ b/plugins/dpkg/gs-plugin-dpkg.c @@ -0,0 +1,133 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <stdlib.h> +#include <gnome-software.h> + +#include "gs-plugin-dpkg.h" + +struct _GsPluginDpkg +{ + GsPlugin parent; +}; + +G_DEFINE_TYPE (GsPluginDpkg, gs_plugin_dpkg, GS_TYPE_PLUGIN) + +#define DPKG_DEB_BINARY "/usr/bin/dpkg-deb" + +static void +gs_plugin_dpkg_init (GsPluginDpkg *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + if (!g_file_test (DPKG_DEB_BINARY, G_FILE_TEST_EXISTS)) { + g_debug ("disabling '%s' as no %s available", + gs_plugin_get_name (plugin), DPKG_DEB_BINARY); + gs_plugin_set_enabled (plugin, FALSE); + } +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + guint i; + g_autofree gchar *content_type = NULL; + g_autofree gchar *output = NULL; + g_auto(GStrv) argv = NULL; + g_auto(GStrv) tokens = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GString) str = NULL; + const gchar *mimetypes[] = { + "application/vnd.debian.binary-package", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (!g_strv_contains (mimetypes, content_type)) + return TRUE; + + /* exec sync */ + argv = g_new0 (gchar *, 5); + argv[0] = g_strdup (DPKG_DEB_BINARY); + argv[1] = g_strdup ("--showformat=${Package}\\n" + "${Version}\\n" + "${Installed-Size}\\n" + "${Homepage}\\n" + "${Description}"); + argv[2] = g_strdup ("-W"); + argv[3] = g_file_get_path (file); + if (!g_spawn_sync (NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL, + NULL, NULL, &output, NULL, NULL, error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + + /* parse output */ + tokens = g_strsplit (output, "\n", 0); + if (g_strv_length (tokens) < 5) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "dpkg-deb output format incorrect:\n\"%s\"\n", output); + return FALSE; + } + + /* create app */ + app = gs_app_new (NULL); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL); + gs_app_add_source (app, tokens[0]); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, tokens[0]); + gs_app_set_version (app, tokens[1]); + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, 1024 * g_ascii_strtoull (tokens[2], NULL, 10)); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, tokens[3]); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, tokens[4]); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + + /* multiline text */ + str = g_string_new (""); + for (i = 5; tokens[i] != NULL; i++) { + if (g_strcmp0 (tokens[i], " .") == 0) { + if (str->len > 0) + g_string_truncate (str, str->len - 1); + g_string_append (str, "\n"); + continue; + } + g_strstrip (tokens[i]); + g_string_append_printf (str, "%s ", tokens[i]); + } + if (str->len > 0) + g_string_truncate (str, str->len - 1); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, str->str); + + /* success */ + gs_app_list_add (list, app); + return TRUE; +} + +static void +gs_plugin_dpkg_class_init (GsPluginDpkgClass *klass) +{ +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_DPKG; +} diff --git a/plugins/dpkg/gs-plugin-dpkg.h b/plugins/dpkg/gs-plugin-dpkg.h new file mode 100644 index 0000000..df16fde --- /dev/null +++ b/plugins/dpkg/gs-plugin-dpkg.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_DPKG (gs_plugin_dpkg_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginDpkg, gs_plugin_dpkg, GS, PLUGIN_DPKG, GsPlugin) + +G_END_DECLS diff --git a/plugins/dpkg/gs-self-test.c b/plugins/dpkg/gs-self-test.c new file mode 100644 index 0000000..ecb567d --- /dev/null +++ b/plugins/dpkg/gs-self-test.c @@ -0,0 +1,87 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_dpkg_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *fn = NULL; + g_autoptr(GFile) file = NULL; + + /* no dpkg, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "dpkg")) { + g_test_skip ("not enabled"); + return; + } + + /* load local file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron-1.1-1.deb"); + g_assert (fn != NULL); + file = g_file_new_for_path (fn); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (app != NULL); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.1-1"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_description (app), ==, + "This is the first paragraph in the example " + "package control file.\nThis is the second paragraph."); + g_assert (gs_app_get_local_file (app) != NULL); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar * const allowlist[] = { + "dpkg", + NULL + }; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ + g_content_type_set_mime_dirs (NULL); + + gs_test_init (&argc, &argv); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/dpkg", + plugin_loader, + (GTestDataFunc) gs_plugins_dpkg_func); + + return g_test_run (); +} diff --git a/plugins/dpkg/meson.build b/plugins/dpkg/meson.build new file mode 100644 index 0000000..594d6e4 --- /dev/null +++ b/plugins/dpkg/meson.build @@ -0,0 +1,35 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginDpkg"'] + +shared_module( + 'gs_plugin_dpkg', + sources : 'gs-plugin-dpkg.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +if get_option('tests') + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-dpkg', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + c_args : cargs, + ) + test('gs-self-test-dpkg', e, suite: ['plugins', 'dpkg'], env: test_env) +endif diff --git a/plugins/dpkg/tests/build-deb.sh b/plugins/dpkg/tests/build-deb.sh new file mode 100755 index 0000000..cbf50f6 --- /dev/null +++ b/plugins/dpkg/tests/build-deb.sh @@ -0,0 +1 @@ +dpkg-deb --build debian chiron-1.1-1.deb diff --git a/plugins/dpkg/tests/chiron-1.1-1.deb b/plugins/dpkg/tests/chiron-1.1-1.deb Binary files differnew file mode 100644 index 0000000..f4f921a --- /dev/null +++ b/plugins/dpkg/tests/chiron-1.1-1.deb diff --git a/plugins/dpkg/tests/debian/DEBIAN/control b/plugins/dpkg/tests/debian/DEBIAN/control new file mode 100644 index 0000000..ad5d9c6 --- /dev/null +++ b/plugins/dpkg/tests/debian/DEBIAN/control @@ -0,0 +1,13 @@ +Package: chiron +Version: 1.1-1 +Section: base +Priority: optional +Architecture: all +Homepage: http://127.0.0.1/ +Maintainer: Richard Hughes <richard@hughsie.com> +Description: Single line synopsis + This is the first + paragraph in the example package + control file. + . + This is the second paragraph. diff --git a/plugins/dpkg/tests/debian/usr/bin/chiron b/plugins/dpkg/tests/debian/usr/bin/chiron new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/dpkg/tests/debian/usr/bin/chiron diff --git a/plugins/dummy/gs-plugin-dummy.c b/plugins/dummy/gs-plugin-dummy.c new file mode 100644 index 0000000..ac584af --- /dev/null +++ b/plugins/dummy/gs-plugin-dummy.c @@ -0,0 +1,1104 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2011-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +#include "gs-plugin-dummy.h" + +/* + * SECTION: + * Provides some dummy data that is useful in self test programs. + */ + +struct _GsPluginDummy { + GsPlugin parent; + + guint quirk_id; + guint allow_updates_id; + gboolean allow_updates_inhibit; + GsApp *cached_origin; + GHashTable *installed_apps; /* id:1 */ + GHashTable *available_apps; /* id:1 */ +}; + +G_DEFINE_TYPE (GsPluginDummy, gs_plugin_dummy, GS_TYPE_PLUGIN) + +/* just flip-flop this every few seconds */ +static gboolean +gs_plugin_dummy_allow_updates_cb (gpointer user_data) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (user_data); + + gs_plugin_set_allow_updates (GS_PLUGIN (self), self->allow_updates_inhibit); + self->allow_updates_inhibit = !self->allow_updates_inhibit; + return G_SOURCE_CONTINUE; +} + +static void +gs_plugin_dummy_init (GsPluginDummy *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + if (g_getenv ("GS_SELF_TEST_DUMMY_ENABLE") == NULL) { + g_debug ("disabling '%s' as not in self test", + gs_plugin_get_name (plugin)); + gs_plugin_set_enabled (plugin, FALSE); + return; + } + + /* need help from appstream */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "os-release"); +} + +static void +gs_plugin_dummy_dispose (GObject *object) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (object); + + g_clear_pointer (&self->installed_apps, g_hash_table_unref); + g_clear_pointer (&self->available_apps, g_hash_table_unref); + g_clear_handle_id (&self->quirk_id, g_source_remove); + g_clear_object (&self->cached_origin); + + G_OBJECT_CLASS (gs_plugin_dummy_parent_class)->dispose (object); +} + +static void +gs_plugin_dummy_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_dummy_setup_async); + + /* toggle this */ + if (g_getenv ("GS_SELF_TEST_TOGGLE_ALLOW_UPDATES") != NULL) { + self->allow_updates_id = g_timeout_add_seconds (10, + gs_plugin_dummy_allow_updates_cb, plugin); + } + + /* 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, "http://www.bbc.co.uk/"); + 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, NULL, self->cached_origin); + + /* keep track of what apps are installed */ + self->installed_apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + self->available_apps = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_insert (self->available_apps, + g_strdup ("chiron.desktop"), + GUINT_TO_POINTER (1)); + g_hash_table_insert (self->available_apps, + g_strdup ("zeus.desktop"), + GUINT_TO_POINTER (1)); + g_hash_table_insert (self->available_apps, + g_strdup ("zeus-spell.addon"), + GUINT_TO_POINTER (1)); + g_hash_table_insert (self->available_apps, + g_strdup ("com.hughski.ColorHug2.driver"), + GUINT_TO_POINTER (1)); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_dummy_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_id (app) != NULL && + g_str_has_prefix (gs_app_get_id (app), "dummy:")) { + gs_app_set_management_plugin (app, plugin); + return; + } + if (g_strcmp0 (gs_app_get_id (app), "mate-spell.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "com.hughski.ColorHug2.driver") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus-spell.addon") == 0 || + g_strcmp0 (gs_app_get_source_default (app), "chiron") == 0) + gs_app_set_management_plugin (app, plugin); +} + +static gboolean +gs_plugin_dummy_delay (GsPlugin *plugin, + GsApp *app, + guint timeout_ms, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = TRUE; + guint i; + guint timeout_us = timeout_ms * 10; + + /* do blocking delay in 1% increments */ + for (i = 0; i < 100; i++) { + g_usleep (timeout_us); + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_utils_error_convert_gio (error); + ret = FALSE; + break; + } + if (app != NULL) + gs_app_set_progress (app, i); + gs_plugin_status_update (plugin, app, + GS_PLUGIN_STATUS_DOWNLOADING); + } + return ret; +} + +typedef struct { + GsApp *app; /* (owned) (nullable) */ + guint percent_complete; +} DelayData; + +static void +delay_data_free (DelayData *data) +{ + g_clear_object (&data->app); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DelayData, delay_data_free) + +static gboolean delay_timeout_cb (gpointer user_data); + +/* Simulate a download on app, updating its progress one percentage point at a + * time, with an overall interval of @timeout_ms to go from 0% to 100%. The + * download is cancelled within @timeout_ms / 100 if @cancellable is cancelled. */ +static void +gs_plugin_dummy_delay_async (GsPlugin *plugin, + GsApp *app, + guint timeout_ms, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(DelayData) data = NULL; + g_autoptr(GSource) source = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_dummy_delay_async); + + data = g_new0 (DelayData, 1); + data->app = (app != NULL) ? g_object_ref (app) : NULL; + data->percent_complete = 0; + g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) delay_data_free); + + source = g_timeout_source_new (timeout_ms / 100); + g_task_attach_source (task, source, delay_timeout_cb); +} + +static gboolean +delay_timeout_cb (gpointer user_data) +{ + GTask *task = G_TASK (user_data); + GsPlugin *plugin = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + DelayData *data = g_task_get_task_data (task); + g_autoptr(GError) local_error = NULL; + + /* Iterate until 100%. */ + if (data->percent_complete >= 100) { + g_task_return_boolean (task, TRUE); + return G_SOURCE_REMOVE; + } + + /* Has the task been cancelled? */ + if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) { + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return G_SOURCE_REMOVE; + } + + /* Update the app’s progress and continue. */ + if (data->app != NULL) + gs_app_set_progress (data->app, data->percent_complete); + gs_plugin_status_update (plugin, data->app, GS_PLUGIN_STATUS_DOWNLOADING); + + data->percent_complete++; + + return G_SOURCE_CONTINUE; +} + +static gboolean +gs_plugin_dummy_delay_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +gs_plugin_dummy_poll_cb (gpointer user_data) +{ + g_autoptr(GsApp) app = NULL; + GsPlugin *plugin = GS_PLUGIN (user_data); + + /* find the app in the per-plugin cache -- this assumes that we can + * calculate the same key as used when calling gs_plugin_cache_add() */ + app = gs_plugin_cache_lookup (plugin, "chiron"); + if (app == NULL) { + g_warning ("app not found in cache!"); + return FALSE; + } + + /* toggle this to animate the hide/show the 3rd party banner */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) { + g_debug ("about to make app distro-provided"); + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + } else { + g_debug ("about to make app 3rd party"); + gs_app_remove_quirk (app, GS_APP_QUIRK_PROVENANCE); + } + + /* continue polling */ + return TRUE; +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *path = NULL; + g_autofree gchar *scheme = NULL; + g_autoptr(GsApp) app = NULL; + + /* not us */ + scheme = gs_utils_get_url_scheme (url); + if (g_strcmp0 (scheme, "dummy") != 0) + return TRUE; + + /* create app */ + path = gs_utils_get_url_path (url); + app = gs_app_new (path); + gs_app_set_management_plugin (app, plugin); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + return TRUE; +} + +static gboolean timeout_cb (gpointer user_data); + +/* Simulate a cancellable delay */ +static void +gs_plugin_dummy_timeout_async (GsPluginDummy *self, + guint timeout_ms, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GSource) source = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_dummy_timeout_async); + + source = g_timeout_source_new (timeout_ms); + + if (cancellable != NULL) { + g_autoptr(GSource) cancellable_source = NULL; + + cancellable_source = g_cancellable_source_new (cancellable); + g_source_set_dummy_callback (cancellable_source); + g_source_add_child_source (source, cancellable_source); + } + + g_task_attach_source (task, source, timeout_cb); +} + +static gboolean +timeout_cb (gpointer user_data) +{ + GTask *task = G_TASK (user_data); + + if (!g_task_return_error_if_cancelled (task)) + g_task_return_boolean (task, TRUE); + + return G_SOURCE_REMOVE; +} + +static gboolean +gs_plugin_dummy_timeout_finish (GsPluginDummy *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsApp *app; + GsApp *proxy; + g_autoptr(GIcon) ic = NULL; + + /* update UI as this might take some time */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + + /* spin */ + if (!gs_plugin_dummy_delay (plugin, NULL, 2000, cancellable, error)) + return FALSE; + + /* use a generic stock icon */ + ic = g_themed_icon_new ("drive-harddisk"); + + /* add a live updatable normal application */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A teaching application"); + gs_app_set_update_details_text (app, "Do not crash when using libvirt."); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_HIGH); + gs_app_add_icon (app, ic); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (app, plugin); + gs_app_list_add (list, app); + g_object_unref (app); + + /* add a offline OS update */ + app = gs_app_new (NULL); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "libvirt-glib-devel"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "Development files for libvirt"); + gs_app_set_update_details_text (app, "Fix several memory leaks."); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_LOW); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + gs_app_add_source (app, "libvirt-glib-devel"); + gs_app_add_source_id (app, "libvirt-glib-devel;0.0.1;noarch;fedora"); + gs_app_set_management_plugin (app, plugin); + gs_app_list_add (list, app); + g_object_unref (app); + + /* add a live OS update */ + app = gs_app_new (NULL); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "chiron-libs"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "library for chiron"); + gs_app_set_update_details_text (app, "Do not crash when using libvirt."); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_HIGH); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_add_source (app, "chiron-libs"); + gs_app_add_source_id (app, "chiron-libs;0.0.1;i386;updates-testing"); + gs_app_set_management_plugin (app, plugin); + gs_app_list_add (list, app); + g_object_unref (app); + + /* add a proxy app update */ + proxy = gs_app_new ("proxy.desktop"); + gs_app_set_name (proxy, GS_APP_QUALITY_NORMAL, "Proxy"); + gs_app_set_summary (proxy, GS_APP_QUALITY_NORMAL, "A proxy app"); + gs_app_set_update_details_text (proxy, "Update all related apps."); + gs_app_set_update_urgency (proxy, AS_URGENCY_KIND_HIGH); + gs_app_add_icon (proxy, ic); + gs_app_set_kind (proxy, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_add_quirk (proxy, GS_APP_QUIRK_IS_PROXY); + gs_app_set_state (proxy, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (proxy, plugin); + gs_app_list_add (list, proxy); + g_object_unref (proxy); + + /* add a proxy related app */ + app = gs_app_new ("proxy-related-app.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Related app"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A related app"); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (app, plugin); + gs_app_add_related (proxy, app); + g_object_unref (app); + + /* add another proxy related app */ + app = gs_app_new ("proxy-another-related-app.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Another Related app"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A related app"); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_management_plugin (app, plugin); + gs_app_add_related (proxy, app); + g_object_unref (app); + + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (plugin); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* remove app */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + gs_app_set_state (app, GS_APP_STATE_REMOVING); + if (!gs_plugin_dummy_delay (plugin, app, 500, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + } + + /* keep track */ + g_hash_table_remove (self->installed_apps, gs_app_get_id (app)); + g_hash_table_insert (self->available_apps, + g_strdup (gs_app_get_id (app)), + GUINT_TO_POINTER (1)); + return TRUE; +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (plugin); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* install app */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0) { + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + if (!gs_plugin_dummy_delay (plugin, app, 500, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + } + + /* keep track */ + g_hash_table_insert (self->installed_apps, + g_strdup (gs_app_get_id (app)), + GUINT_TO_POINTER (1)); + g_hash_table_remove (self->available_apps, gs_app_get_id (app)); + + return TRUE; +} + +gboolean +gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (plugin); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + if (!g_str_has_prefix (gs_app_get_id (app), "proxy")) { + /* always fail */ + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "no network connection is available"); + gs_utils_error_add_origin_id (error, self->cached_origin); + return FALSE; + } + + /* simulate an update for 4 seconds */ + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + for (guint i = 1; i <= 4; ++i) { + gs_app_set_progress (app, 25 * i); + sleep (1); + } + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + return TRUE; +} + +static gboolean +refine_app (GsPluginDummy *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* make the local system EOL */ + if (gs_app_get_metadata_item (app, "GnomeSoftware::CpeName") != NULL) + gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE); + + /* state */ + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) { + if (g_hash_table_lookup (self->installed_apps, + gs_app_get_id (app)) != NULL) + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + if (g_hash_table_lookup (self->available_apps, + gs_app_get_id (app)) != NULL) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + } + + /* kind */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "mate-spell.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "com.hughski.ColorHug2.driver") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0) { + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN) + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + } + + /* license */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) { + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0 || + g_strcmp0 (gs_app_get_id (app), "zeus.desktop") == 0) + gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, "GPL-2.0+"); + } + + /* homepage */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) { + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + "http://www.test.org/"); + } + } + + /* origin */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) { + if (g_strcmp0 (gs_app_get_id (app), "zeus-spell.addon") == 0) + gs_app_set_origin (app, "london-east"); + } + + /* default */ + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + if (gs_app_get_name (app) == NULL) + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "tmp"); + if (gs_app_get_summary (app) == NULL) + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "tmp"); + if (gs_app_get_icons (app) == NULL) { + g_autoptr(GIcon) ic = g_themed_icon_new ("drive-harddisk"); + gs_app_add_icon (app, ic); + } + } + + /* description */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) { + if (g_strcmp0 (gs_app_get_id (app), "chiron.desktop") == 0) { + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, + "long description!"); + } + } + + /* add fake review */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) { + g_autoptr(AsReview) review1 = NULL; + g_autoptr(AsReview) review2 = NULL; + g_autoptr(GDateTime) dt = NULL; + + dt = g_date_time_new_now_utc (); + + /* set first review */ + review1 = as_review_new (); + as_review_set_rating (review1, 50); + as_review_set_reviewer_name (review1, "Angela Avery"); + as_review_set_summary (review1, "Steep learning curve, but worth it"); + as_review_set_description (review1, "Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used. Best overall 3D application I've ever used overall 3D application I've ever used."); + as_review_set_version (review1, "3.16.4"); + as_review_set_date (review1, dt); + gs_app_add_review (app, review1); + + /* set self review */ + review2 = as_review_new (); + as_review_set_rating (review2, 100); + as_review_set_reviewer_name (review2, "Just Myself"); + as_review_set_summary (review2, "I like this application"); + as_review_set_description (review2, "I'm not very wordy myself."); + as_review_set_version (review2, "3.16.3"); + as_review_set_date (review2, dt); + as_review_set_flags (review2, AS_REVIEW_FLAG_SELF); + gs_app_add_review (app, review2); + } + + /* add fake ratings */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS) { + g_autoptr(GArray) ratings = NULL; + const gint data[] = { 0, 10, 20, 30, 15, 2 }; + ratings = g_array_sized_new (FALSE, FALSE, sizeof (gint), 6); + g_array_append_vals (ratings, data, 6); + gs_app_set_review_ratings (app, ratings); + } + + /* add a rating */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) { + gs_app_set_rating (app, 66); + } + + return TRUE; +} + +static void +gs_plugin_dummy_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (plugin); + 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_dummy_refine_async); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + if (!refine_app (self, app, 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_dummy_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void list_apps_timeout_cb (GObject *object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_dummy_list_apps_async (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(GsAppList) list = gs_app_list_new (); + GDateTime *released_since = NULL; + GsAppQueryTristate is_curated = GS_APP_QUERY_TRISTATE_UNSET; + guint max_results = 0; + GsCategory *category = NULL; + GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET; + const gchar * const *keywords = NULL; + GsApp *alternate_of = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_dummy_list_apps_async); + + if (query != NULL) { + released_since = gs_app_query_get_released_since (query); + is_curated = gs_app_query_get_is_curated (query); + max_results = gs_app_query_get_max_results (query); + category = gs_app_query_get_category (query); + is_installed = gs_app_query_get_is_installed (query); + keywords = gs_app_query_get_keywords (query); + alternate_of = gs_app_query_get_alternate_of (query); + } + + /* Currently only support a subset of query properties, and only one set at once. + * Also don’t currently support GS_APP_QUERY_TRISTATE_FALSE. */ + if ((released_since == NULL && + is_curated == GS_APP_QUERY_TRISTATE_UNSET && + category == NULL && + is_installed == GS_APP_QUERY_TRISTATE_UNSET && + keywords == NULL && + alternate_of == NULL) || + is_curated == GS_APP_QUERY_TRISTATE_FALSE || + is_installed == GS_APP_QUERY_TRISTATE_FALSE || + gs_app_query_get_n_properties_set (query) != 1) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported query"); + return; + } + + if (released_since != NULL) { + g_autoptr(GIcon) icon = g_themed_icon_new ("chiron.desktop"); + g_autoptr(GsApp) app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "View and use virtual machines"); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, "http://www.box.org"); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + gs_app_add_icon (app, icon); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_management_plugin (app, plugin); + + gs_app_list_add (list, app); + } + + if (is_curated != GS_APP_QUERY_TRISTATE_UNSET) { + g_autoptr(GsApp) app1 = NULL; + g_autoptr(GsApp) app2 = NULL; + + /* Hacky way of letting callers indicate which set of results + * they want, for unit testing. */ + if (max_results == 6) { + const gchar *apps[] = { "chiron.desktop", "zeus.desktop" }; + for (gsize i = 0; i < G_N_ELEMENTS (apps); i++) { + g_autoptr(GsApp) app = gs_app_new (apps[i]); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + } + } else { + /* add wildcard */ + app1 = gs_app_new ("zeus.desktop"); + gs_app_add_quirk (app1, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_metadata (app1, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app1); + } + } + + if (category != NULL) { + g_autoptr(GIcon) icon = g_themed_icon_new ("chiron.desktop"); + g_autoptr(GsApp) app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "View and use virtual machines"); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, "http://www.box.org"); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + gs_app_add_icon (app, icon); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_management_plugin (app, plugin); + gs_app_list_add (list, app); + } + + if (is_installed != GS_APP_QUERY_TRISTATE_UNSET) { + const gchar *packages[] = { "zeus", "zeus-common", NULL }; + const gchar *app_ids[] = { "Uninstall Zeus.desktop", NULL }; + + /* add all packages */ + for (gsize i = 0; packages[i] != NULL; i++) { + g_autoptr(GsApp) app = gs_app_new (NULL); + gs_app_add_source (app, packages[i]); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_origin (app, "london-west"); + gs_app_set_management_plugin (app, plugin); + gs_app_list_add (list, app); + } + + /* add all app-ids */ + for (gsize i = 0; app_ids[i] != NULL; i++) { + g_autoptr(GsApp) app = gs_app_new (app_ids[i]); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_management_plugin (app, plugin); + gs_app_list_add (list, app); + } + } + + if (keywords != NULL) { + if (g_strcmp0 (keywords[0], "hang") == 0) { + /* hang the plugin for 5 seconds */ + gs_plugin_dummy_timeout_async (self, 5000, cancellable, + list_apps_timeout_cb, g_steal_pointer (&task)); + return; + } else if (g_strcmp0 (keywords[0], "chiron") == 0) { + g_autoptr(GsApp) app = NULL; + + /* does the app already exist? */ + app = gs_plugin_cache_lookup (plugin, "chiron"); + if (app != NULL) { + g_debug ("using %s fom the cache", gs_app_get_id (app)); + gs_app_list_add (list, app); + } else { + g_autoptr(GIcon) icon = NULL; + + /* set up a timeout to emulate getting a GFileMonitor callback */ + self->quirk_id = + g_timeout_add_seconds (1, gs_plugin_dummy_poll_cb, plugin); + + /* use a generic stock icon */ + icon = g_themed_icon_new ("drive-harddisk"); + + /* add a live updatable normal application */ + app = gs_app_new ("chiron.desktop"); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A teaching application"); + gs_app_add_icon (app, icon); + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, 42 * 1024 * 1024); + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 50 * 1024 * 1024); + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + gs_app_set_management_plugin (app, plugin); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + + /* add to cache so it can be found by the flashing callback */ + gs_plugin_cache_add (plugin, NULL, app); + } + } else { + /* Don’t do anything */ + } + } + + if (alternate_of != NULL) { + if (g_strcmp0 (gs_app_get_id (alternate_of), "zeus.desktop") == 0) { + g_autoptr(GsApp) app = gs_app_new ("chiron.desktop"); + gs_app_list_add (list, app); + } + } + + g_task_return_pointer (task, g_steal_pointer (&list), (GDestroyNotify) g_object_unref); +} + +static void +list_apps_timeout_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginDummy *self = GS_PLUGIN_DUMMY (object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + /* Return a cancelled error, or an empty app list after hanging. */ + if (gs_plugin_dummy_timeout_finish (self, result, &local_error)) + g_task_return_pointer (task, gs_app_list_new (), (GDestroyNotify) g_object_unref); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static GsAppList * +gs_plugin_dummy_list_apps_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +gs_plugin_dummy_list_distro_upgrades_async (GsPlugin *plugin, + GsPluginListDistroUpgradesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GIcon) ic = NULL; + g_autofree gchar *background_filename = NULL; + g_autofree gchar *css = NULL; + g_autoptr(GTask) task = NULL; + g_autoptr(GsAppList) list = gs_app_list_new (); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_dummy_list_distro_upgrades_async); + + /* use stock icon */ + ic = g_themed_icon_new ("system-component-addon"); + + /* get existing item from the cache */ + app = gs_plugin_cache_lookup (plugin, "user/*/os-upgrade/org.fedoraproject.release-rawhide.upgrade/*"); + if (app != NULL) { + gs_app_list_add (list, app); + + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); + return; + } + + app = gs_app_new ("org.fedoraproject.release-rawhide.upgrade"); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER); + gs_app_set_kind (app, AS_COMPONENT_KIND_OPERATING_SYSTEM); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, "Fedora"); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + "A major upgrade, with new features and added polish."); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + "https://fedoraproject.org/wiki/Releases/24/Schedule"); + 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_set_version (app, "34"); + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, 256 * 1024 * 1024); + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 1024 * 1024 * 1024); + gs_app_set_license (app, GS_APP_QUALITY_LOWEST, "LicenseRef-free"); + gs_app_set_management_plugin (app, plugin); + + /* Check for a background image in the standard location. */ + background_filename = gs_utils_get_upgrade_background ("34"); + + if (background_filename != NULL) + css = g_strconcat ("background: url('file://", background_filename, "');" + "background-size: 100% 100%;", NULL); + gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", css); + + gs_app_add_icon (app, ic); + gs_app_list_add (list, app); + + gs_plugin_cache_add (plugin, NULL, app); + + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); +} + +static GsAppList * +gs_plugin_dummy_list_distro_upgrades_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +gboolean +gs_plugin_download_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + return gs_plugin_dummy_delay (plugin, app, 5100, cancellable, error); +} + +static void refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_dummy_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + g_autoptr(GsApp) app = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_dummy_refresh_metadata_async); + + app = gs_app_new (NULL); + gs_plugin_dummy_delay_async (plugin, app, 3100, cancellable, refresh_metadata_cb, g_steal_pointer (&task)); +} + +static void +refresh_metadata_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; + + if (!gs_plugin_dummy_delay_finish (plugin, result, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_dummy_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +{ + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + g_debug ("starting download"); + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + if (!gs_plugin_dummy_delay (plugin, app, 5000, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + return TRUE; +} + +gboolean +gs_plugin_app_upgrade_trigger (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +{ + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* NOP */ + return TRUE; +} + +gboolean +gs_plugin_update_cancel (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +{ + return TRUE; +} + +static void +gs_plugin_dummy_class_init (GsPluginDummyClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_dummy_dispose; + + plugin_class->setup_async = gs_plugin_dummy_setup_async; + plugin_class->setup_finish = gs_plugin_dummy_setup_finish; + plugin_class->refine_async = gs_plugin_dummy_refine_async; + plugin_class->refine_finish = gs_plugin_dummy_refine_finish; + plugin_class->list_apps_async = gs_plugin_dummy_list_apps_async; + plugin_class->list_apps_finish = gs_plugin_dummy_list_apps_finish; + plugin_class->refresh_metadata_async = gs_plugin_dummy_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_dummy_refresh_metadata_finish; + plugin_class->list_distro_upgrades_async = gs_plugin_dummy_list_distro_upgrades_async; + plugin_class->list_distro_upgrades_finish = gs_plugin_dummy_list_distro_upgrades_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_DUMMY; +} diff --git a/plugins/dummy/gs-plugin-dummy.h b/plugins/dummy/gs-plugin-dummy.h new file mode 100644 index 0000000..6ec64a8 --- /dev/null +++ b/plugins/dummy/gs-plugin-dummy.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_DUMMY (gs_plugin_dummy_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginDummy, gs_plugin_dummy, GS, PLUGIN_DUMMY, GsPlugin) + +G_END_DECLS diff --git a/plugins/dummy/gs-self-test.c b/plugins/dummy/gs-self-test.c new file mode 100644 index 0000000..c6ce9bd --- /dev/null +++ b/plugins/dummy/gs-self-test.c @@ -0,0 +1,966 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> +#include <locale.h> + +#include "gnome-software-private.h" + +#include "gs-test.h" + +const gchar * const allowlist[] = { + "appstream", + "dummy", + "generic-updates", + "hardcoded-blocklist", + "icons", + "provenance", + "provenance-license", + NULL +}; + +static guint _status_changed_cnt = 0; + +typedef struct { + GError *error; + GMainLoop *loop; +} GsDummyTestHelper; + +static GsDummyTestHelper * +gs_dummy_test_helper_new (void) +{ + return g_new0 (GsDummyTestHelper, 1); +} + +static void +gs_dummy_test_helper_free (GsDummyTestHelper *helper) +{ + if (helper->error != NULL) + g_error_free (helper->error); + if (helper->loop != NULL) + g_main_loop_unref (helper->loop); + g_free (helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsDummyTestHelper, gs_dummy_test_helper_free) + +static void +gs_plugin_loader_status_changed_cb (GsPluginLoader *plugin_loader, + GsApp *app, + GsPluginStatus status, + gpointer user_data) +{ + _status_changed_cnt++; +} + +static void +gs_plugins_dummy_install_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + GsPlugin *plugin; + + /* install */ + app = gs_app_new ("chiron.desktop"); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "dummy"); + gs_app_set_management_plugin (app, plugin); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + + /* remove */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); +} + +static void +gs_plugins_dummy_error_func (GsPluginLoader *plugin_loader) +{ + const GError *app_error; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) events = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginEvent) event = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPlugin *plugin; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* update, which should cause an error to be emitted */ + app = gs_app_new ("chiron.desktop"); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "dummy"); + gs_app_set_management_plugin (app, plugin); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + /* get last active event */ + event = gs_plugin_loader_get_event_default (plugin_loader); + g_assert (event != NULL); + g_assert (gs_plugin_event_get_app (event) == app); + + /* check all the events */ + events = gs_plugin_loader_get_events (plugin_loader); + g_assert_cmpint (events->len, ==, 1); + event = g_ptr_array_index (events, 0); + g_assert (gs_plugin_event_get_app (event) == app); + app_error = gs_plugin_event_get_error (event); + g_assert (app_error != NULL); + g_assert_error (app_error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED); +} + +static void +gs_plugins_dummy_refine_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPlugin *plugin; + + /* get the extra bits */ + app = gs_app_new ("chiron.desktop"); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "dummy"); + gs_app_set_management_plugin (app, plugin); + plugin_job = gs_plugin_job_refine_new_for_app (app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert_cmpstr (gs_app_get_license (app), ==, "GPL-2.0+"); + g_assert_cmpstr (gs_app_get_description (app), !=, NULL); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://www.test.org/"); +} + +static void +gs_plugins_dummy_metadata_quirks (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPlugin *plugin; + + /* get the extra bits */ + app = gs_app_new ("chiron.desktop"); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "dummy"); + gs_app_set_management_plugin (app, plugin); + plugin_job = gs_plugin_job_refine_new_for_app (app, GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert_cmpstr (gs_app_get_description (app), !=, NULL); + + /* check the not-launchable quirk */ + + g_assert (!gs_app_has_quirk(app, GS_APP_QUIRK_NOT_LAUNCHABLE)); + + gs_app_set_metadata (app, "GnomeSoftware::quirks::not-launchable", "true"); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refine_new_for_app (app, GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert (gs_app_has_quirk(app, GS_APP_QUIRK_NOT_LAUNCHABLE)); + + gs_app_set_metadata (app, "GnomeSoftware::quirks::not-launchable", NULL); + gs_app_set_metadata (app, "GnomeSoftware::quirks::not-launchable", "false"); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refine_new_for_app (app, GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + g_assert (!gs_app_has_quirk(app, GS_APP_QUIRK_NOT_LAUNCHABLE)); +} + +static void +gs_plugins_dummy_key_colors_func (GsPluginLoader *plugin_loader) +{ + GArray *array; + gboolean ret; + guint i; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + + /* get the extra bits */ + app = gs_app_new ("chiron.desktop"); + plugin_job = gs_plugin_job_refine_new_for_app (app, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + array = gs_app_get_key_colors (app); + g_assert_cmpint (array->len, <=, 3); + g_assert_cmpint (array->len, >, 0); + + /* check values are in range */ + for (i = 0; i < array->len; i++) { + const GdkRGBA *kc = &g_array_index (array, GdkRGBA, i); + g_assert_cmpfloat (kc->red, >=, 0.f); + g_assert_cmpfloat (kc->red, <=, 1.f); + g_assert_cmpfloat (kc->green, >=, 0.f); + g_assert_cmpfloat (kc->green, <=, 1.f); + g_assert_cmpfloat (kc->blue, >=, 0.f); + g_assert_cmpfloat (kc->blue, <=, 1.f); + g_assert_cmpfloat (kc->alpha, >=, 0.f); + g_assert_cmpfloat (kc->alpha, <=, 1.f); + } +} + +static void +gs_plugins_dummy_updates_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the updates list */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there are three entries */ + g_assert_cmpint (gs_app_list_length (list), ==, 3); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "chiron.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, "Do not crash when using libvirt."); + g_assert_cmpint (gs_app_get_update_urgency (app), ==, AS_URGENCY_KIND_HIGH); + + /* get the virtual non-apps OS update */ + app = gs_app_list_index (list, 2); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.gnome.Software.OsUpdate"); + g_assert_cmpstr (gs_app_get_name (app), ==, "System Updates"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "General system updates, such as security or bug fixes, and performance improvements."); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_GENERIC); + g_assert_cmpint (gs_app_get_special_kind (app), ==, GS_APP_SPECIAL_KIND_OS_UPDATE); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE); + g_assert_cmpint (gs_app_list_length (gs_app_get_related (app)), ==, 2); + + /* get the virtual non-apps OS update */ + app = gs_app_list_index (list, 1); + g_assert_cmpstr (gs_app_get_id (app), ==, "proxy.desktop"); + g_assert (gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpint (gs_app_list_length (gs_app_get_related (app)), ==, 2); +} + +static void +gs_plugins_dummy_distro_upgrades_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the updates list */ + plugin_job = gs_plugin_job_list_distro_upgrades_new (GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE, + GS_PLUGIN_REFINE_FLAGS_NONE); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is one entry */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.fedoraproject.release-rawhide.upgrade"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_OPERATING_SYSTEM); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + + /* this should be set with a higher priority by AppStream */ + g_assert_cmpstr (gs_app_get_summary (app), ==, "Release specific tagline"); + + /* download the update */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE); + + /* trigger the update */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_TRIGGER, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE); +} + +static gboolean +filter_valid_cb (GsApp *app, + gpointer user_data) +{ + return gs_plugin_loader_app_is_valid (app, GS_PLUGIN_REFINE_FLAGS_NONE); +} + +static void +gs_plugins_dummy_installed_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *addon; + g_autoptr(GsAppList) addons = NULL; + g_autofree gchar *menu_path = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppQuery) query = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GIcon) icon = NULL; + GsPluginRefineFlags refine_flags; + + /* get installed packages */ + refine_flags = (GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE); + + query = gs_app_query_new ("is-installed", GS_APP_QUERY_TRISTATE_TRUE, + "refine-flags", refine_flags, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "filter-func", filter_valid_cb, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is one entry */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "zeus.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_name (app), ==, "Zeus"); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "zeus"); + icon = gs_app_get_icon_for_size (app, 48, 1, NULL); + g_assert_nonnull (icon); + g_clear_object (&icon); + + /* check various bitfields */ + g_assert (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)); + g_assert_cmpstr (gs_app_get_license (app), ==, "GPL-2.0+"); + g_assert (gs_app_get_license_is_free (app)); + + /* check kudos */ + g_assert_true (gs_app_has_kudo (app, GS_APP_KUDO_MY_LANGUAGE)); + + /* check categories */ + g_assert (gs_app_has_category (app, "Player")); + g_assert (gs_app_has_category (app, "AudioVideo")); + g_assert (!gs_app_has_category (app, "ImageProcessing")); + g_assert (gs_app_get_menu_path (app) != NULL); + menu_path = g_strjoinv ("->", gs_app_get_menu_path (app)); + g_assert_cmpstr (menu_path, ==, "Create->Music Players"); + + /* check addon */ + addons = gs_app_dup_addons (app); + g_assert_nonnull (addons); + g_assert_cmpint (gs_app_list_length (addons), ==, 1); + addon = gs_app_list_index (addons, 0); + g_assert_cmpstr (gs_app_get_id (addon), ==, "zeus-spell.addon"); + g_assert_cmpint (gs_app_get_kind (addon), ==, AS_COMPONENT_KIND_ADDON); + g_assert_cmpint (gs_app_get_state (addon), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_name (addon), ==, "Spell Check"); + g_assert_cmpstr (gs_app_get_source_default (addon), ==, "zeus-spell"); + g_assert_cmpstr (gs_app_get_license (addon), ==, + "LicenseRef-free=https://www.debian.org/"); + icon = gs_app_get_icon_for_size (addon, 48, 1, NULL); + g_assert_null (icon); +} + +static void +gs_plugins_dummy_search_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + /* get search result based on addon keyword */ + keywords[0] = "zeus"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is at least one entry, the parent app, which must be first */ + g_assert_cmpint (gs_app_list_length (list), >=, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "zeus.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); +} + +static void +gs_plugins_dummy_search_alternate_func (GsPluginLoader *plugin_loader) +{ + GsApp *app_tmp; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppQuery) query = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get search result based on addon keyword */ + app = gs_app_new ("zeus.desktop"); + query = gs_app_query_new ("alternate-of", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_priority, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is the original app, and the alternate */ + g_assert_cmpint (gs_app_list_length (list), ==, 2); + app_tmp = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app_tmp), ==, "chiron.desktop"); + g_assert_cmpint (gs_app_get_kind (app_tmp), ==, AS_COMPONENT_KIND_DESKTOP_APP); + app_tmp = gs_app_list_index (list, 1); + g_assert_cmpstr (gs_app_get_id (app_tmp), ==, "zeus.desktop"); + g_assert_cmpint (gs_app_get_kind (app_tmp), ==, AS_COMPONENT_KIND_DESKTOP_APP); +} + +static void +gs_plugins_dummy_url_to_app_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP, + "search", "dummy://chiron.desktop", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (app != NULL); + g_assert_cmpstr (gs_app_get_id (app), ==, "chiron.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); +} + +static void +gs_plugins_dummy_plugin_cache_func (GsPluginLoader *plugin_loader) +{ + GsApp *app1; + GsApp *app2; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list1 = NULL; + g_autoptr(GsAppList) list2 = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* ensure we get the same results back from calling the methods twice */ + plugin_job = gs_plugin_job_list_distro_upgrades_new (GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE, + GS_PLUGIN_REFINE_FLAGS_NONE); + list1 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list1 != NULL); + g_assert_cmpint (gs_app_list_length (list1), ==, 1); + app1 = gs_app_list_index (list1, 0); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_list_distro_upgrades_new (GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE, + GS_PLUGIN_REFINE_FLAGS_NONE); + list2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list2 != NULL); + g_assert_cmpint (gs_app_list_length (list2), ==, 1); + app2 = gs_app_list_index (list2, 0); + + /* make sure there is one GObject */ + g_assert_cmpstr (gs_app_get_id (app1), ==, gs_app_get_id (app2)); + g_assert (app1 == app2); +} + +static void +gs_plugins_dummy_wildcard_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list1 = NULL; + g_autoptr(GsAppList) list2 = NULL; + const gchar *expected_apps2[] = { "chiron.desktop", "zeus.desktop", NULL }; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + + /* use the plugin's default curated list, indicated by setting max-results=5 */ + query = gs_app_query_new ("is-curated", GS_APP_QUERY_TRISTATE_TRUE, + "max-results", 5, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + + list1 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list1 != NULL); + g_assert_cmpint (gs_app_list_length (list1), ==, 1); + g_object_unref (plugin_job); + g_object_unref (query); + + /* use the plugin’s second list, indicated by setting max-results=6 */ + query = gs_app_query_new ("is-curated", GS_APP_QUERY_TRISTATE_TRUE, + "max-results", 6, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + + list2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list2 != NULL); + + g_assert_cmpint (gs_app_list_length (list2), ==, g_strv_length ((gchar **) expected_apps2)); + + for (guint i = 0; i < gs_app_list_length (list2); ++i) { + GsApp *app = gs_app_list_index (list2, i); + g_assert (g_strv_contains (expected_apps2, gs_app_get_id (app))); + } +} + +static void +plugin_job_action_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsDummyTestHelper *helper = (GsDummyTestHelper *) user_data; + + gs_plugin_loader_job_action_finish (plugin_loader, res, &helper->error); + if (helper->loop != NULL) + g_main_loop_quit (helper->loop); +} + +static void +gs_plugins_dummy_limit_parallel_ops_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsAppList) list = NULL; + GsApp *app1 = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app3 = NULL; + GsPlugin *plugin; + g_autoptr(GsPluginJob) plugin_job1 = NULL; + g_autoptr(GsPluginJob) plugin_job2 = NULL; + g_autoptr(GsPluginJob) plugin_job3 = NULL; + g_autoptr(GMainContext) context = NULL; + g_autoptr(GsDummyTestHelper) helper1 = gs_dummy_test_helper_new (); + g_autoptr(GsDummyTestHelper) helper2 = gs_dummy_test_helper_new (); + g_autoptr(GsDummyTestHelper) helper3 = gs_dummy_test_helper_new (); + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* get the updates list */ + plugin_job1 = gs_plugin_job_list_distro_upgrades_new (GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE, + GS_PLUGIN_REFINE_FLAGS_NONE); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job1, NULL, &helper3->error); + gs_test_flush_main_context (); + g_assert_no_error (helper3->error); + g_assert (list != NULL); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app1 = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app1), ==, "org.fedoraproject.release-rawhide.upgrade"); + g_assert_cmpint (gs_app_get_kind (app1), ==, AS_COMPONENT_KIND_OPERATING_SYSTEM); + g_assert_cmpint (gs_app_get_state (app1), ==, GS_APP_STATE_AVAILABLE); + + /* allow only one operation at a time */ + gs_plugin_loader_set_max_parallel_ops (plugin_loader, 1); + + app2 = gs_app_new ("chiron.desktop"); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "dummy"); + gs_app_set_management_plugin (app2, plugin); + gs_app_set_state (app2, GS_APP_STATE_AVAILABLE); + + /* use "proxy" prefix so the update function succeeds... */ + app3 = gs_app_new ("proxy-zeus.desktop"); + gs_app_set_management_plugin (app3, plugin); + gs_app_set_state (app3, GS_APP_STATE_UPDATABLE_LIVE); + + context = g_main_context_new (); + helper3->loop = g_main_loop_new (context, FALSE); + g_main_context_push_thread_default (context); + + /* call a few operations at the "same time" */ + + /* download an upgrade */ + g_object_unref (plugin_job1); + plugin_job1 = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "app", app1, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job1, + NULL, + plugin_job_action_cb, + helper1); + + /* install an app */ + plugin_job2 = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app2, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job2, + NULL, + plugin_job_action_cb, + helper2); + + /* update an app */ + plugin_job3 = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app3, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job3, + NULL, + plugin_job_action_cb, + helper3); + + /* since we have only 1 parallel installation op possible, + * verify the last operations are pending */ + g_assert_cmpint (gs_app_get_state (app2), ==, GS_APP_STATE_QUEUED_FOR_INSTALL); + g_assert_cmpint (gs_app_get_pending_action (app2), ==, GS_PLUGIN_ACTION_INSTALL); + g_assert_cmpint (gs_app_get_state (app3), ==, GS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpint (gs_app_get_pending_action (app3), ==, GS_PLUGIN_ACTION_UPDATE); + + /* wait for the 2nd installation to finish, it means the 1st should have been + * finished too */ + g_main_loop_run (helper3->loop); + g_main_context_pop_thread_default (context); + + gs_test_flush_main_context (); + g_assert_no_error (helper1->error); + g_assert_no_error (helper2->error); + g_assert_no_error (helper3->error); + + g_assert_cmpint (gs_app_get_state (app1), ==, GS_APP_STATE_UPDATABLE); + g_assert_cmpint (gs_app_get_state (app2), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (app3), ==, GS_APP_STATE_INSTALLED); + + /* set the default max parallel ops */ + gs_plugin_loader_set_max_parallel_ops (plugin_loader, 0); +} + +static void +gs_plugins_dummy_app_size_calc_func (GsPluginLoader *loader) +{ + g_autoptr(GsApp) app1 = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) runtime = NULL; + guint64 value = 0; + + app1 = gs_app_new ("app1"); + gs_app_set_state (app1, GS_APP_STATE_AVAILABLE); + gs_app_set_size_download (app1, GS_SIZE_TYPE_VALID, 1); + gs_app_set_size_installed (app1, GS_SIZE_TYPE_VALID, 1000); + g_assert_cmpint (gs_app_get_size_download (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 1); + g_assert_cmpint (gs_app_get_size_download_dependencies (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + g_assert_cmpint (gs_app_get_size_installed (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 1000); + g_assert_cmpint (gs_app_get_size_installed_dependencies (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + + app2 = gs_app_new ("app2"); + gs_app_set_state (app2, GS_APP_STATE_AVAILABLE); + gs_app_set_size_download (app2, GS_SIZE_TYPE_VALID, 20); + gs_app_set_size_installed (app2, GS_SIZE_TYPE_VALID, 20000); + g_assert_cmpint (gs_app_get_size_download (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 20); + g_assert_cmpint (gs_app_get_size_download_dependencies (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + g_assert_cmpint (gs_app_get_size_installed (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 20000); + g_assert_cmpint (gs_app_get_size_installed_dependencies (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + + runtime = gs_app_new ("runtime"); + gs_app_set_state (runtime, GS_APP_STATE_AVAILABLE); + gs_app_set_size_download (runtime, GS_SIZE_TYPE_VALID, 300); + gs_app_set_size_installed (runtime, GS_SIZE_TYPE_VALID, 300000); + g_assert_cmpint (gs_app_get_size_download (runtime, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 300); + g_assert_cmpint (gs_app_get_size_download_dependencies (runtime, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + g_assert_cmpint (gs_app_get_size_installed (runtime, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 300000); + g_assert_cmpint (gs_app_get_size_installed_dependencies (runtime, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + + gs_app_set_runtime (app1, runtime); + g_assert_cmpint (gs_app_get_size_download (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 1); + g_assert_cmpint (gs_app_get_size_download_dependencies (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 300); + g_assert_cmpint (gs_app_get_size_installed (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 1000); + g_assert_cmpint (gs_app_get_size_installed_dependencies (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + + gs_app_set_runtime (app2, runtime); + g_assert_cmpint (gs_app_get_size_download (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 20); + g_assert_cmpint (gs_app_get_size_download_dependencies (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 300); + g_assert_cmpint (gs_app_get_size_installed (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 20000); + g_assert_cmpint (gs_app_get_size_installed_dependencies (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); + + gs_app_add_related (app1, app2); + g_assert_cmpint (gs_app_get_size_download (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 1); + g_assert_cmpint (gs_app_get_size_download_dependencies (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 320); + g_assert_cmpint (gs_app_get_size_installed (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 1000); + g_assert_cmpint (gs_app_get_size_installed_dependencies (app1, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 20000); + + g_assert_cmpint (gs_app_get_size_download (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 20); + g_assert_cmpint (gs_app_get_size_download_dependencies (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 300); + g_assert_cmpint (gs_app_get_size_installed (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 20000); + g_assert_cmpint (gs_app_get_size_installed_dependencies (app2, &value), ==, GS_SIZE_TYPE_VALID); + g_assert_cmpint (value, ==, 0); +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *xml = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ + g_content_type_set_mime_dirs (NULL); + + /* Force the GTK resources to be registered, needed for fallback icons. */ + gtk_init_check (); + + /* Similarly, add the system-wide icon theme path before it’s + * overwritten by %G_TEST_OPTION_ISOLATE_DIRS. */ + gs_test_expose_icon_theme_paths (); + + gs_test_init (&argc, &argv); + g_setenv ("GS_XMLB_VERBOSE", "1", TRUE); + + /* set all the things required as a dummy test harness */ + setlocale (LC_MESSAGES, "en_GB.UTF-8"); + g_setenv ("GS_SELF_TEST_DUMMY_ENABLE", "1", TRUE); + g_setenv ("GS_SELF_TEST_PROVENANCE_SOURCES", "london*,boston", TRUE); + g_setenv ("GS_SELF_TEST_PROVENANCE_LICENSE_SOURCES", "london*,boston", TRUE); + g_setenv ("GS_SELF_TEST_PROVENANCE_LICENSE_URL", "https://www.debian.org/", TRUE); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-dummy-test-XXXXXX", NULL); + g_assert (tmp_root != NULL); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + + xml = g_strdup ("<?xml version=\"1.0\"?>\n" + "<components version=\"0.9\">\n" + " <component type=\"desktop\">\n" + " <id>chiron.desktop</id>\n" + " <name>Chiron</name>\n" + " <pkgname>chiron</pkgname>\n" + " </component>\n" + " <component type=\"desktop\">\n" + " <id>zeus.desktop</id>\n" + " <name>Zeus</name>\n" + " <summary>A teaching application</summary>\n" + " <pkgname>zeus</pkgname>\n" + " <icon type=\"stock\">drive-harddisk</icon>\n" + " <categories>\n" + " <category>AudioVideo</category>\n" + " <category>Player</category>\n" + " </categories>\n" + " <languages>\n" + " <lang percentage=\"100\">en_GB</lang>\n" + " </languages>\n" + " </component>\n" + " <component type=\"desktop\">\n" + " <id>mate-spell.desktop</id>\n" + " <name>Spell</name>\n" + " <summary>A spelling application for MATE</summary>\n" + " <pkgname>mate-spell</pkgname>\n" + " <icon type=\"stock\">drive-harddisk</icon>\n" + " <project_group>MATE</project_group>\n" + " </component>\n" + " <component type=\"addon\">\n" + " <id>zeus-spell.addon</id>\n" + " <extends>zeus.desktop</extends>\n" + " <name>Spell Check</name>\n" + " <summary>Check the spelling when teaching</summary>\n" + " <pkgname>zeus-spell</pkgname>\n" + " </component>\n" + " <component type=\"desktop\">\n" + " <id>Uninstall Zeus.desktop</id>\n" + " <name>Uninstall Zeus</name>\n" + " <summary>Uninstall the teaching application</summary>\n" + " <icon type=\"stock\">drive-harddisk</icon>\n" + " </component>\n" + " <component type=\"os-upgrade\">\n" + " <id>org.fedoraproject.release-rawhide.upgrade</id>\n" + " <name>Fedora Rawhide</name>\n" + " <summary>Release specific tagline</summary>\n" + " <pkgname>fedora-release</pkgname>\n" + " </component>\n" + " <info>\n" + " <scope>user</scope>\n" + " </info>\n" + "</components>\n"); + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + g_signal_connect (plugin_loader, "status-changed", + G_CALLBACK (gs_plugin_loader_status_changed_cb), NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + g_assert (!gs_plugin_loader_get_enabled (plugin_loader, "notgoingtoexist")); + g_assert (gs_plugin_loader_get_enabled (plugin_loader, "appstream")); + g_assert (gs_plugin_loader_get_enabled (plugin_loader, "dummy")); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/dummy/wildcard", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_wildcard_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/plugin-cache", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_plugin_cache_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/key-colors", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_key_colors_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/search", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_search_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/search-alternate", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_search_alternate_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/url-to-app", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_url_to_app_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/install", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_install_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/error", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_error_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/installed", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_installed_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/refine", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_refine_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/updates", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_updates_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/distro-upgrades", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_distro_upgrades_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/metadata-quirks", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_metadata_quirks); + g_test_add_data_func ("/gnome-software/plugins/dummy/limit-parallel-ops", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_limit_parallel_ops_func); + g_test_add_data_func ("/gnome-software/plugins/dummy/app-size-calc", + plugin_loader, + (GTestDataFunc) gs_plugins_dummy_app_size_calc_func); + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/dummy/meson.build b/plugins/dummy/meson.build new file mode 100644 index 0000000..5b098db --- /dev/null +++ b/plugins/dummy/meson.build @@ -0,0 +1,35 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginDummy"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] + +shared_module( + 'gs_plugin_dummy', + sources : 'gs-plugin-dummy.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [plugin_libs], +) + +if get_option('tests') + e = executable( + 'gs-self-test-dummy', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + c_args : cargs, + ) + test('gs-self-test-dummy', e, suite: ['plugins', 'dummy'], env: test_env) +endif diff --git a/plugins/eos-updater/com.endlessm.Updater.xml b/plugins/eos-updater/com.endlessm.Updater.xml new file mode 100644 index 0000000..641c7f4 --- /dev/null +++ b/plugins/eos-updater/com.endlessm.Updater.xml @@ -0,0 +1,292 @@ +<!DOCTYPE node PUBLIC +'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN' +'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'> +<node> + <!-- + com.endlessm.Updater: + @short_description: Endless OS updater control interface + + Interface to query the state of the OS updater, and to control it: checking + for, downloading and applying updates. + + The updater moves through a series of states (see the `State` property and + `StateChanged` signal), with state transitions triggered by the appropriate + method call, or by an error in an operation. It will return a + `com.endlessm.Updater.Error.WrongState` error if you attempt to perform an + invalid state transition. + --> + <interface name="com.endlessm.Updater"> + <!-- + Poll: + + Check for updates. This may be performed from the `Ready`, + `UpdateAvailable`, `UpdateReady` or `Error` states. It will immediately + transition to the `Polling` state. If an update is then successfully + found, it will transition to the `UpdateAvailable` state. If no update is + available, it will transition to the `Ready` state. If there is an error + checking for an update, it will transition to the `Error` state. + + If an update is found, its details will be reported through the updater’s + properties once the updater reaches the `UpdateAvailable` state. In + particular, through the `UpdateID`, `UpdateRefspec`, `UpdateLabel` and + `UpdateMessage` properties. + --> + <method name="Poll"></method> + + <!-- + PollVolume: + @path: Absolute path to the root directory of a volume to check. + + Like `Poll`, except this polls a specific removable volume (such as a USB + stick), rather than the internet. + + If a `.ostree/repo` directory is available beneath @path, and it contains + a valid OSTree repository, that repository will be checked for updates. + If no such directory exists, the updater will transition to state `Ready` + as no update is available. + --> + <method name="PollVolume"> + <arg name="path" type="s" direction="in"/> + </method> + + <!-- + Fetch: + + Download an update. This may be performed from the `UpdateAvailable` state + only. It will immediately transition to the `Fetching` state. If the + update is successfully downloaded, it will transition to the `UpdateReady` + state; if there was an error it will transition to `Error`. + + Download progress will be reported through the updater’s properties; in + particular, `DownloadedBytes`. + --> + <method name="Fetch"></method> + + <!-- + FetchFull: + @options: Potentially empty dictionary of options. + + Like `Fetch`, except options may be provided to affect its behaviour. + Currently, the following options are supported (unsupported options are + ignored): + + * `force` (type: `b`): If true, force the download without scheduling + it through the system’s metered data scheduler. Typically, this would + be true in response to an explicit user action, and false otherwise. + * `scheduling-timeout-seconds` (type: `u`): Number of seconds to wait + for permission to download from the system’s metered data scheduler, + before returning a `com.endlessm.Updater.Error.MeteredConnection` + error and cancelling the download. Pass zero to disable the timeout. + --> + <method name="FetchFull"> + <arg name="options" type="a{sv}" direction="in"/> + </method> + + <!-- + Apply: + + Apply a downloaded update so that it’s available to boot into on the next + boot. This may be performed from the `UpdateReady` state only. It will + immediately transition to the `Applying` state. If the update is + successfully applied, it will transition to the `UpdateApplied` state; if + there was an error it will transition to `Error`. + --> + <method name="Apply"></method> + + <!-- + Cancel: + + Cancel the ongoing poll, fetch or apply operation. This may be performed + from the `Polling`, `Fetching` or `ApplyingUpdate` states. It will cancel + the operation then transition to the `Error` state, with the error + `com.endlessm.Updater.Error.Cancelled`. + --> + <method name="Cancel"></method> + + <!-- + State: + + Current state of the updater. This will be one of the following: + + * `0` (`None`): No state. + * `1` (`Ready`): Ready to perform an action. + * `2` (`Error`): An error occurred. See the `ErrorName` and + `ErrorMessage` properties for details. + * `3` (`Polling`): Checking for updates. + * `4` (`UpdateAvailable`): An update is available. See the `UpdateID`, + `UpdateRefspec`, `UpdateLabel` and `UpdateMessage` properties for + details. + * `5` (`Fetching`): Downloading an update. See the `DownloadedBytes` + property for progress updates. + * `6` (`UpdateReady`): Update downloaded and ready to apply. + * `7` (`ApplyingUpdate`): Applying an update. + * `8` (`UpdateApplied`): Update applied and ready to reboot into. + + State changes are notified using the `StateChanged` signal. + --> + <property name="State" type="u" access="read"/> + + <!-- + UpdateID: + + Checksum of the OSTree commit available as an update, or the empty string + if no update is available. + --> + <property name="UpdateID" type="s" access="read"/> + + <!-- + UpdateRefspec: + + Refspec (remote name and branch name) of the OSTree commit available as an + update, or the empty string if no update is available. + --> + <property name="UpdateRefspec" type="s" access="read"/> + + <!-- + OriginalRefspec: + + Refspec (remote name and branch name) of the currently booted OSTree + commit. + --> + <property name="OriginalRefspec" type="s" access="read"/> + + <!-- + CurrentID: + + Checksum of the currently booted OSTree commit. + --> + <property name="CurrentID" type="s" access="read"/> + + <!-- + UpdateLabel: + + Subject of the OSTree commit available as an update, or the empty string + if it’s not set or if no update is available. This is the title of the + update. + --> + <property name="UpdateLabel" type="s" access="read"/> + + <!-- + UpdateMessage: + + Description body of the OSTree commit available as an update, or the empty + string if it’s not set or if no update is available. This is the + description of the update. + --> + <property name="UpdateMessage" type="s" access="read"/> + + <!-- + Version: + + Version number of the OSTree commit available as an update, or the empty + string if it’s not set or if no update is available. + --> + <property name="Version" type="s" access="read"/> + + <!-- + DownloadSize: + + Size (in bytes) of the update when downloaded, or `-1` if an update is + available but its download size is unknown. `0` if no update is available. + --> + <property name="DownloadSize" type="x" access="read"/> + + <!-- + DownloadedBytes: + + Number of bytes of the update which have already been downloaded. This + will be `0` before a download starts, and could be `-1` if the + `DownloadSize` is unknown. + --> + <property name="DownloadedBytes" type="x" access="read"/> + + <!-- + UnpackedSize: + + Size (in bytes) of the update when unpacked, or `-1` if an update is + available but its unpacked size is unknown. `0` if no update is available. + --> + <property name="UnpackedSize" type="x" access="read"/> + + <!-- + FullDownloadSize: + + Version of `DownloadSize` which also includes the sizes of parts of the + update which are already present locally (and hence which don’t need to + be downloaded again). + --> + <property name="FullDownloadSize" type="x" access="read"/> + + <!-- + FullUnpackedSize: + + Version of `UnpackedSize` which also includes the sizes of parts of the + update which are already unpacked locally (and hence which won’t occupy + further disk space once the update is applied). + --> + <property name="FullUnpackedSize" type="x" access="read"/> + + <!-- + ErrorCode: + + Error code of the current error, or `0` if no error has been reported. + This is in an unspecified error doman, and hence is useless. + + Deprecated: Use `ErrorName` instead. + --> + <property name="ErrorCode" type="u" access="read"> + <annotation name="org.freedesktop.DBus.Deprecated" value="true"/> + </property> + + <!-- + ErrorName: + + A fully-qualified D-Bus error name, as might be returned from a D-Bus + method. + + This is the empty string if no error has been reported. + + Known errors include: + + * `com.endlessm.Updater.Error.WrongState`: Method was called in a state + which doesn’t support that method. + * `com.endlessm.Updater.Error.LiveBoot`: The updater cannot be used + because the current system is a live boot. + * `com.endlessm.Updater.Error.WrongConfiguration`: A configuration file + contains an error. + * `com.endlessm.Updater.Error.NotOstreeSystem`: The updater cannot be + used because the current system is not booted from an OSTree commit. + * `com.endlessm.Updater.Error.Fetching`: Error when downloading an + update. + * `com.endlessm.Updater.Error.MalformedAutoinstallSpec`: An autoinstall + specification in the pending update contains an error. + * `com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec`: An + autoinstall specification in the pending update contains an unknown + entry. + * `com.endlessm.Updater.Error.FlatpakRemoteConflict`: An autoinstall + specification in the pending update contains a remote name which + doesn’t match the system’s configuration. + * `com.endlessm.Updater.Error.MeteredConnection`: A fetch operation timed + out while waiting for permission to download. + --> + <property name="ErrorName" type="s" access="read"/> + + <!-- + ErrorMessage: + + A human-readable (but unlocalised) error message, or the empty string if + no error has been reported. + --> + <property name="ErrorMessage" type="s" access="read"/> + + <!-- + StateChanged: + @state: The new state. + + Signal notifying of a change in the `State` property. + --> + <signal name="StateChanged"> + <arg type="u" name="state"/> + </signal> + </interface> +</node> diff --git a/plugins/eos-updater/gs-plugin-eos-updater.c b/plugins/eos-updater/gs-plugin-eos-updater.c new file mode 100644 index 0000000..e4c6260 --- /dev/null +++ b/plugins/eos-updater/gs-plugin-eos-updater.c @@ -0,0 +1,1134 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016-2019 Endless Mobile, Inc + * + * Authors: + * Joaquim Rocha <jrocha@endlessm.com> + * Philip Withnall <withnall@endlessm.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> +#include <gio/gio.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <glib-object.h> +#include <gnome-software.h> +#include <gs-plugin.h> +#include <gs-utils.h> +#include <math.h> +#include <ostree.h> + +#include "gs-eos-updater-generated.h" +#include "gs-plugin-eos-updater.h" +#include "gs-plugin-private.h" + +/* + * SECTION: + * Plugin to poll for, download and apply OS updates using the `eos-updater` + * service when running on Endless OS. + * + * This plugin is only useful on Endless OS. + * + * It creates a proxy for the `eos-updater` D-Bus service, which implements a + * basic state machine which progresses through several states in order to + * download updates: `Ready` (doing nothing) → `Poll` (checking for updates) → + * `Fetch` (downloading an update) → `Apply` (deploying the update’s OSTree, + * before a reboot). Any state may transition to the `Error` state at any time, + * and the daemon may disappear at any time. + * + * This plugin follows the state transitions signalled by the daemon, and + * updates the state of a single #GsApp instance (`os_upgrade`) to reflect the + * OS upgrade in the UI. + * + * Calling gs_plugin_eos_updater_refresh_metadata_async() will result in this + * plugin calling the `Poll()` method on the `eos-updater` daemon to check for a + * new update. + * + * Calling gs_plugin_app_upgrade_download() will result in this plugin calling + * a sequence of methods on the `eos-updater` daemon to check for, download and + * apply an update. Typically, gs_plugin_app_upgrade_download() should be called + * once `eos-updater` is already in the `UpdateAvailable` state. It will report + * progress information, with the first 75 percentage points of the progress + * reporting the download progress, and the final 25 percentage points reporting + * the OSTree deployment progress. The final 25 percentage points are currently + * faked because we can’t get reasonable progress data out of OSTree. + * + * The proxy object (`updater_proxy`) uses the thread-default main context from + * the gs_plugin_eos_updater_setup() function, which is currently the global default main + * context from gnome-software’s main thread. This means all the signal + * callbacks from the proxy will be executed in the main thread, and *must not + * block*. + * + * Asynchronous plugin vfuncs (such as + * gs_plugin_eos_updater_refresh_metadata_async()) are run in gnome-software’s + * main thread and *must not block*. As they all call D-Bus methods, the work + * they do is minimal and hence is OK to happen in the main thread. + * + * The other functions (gs_plugin_app_upgrade_download(), + * etc.) are called in #GTask worker threads. They are allowed to call methods + * on the proxy; the main thread is only allowed to receive signals and check + * properties on the proxy, to avoid blocking. Consequently, worker threads need + * to block on the main thread receiving state change signals from + * `eos-updater`. Receipt of these signals is notified through + * `state_change_cond`. This means that all functions which access + * `GsPluginEosUpdater` must lock it using the `mutex`. + * + * `updater_proxy`, `os_upgrade` and `cancellable` are only set in + * gs_plugin_eos_updater_setup(), and are both internally thread-safe — so they can both be + * dereferenced and have their methods called from any thread without + * necessarily holding `mutex`. + * + * Cancellation of any operations on the `eos-updater` daemon (polling, fetching + * or applying) is implemented by calling the `Cancel()` method on it. This is + * permanently connected to the private `cancellable` #GCancellable instance, + * which persists for the lifetime of the plugin. The #GCancellable instances + * for various operations can be temporarily chained to it for the duration of + * each operation. + * + * FIXME: Once all methods are made asynchronous, the locking can be dropped + * from this plugin. + */ + +static const guint max_progress_for_update = 75; /* percent */ + +typedef enum { + EOS_UPDATER_STATE_NONE = 0, + EOS_UPDATER_STATE_READY, + EOS_UPDATER_STATE_ERROR, + EOS_UPDATER_STATE_POLLING, + EOS_UPDATER_STATE_UPDATE_AVAILABLE, + EOS_UPDATER_STATE_FETCHING, + EOS_UPDATER_STATE_UPDATE_READY, + EOS_UPDATER_STATE_APPLYING_UPDATE, + EOS_UPDATER_STATE_UPDATE_APPLIED, +} EosUpdaterState; +#define EOS_UPDATER_N_STATES (EOS_UPDATER_STATE_UPDATE_APPLIED + 1) + +static const gchar * +eos_updater_state_to_str (EosUpdaterState state) +{ + const gchar * const eos_updater_state_str[] = { + "None", + "Ready", + "Error", + "Polling", + "UpdateAvailable", + "Fetching", + "UpdateReady", + "ApplyingUpdate", + "UpdateApplied", + }; + + G_STATIC_ASSERT (G_N_ELEMENTS (eos_updater_state_str) == EOS_UPDATER_N_STATES); + + g_return_val_if_fail ((gint) state < EOS_UPDATER_N_STATES, "unknown"); + return eos_updater_state_str[state]; +} + +static void +gs_eos_updater_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* parse remote eos-updater error */ + if (g_dbus_error_is_remote_error (error)) { + g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); + + g_dbus_error_strip_remote_error (error); + + if (g_str_equal (remote_error, "com.endlessm.Updater.Error.WrongState")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.LiveBoot") || + g_str_equal (remote_error, "com.endlessm.Updater.Error.NotOstreeSystem") || + g_str_equal (remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) { + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.WrongConfiguration")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.Fetching")) { + error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.MalformedAutoinstallSpec") || + g_str_equal (remote_error, "com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec") || + g_str_equal (remote_error, "com.endlessm.Updater.Error.FlatpakRemoteConflict")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.MeteredConnection")) { + error->code = GS_PLUGIN_ERROR_NO_NETWORK; + } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.Cancelled")) { + error->code = GS_PLUGIN_ERROR_CANCELLED; + } else { + g_warning ("Can’t reliably fixup remote error ‘%s’", remote_error); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; + return; + } + + /* this is allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this is allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; +} + +/* the percentage of the progress bar to use for applying the OS upgrade; + * we need to fake the progress in this percentage because applying the OS upgrade + * can take a long time and we don't want the user to think that the upgrade has + * stalled */ +static const guint upgrade_apply_progress_range = 100 - max_progress_for_update; /* percent */ +static const gfloat upgrade_apply_max_time = 600.0; /* sec */ +static const gfloat upgrade_apply_step_time = 0.250; /* sec */ + +static void sync_state_from_updater_unlocked (GsPluginEosUpdater *self); + +struct _GsPluginEosUpdater +{ + GsPlugin parent; + + /* These members are only set once in gs_plugin_eos_updater_setup(), and are + * internally thread-safe, so can be accessed without holding @mutex: */ + GsEosUpdater *updater_proxy; /* (owned) */ + GsApp *os_upgrade; /* (owned) */ + GCancellable *cancellable; /* (owned) */ + gulong cancelled_id; + + /* These members must only ever be accessed from the main thread, so + * can be accessed without holding @mutex: */ + gfloat upgrade_fake_progress; + guint upgrade_fake_progress_handler; + + /* State synchronisation between threads: */ + GMutex mutex; + GCond state_change_cond; /* locked by @mutex */ +}; + +G_DEFINE_TYPE (GsPluginEosUpdater, gs_plugin_eos_updater, GS_TYPE_PLUGIN) + +static void +os_upgrade_cancelled_cb (GCancellable *cancellable, + gpointer user_data) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (user_data); + + g_debug ("%s: Cancelling upgrade", G_STRFUNC); + gs_eos_updater_call_cancel (self->updater_proxy, NULL, NULL, NULL); +} + +static gboolean +should_add_os_upgrade (GsAppState state) +{ + switch (state) { + case GS_APP_STATE_AVAILABLE: + case GS_APP_STATE_AVAILABLE_LOCAL: + case GS_APP_STATE_UPDATABLE: + case GS_APP_STATE_QUEUED_FOR_INSTALL: + case GS_APP_STATE_INSTALLING: + case GS_APP_STATE_UPDATABLE_LIVE: + return TRUE; + case GS_APP_STATE_UNKNOWN: + case GS_APP_STATE_INSTALLED: + case GS_APP_STATE_UNAVAILABLE: + case GS_APP_STATE_REMOVING: + default: + return FALSE; + } +} + +/* Wrapper around gs_app_set_state() which ensures we also notify of update + * changes if we change between non-upgradable and upgradable states, so that + * the app is notified to appear in the UI. */ +static void +app_set_state (GsPlugin *plugin, + GsApp *app, + GsAppState new_state) +{ + GsAppState old_state = gs_app_get_state (app); + + if (new_state == old_state) + return; + + gs_app_set_state (app, new_state); + + if (should_add_os_upgrade (old_state) != + should_add_os_upgrade (new_state)) { + g_debug ("%s: Calling gs_plugin_updates_changed()", G_STRFUNC); + gs_plugin_updates_changed (plugin); + } +} + +static gboolean +eos_updater_error_is_cancelled (const gchar *error_name) +{ + return (g_strcmp0 (error_name, "com.endlessm.Updater.Error.Cancelled") == 0); +} + +/* This will be invoked in the main thread. */ +static void +updater_state_changed (GsPluginEosUpdater *self) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + + g_debug ("%s", G_STRFUNC); + + sync_state_from_updater_unlocked (self); + + /* Signal any blocked threads; typically this will be + * gs_plugin_app_upgrade_download() in a #GTask worker thread. */ + g_cond_broadcast (&self->state_change_cond); +} + +/* This will be invoked in the main thread. */ +static void +updater_downloaded_bytes_changed (GsPluginEosUpdater *self) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + + sync_state_from_updater_unlocked (self); +} + +/* This will be invoked in the main thread, but doesn’t currently need to hold + * `mutex` since it only accesses `self->updater_proxy` and `self->os_upgrade`, + * both of which are internally thread-safe. */ +static void +updater_version_changed (GsPluginEosUpdater *self) +{ + const gchar *version = gs_eos_updater_get_version (self->updater_proxy); + + /* If eos-updater goes away, we want to retain the previously set value + * of the version, for use in error messages. */ + if (version != NULL) + gs_app_set_version (self->os_upgrade, version); +} + +/* This will be invoked in the main thread, but doesn’t currently need to hold + * `mutex` since `self->updater_proxy` and `self->os_upgrade` are both + * thread-safe, and `self->upgrade_fake_progress` and + * `self->upgrade_fake_progress_handler` are only ever accessed from the main + * thread. */ +static gboolean +fake_os_upgrade_progress_cb (gpointer user_data) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (user_data); + gfloat normal_step; + guint new_progress; + const gfloat fake_progress_max = 99.0; + + if (gs_eos_updater_get_state (self->updater_proxy) != EOS_UPDATER_STATE_APPLYING_UPDATE || + self->upgrade_fake_progress > fake_progress_max) { + self->upgrade_fake_progress = 0; + self->upgrade_fake_progress_handler = 0; + return G_SOURCE_REMOVE; + } + + normal_step = (gfloat) upgrade_apply_progress_range / + (upgrade_apply_max_time / upgrade_apply_step_time); + + self->upgrade_fake_progress += normal_step; + + new_progress = max_progress_for_update + + (guint) round (self->upgrade_fake_progress); + gs_app_set_progress (self->os_upgrade, + MIN (new_progress, (guint) fake_progress_max)); + + g_debug ("OS upgrade fake progress: %f", self->upgrade_fake_progress); + + return G_SOURCE_CONTINUE; +} + +/* This method deals with the synchronization between the EOS updater's states + * (D-Bus service) and the OS upgrade's states (GsApp), in order to show the user + * what is happening and what they can do. + * + * It must be called with self->mutex already locked. */ +static void +sync_state_from_updater_unlocked (GsPluginEosUpdater *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + GsApp *app = self->os_upgrade; + EosUpdaterState state; + GsAppState previous_app_state = gs_app_get_state (app); + GsAppState current_app_state; + + /* in case the OS upgrade has been disabled */ + if (self->updater_proxy == NULL) { + g_debug ("%s: Updater disabled", G_STRFUNC); + return; + } + + state = gs_eos_updater_get_state (self->updater_proxy); + g_debug ("EOS Updater state changed: %s", eos_updater_state_to_str (state)); + + switch (state) { + case EOS_UPDATER_STATE_NONE: + case EOS_UPDATER_STATE_READY: { + app_set_state (plugin, app, GS_APP_STATE_UNKNOWN); + break; + } case EOS_UPDATER_STATE_POLLING: { + /* Nothing to do here. */ + break; + } case EOS_UPDATER_STATE_UPDATE_AVAILABLE: { + gint64 total_size; + + app_set_state (plugin, app, GS_APP_STATE_AVAILABLE); + + /* The property returns -1 to indicate unknown size */ + total_size = gs_eos_updater_get_download_size (self->updater_proxy); + if (total_size >= 0) + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, total_size); + else + gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWN, 0); + + break; + } + case EOS_UPDATER_STATE_FETCHING: { + gint64 total_size = 0; + gint64 downloaded = 0; + guint progress = 0; + + /* FIXME: Set to QUEUED_FOR_INSTALL if we’re waiting for metered + * data permission. */ + app_set_state (plugin, app, GS_APP_STATE_INSTALLING); + + downloaded = gs_eos_updater_get_downloaded_bytes (self->updater_proxy); + total_size = gs_eos_updater_get_download_size (self->updater_proxy); + + if (total_size == 0) { + g_debug ("OS upgrade %s total size is 0!", + gs_app_get_unique_id (app)); + progress = GS_APP_PROGRESS_UNKNOWN; + } else if (downloaded < 0 || total_size < 0) { + /* Both properties return -1 to indicate unknown */ + progress = GS_APP_PROGRESS_UNKNOWN; + } else { + /* set progress only up to a max percentage, leaving the + * remaining for applying the update */ + progress = (gfloat) downloaded / (gfloat) total_size * + (gfloat) max_progress_for_update; + } + gs_app_set_progress (app, progress); + + break; + } + case EOS_UPDATER_STATE_UPDATE_READY: { + app_set_state (plugin, app, GS_APP_STATE_UPDATABLE); + break; + } + case EOS_UPDATER_STATE_APPLYING_UPDATE: { + /* set as 'installing' because if it is applying the update, we + * want to show the progress bar */ + app_set_state (plugin, app, GS_APP_STATE_INSTALLING); + + /* set up the fake progress to inform the user that something + * is still being done (we don't get progress reports from + * deploying updates) */ + if (self->upgrade_fake_progress_handler != 0) + g_source_remove (self->upgrade_fake_progress_handler); + self->upgrade_fake_progress = 0; + self->upgrade_fake_progress_handler = + g_timeout_add ((guint) (1000.0 * upgrade_apply_step_time), + (GSourceFunc) fake_os_upgrade_progress_cb, + self); + + break; + } + case EOS_UPDATER_STATE_UPDATE_APPLIED: { + app_set_state (plugin, app, GS_APP_STATE_UPDATABLE); + + break; + } + case EOS_UPDATER_STATE_ERROR: { + const gchar *error_name; + const gchar *error_message; + + error_name = gs_eos_updater_get_error_name (self->updater_proxy); + error_message = gs_eos_updater_get_error_message (self->updater_proxy); + + /* unless the error is because the user cancelled the upgrade, + * we should make sure it gets in the journal */ + if (!eos_updater_error_is_cancelled (error_name)) + g_warning ("Got OS upgrade error state with name '%s': %s", + error_name, error_message); + + /* We can’t recover the app state since eos-updater needs to + * go through the ready → poll → fetch → apply loop again in + * order to recover its state. So go back to ‘unknown’. */ + app_set_state (plugin, app, GS_APP_STATE_UNKNOWN); + + /* Cancelling anything in the updater will result in a + * transition to the Error state. Use that as a cue to reset + * our #GCancellable ready for next time. */ + g_cancellable_reset (self->cancellable); + + break; + } + default: + g_warning ("Encountered unknown eos-updater state: %u", state); + break; + } + + current_app_state = gs_app_get_state (app); + + g_debug ("%s: Old app state: %s; new app state: %s", + G_STRFUNC, gs_app_state_to_string (previous_app_state), + gs_app_state_to_string (current_app_state)); + + /* if the state changed from or to 'unknown', we need to notify that a + * new update should be shown */ + if (should_add_os_upgrade (previous_app_state) != + should_add_os_upgrade (current_app_state)) { + g_debug ("%s: Calling gs_plugin_updates_changed()", G_STRFUNC); + gs_plugin_updates_changed (plugin); + } +} + +static void proxy_new_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/* This is called in the main thread, so will end up creating an @updater_proxy + * which is tied to the main thread’s #GMainContext. */ +static void +gs_plugin_eos_updater_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_eos_updater_setup_async); + + g_debug ("%s", G_STRFUNC); + + g_mutex_init (&self->mutex); + g_cond_init (&self->state_change_cond); + + locker = g_mutex_locker_new (&self->mutex); + + self->cancellable = g_cancellable_new (); + self->cancelled_id = + g_cancellable_connect (self->cancellable, + G_CALLBACK (os_upgrade_cancelled_cb), + self, NULL); + + /* Check that the proxy exists (and is owned; it should auto-start) so + * we can disable the plugin for systems which don’t have eos-updater. + * Throughout the rest of the plugin, errors from the daemon + * (particularly where it has disappeared off the bus) are ignored, and + * the poll/fetch/apply sequence is run through again to recover from + * the error. This is the only point in the plugin where we consider an + * error from eos-updater to be fatal to the plugin. */ + gs_eos_updater_proxy_new (gs_plugin_get_system_bus_connection (plugin), + G_DBUS_PROXY_FLAGS_NONE, + "com.endlessm.Updater", + "/com/endlessm/Updater", + cancellable, + proxy_new_cb, + g_steal_pointer (&task)); +} + +static void +proxy_new_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginEosUpdater *self = g_task_get_source_object (task); + g_autofree gchar *name_owner = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GIcon) ic = NULL; + g_autofree gchar *background_filename = NULL; + g_autofree gchar *css = NULL; + g_autofree gchar *summary = NULL; + g_autofree gchar *version = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GError) local_error = NULL; + const gchar *os_name; + + locker = g_mutex_locker_new (&self->mutex); + + self->updater_proxy = gs_eos_updater_proxy_new_finish (result, &local_error); + if (self->updater_proxy == NULL) { + gs_eos_updater_error_convert (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->updater_proxy)); + + if (name_owner == NULL) { + g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Couldn’t create EOS Updater proxy: couldn’t get name owner"); + return; + } + + g_signal_connect_object (self->updater_proxy, "notify::state", + G_CALLBACK (updater_state_changed), + self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->updater_proxy, + "notify::downloaded-bytes", + G_CALLBACK (updater_downloaded_bytes_changed), + self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->updater_proxy, "notify::version", + G_CALLBACK (updater_version_changed), + self, G_CONNECT_SWAPPED); + + /* prepare EOS upgrade app + sync initial state */ + + /* use stock icon */ + ic = g_themed_icon_new ("system-component-addon"); + + /* Check for a background image in the standard location. */ + background_filename = gs_utils_get_upgrade_background (NULL); + + if (background_filename != NULL) + css = g_strconcat ("background: url('file://", background_filename, "');" + "background-size: 100% 100%;", NULL); + + os_release = gs_os_release_new (&local_error); + if (local_error) { + g_warning ("Failed to get OS release information: %s", local_error->message); + /* Just a fallback, do not localize */ + os_name = "Endless OS"; + g_clear_error (&local_error); + } else { + os_name = gs_os_release_get_name (os_release); + } + + g_object_get (G_OBJECT (self->updater_proxy), + "version", &version, + "update-message", &summary, + NULL); + + if (summary == NULL || *summary == '\0') { + g_clear_pointer (&summary, g_free); + g_object_get (G_OBJECT (self->updater_proxy), + "update-label", &summary, + NULL); + } + + if (summary == NULL || *summary == '\0') { + g_clear_pointer (&summary, g_free); + /* Translators: The '%s' is replaced with the OS name, like "Endless OS" */ + summary = g_strdup_printf (_("%s update with new features and fixes."), os_name); + } + + /* create the OS upgrade */ + app = gs_app_new ("com.endlessm.EOS.upgrade"); + gs_app_add_icon (app, ic); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_kind (app, AS_COMPONENT_KIND_OPERATING_SYSTEM); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, os_name); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, summary); + gs_app_set_version (app, version == NULL ? "" : version); + 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_set_management_plugin (app, GS_PLUGIN (self)); + gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", css); + + self->os_upgrade = g_steal_pointer (&app); + + /* sync initial state */ + sync_state_from_updater_unlocked (self); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_eos_updater_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_eos_updater_init (GsPluginEosUpdater *self) +{ +} + +static void +gs_plugin_eos_updater_dispose (GObject *object) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (object); + + if (self->upgrade_fake_progress_handler != 0) { + g_source_remove (self->upgrade_fake_progress_handler); + self->upgrade_fake_progress_handler = 0; + } + + if (self->updater_proxy != NULL) { + g_signal_handlers_disconnect_by_func (self->updater_proxy, + G_CALLBACK (updater_state_changed), + self); + g_signal_handlers_disconnect_by_func (self->updater_proxy, + G_CALLBACK (updater_downloaded_bytes_changed), + self); + g_signal_handlers_disconnect_by_func (self->updater_proxy, + G_CALLBACK (updater_version_changed), + self); + } + + g_cancellable_cancel (self->cancellable); + if (self->cancellable != NULL && self->cancelled_id != 0) + g_cancellable_disconnect (self->cancellable, self->cancelled_id); + g_clear_object (&self->cancellable); + + g_clear_object (&self->updater_proxy); + + g_clear_object (&self->os_upgrade); + + G_OBJECT_CLASS (gs_plugin_eos_updater_parent_class)->dispose (object); +} + +static void +gs_plugin_eos_updater_finalize (GObject *object) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (object); + + g_cond_clear (&self->state_change_cond); + g_mutex_clear (&self->mutex); + + G_OBJECT_CLASS (gs_plugin_eos_updater_parent_class)->finalize (object); +} + +static void poll_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +/* Called in the main thread. */ +static void +gs_plugin_eos_updater_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin); + EosUpdaterState updater_state; + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_eos_updater_refresh_metadata_async); + + /* We let the eos-updater daemon do its own caching, so ignore the + * @cache_age_secs, unless it’s %G_MAXUINT64, which signifies startup of g-s. + * In that case, it’s probably just going to load the system too much to + * do an update check now. We can wait. */ + g_debug ("%s: cache_age_secs: %" G_GUINT64_FORMAT, G_STRFUNC, cache_age_secs); + + if (cache_age_secs == G_MAXUINT64) { + g_task_return_boolean (task, TRUE); + return; + } + + /* check if the OS upgrade has been disabled */ + if (self->updater_proxy == NULL) { + g_debug ("%s: Updater disabled", G_STRFUNC); + g_task_return_boolean (task, TRUE); + return; + } + + /* poll in the error/none/ready states to check if there's an + * update available */ + updater_state = gs_eos_updater_get_state (self->updater_proxy); + switch (updater_state) { + case EOS_UPDATER_STATE_ERROR: + case EOS_UPDATER_STATE_NONE: + case EOS_UPDATER_STATE_READY: + gs_eos_updater_call_poll (self->updater_proxy, + cancellable, + poll_cb, + g_steal_pointer (&task)); + return; + default: + g_debug ("%s: Updater in state %s; not polling", + G_STRFUNC, eos_updater_state_to_str (updater_state)); + g_task_return_boolean (task, TRUE); + return; + } +} + +static void +poll_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsEosUpdater *updater_proxy = GS_EOS_UPDATER (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + if (!gs_eos_updater_call_poll_finish (updater_proxy, result, &local_error)) { + gs_eos_updater_error_convert (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + } else { + g_task_return_boolean (task, TRUE); + } +} + +static gboolean +gs_plugin_eos_updater_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +/* Called in the main thread. */ +static void +gs_plugin_eos_updater_list_distro_upgrades_async (GsPlugin *plugin, + GsPluginListDistroUpgradesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(GsAppList) list = gs_app_list_new (); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_eos_updater_list_distro_upgrades_async); + + g_debug ("%s", G_STRFUNC); + + /* if we are testing the plugin, then always add the OS upgrade */ + if (g_getenv ("GS_PLUGIN_EOS_TEST") != NULL) { + gs_app_set_state (self->os_upgrade, GS_APP_STATE_AVAILABLE); + gs_app_list_add (list, self->os_upgrade); + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); + return; + } + + /* check if the OS upgrade has been disabled */ + if (self->updater_proxy == NULL) { + g_debug ("%s: Updater disabled", G_STRFUNC); + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); + return; + } + + if (should_add_os_upgrade (gs_app_get_state (self->os_upgrade))) { + g_debug ("Adding EOS upgrade: %s", + gs_app_get_unique_id (self->os_upgrade)); + gs_app_list_add (list, self->os_upgrade); + } else { + g_debug ("Not adding EOS upgrade"); + } + + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); +} + +static GsAppList * +gs_plugin_eos_updater_list_distro_upgrades_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +/* Must be called with self->mutex already locked. */ +static gboolean +wait_for_state_change_unlocked (GsPluginEosUpdater *self, + GCancellable *cancellable, + GError **error) +{ + EosUpdaterState old_state, new_state; + + old_state = new_state = gs_eos_updater_get_state (self->updater_proxy); + g_debug ("%s: Old state ‘%s’", G_STRFUNC, eos_updater_state_to_str (old_state)); + + while (new_state == old_state && + !g_cancellable_is_cancelled (cancellable)) { + g_cond_wait (&self->state_change_cond, &self->mutex); + new_state = gs_eos_updater_get_state (self->updater_proxy); + } + + if (!g_cancellable_set_error_if_cancelled (cancellable, error)) { + g_debug ("%s: New state ‘%s’", G_STRFUNC, eos_updater_state_to_str (new_state)); + return TRUE; + } else { + g_debug ("%s: Cancelled", G_STRFUNC); + return FALSE; + } +} + +/* Could be executed in any thread. No need to hold `self->mutex` since we don’t + * access anything which is not thread-safe. */ +static void +cancelled_cb (GCancellable *ui_cancellable, + gpointer user_data) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (user_data); + + /* Chain cancellation. */ + g_debug ("Propagating OS download cancellation from %p to %p", + ui_cancellable, self->cancellable); + g_cancellable_cancel (self->cancellable); + + /* And wake up anything blocking on a state change. */ + g_cond_broadcast (&self->state_change_cond); +} + +/* Called in a #GTask worker thread, and it needs to hold `self->mutex` due to + * synchronising on state with the main thread. */ +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin); + gulong cancelled_id = 0; + EosUpdaterState state; + gboolean done, allow_restart; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* if the OS upgrade has been disabled */ + if (self->updater_proxy == NULL) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "The OS upgrade has been disabled in the EOS plugin"); + return FALSE; + } + + g_assert (app == self->os_upgrade); + + /* Set up cancellation. */ + g_debug ("Chaining cancellation from %p to %p", cancellable, self->cancellable); + if (cancellable != NULL) { + cancelled_id = g_cancellable_connect (cancellable, + G_CALLBACK (cancelled_cb), + plugin, NULL); + } + + /* Step through the state machine until we are finished downloading and + * applying the update, or until an error occurs. All of the D-Bus calls + * here will block until the method call is complete. */ + state = gs_eos_updater_get_state (self->updater_proxy); + + done = FALSE; + allow_restart = (state == EOS_UPDATER_STATE_NONE || + state == EOS_UPDATER_STATE_READY || + state == EOS_UPDATER_STATE_ERROR); + + while (!done && !g_cancellable_is_cancelled (cancellable)) { + state = gs_eos_updater_get_state (self->updater_proxy); + g_debug ("%s: State ‘%s’", G_STRFUNC, eos_updater_state_to_str (state)); + + switch (state) { + case EOS_UPDATER_STATE_NONE: + case EOS_UPDATER_STATE_READY: { + /* Poll for an update. This typically only happens if + * we’ve drifted out of sync with the updater process + * due to it dying. In that case, only restart once + * before giving up, so we don’t end up in an endless + * loop (say, if eos-updater always died 50% of the way + * through a download). */ + if (allow_restart) { + allow_restart = FALSE; + g_debug ("Restarting OS upgrade from none/ready state"); + if (!gs_eos_updater_call_poll_sync (self->updater_proxy, + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + } else { + /* Display an error to the user. */ + g_autoptr(GError) error_local = NULL; + g_autoptr(GsPluginEvent) event = NULL; + + g_set_error_literal (&error_local, GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + _("EOS update service could not fetch and apply the update.")); + gs_eos_updater_error_convert (&error_local); + + event = gs_plugin_event_new ("app", app, + "action", GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + + /* Error out. */ + done = TRUE; + } + + break; + } case EOS_UPDATER_STATE_POLLING: { + /* Nothing to do here. */ + break; + } case EOS_UPDATER_STATE_UPDATE_AVAILABLE: { + g_auto(GVariantDict) options_dict = G_VARIANT_DICT_INIT (NULL); + + /* when the OS upgrade was started by the user and the + * updater reports an available update, (meaning we were + * polling before), we should readily call fetch */ + g_variant_dict_insert (&options_dict, "force", "b", TRUE); + + if (!gs_eos_updater_call_fetch_full_sync (self->updater_proxy, + g_variant_dict_end (&options_dict), + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + break; + } + case EOS_UPDATER_STATE_FETCHING: { + /* Nothing to do here. */ + break; + } + case EOS_UPDATER_STATE_UPDATE_READY: { + /* if there's an update ready to deployed, and it was started by + * the user, we should proceed to applying the upgrade */ + gs_app_set_progress (app, max_progress_for_update); + + if (!gs_eos_updater_call_apply_sync (self->updater_proxy, + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + break; + } + case EOS_UPDATER_STATE_APPLYING_UPDATE: { + /* Nothing to do here. */ + break; + } + case EOS_UPDATER_STATE_UPDATE_APPLIED: { + /* Done! */ + done = TRUE; + break; + } + case EOS_UPDATER_STATE_ERROR: { + const gchar *error_name; + const gchar *error_message; + g_autoptr(GError) error_local = NULL; + + error_name = gs_eos_updater_get_error_name (self->updater_proxy); + error_message = gs_eos_updater_get_error_message (self->updater_proxy); + error_local = g_dbus_error_new_for_dbus_error (error_name, error_message); + + /* Display an error to the user, unless they cancelled + * the download. */ + if (!eos_updater_error_is_cancelled (error_name)) { + g_autoptr(GsPluginEvent) event = NULL; + + gs_eos_updater_error_convert (&error_local); + + event = gs_plugin_event_new ("app", app, + "action", GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + } + + /* Unconditionally call Poll() to get the updater out + * of the error state and to allow the update to be + * displayed in the UI again and retried. Exit the + * state change loop immediately, though, to prevent + * possible endless loops between the Poll/Error + * states. */ + allow_restart = FALSE; + g_debug ("Restarting OS upgrade on error"); + if (!gs_eos_updater_call_poll_sync (self->updater_proxy, + cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + /* Error out. */ + done = TRUE; + + break; + } + default: + g_warning ("Encountered unknown eos-updater state: %u", state); + break; + } + + /* Block on the next state change. */ + if (!done && + !wait_for_state_change_unlocked (self, cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + } + + if (cancellable != NULL && cancelled_id != 0) { + g_debug ("Disconnecting cancellable %p", cancellable); + g_cancellable_disconnect (cancellable, cancelled_id); + } + + /* Process the final state. */ + if (gs_eos_updater_get_state (self->updater_proxy) == EOS_UPDATER_STATE_ERROR) { + const gchar *error_name; + const gchar *error_message; + g_autoptr(GError) error_local = NULL; + + error_name = gs_eos_updater_get_error_name (self->updater_proxy); + error_message = gs_eos_updater_get_error_message (self->updater_proxy); + error_local = g_dbus_error_new_for_dbus_error (error_name, error_message); + gs_eos_updater_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + + return FALSE; + } else if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_eos_updater_error_convert (error); + return FALSE; + } + + return TRUE; +} + +static void +gs_plugin_eos_updater_class_init (GsPluginEosUpdaterClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_eos_updater_dispose; + object_class->finalize = gs_plugin_eos_updater_finalize; + + plugin_class->setup_async = gs_plugin_eos_updater_setup_async; + plugin_class->setup_finish = gs_plugin_eos_updater_setup_finish; + plugin_class->refresh_metadata_async = gs_plugin_eos_updater_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_eos_updater_refresh_metadata_finish; + plugin_class->list_distro_upgrades_async = gs_plugin_eos_updater_list_distro_upgrades_async; + plugin_class->list_distro_upgrades_finish = gs_plugin_eos_updater_list_distro_upgrades_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_EOS_UPDATER; +} diff --git a/plugins/eos-updater/gs-plugin-eos-updater.h b/plugins/eos-updater/gs-plugin-eos-updater.h new file mode 100644 index 0000000..a87b649 --- /dev/null +++ b/plugins/eos-updater/gs-plugin-eos-updater.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_EOS_UPDATER (gs_plugin_eos_updater_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginEosUpdater, gs_plugin_eos_updater, GS, PLUGIN_EOS_UPDATER, GsPlugin) + +G_END_DECLS diff --git a/plugins/eos-updater/meson.build b/plugins/eos-updater/meson.build new file mode 100644 index 0000000..19edf22 --- /dev/null +++ b/plugins/eos-updater/meson.build @@ -0,0 +1,25 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginEosUpdater"'] + +eos_updater_generated = gnome.gdbus_codegen( + 'gs-eos-updater-generated', + sources : ['com.endlessm.Updater.xml'], + interface_prefix : 'com.endlessm.', + namespace : 'GsEos', +) + +shared_module( + 'gs_plugin_eos-updater', + eos_updater_generated, + sources : 'gs-plugin-eos-updater.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ + plugin_libs, + ostree, + ], +) diff --git a/plugins/eos-updater/tests/eos_updater.py b/plugins/eos-updater/tests/eos_updater.py new file mode 100644 index 0000000..5e4aa8d --- /dev/null +++ b/plugins/eos-updater/tests/eos_updater.py @@ -0,0 +1,414 @@ +'''eos-updater mock template + +This creates a mock eos-updater interface (com.endlessm.Updater), with several +methods on the Mock sidecar interface which allow its internal state flow to be +controlled. + +A typical call chain for this would be: + - Test harness calls SetPollAction('update', {}, '', '') + - SUT calls Poll() + - Test harness calls FinishPoll() + - SUT calls Fetch() + - Test harness calls FinishFetch() + - SUT calls Apply() + - Test harness calls FinishApply() + +Errors can be simulated by specifying an `early-error` or `late-error` as the +action in a Set*Action() call. `early-error` will result in the associated +Poll() call (for example) transitioning to the error state. `late-error` will +result in a transition to the error state only once (for example) FinishPoll() +is called. + +See the implementation of each Set*Action() method for the set of actions it +supports. + +Usage: + python3 -m dbusmock \ + --template ./plugins/eos-updater/tests/mock-eos-updater.py +''' + +# This program 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. See http://www.gnu.org/copyleft/lgpl.html for the full +# text of the license. +# +# The LGPL 2.1+ has been chosen as that’s the license eos-updater is under. + +from enum import IntEnum +from gi.repository import GLib +import time + +import dbus +import dbus.mainloop.glib +from dbusmock import MOCK_IFACE + + +__author__ = 'Philip Withnall' +__email__ = 'withnall@endlessm.com' +__copyright__ = '© 2019 Endless Mobile Inc.' +__license__ = 'LGPL 2.1+' + + +class UpdaterState(IntEnum): + NONE = 0 + READY = 1 + ERROR = 2 + POLLING = 3 + UPDATE_AVAILABLE = 4 + FETCHING = 5 + UPDATE_READY = 6 + APPLYING_UPDATE = 7 + UPDATE_APPLIED = 8 + + +BUS_NAME = 'com.endlessm.Updater' +MAIN_OBJ = '/com/endlessm/Updater' +MAIN_IFACE = 'com.endlessm.Updater' +SYSTEM_BUS = True + + +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + +def load(mock, parameters): + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary({ + 'State': dbus.UInt32(parameters.get('State', 1)), + 'UpdateID': dbus.String(parameters.get('UpdateID', '')), + 'UpdateRefspec': dbus.String(parameters.get('UpdateRefspec', '')), + 'OriginalRefspec': + dbus.String(parameters.get('OriginalRefspec', '')), + 'CurrentID': dbus.String(parameters.get('CurrentID', '')), + 'UpdateLabel': dbus.String(parameters.get('UpdateLabel', '')), + 'UpdateMessage': dbus.String(parameters.get('UpdateMessage', '')), + 'Version': dbus.String(parameters.get('Version', '')), + 'DownloadSize': dbus.Int64(parameters.get('DownloadSize', 0)), + 'DownloadedBytes': + dbus.Int64(parameters.get('DownloadedBytes', 0)), + 'UnpackedSize': dbus.Int64(parameters.get('UnpackedSize', 0)), + 'FullDownloadSize': + dbus.Int64(parameters.get('FullDownloadSize', 0)), + 'FullUnpackedSize': + dbus.Int64(parameters.get('FullUnpackedSize', 0)), + 'ErrorCode': dbus.UInt32(parameters.get('ErrorCode', 0)), + 'ErrorName': dbus.String(parameters.get('ErrorName', '')), + 'ErrorMessage': dbus.String(parameters.get('ErrorMessage', '')), + }, signature='sv')) + + # Set up initial state + mock.__poll_action = 'no-update' + mock.__fetch_action = 'success' + mock.__apply_action = 'success' + + # Set up private methods + mock.__set_properties = __set_properties + mock.__change_state = __change_state + mock.__set_error = __set_error + mock.__check_state = __check_state + + +# +# Internal utility methods +# + +# Values in @properties must have variant_level≥1 +def __set_properties(self, iface, properties): + for key, value in properties.items(): + self.props[iface][key] = value + self.EmitSignal(dbus.PROPERTIES_IFACE, 'PropertiesChanged', 'sa{sv}as', [ + iface, + properties, + [], + ]) + + +def __change_state(self, new_state): + props = { + 'State': dbus.UInt32(new_state, variant_level=1) + } + + # Reset error state if necessary. + if new_state != UpdaterState.ERROR and \ + self.props[MAIN_IFACE]['ErrorName'] != '': + props['ErrorCode'] = dbus.UInt32(0, variant_level=1) + props['ErrorName'] = dbus.String('', variant_level=1) + props['ErrorMessage'] = dbus.String('', variant_level=1) + + self.__set_properties(self, MAIN_IFACE, props) + self.EmitSignal(MAIN_IFACE, 'StateChanged', 'u', [dbus.UInt32(new_state)]) + + +def __set_error(self, error_name, error_message): + assert(error_name != '') + + self.__set_properties(self, MAIN_IFACE, { + 'ErrorName': dbus.String(error_name, variant_level=1), + 'ErrorMessage': dbus.String(error_message, variant_level=1), + 'ErrorCode': dbus.UInt32(1, variant_level=1), + }) + self.__change_state(self, UpdaterState.ERROR) + + +def __check_state(self, allowed_states): + if self.props[MAIN_IFACE]['State'] not in allowed_states: + raise dbus.exceptions.DBusException( + 'Call not allowed in this state', + name='com.endlessm.Updater.Error.WrongState') + + +# +# Updater methods which are too big for squeezing into AddMethod() +# + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Poll(self): + self.__check_state(self, set([ + UpdaterState.READY, + UpdaterState.UPDATE_AVAILABLE, + UpdaterState.UPDATE_READY, + UpdaterState.ERROR, + ])) + + self.__change_state(self, UpdaterState.POLLING) + + if self.__poll_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__poll_error_name, + self.__poll_error_message) + else: + # we now expect the test harness to call FinishPoll() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='s', out_signature='') +def PollVolume(self, path): + # FIXME: Currently unsupported + return self.Poll() + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Fetch(self): + return self.FetchFull() + + +@dbus.service.method(MAIN_IFACE, in_signature='a{sv}', out_signature='') +def FetchFull(self, options=None): + self.__check_state(self, set([UpdaterState.UPDATE_AVAILABLE])) + + self.__change_state(self, UpdaterState.FETCHING) + + if self.__fetch_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__fetch_error_name, + self.__fetch_error_message) + else: + # we now expect the test harness to call FinishFetch() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Apply(self): + self.__check_state(self, set([UpdaterState.UPDATE_READY])) + + self.__change_state(self, UpdaterState.APPLYING_UPDATE) + + if self.__apply_action == 'early-error': + time.sleep(0.5) + self.__set_error(self, self.__apply_error_name, + self.__apply_error_message) + else: + # we now expect the test harness to call FinishApply() on the mock + # interface + pass + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='') +def Cancel(self): + self.__check_state(self, set([ + UpdaterState.POLLING, + UpdaterState.FETCHING, + UpdaterState.APPLYING_UPDATE, + ])) + + time.sleep(1) + self.__set_error(self, 'com.endlessm.Updater.Error.Cancelled', + 'Update was cancelled') + + +# +# Convenience methods on the mock +# + +@dbus.service.method(MOCK_IFACE, in_signature='sa{sv}ss', out_signature='') +def SetPollAction(self, action, update_properties, error_name, error_message): + '''Set the action to happen when the SUT calls Poll(). + + This sets the action which will happen when Poll() (and subsequently + FinishPoll()) are called, including the details of the error which will be + returned or the new update which will be advertised. + ''' + # Provide a default update. + if not update_properties: + update_properties = { + 'UpdateID': dbus.String('f' * 64, variant_level=1), + 'UpdateRefspec': + dbus.String('remote:new-refspec', variant_level=1), + 'OriginalRefspec': + dbus.String('remote:old-refspec', variant_level=1), + 'CurrentID': dbus.String('1' * 64, variant_level=1), + 'UpdateLabel': dbus.String('New OS Update', variant_level=1), + 'UpdateMessage': + dbus.String('Some release notes.', variant_level=1), + 'Version': dbus.String('3.7.0', variant_level=1), + 'DownloadSize': dbus.Int64(1000000000, variant_level=1), + 'UnpackedSize': dbus.Int64(1500000000, variant_level=1), + 'FullDownloadSize': dbus.Int64(1000000000 * 0.8, variant_level=1), + 'FullUnpackedSize': dbus.Int64(1500000000 * 0.8, variant_level=1), + } + + self.__poll_action = action + self.__poll_update_properties = update_properties + self.__poll_error_name = error_name + self.__poll_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='') +def FinishPoll(self): + self.__check_state(self, set([UpdaterState.POLLING])) + + if self.__poll_action == 'no-update': + self.__change_state(self, UpdaterState.READY) + elif self.__poll_action == 'update': + assert(set([ + 'UpdateID', + 'UpdateRefspec', + 'OriginalRefspec', + 'CurrentID', + 'UpdateLabel', + 'UpdateMessage', + 'Version', + 'FullDownloadSize', + 'FullUnpackedSize', + 'DownloadSize', + 'UnpackedSize', + ]) <= set(self.__poll_update_properties.keys())) + + # Set the initial DownloadedBytes based on whether we know the full + # download size. + props = self.__poll_update_properties + if props['DownloadSize'] < 0: + props['DownloadedBytes'] = dbus.Int64(-1, variant_level=1) + else: + props['DownloadedBytes'] = dbus.Int64(0, variant_level=1) + + self.__set_properties(self, MAIN_IFACE, props) + self.__change_state(self, UpdaterState.UPDATE_AVAILABLE) + elif self.__poll_action == 'early-error': + # Handled in Poll() itself. + pass + elif self.__poll_action == 'late-error': + self.__set_error(self, self.__poll_error_name, + self.__poll_error_message) + else: + assert(False) + + +@dbus.service.method(MOCK_IFACE, in_signature='sss', out_signature='') +def SetFetchAction(self, action, error_name, error_message): + '''Set the action to happen when the SUT calls Fetch(). + + This sets the action which will happen when Fetch() (and subsequently + FinishFetch()) are called, including the details of the error which will be + returned, if applicable. + ''' + self.__fetch_action = action + self.__fetch_error_name = error_name + self.__fetch_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='', + async_callbacks=('success_cb', 'error_cb')) +def FinishFetch(self, success_cb, error_cb): + '''Finish a pending client call to Fetch(). + + This is implemented using async_callbacks since if the fetch action is + ‘success’ it will block until the simulated download is complete, emitting + download progress signals throughout. As it’s implemented asynchronously, + this allows any calls to Cancel() to be handled by the mock service + part-way through the fetch. + ''' + self.__check_state(self, set([UpdaterState.FETCHING])) + + if self.__fetch_action == 'success': + # Simulate the download. + i = 0 + download_size = self.props[MAIN_IFACE]['DownloadSize'] + + def _download_progress_cb(): + nonlocal i + + # Allow cancellation. + if self.props[MAIN_IFACE]['State'] != UpdaterState.FETCHING: + return False + + downloaded_bytes = (i / 100.0) * download_size + self.__set_properties(self, MAIN_IFACE, { + 'DownloadedBytes': + dbus.Int64(downloaded_bytes, variant_level=1), + }) + + i += 1 + + # Keep looping until the download is complete. + if i <= 100: + return True + + # When the download is complete, change the service state and + # finish the asynchronous FinishFetch() call. + self.__change_state(self, UpdaterState.UPDATE_READY) + success_cb() + return False + + GLib.timeout_add(100, _download_progress_cb) + elif self.__fetch_action == 'early-error': + # Handled in Fetch() itself. + success_cb() + elif self.__fetch_action == 'late-error': + self.__set_error(self, self.__fetch_error_name, + self.__fetch_error_message) + success_cb() + else: + assert(False) + + +@dbus.service.method(MOCK_IFACE, in_signature='sss', out_signature='') +def SetApplyAction(self, action, error_name, error_message): + '''Set the action to happen when the SUT calls Apply(). + + This sets the action which will happen when Apply() (and subsequently + FinishApply()) are called, including the details of the error which will be + returned, if applicable. + ''' + self.__apply_action = action + self.__apply_error_name = error_name + self.__apply_error_message = error_message + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='') +def FinishApply(self): + self.__check_state(self, set([UpdaterState.APPLYING_UPDATE])) + + if self.__apply_action == 'success': + self.__change_state(self, UpdaterState.UPDATE_APPLIED) + elif self.__apply_action == 'early-error': + # Handled in Apply() itself. + pass + elif self.__apply_action == 'late-error': + self.__set_error(self, self.__apply_error_name, + self.__apply_error_message) + else: + assert(False) diff --git a/plugins/eos-updater/tests/manual-test.py b/plugins/eos-updater/tests/manual-test.py new file mode 100755 index 0000000..b6413d9 --- /dev/null +++ b/plugins/eos-updater/tests/manual-test.py @@ -0,0 +1,434 @@ +#!/usr/bin/python3 + +# This program 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. See http://www.gnu.org/copyleft/lgpl.html for the full +# text of the license. +# +# The LGPL 2.1+ has been chosen as that’s the license eos-updater is under. + + +from enum import IntEnum +import os +import time +import unittest +import dbus +import dbusmock +import ddt + + +__author__ = 'Philip Withnall' +__email__ = 'withnall@endlessm.com' +__copyright__ = '© 2019 Endless Mobile Inc.' +__license__ = 'LGPL 2.1+' + + +class UpdaterState(IntEnum): + '''eos-updater states; see its State property''' + NONE = 0 + READY = 1 + ERROR = 2 + POLLING = 3 + UPDATE_AVAILABLE = 4 + FETCHING = 5 + UPDATE_READY = 6 + APPLYING_UPDATE = 7 + UPDATE_APPLIED = 8 + + +@ddt.ddt +class ManualTest(dbusmock.DBusTestCase): + '''A manual test of the eos-updater plugin in gnome-software. + + It creates a mock eos-updater D-Bus daemon, on the real system bus (because + otherwise gnome-software’s other plugins can’t communicate with their + system daemons; to fix this, we’d need to mock those up too). The test + harness provides the user with instructions about how to run gnome-software + and what to do in it, waiting for them to press enter between steps. + + FIXME: This test could potentially eventually be automated by doing the UI + steps using Dogtail or OpenQA. + + It tests various classes of interaction between the plugin and the daemon: + normal update process (with and without an update available); error returns + from the daemon; cancellation of the daemon by another process; + cancellation of the daemon from gnome-software; and the daemon unexpectedly + going away (i.e. crashing). + ''' + + @classmethod + def setUpClass(cls): + # FIXME: See the comment below about why we currently run on the actual + # system bus. + # cls.start_system_bus() + cls.dbus_con = cls.get_dbus(True) + + def setUp(self): + # Work out the path to the dbusmock template in the same directory as + # this file. + self_path = os.path.dirname(os.path.realpath(__file__)) + template_path = os.path.join(self_path, 'eos_updater.py') + + # Spawn a python-dbusmock server. Use the actual system bus, since + # gnome-software needs to access various other services (such as + # packagekit) which we don’t currently mock (FIXME). + (self.p_mock, self.obj_eos_updater) = self.spawn_server_template( + template_path, {}, stdout=None) + self.dbusmock = dbus.Interface(self.obj_eos_updater, + dbusmock.MOCK_IFACE) + + def tearDown(self): + self.kill_gnome_software() + self.p_mock.terminate() + self.p_mock.wait() + + def launch_gnome_software(self): + '''Instruct the user to launch gnome-software''' + print('Launch gnome-software with:') + print('gnome-software --verbose') + self.manual_check('Press enter to continue') + + def kill_gnome_software(self): + '''Instruct the user to kill gnome-software''' + print('Kill gnome-software with:') + print('pkill gnome-software') + self.manual_check('Press enter to continue') + + def await_state(self, state): + '''Block until eos-updater reaches the given `state`''' + print('Awaiting state %u' % state) + props_iface = dbus.Interface(self.obj_eos_updater, + dbus.PROPERTIES_IFACE) + while props_iface.Get('com.endlessm.Updater', 'State') != state: + time.sleep(0.2) + + def manual_check(self, prompt): + '''Instruct the user to do a manual check and block until done''' + input('\033[92;1m' + prompt + '\033[0m\n') + + def test_poll_no_update(self): + '''Test that no updates are shown if eos-updater successfully says + there are none.''' + self.dbusmock.SetPollAction( + 'no-update', dbus.Dictionary({}, signature='sv'), '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check there are no EOS updates listed') + self.await_state(UpdaterState.READY) + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.LiveBoot', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.NotOstreeSystem', + 'com.endlessm.Updater.Error.Cancelled') + def test_poll_early_error(self, error_name): + '''Test that a D-Bus error return from Poll() is handled correctly.''' + self.dbusmock.SetPollAction( + 'early-error', dbus.Dictionary({}, signature='sv'), + error_name, 'Some error message.') + + self.launch_gnome_software() + self.await_state(UpdaterState.ERROR) + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check there are no EOS updates listed, and a ' + 'GsPluginEosUpdater error is printed on the ' + 'terminal') + else: + self.manual_check('Check there are no EOS updates listed, and no ' + 'GsPluginEosUpdater cancellation error is ' + 'printed on the terminal') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.LiveBoot', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.NotOstreeSystem', + 'com.endlessm.Updater.Error.Cancelled') + def test_poll_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Poll() is handled correctly.''' + self.dbusmock.SetPollAction( + 'late-error', dbus.Dictionary({}, signature='sv'), + error_name, 'Some error message.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check there are no EOS updates listed, and a ' + 'GsPluginEosUpdater error is printed on the ' + 'terminal') + else: + self.manual_check('Check there are no EOS updates listed, and no ' + 'GsPluginEosUpdater cancellation error is ' + 'printed on the terminal') + self.await_state(UpdaterState.ERROR) + + @ddt.data(True, False) + def test_update_available(self, manually_refresh): + '''Test that the entire update process works if an update is + available.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + if manually_refresh: + self.manual_check('Check an EOS update is listed; press the ' + 'Refresh button') + + # TODO: if you proceed through the test slowly, this sometimes doesn’t + # work + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.dbusmock.FinishApply() + + self.manual_check('Check the banner says to ‘Restart Now’ (don’t ' + 'click it)') + self.await_state(UpdaterState.UPDATE_APPLIED) + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Fetching', + 'com.endlessm.Updater.Error.MalformedAutoinstallSpec', + 'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec', + 'com.endlessm.Updater.Error.FlatpakRemoteConflict', + 'com.endlessm.Updater.Error.MeteredConnection', + 'com.endlessm.Updater.Error.Cancelled') + def test_fetch_early_error(self, error_name): + '''Test that a D-Bus error return from Fetch() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('early-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check a fetch error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Fetching', + 'com.endlessm.Updater.Error.MalformedAutoinstallSpec', + 'com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec', + 'com.endlessm.Updater.Error.FlatpakRemoteConflict', + 'com.endlessm.Updater.Error.MeteredConnection', + 'com.endlessm.Updater.Error.Cancelled') + def test_fetch_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Fetch() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('late-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check a fetch error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Cancelled') + def test_apply_early_error(self, error_name): + '''Test that a D-Bus error return from Apply() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('early-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check an apply error is displayed after the ' + 'update reached ~75% completion') + else: + self.manual_check('Check no cancellation error is displayed after ' + 'the update reached ~75% completion') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + @ddt.data('com.endlessm.Updater.Error.WrongState', + 'com.endlessm.Updater.Error.WrongConfiguration', + 'com.endlessm.Updater.Error.Cancelled') + def test_apply_late_error(self, error_name): + '''Test that a transition to the Error state after successfully calling + Apply() is handled correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('late-error', error_name, + 'Some error or other.') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.dbusmock.FinishApply() + + self.await_state(UpdaterState.ERROR) + if error_name != 'com.endlessm.Updater.Error.Cancelled': + self.manual_check('Check an apply error is displayed') + else: + self.manual_check('Check no cancellation error is displayed') + + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + self.manual_check('Check an EOS update is listed again') + + def test_no_eos_updater_running(self): + '''Test that the plugin doesn’t make a fuss if eos-updater is + unavailable.''' + self.p_mock.kill() + + self.launch_gnome_software() + + self.manual_check('Check there are no EOS updates listed, and no ' + 'errors shown') + + def test_fetch_ui_cancellation(self): + '''Test that cancelling a download from the UI works correctly.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button, then shortly afterwards press the Cancel ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.await_state(UpdaterState.ERROR) + self.manual_check('Check a fetch cancellation error is displayed') + + def test_poll_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + polling for updates.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.p_mock.kill() + + self.manual_check('Check no error is shown for the poll failure') + self.setUp() + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + + self.manual_check('Press the Refresh button and check an update is ' + 'shown') + # TODO: It may take a few minutes for the update to appear on the + # updates page + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + def test_fetch_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + fetching an update.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.p_mock.kill() + + self.manual_check('Check an error is shown for the fetch failure') + + def test_apply_eos_updater_dies(self): + '''Test that gnome-software recovers if eos-updater dies while + applying an update.''' + self.dbusmock.SetPollAction( + 'update', dbus.Dictionary({}, signature='sv'), '', '') + self.dbusmock.SetFetchAction('success', '', '') + self.dbusmock.SetApplyAction('success', '', '') + + self.launch_gnome_software() + self.await_state(UpdaterState.POLLING) + self.dbusmock.FinishPoll() + + self.manual_check('Check an EOS update is listed; press the Download ' + 'button') + self.await_state(UpdaterState.FETCHING) + self.dbusmock.FinishFetch() + + self.manual_check('Check the download has paused at ~75% complete ' + '(waiting to apply)') + self.await_state(UpdaterState.APPLYING_UPDATE) + self.p_mock.kill() + + self.manual_check('Check an error is shown for the apply failure') + + +if __name__ == '__main__': + unittest.main() diff --git a/plugins/epiphany/gs-plugin-epiphany.c b/plugins/epiphany/gs-plugin-epiphany.c new file mode 100644 index 0000000..537ed40 --- /dev/null +++ b/plugins/epiphany/gs-plugin-epiphany.c @@ -0,0 +1,1183 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021-2022 Matthew Leeds <mwleeds@protonmail.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> +#include <glib/gi18n.h> +#include <gnome-software.h> +#include <fcntl.h> +#include <gio/gunixfdlist.h> +#include <glib/gstdio.h> + +#include "gs-epiphany-generated.h" +#include "gs-plugin-epiphany.h" +#include "gs-plugin-private.h" + +/* + * SECTION: + * This plugin uses Epiphany to install, launch, and uninstall web applications. + * + * If the org.gnome.Epiphany.WebAppProvider D-Bus interface is not present or + * the DynamicLauncher portal is not available then it self-disables. This + * should work with both Flatpak'd and traditionally packaged Epiphany, for new + * enough versions of Epiphany. + * + * It's worth noting that this plugin has to deal with two different app IDs + * for installed web apps: + * + * 1. The app ID used in the <id> element in the AppStream metainfo file, which + * looks like "org.gnome.Software.WebApp_527a2dd6729c3574227c145bbc447997f0048537.desktop" + * See https://gitlab.gnome.org/mwleeds/gnome-pwa-list/-/blob/6e8b17b018f99dbf00b1fa956ed75c4a0ccbf389/pwa-metainfo-generator.py#L84-89 + * This app ID is used for gs_app_new() so that the appstream plugin + * refines the apps created here, and used for the plugin cache. + * + * 2. The app ID generated by Epiphany when installing a web app, which looks + * like "org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043.desktop" + * though it can have a different prefix if Epiphany was built with, for + * example, a development profile. Throughout this plugin this type of app + * ID is handled with a variable called "installed_app_id". This app ID is + * used in method calls to the org.gnome.Epiphany.WebAppProvider interface, + * and used for gs_app_set_launchable() and g_desktop_app_info_new(). + * + * Since: 43 + */ + +struct _GsPluginEpiphany +{ + GsPlugin parent; + + GsWorkerThread *worker; /* (owned) */ + + GsEphyWebAppProvider *epiphany_proxy; /* (owned) */ + GDBusProxy *launcher_portal_proxy; /* (owned) */ + GFileMonitor *monitor; /* (owned) */ + guint changed_id; + /* protects installed_apps_cached, url_id_map, and the plugin cache */ + GMutex installed_apps_mutex; + /* installed_apps_cached: whether the plugin cache has all installed apps */ + gboolean installed_apps_cached; + GHashTable *url_id_map; /* (owned) (not nullable) (element-type utf8 utf8) */ + + /* default permissions, shared between all applications */ + GsAppPermissions *permissions; /* (owned) (not nullable) */ +}; + +G_DEFINE_TYPE (GsPluginEpiphany, gs_plugin_epiphany, GS_TYPE_PLUGIN) + +#define assert_in_worker(self) \ + g_assert (gs_worker_thread_is_in_worker_context (self->worker)) + +static void +gs_epiphany_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* parse remote epiphany-webapp-provider error */ + if (g_dbus_error_is_remote_error (error)) { + g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); + + g_dbus_error_strip_remote_error (error); + + if (g_str_equal (remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) { + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + } else if (g_str_has_prefix (remote_error, "org.gnome.Epiphany.WebAppProvider.Error")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else { + g_warning ("Can’t reliably fixup remote error ‘%s’", remote_error); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; + return; + } + + /* this is allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this is allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; +} + +/* Run in the main thread. */ +static void +gs_plugin_epiphany_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (user_data); + + { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex); + gs_plugin_cache_invalidate (GS_PLUGIN (self)); + g_hash_table_remove_all (self->url_id_map); + self->installed_apps_cached = FALSE; + } + + /* FIXME: With the current API this is the only way to reload the list + * of installed apps. + */ + gs_plugin_reload (GS_PLUGIN (self)); +} + +static void +epiphany_web_app_provider_proxy_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +dynamic_launcher_portal_proxy_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_epiphany_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(GError) local_error = NULL; + g_autofree char *portal_apps_path = NULL; + g_autoptr(GFile) portal_apps_file = NULL; + GDBusConnection *connection; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_epiphany_setup_async); + + g_debug ("%s", G_STRFUNC); + + self->installed_apps_cached = FALSE; + + /* This is a mapping from URL to app ID, where the app ID comes from + * Epiphany. This allows us to use that app ID rather than the + * AppStream app ID in certain contexts (see the comment at the top of + * this file). + */ + self->url_id_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + /* Watch for changes to the set of installed apps in the main thread. + * This will also trigger when other apps' dynamic launchers are + * installed or removed but that is expected to be infrequent. + */ + portal_apps_path = g_build_filename (g_get_user_data_dir (), "xdg-desktop-portal", "applications", NULL); + portal_apps_file = g_file_new_for_path (portal_apps_path); + /* Monitoring the directory works even if it doesn't exist yet */ + self->monitor = g_file_monitor_directory (portal_apps_file, G_FILE_MONITOR_WATCH_MOVES, + cancellable, &local_error); + if (self->monitor == NULL) { + gs_epiphany_error_convert (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + self->changed_id = g_signal_connect (self->monitor, "changed", + G_CALLBACK (gs_plugin_epiphany_changed_cb), self); + + connection = gs_plugin_get_session_bus_connection (GS_PLUGIN (self)); + g_assert (connection != NULL); + + gs_ephy_web_app_provider_proxy_new (connection, + G_DBUS_PROXY_FLAGS_NONE, + "org.gnome.Epiphany.WebAppProvider", + "/org/gnome/Epiphany/WebAppProvider", + cancellable, + epiphany_web_app_provider_proxy_created_cb, + g_steal_pointer (&task)); +} + +static void +epiphany_web_app_provider_proxy_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + g_autofree gchar *name_owner = NULL; + g_autoptr(GError) local_error = NULL; + GsPluginEpiphany *self = g_task_get_source_object (task); + GDBusConnection *connection; + GCancellable *cancellable; + + /* Check that the proxy exists (and is owned; it should auto-start) so + * we can disable the plugin for systems which don’t have new enough + * Epiphany. + */ + self->epiphany_proxy = gs_ephy_web_app_provider_proxy_new_finish (result, &local_error); + + if (self->epiphany_proxy == NULL) { + gs_epiphany_error_convert (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->epiphany_proxy)); + if (name_owner == NULL) { + g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Couldn’t create Epiphany WebAppProvider proxy: couldn’t get name owner"); + return; + } + + connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (self->epiphany_proxy)); + cancellable = g_task_get_cancellable (task); + + g_dbus_proxy_new (connection, + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, + NULL, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.DynamicLauncher", + cancellable, + dynamic_launcher_portal_proxy_created_cb, + g_steal_pointer (&task)); +} + +static void +dynamic_launcher_portal_proxy_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GVariant) version = NULL; + g_autoptr(GError) local_error = NULL; + GsPluginEpiphany *self = g_task_get_source_object (task); + + /* Check that the proxy exists (and is owned; it should auto-start) so + * we can disable the plugin for systems which don’t have new enough + * Epiphany. + */ + self->launcher_portal_proxy = g_dbus_proxy_new_finish (result, &local_error); + + if (self->launcher_portal_proxy == NULL) { + gs_epiphany_error_convert (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + version = g_dbus_proxy_get_cached_property (self->launcher_portal_proxy, "version"); + if (version == NULL) { + g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Dynamic launcher portal not available"); + return; + } else { + g_debug ("Found version %" G_GUINT32_FORMAT " of the dynamic launcher portal", + g_variant_get_uint32 (version)); + } + + /* Start up a worker thread to process all the plugin’s function calls. */ + self->worker = gs_worker_thread_new ("gs-plugin-epiphany"); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_epiphany_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_epiphany_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_epiphany_shutdown_async); + + /* Stop the worker thread. */ + gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task)); +} + +static void +shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginEpiphany *self = g_task_get_source_object (task); + g_autoptr(GsWorkerThread) worker = NULL; + g_autoptr(GError) local_error = NULL; + + worker = g_steal_pointer (&self->worker); + + if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_epiphany_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_epiphany_init (GsPluginEpiphany *self) +{ + /* Re-used permissions by all GsApp instances; do not modify it out + of this place. */ + self->permissions = gs_app_permissions_new (); + gs_app_permissions_set_flags (self->permissions, GS_APP_PERMISSIONS_FLAGS_NETWORK); + gs_app_permissions_seal (self->permissions); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (GS_PLUGIN (self), "org.gnome.Software.Plugin.Epiphany"); + + /* need help from appstream */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + + /* prioritize over packages */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_BETTER_THAN, "packagekit"); +} + +static void +gs_plugin_epiphany_dispose (GObject *object) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object); + + if (self->changed_id > 0) { + g_signal_handler_disconnect (self->monitor, self->changed_id); + self->changed_id = 0; + } + + g_clear_object (&self->epiphany_proxy); + g_clear_object (&self->launcher_portal_proxy); + g_clear_object (&self->monitor); + g_clear_object (&self->worker); + g_clear_pointer (&self->url_id_map, g_hash_table_unref); + + G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->dispose (object); +} + +static void +gs_plugin_epiphany_finalize (GObject *object) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object); + + g_mutex_clear (&self->installed_apps_mutex); + g_clear_object (&self->permissions); + + G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->finalize (object); +} + +static gboolean ensure_installed_apps_cache (GsPluginEpiphany *self, + GCancellable *cancellable, + GError **error); + +/* Run in @worker. The caller must have already done ensure_installed_apps_cache() */ +static void +gs_epiphany_refine_app_state (GsPlugin *plugin, + GsApp *app) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin); + + assert_in_worker (self); + + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) { + g_autoptr(GsApp) cached_app = NULL; + const char *appstream_source; + + /* If we have a cached app, set the state from there. Otherwise + * only set the state to available if the app came from + * appstream data, because there's no way to re-install an app + * in Software that was originally installed from Epiphany, + * unless we have appstream metainfo for it. + */ + cached_app = gs_plugin_cache_lookup (plugin, gs_app_get_id (app)); + appstream_source = gs_app_get_metadata_item (app, "appstream::source-file"); + if (cached_app) + gs_app_set_state (app, gs_app_get_state (cached_app)); + else if (appstream_source) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + else { + gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE); + gs_app_set_url_missing (app, + "https://gitlab.gnome.org/GNOME/gnome-software/-/wikis/How-to-reinstall-a-web-app"); + } + } +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, + GsApp *app) +{ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_WEB_APP && + gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE) { + gs_app_set_management_plugin (app, plugin); + } +} + +static gint +get_priority_for_interactivity (gboolean interactive) +{ + return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW; +} + +static void list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_epiphany_list_apps_async (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE); + + task = gs_plugin_list_apps_data_new_task (plugin, query, flags, + cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_epiphany_list_apps_async); + + /* Queue a job to get the apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + list_apps_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker */ +static void +refine_app (GsPluginEpiphany *self, + GsApp *app, + GsPluginRefineFlags flags, + GUri *uri, + const char *url) +{ + const char *hostname; + const char *installed_app_id; + const struct { + const gchar *hostname; + const gchar *license_spdx; + } app_licenses[] = { + /* Keep in alphabetical order by hostname */ + { "app.diagrams.net", "Apache-2.0" }, + { "devdocs.io", "MPL-2.0" }, + { "discourse.flathub.org", "GPL-2.0-or-later" }, + { "discourse.gnome.org", "GPL-2.0-or-later" }, + { "excalidraw.com", "MIT" }, + { "pinafore.social", "AGPL-3.0-only" }, + { "snapdrop.net", "GPL-3.0-only" }, + { "stackedit.io", "Apache-2.0" }, + { "squoosh.app", "Apache-2.0" }, + }; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (uri != NULL); + g_return_if_fail (url != NULL); + + gs_app_set_origin (app, "gnome-web"); + gs_app_set_origin_ui (app, _("GNOME Web")); + + gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER); + gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_URL, url); + + installed_app_id = g_hash_table_lookup (self->url_id_map, url); + if (installed_app_id) { + gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_app_id); + } + + /* Hard-code the licenses as it's hard to get them programmatically. We + * can move them to an AppStream file if needed. + */ + hostname = g_uri_get_host (uri); + if (gs_app_get_license (app) == NULL && hostname != NULL) { + for (gsize i = 0; i < G_N_ELEMENTS (app_licenses); i++) { + if (g_str_equal (hostname, app_licenses[i].hostname)) { + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, + app_licenses[i].license_spdx); + break; + } + } + } + + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0); + + /* Use the default permissions */ + gs_app_set_permissions (app, self->permissions); + + if (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url); + + /* Use the domain name (e.g. "discourse.gnome.org") as a fallback summary. + * FIXME: Fetch the summary from the site's webapp manifest. + */ + if (gs_app_get_summary (app) == NULL) { + if (hostname != NULL && *hostname != '\0') + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, hostname); + else + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, url); + } + + if (installed_app_id == NULL) + return; + + { + const gchar *name; + g_autofree char *icon_path = NULL; + goffset desktop_size = 0, icon_size = 0; + g_autoptr(GDesktopAppInfo) desktop_info = NULL; + g_autoptr(GFileInfo) file_info = NULL; + g_autoptr(GFile) icon_file = NULL; + + desktop_info = g_desktop_app_info_new (installed_app_id); + + if (desktop_info == NULL) { + g_warning ("Couldn't get GDesktopAppInfo for app %s", installed_app_id); + return; + } + + name = g_app_info_get_name (G_APP_INFO (desktop_info)); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name); + + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) { + g_autoptr(GFile) desktop_file = NULL; + const gchar *desktop_path; + guint64 install_date = 0; + + desktop_path = g_desktop_app_info_get_filename (desktop_info); + g_assert (desktop_path); + desktop_file = g_file_new_for_path (desktop_path); + + file_info = g_file_query_info (desktop_file, + G_FILE_ATTRIBUTE_TIME_CREATED "," G_FILE_ATTRIBUTE_STANDARD_SIZE, + 0, NULL, NULL); + if (file_info) { + install_date = g_file_info_get_attribute_uint64 (file_info, G_FILE_ATTRIBUTE_TIME_CREATED); + desktop_size = g_file_info_get_size (file_info); + } + if (install_date) { + gs_app_set_install_date (app, install_date); + } + } + + icon_path = g_desktop_app_info_get_string (desktop_info, "Icon"); + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE && + icon_path) { + icon_file = g_file_new_for_path (icon_path); + + g_clear_object (&file_info); + file_info = g_file_query_info (icon_file, + G_FILE_ATTRIBUTE_STANDARD_SIZE, + 0, NULL, NULL); + if (file_info) + icon_size = g_file_info_get_size (file_info); + } + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON && + gs_app_get_icons (app) == NULL && + icon_path) { + g_autoptr(GIcon) icon = g_file_icon_new (icon_file); + g_autofree char *icon_dir = g_path_get_dirname (icon_path); + g_autofree char *icon_dir_basename = g_path_get_basename (icon_dir); + const char *x; + guint64 width = 0; + + /* dir should be either scalable or e.g. 512x512 */ + if (g_strcmp0 (icon_dir_basename, "scalable") == 0) { + /* Ensure scalable icons are preferred */ + width = 4096; + } else if ((x = strchr (icon_dir_basename, 'x')) != NULL) { + g_ascii_string_to_unsigned (x + 1, 10, 1, G_MAXINT, &width, NULL); + } + if (width > 0 && width <= 4096) { + gs_icon_set_width (icon, width); + gs_icon_set_height (icon, width); + } else { + g_warning ("Unexpectedly unable to determine width of icon %s", icon_path); + } + + gs_app_add_icon (app, icon); + } + if (desktop_size > 0 || icon_size > 0) { + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, desktop_size + icon_size); + } + } +} + +/* Run in @worker */ +static GsApp * +gs_epiphany_create_app (GsPluginEpiphany *self, + const char *id) +{ + g_autoptr(GsApp) app = NULL; + + assert_in_worker (self); + + app = gs_plugin_cache_lookup (GS_PLUGIN (self), id); + if (app != NULL) + return g_steal_pointer (&app); + + app = gs_app_new (id); + gs_app_set_management_plugin (app, GS_PLUGIN (self)); + gs_app_set_kind (app, AS_COMPONENT_KIND_WEB_APP); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (GS_PLUGIN (self))); + + gs_plugin_cache_add (GS_PLUGIN (self), id, app); + return g_steal_pointer (&app); +} + +static gchar * /* (transfer full) */ +generate_app_id_for_url (const gchar *url) +{ + /* Generate the app ID used in the AppStream data using the + * same method as pwa-metainfo-generator.py in + * https://gitlab.gnome.org/mwleeds/gnome-pwa-list + * Using this app ID rather than the one provided by Epiphany + * makes it possible for the appstream plugin to refine the + * GsApp we create (see the comment at the top of this file). + */ + g_autofree gchar *url_hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, url, -1); + return g_strconcat ("org.gnome.Software.WebApp_", url_hash, ".desktop", NULL); +} + +/* Run in @worker */ +static gboolean +ensure_installed_apps_cache (GsPluginEpiphany *self, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GVariant) webapps_v = NULL; + g_auto(GStrv) webapps = NULL; + guint n_webapps; + g_autoptr(GsAppList) installed_cache = gs_app_list_new (); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex); + + assert_in_worker (self); + + if (self->installed_apps_cached) + return TRUE; + + if (!gs_ephy_web_app_provider_call_get_installed_apps_sync (self->epiphany_proxy, + &webapps, + cancellable, + error)) { + gs_epiphany_error_convert (error); + return FALSE; + } + + n_webapps = g_strv_length (webapps); + g_debug ("%s: epiphany-webapp-provider returned %u installed web apps", G_STRFUNC, n_webapps); + for (guint i = 0; i < n_webapps; i++) { + const gchar *desktop_file_id = webapps[i]; + const gchar *url = NULL; + g_autofree char *metainfo_app_id = NULL; + const gchar *exec; + int argc; + GsPluginRefineFlags refine_flags; + g_auto(GStrv) argv = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GDesktopAppInfo) desktop_info = NULL; + g_autoptr(GUri) uri = NULL; + + g_debug ("%s: Working on installed web app %s", G_STRFUNC, desktop_file_id); + + desktop_info = g_desktop_app_info_new (desktop_file_id); + + if (desktop_info == NULL) { + g_warning ("Epiphany returned a non-existent or invalid desktop ID %s", desktop_file_id); + continue; + } + + /* This way of getting the URL is a bit hacky but it's what + * Epiphany does, specifically in + * ephy_web_application_for_profile_directory() which lives in + * https://gitlab.gnome.org/GNOME/epiphany/-/blob/master/lib/ephy-web-app-utils.c + */ + exec = g_app_info_get_commandline (G_APP_INFO (desktop_info)); + if (g_shell_parse_argv (exec, &argc, &argv, NULL)) { + g_assert (argc > 0); + url = argv[argc - 1]; + } + if (!url || !(uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL))) { + g_warning ("Failed to parse URL for web app %s: %s", + desktop_file_id, url ? url : "(null)"); + continue; + } + + /* Store the installed app id for use in refine_app() */ + g_hash_table_insert (self->url_id_map, g_strdup (url), + g_strdup (desktop_file_id)); + + metainfo_app_id = generate_app_id_for_url (url); + g_debug ("Creating GsApp for webapp with URL %s using app ID %s (desktop file id: %s)", + url, metainfo_app_id, desktop_file_id); + + /* App gets added to the plugin cache here */ + app = gs_epiphany_create_app (self, metainfo_app_id); + + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID; + refine_app (self, app, refine_flags, uri, url); + } + + /* Update the state on any apps that were uninstalled outside + * gnome-software + */ + gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), installed_cache, GS_APP_STATE_INSTALLED); + for (guint i = 0; i < gs_app_list_length (installed_cache); i++) { + GsApp *app = gs_app_list_index (installed_cache, i); + const char *installed_app_id; + const char *appstream_source; + + installed_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID); + if (installed_app_id == NULL) { + g_warning ("Installed app unexpectedly has no desktop id: %s", gs_app_get_id (app)); + continue; + } + + if (g_strv_contains ((const char * const *)webapps, installed_app_id)) + continue; + + gs_plugin_cache_remove (GS_PLUGIN (self), gs_app_get_id (app)); + + appstream_source = gs_app_get_metadata_item (app, "appstream::source-file"); + if (appstream_source) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + else + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + } + + self->installed_apps_cached = TRUE; + return TRUE; +} + +/* Run in @worker */ +static void +list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object); + g_autoptr(GsAppList) list = gs_app_list_new (); + GsPluginListAppsData *data = task_data; + GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET; + const gchar * const *keywords = NULL; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (data->query != NULL) { + is_installed = gs_app_query_get_is_installed (data->query); + keywords = gs_app_query_get_keywords (data->query); + } + + /* Currently only support a subset of query properties, and only one set at once. + * Also don’t currently support GS_APP_QUERY_TRISTATE_FALSE. */ + if ((is_installed == GS_APP_QUERY_TRISTATE_UNSET && + keywords == NULL) || + is_installed == GS_APP_QUERY_TRISTATE_FALSE || + gs_app_query_get_n_properties_set (data->query) != 1) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported query"); + return; + } + + /* Ensure the cache is up to date. */ + if (!ensure_installed_apps_cache (self, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (is_installed == GS_APP_QUERY_TRISTATE_TRUE) + gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), list, GS_APP_STATE_INSTALLED); + else if (keywords != NULL) { + for (gsize i = 0; keywords[i]; i++) { + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, self->url_id_map); + while (g_hash_table_iter_next (&iter, &key, &value)) { + const gchar *url = key; + const gchar *app_id = value; + if (g_strcmp0 (app_id, keywords[i]) == 0) { + g_autoptr(GsApp) app = NULL; + g_autofree gchar *metainfo_app_id = NULL; + metainfo_app_id = generate_app_id_for_url (url); + app = gs_plugin_cache_lookup (GS_PLUGIN (self), metainfo_app_id); + if (app != NULL) + gs_app_list_add (list, app); + break; + } + } + } + } + + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); +} + +static GsAppList * +gs_plugin_epiphany_list_apps_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_plugin_epiphany_list_apps_async, FALSE); + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +gs_epiphany_refine_app (GsPluginEpiphany *self, + GsApp *app, + GsPluginRefineFlags refine_flags, + const char *url) +{ + g_autoptr(GUri) uri = NULL; + + g_return_if_fail (url != NULL && *url != '\0'); + + if (!(uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL))) { + g_warning ("Failed to parse URL for web app %s: %s", gs_app_get_id (app), url); + return; + } + + refine_app (self, app, refine_flags, uri, url); +} + +static void refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_epiphany_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_epiphany_refine_async); + + /* Queue a job for the refine. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refine_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object); + GsPluginRefineData *data = task_data; + GsPluginRefineFlags flags = data->flags; + GsAppList *list = data->list; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (!ensure_installed_apps_cache (self, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + const char *url; + + /* not us */ + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_WEB_APP || + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE) + continue; + + url = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_URL); + if (url == NULL || *url == '\0') { + /* A launchable URL is required by the AppStream spec */ + g_warning ("Web app %s missing launchable url", gs_app_get_id (app)); + continue; + } + + g_debug ("epiphany: refining app %s", gs_app_get_id (app)); + gs_epiphany_refine_app (self, app, flags, url); + gs_epiphany_refine_app_state (GS_PLUGIN (self), app); + + /* Usually the way to refine wildcard apps is to create a new + * GsApp and add it to the results list, but in this case we + * need to use the app that was refined by the appstream plugin + * as it has all the metadata set already, and this is the only + * plugin for dealing with web apps, so it should be safe to + * adopt the wildcard app. + */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + gs_app_remove_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_management_plugin (app, GS_PLUGIN (self)); + gs_plugin_cache_add (GS_PLUGIN (self), gs_app_get_id (app), app); + } + } + + /* success */ + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_epiphany_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_plugin_epiphany_refine_async, FALSE); + return g_task_propagate_boolean (G_TASK (result), error); +} + +static GVariant * +get_serialized_icon (GsApp *app, + GIcon *icon) +{ + g_autofree char *icon_path = NULL; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GIcon) bytes_icon = NULL; + g_autoptr(GVariant) icon_v = NULL; + guint icon_width; + + /* Note: GsRemoteIcon will work on this GFileIcon code path. + * The icons plugin should have called + * gs_app_ensure_icons_downloaded() for us. + */ + if (!G_IS_FILE_ICON (icon)) + return NULL; + + icon_path = g_file_get_path (g_file_icon_get_file (G_FILE_ICON (icon))); + if (!g_str_has_suffix (icon_path, ".png") && + !g_str_has_suffix (icon_path, ".svg") && + !g_str_has_suffix (icon_path, ".jpeg") && + !g_str_has_suffix (icon_path, ".jpg")) { + g_warning ("Icon for app %s has unsupported file extension: %s", + gs_app_get_id (app), icon_path); + return NULL; + } + + /* Scale down to the portal's size limit if needed */ + icon_width = gs_icon_get_width (icon); + if (icon_width > 512) + icon_width = 512; + + /* Serialize the icon as a #GBytesIcon since that's + * what the dynamic launcher portal requires. + */ + stream = g_loadable_icon_load (G_LOADABLE_ICON (icon), icon_width, NULL, NULL, NULL); + + /* Icons are usually smaller than 1 MiB. Set a 10 MiB + * limit so we can't use a huge amount of memory or hit + * the D-Bus message size limit + */ + if (stream) + bytes = g_input_stream_read_bytes (stream, 10485760 /* 10 MiB */, NULL, NULL); + if (bytes) + bytes_icon = g_bytes_icon_new (bytes); + if (bytes_icon) + icon_v = g_icon_serialize (bytes_icon); + + return g_steal_pointer (&icon_v); +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin); + const char *url; + const char *name; + const char *token = NULL; + g_autofree char *installed_app_id = NULL; + g_autoptr(GVariant) token_v = NULL; + g_autoptr(GVariant) icon_v = NULL; + GVariantBuilder opt_builder; + const int icon_sizes[] = {512, 192, 128, 1}; + + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + url = gs_app_get_url (app, AS_URL_KIND_HOMEPAGE); + if (url == NULL || *url == '\0') { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Can't install web app %s without url", + gs_app_get_id (app)); + return FALSE; + } + name = gs_app_get_name (app); + if (name == NULL || *name == '\0') { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Can't install web app %s without name", + gs_app_get_id (app)); + return FALSE; + } + for (guint i = 0; i < G_N_ELEMENTS (icon_sizes); i++) { + GIcon *icon = gs_app_get_icon_for_size (app, icon_sizes[i], 1, NULL); + if (icon != NULL) + icon_v = get_serialized_icon (app, icon); + if (icon_v != NULL) + break; + } + if (icon_v == NULL) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Can't install web app %s without icon", + gs_app_get_id (app)); + return FALSE; + } + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + /* First get a token from xdg-desktop-portal so Epiphany can do the + * installation without user confirmation + */ + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + token_v = g_dbus_proxy_call_sync (self->launcher_portal_proxy, + "RequestInstallToken", + g_variant_new ("(sva{sv})", + name, icon_v, &opt_builder), + G_DBUS_CALL_FLAGS_NONE, + -1, cancellable, error); + if (token_v == NULL) { + gs_epiphany_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* Then pass the token to Epiphany which will use xdg-desktop-portal to + * complete the installation + */ + g_variant_get (token_v, "(&s)", &token); + if (!gs_ephy_web_app_provider_call_install_sync (self->epiphany_proxy, + url, name, token, + &installed_app_id, + cancellable, + error)) { + gs_epiphany_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex); + g_hash_table_insert (self->url_id_map, g_strdup (url), + g_strdup (installed_app_id)); + } + + gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_app_id); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin); + const char *installed_app_id; + const char *url; + + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + installed_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID); + if (installed_app_id == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "App can't be uninstalled without installed app ID"); + gs_app_set_state_recover (app); + return FALSE; + } + + gs_app_set_state (app, GS_APP_STATE_REMOVING); + if (!gs_ephy_web_app_provider_call_uninstall_sync (self->epiphany_proxy, + installed_app_id, + cancellable, + error)) { + gs_epiphany_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + url = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_URL); + if (url != NULL && *url != '\0') { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex); + g_hash_table_remove (self->url_id_map, url); + } + + /* The app is not necessarily available; it may have been installed + * directly in Epiphany + */ + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + + return TRUE; +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + return gs_plugin_app_launch (plugin, app, error); +} + +static void +gs_plugin_epiphany_class_init (GsPluginEpiphanyClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_epiphany_dispose; + object_class->finalize = gs_plugin_epiphany_finalize; + + plugin_class->setup_async = gs_plugin_epiphany_setup_async; + plugin_class->setup_finish = gs_plugin_epiphany_setup_finish; + plugin_class->shutdown_async = gs_plugin_epiphany_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_epiphany_shutdown_finish; + plugin_class->refine_async = gs_plugin_epiphany_refine_async; + plugin_class->refine_finish = gs_plugin_epiphany_refine_finish; + plugin_class->list_apps_async = gs_plugin_epiphany_list_apps_async; + plugin_class->list_apps_finish = gs_plugin_epiphany_list_apps_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_EPIPHANY; +} diff --git a/plugins/epiphany/gs-plugin-epiphany.h b/plugins/epiphany/gs-plugin-epiphany.h new file mode 100644 index 0000000..955d059 --- /dev/null +++ b/plugins/epiphany/gs-plugin-epiphany.h @@ -0,0 +1,20 @@ +/* -*- 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@protonmail.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_EPIPHANY (gs_plugin_epiphany_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginEpiphany, gs_plugin_epiphany, GS, PLUGIN_EPIPHANY, GsPlugin) + +G_END_DECLS diff --git a/plugins/epiphany/gs-self-test.c b/plugins/epiphany/gs-self-test.c new file mode 100644 index 0000000..1b91021 --- /dev/null +++ b/plugins/epiphany/gs-self-test.c @@ -0,0 +1,240 @@ +/* -*- 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) 2021 Matthew Leeds <mwleeds@protonmail.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" +#include "gs-dynamic-launcher-portal-iface.h" +#include "gs-epiphany-generated.h" + +#include <libglib-testing/dbus-queue.h> + +/* This is run in a worker thread */ +static void +epiphany_and_portal_mock_server_cb (GtDBusQueue *queue, + gpointer user_data) +{ + { + g_autoptr(GDBusMethodInvocation) invocation = NULL; + g_autoptr(GVariant) properties_variant = NULL; + const char *property_interface; + invocation = gt_dbus_queue_assert_pop_message (queue, + "/org/gnome/Epiphany/WebAppProvider", + "org.freedesktop.DBus.Properties", + "GetAll", "(&s)", + &property_interface); + g_assert_cmpstr (property_interface, ==, "org.gnome.Epiphany.WebAppProvider"); + properties_variant = g_variant_new_parsed ("({'Version': <@u 1>},)"); + g_dbus_method_invocation_return_value (invocation, g_steal_pointer (&properties_variant)); + } + { + g_autoptr(GDBusMethodInvocation) invocation = NULL; + g_autoptr(GVariant) properties_variant = NULL; + const char *property_interface, *props_dict; + invocation = gt_dbus_queue_assert_pop_message (queue, + "/org/freedesktop/portal/desktop", + "org.freedesktop.DBus.Properties", + "GetAll", "(&s)", + &property_interface); + g_assert_cmpstr (property_interface, ==, "org.freedesktop.portal.DynamicLauncher"); + props_dict = "({'version': <@u 1>,'SupportedLauncherTypes': <@u 3>},)"; + properties_variant = g_variant_new_parsed (props_dict); + g_dbus_method_invocation_return_value (invocation, g_steal_pointer (&properties_variant)); + } + { + g_autoptr(GDBusMethodInvocation) invocation = NULL; + const char *installed_apps[] = {"org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043.desktop", NULL}; + invocation = gt_dbus_queue_assert_pop_message (queue, + "/org/gnome/Epiphany/WebAppProvider", + "org.gnome.Epiphany.WebAppProvider", + "GetInstalledApps", "()", NULL); + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(^as)", installed_apps)); + } +} + +static GtDBusQueue * +bus_set_up (void) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(GtDBusQueue) queue = NULL; + + queue = gt_dbus_queue_new (); + + gt_dbus_queue_connect (queue, &local_error); + g_assert_no_error (local_error); + + gt_dbus_queue_own_name (queue, "org.freedesktop.portal.Desktop"); + + gt_dbus_queue_export_object (queue, + "/org/freedesktop/portal/desktop", + (GDBusInterfaceInfo *) &org_freedesktop_portal_dynamic_launcher_interface, + &local_error); + g_assert_no_error (local_error); + + gt_dbus_queue_own_name (queue, "org.gnome.Epiphany.WebAppProvider"); + + gt_dbus_queue_export_object (queue, + "/org/gnome/Epiphany/WebAppProvider", + gs_ephy_web_app_provider_interface_info (), + &local_error); + g_assert_no_error (local_error); + + gt_dbus_queue_set_server_func (queue, epiphany_and_portal_mock_server_cb, + NULL); + + return g_steal_pointer (&queue); +} + +static void +gs_plugins_epiphany_func (GsPluginLoader *plugin_loader) +{ + g_assert_true (gs_plugin_loader_get_enabled (plugin_loader, "epiphany")); +} + +static char * +create_fake_desktop_file (const char *app_id) +{ + g_autofree char *contents = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *desktop_path = NULL; + g_autofree char *icon_path = NULL; + + /* Use an icon we already have locally */ + icon_path = gs_test_get_filename (TESTDATADIR, "icons/hicolor/scalable/org.gnome.Software.svg"); + g_assert (icon_path != NULL); + + /* Use true instead of epiphany in Exec and TryExec; otherwise + * g_desktop_app_info_new() in the plugin code will look for an + * epiphany binary and fail. + */ + contents = g_strdup_printf ("[Desktop Entry]\n" + "Name=Pinafore\n" + "Exec=true --application-mode \"--profile=/home/nobody/.local/share/%s\" https://pinafore.social/\n" + "StartupNotify=true\n" + "Terminal=false\n" + "Type=Application\n" + "Categories=GNOME;GTK;\n" + "Icon=%s\n" + "StartupWMClass=%s\n" + "X-Purism-FormFactor=Workstation;Mobile;\n" + "TryExec=true\n", + app_id, icon_path, app_id); + + desktop_path = g_strconcat (g_get_user_data_dir (), G_DIR_SEPARATOR_S, + "applications", G_DIR_SEPARATOR_S, + app_id, ".desktop", NULL); + + g_debug ("Creating a fake desktop file at path: %s", desktop_path); + gs_mkdir_parent (desktop_path, &error); + g_assert_no_error (error); + g_file_set_contents (desktop_path, contents, -1, &error); + g_assert_no_error (error); + + return g_steal_pointer (&desktop_path); +} + +static void +gs_plugins_epiphany_installed_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsAppQuery) query = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GIcon) icon = NULL; + g_autoptr(GsAppList) list = NULL; + GsApp *app; + const char *app_id = "org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043"; + const char *metainfo_app_id = "org.gnome.Software.WebApp_e636aa5f2069f6e9c02deccc7b65f43da7985e32.desktop"; + const char *launchable_app_id; + g_autofree char *app_id_desktop = NULL; + g_autofree char *desktop_path = NULL; + g_autofree char *origin_ui = NULL; + + app_id_desktop = g_strdup_printf ("%s.desktop", app_id); + desktop_path = create_fake_desktop_file (app_id); + + query = gs_app_query_new ("is-installed", GS_APP_QUERY_TRISTATE_TRUE, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (list); + + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, metainfo_app_id); + launchable_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID); + g_assert_cmpstr (launchable_app_id, ==, app_id_desktop); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_WEB_APP); + g_assert_cmpint (gs_app_get_scope (app), ==, AS_COMPONENT_SCOPE_USER); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_name (app), ==, "Pinafore"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "pinafore.social"); + g_assert_cmpstr (gs_app_get_origin (app), ==, "gnome-web"); + origin_ui = gs_app_dup_origin_ui (app, TRUE); + g_assert_cmpstr (origin_ui, ==, "GNOME Web"); + icon = gs_app_get_icon_for_size (app, 4096, 1, NULL); + g_assert_nonnull (icon); + g_clear_object (&icon); + + gs_utils_unlink (desktop_path, NULL); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autofree gchar *fn = NULL; + g_autofree gchar *xml = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + g_autoptr(GtDBusQueue) queue = NULL; + int res; + const gchar *allowlist[] = { + "epiphany", + "icons", + NULL + }; + + gs_test_init (&argc, &argv); + g_setenv ("GS_XMLB_VERBOSE", "1", TRUE); + + /* Set up mock D-Bus services for the Epiphany WebAppProvider and the + * DynamicLauncher portal + */ + queue = bus_set_up (); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (gt_dbus_queue_get_client_connection (queue), NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/epiphany/enabled", + plugin_loader, + (GTestDataFunc) gs_plugins_epiphany_func); + g_test_add_data_func ("/gnome-software/plugins/epiphany/installed", + plugin_loader, + (GTestDataFunc) gs_plugins_epiphany_installed_func); + + res = g_test_run (); + gt_dbus_queue_disconnect (queue, TRUE); + return res; +} diff --git a/plugins/epiphany/meson.build b/plugins/epiphany/meson.build new file mode 100644 index 0000000..05fc7b6 --- /dev/null +++ b/plugins/epiphany/meson.build @@ -0,0 +1,87 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginEpiphany"'] + +# The WebAppProvider interface xml comes from: +# https://gitlab.gnome.org/GNOME/epiphany/-/blob/master/src/webapp-provider/org.gnome.Epiphany.WebAppProvider.xml +epiphany_generated = gnome.gdbus_codegen( + 'gs-epiphany-generated', + sources : ['org.gnome.Epiphany.WebAppProvider.xml'], + interface_prefix : 'org.gnome.Epiphany', + namespace : 'GsEphy', +) + +shared_module( + 'gs_plugin_epiphany', + epiphany_generated, + sources : 'gs-plugin-epiphany.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware, + ], +) +metainfo = 'org.gnome.Software.Plugin.Epiphany.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo'), +) + +if get_option('tests') + # The DynamicLauncher interface xml comes from: + # https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.DynamicLauncher.xml + gdbus_codegen = find_program('gdbus-codegen') + dynamic_launcher_portal_iface_h = custom_target( + 'gs-dynamic-launcher-portal-iface.h', + input: ['org.freedesktop.portal.DynamicLauncher.xml'], + output: ['gs-dynamic-launcher-portal-iface.h'], + command: [gdbus_codegen, + '--interface-info-header', + '--output', '@OUTPUT@', + '@INPUT@'], + ) + dynamic_launcher_portal_iface_c = custom_target( + 'gs-dynamic-launcher-portal-iface.c', + input: ['org.freedesktop.portal.DynamicLauncher.xml'], + output: ['gs-dynamic-launcher-portal-iface.c'], + command: [gdbus_codegen, + '--interface-info-body', + '--output', '@OUTPUT@', + '@INPUT@'], + ) + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), '..', '..', 'data') + '"'] + e = executable( + 'gs-self-test-epiphany', + compiled_schemas, + sources : [ + 'gs-self-test.c', + dynamic_launcher_portal_iface_c, + dynamic_launcher_portal_iface_h, + epiphany_generated, + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + dependency('glib-testing-0', fallback: ['libglib-testing', 'libglib_testing_dep']), + ], + link_with : [ + libgnomesoftware, + ], + c_args : cargs, + ) + test('gs-self-test-epiphany', e, suite: ['plugins', 'epiphany'], env: test_env) +endif diff --git a/plugins/epiphany/org.freedesktop.portal.DynamicLauncher.xml b/plugins/epiphany/org.freedesktop.portal.DynamicLauncher.xml new file mode 100644 index 0000000..65aa90e --- /dev/null +++ b/plugins/epiphany/org.freedesktop.portal.DynamicLauncher.xml @@ -0,0 +1,332 @@ +<?xml version="1.0"?> +<!-- + Copyright (C) 2021 Matthew Leeds + + 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 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, see <http://www.gnu.org/licenses/>. + + Author: Matthew Leeds <mwleeds@protonmail.com> +--> + +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + <!-- + org.freedesktop.portal.DynamicLauncher: + @short_description: Portal for installing application launchers onto the + desktop + + The DynamicLauncher portal allows sandboxed (or unsandboxed) applications + to install launchers (.desktop files) which have an icon associated with them + and which execute a command in the application. The desktop environment + would display the launcher to the user in its menu of installed applications. + For example this can be used by a sandboxed browser to install web app + launchers. The portal also allows apps to uninstall the launchers, launch + them, and read the desktop file and icon data for them. + + The standard way to install a launcher is to use the PrepareInstall() method + which results in a dialog being presented to the user so they can confirm + they want to install the launcher. Then, the token returned by PrepareInstall() + would be passed to the Install() method to complete the installation. + + However, in the rare circumstance that an unsandboxed process such as a + system component needs to install a launcher without user interaction, this + can be accomplished by using the RequestInstallToken() method and passing + the acquired token to Install(). + + This documentation describes version 1 of this interface. + --> + <interface name="org.freedesktop.portal.DynamicLauncher"> + <!-- + Install: + @token: Token proving authorization of the installation + @desktop_file_id: The .desktop file name to be used + @desktop_entry: The text of the Desktop Entry file to be installed, see below + @options: Vardict with optional further information + + Installs a .desktop launcher and icon into appropriate directories to + allow the desktop environment to find them. Please note that this method + overwrites any existing launcher with the same id. If you want to + present the user with a confirmation dialog in that case, you can check + for it using the GetDesktopEntry() method and clean up any state from + the previous launcher if you want. + + @token must be a token that was returned by a previous + org.freedesktop.portal.DynamicLauncher.PrepareInstall() or + org.freedesktop.portal.DynamicLauncher.RequestInstallToken() call. + The token can only be used once and is valid for up to five minutes. + + The icon and name used for the launcher will be the ones from the previous + org.freedesktop.portal.DynamicLauncher.PrepareInstall() or + org.freedesktop.portal.DynamicLauncher.RequestInstallToken() call. + + The @desktop_file_id must have ".desktop" as a suffix. Except in the + special case when the calling process has no associated app ID, + @desktop_file_id must have the app ID followed by a period as a prefix, + regardless of whether the calling process is sandboxed or unsandboxed. + + The @desktop_entry should be a valid desktop entry file beginning with + "[Desktop Entry]", except it should not include Name= or Icon= entries + (if present, these will be overwritten by the portal implementation). + The Exec= entry will be rewritten to call the sandboxed application e.g. + via "flatpak run", if the application is sandboxed. + + It is recommended to include a TryExec= line with either a binary name + or an absolute path. The launcher will be deleted if the TryExec binary + cannot be found on session start. + + The @options vardict currently has no supported entries. + --> + <method name="Install"> + <arg type="s" name="token" direction="in"/> + <arg type="s" name="desktop_file_id" direction="in"/> + <arg type="s" name="desktop_entry" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + </method> + <!-- + PrepareInstall: + @parent_window: Identifier for the application window, see <link linkend="parent_window">Common Conventions</link> + @name: The default name for the launcher + @icon_v: A #GBytesIcon icon as returned by g_icon_serialize(). Must be + a png or jpeg no larger than 512x512, or an svg + @options: Vardict with optional further information + @handle: Object path for the #org.freedesktop.portal.Request object representing this call + + Presents a dialog to the user to allow them to see the icon, potentially + change the name, and confirm installation of the launcher. + + Supported keys in the @options vardict: + <variablelist> + <varlistentry> + <term>handle_token s</term> + <listitem><para> + A string that will be used as the last element of the @handle. Must be a valid + object path element. See the #org.freedesktop.portal.Request documentation for + more information about the @handle. + </para></listitem> + </varlistentry> + <varlistentry> + <term>modal b</term> + <listitem><para> + Whether to make the dialog modal. Defaults to yes. + </para></listitem> + </varlistentry> + <varlistentry> + <term>launcher_type u</term> + <listitem><para> + The type of launcher being created. For supported values see the + SupportedLauncherTypes property. Defaults to "Application". + </para></listitem> + </varlistentry> + <varlistentry> + <term>target s</term> + <listitem><para> + For a launcher of type "Webapp", this is the URL of the web app + being installed. This is displayed in the user-facing dialog. + For other launcher types, this is not needed. + </para></listitem> + </varlistentry> + <varlistentry> + <term>editable_name b</term> + <listitem><para> + If true, the user will be able to edit the name of the launcher. + Defaults to true. + </para></listitem> + </varlistentry> + <varlistentry> + <term>editable_icon b</term> + <listitem><para> + If true, the user will be able to edit the icon of the launcher, + if the implementation supports this. Defaults to false. + </para></listitem> + </varlistentry> + </variablelist> + + The following results get returned via the #org.freedesktop.portal.Request::Response signal: + <variablelist> + <varlistentry> + <term>name s</term> + <listitem><para> + The name chosen by the user for the launcher. + </para></listitem> + </varlistentry> + <varlistentry> + <term>token s</term> + <listitem><para> + Token that can be passed to a subsequent org.freedesktop.portal.DynamicLauncher.Install() call to + complete the installation without another dialog. + </para></listitem> + </varlistentry> + </variablelist> + --> + <method name="PrepareInstall"> + <arg type="s" name="parent_window" direction="in"/> + <arg type="s" name="name" direction="in"/> + <arg type="v" name="icon_v" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="o" name="handle" direction="out"/> + </method> + <!-- + RequestInstallToken: + @name: The name that will be used in the desktop file + @icon_v: A #GBytesIcon icon as returned by g_icon_serialize(). Must be + a png or jpeg no larger than 512x512, or an svg + @options: Vardict with optional further information + @token: the token to be used with the Install() method + + This method is intended for use only by specific components that + have their application ID allowlisted in the portal backend (e.g. GNOME + Software and KDE Discover). It is otherwise not guaranteed to work. + + The token returned by this method can be used to avoid the need for a + confirmation dialog; the token can be passed to the Install() method + just as if it were acquired via the PrepareInstall() method. + + The @options vardict currently has no supported entries. + --> + <method name="RequestInstallToken"> + <arg type="s" name="name" direction="in"/> + <arg type="v" name="icon_v" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="token" direction="out"/> + </method> + <!-- + Uninstall: + @desktop_file_id: The .desktop file name + @options: Vardict with optional further information + + This method deletes the desktop file and corresponding icon from the + appropriate directories to remove the launcher referred to by + @desktop_file_id. + + The @desktop_file_id must have ".desktop" as a suffix. Except in the + special case when the calling process has no associated app ID, + @desktop_file_id must have the app ID followed by a period as a prefix, + regardless of whether the calling process is sandboxed or unsandboxed. + + For example, Epiphany, which has the app ID "org.gnome.Epiphany" + in stable builds, might use a @desktop_file_id like + "org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043.desktop" + In that example the desktop file would exist at the path + "~/.local/share/xdg-desktop-portal/applications/org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043.desktop" + with a sym link in "~/.local/share/applications/". + The checksum at the end of the file name is an implementation detail in + Epiphany and not required by the portal. + + This method is intended to be called by the application that created the + launcher, e.g. a web browser, so it can clean up associated data as part + of the uninstallation. Consequently, the proper way for a software center + to remove a launcher is by using the APIs provided by the application + that installed it. For example, for GNOME Software to remove web + launchers created by Epiphany, it would use the + org.gnome.Epiphany.WebAppProvider D-Bus interface. + + Please note that this method call will fail if the specified launcher + already does not exist. + + The @options vardict currently has no supported entries. + --> + <method name="Uninstall"> + <arg type="s" name="desktop_file_id" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + </method> + <!-- + GetDesktopEntry: + @desktop_file_id: The .desktop file name + @contents: the contents of the named .desktop file + + This function returns the contents of a desktop file with the name + @desktop_file_id in @contents. + + The @desktop_file_id must have ".desktop" as a suffix. Except in the + special case when the calling process has no associated app ID, + @desktop_file_id must have the app ID followed by a period as a prefix. + + This method only works for desktop files that were created by the + dynamic launcher portal. + --> + <method name="GetDesktopEntry"> + <arg type="s" name="desktop_file_id" direction="in"/> + <arg type="s" name="contents" direction="out"/> + </method> + <!-- + GetIcon: + @desktop_file_id: The .desktop file name + @icon_v: the icon as a serialized #GBytesIcon + @icon_format: one of "png", "jpeg", "svg" + @icon_size: the width and height in pixels of the icon + + This function returns the contents of the icon specified in the "Icon" + key of the desktop file with the name @desktop_file_id in @icon_v. The + icon #GVariant can be passed to g_icon_deserialize() to reconstruct the + #GIcon. + + The @desktop_file_id must have ".desktop" as a suffix. Except in the + special case when the calling process has no associated app ID, + @desktop_file_id must have the app ID followed by a period as a prefix. + + The format and size of the icon are returned in @icon_format and + @icon_size. For svg icons, @icon_size is currently always set to 4096, + but don't depend on that as it may change in the future. + + This method only works for desktop files that were created by the + dynamic launcher portal. + --> + <method name="GetIcon"> + <arg type="s" name="desktop_file_id" direction="in"/> + <arg type="v" name="icon_v" direction="out"/> + <arg type="s" name="icon_format" direction="out"/> + <arg type="u" name="icon_size" direction="out"/> + </method> + <!-- + Launch: + @desktop_file_id: The .desktop file name + @options: Vardict with optional further onformation + + This function launches the app specified by @desktop_file_id. + + The @desktop_file_id must have ".desktop" as a suffix. Except in the + special case when the calling process has no associated app ID, + @desktop_file_id must have the app ID followed by a period as a prefix. + + This method only works for desktop files that were created by the + dynamic launcher portal. + + Supported keys in the @options vardict include: + <variablelist> + <varlistentry> + <term>activation_token s</term> + <listitem><para> + A token that can be used to activate the chosen application. + </para><para> + The activation_token option was introduced in version 1 of the interface. + </para></listitem> + </varlistentry> + </variablelist> + --> + <method name="Launch"> + <arg type="s" name="desktop_file_id" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + </method> + <!-- + SupportedLauncherTypes: + + A bitmask of available launcher types. Currently defined types are: + + <simplelist> + <member>1: Application. A launcher that represents an application.</member> + <member>2: Webapp. A launcher that represents a web app.</member> + </simplelist> + --> + <property name="SupportedLauncherTypes" type="u" access="read"/> + <property name="version" type="u" access="read"/> + </interface> +</node> diff --git a/plugins/epiphany/org.gnome.Epiphany.WebAppProvider.xml b/plugins/epiphany/org.gnome.Epiphany.WebAppProvider.xml new file mode 100644 index 0000000..6c2954d --- /dev/null +++ b/plugins/epiphany/org.gnome.Epiphany.WebAppProvider.xml @@ -0,0 +1,85 @@ +<!DOCTYPE node PUBLIC +'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN' +'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'> +<node> + + <!-- + org.gnome.Epiphany.WebAppProvider: + @short_description: Webapp provider interface + + The interface used for handling Epiphany Webapps in GNOME Software, or other + clients (version 1). + --> + <interface name="org.gnome.Epiphany.WebAppProvider"> + <!-- + GetInstalledApps: + @desktop_file_ids: An array of .desktop file names, one for each + installed web app, with the .desktop suffix included + + Returns the set of installed Epiphany web applications. The caller can + use them with g_desktop_app_info_new() if outside the sandbox. + --> + <method name="GetInstalledApps"> + <arg type="as" name="webapps" direction="out" /> + </method> + + <!-- + Install: + @url: the URL of the web app + @name: the human readable name of the web app + @install_token: the token acquired via org.freedesktop.portal.InstallDynamicLauncher + @desktop_file_id: the desktop file id of the installed app, with a + ".desktop" suffix + + Installs a web app. This interface is expected to be used by trusted + system components such as GNOME Software, which can acquire an + @install_token using the portal method + org.freedesktop.portal.DynamicLauncher.RequestInstallToken(). This allows Epiphany + to install the web app without user interaction and despite being sandboxed. + This is desirable because the user would've already clicked "Install" in + Software; they should not have to confirm the operation again in a different + app (Epiphany). + + The @install_token must be provided so that Epiphany can complete the + installation without a user-facing dialog. The icon given to + org.freedesktop.portal.InstallDynamicLauncher.RequestInstallToken() will + be used, and the name given to that method should match the @name given here. + + If the arguments passed are invalid this method returns the error + `org.gnome.Epiphany.WebAppProvider.Error.InvalidArgs`, and otherwise + `org.gnome.Epiphany.WebAppProvider.Error.Failed`. + --> + <method name="Install"> + <arg type="s" name="url" direction="in" /> + <arg type="s" name="name" direction="in" /> + <arg type="s" name="install_token" direction="in" /> + <arg type="s" name="desktop_file_id" direction="out" /> + </method> + + <!-- + Uninstall: + @desktop_file_id: the filename of the .desktop file for an installed web + app, with the .desktop suffix + + Uninstalls a web app. Note that the @desktop_file_id is just a filename + not a full path, and it's the same one returned by the + GetInstalledWebApps() method. + + The error `org.gnome.Epiphany.WebAppProvider.Error.NotInstalled` will be + returned if the specified web app is not installed. The other possible + error values are `org.gnome.Epiphany.WebAppProvider.Error.InvalidArgs` + and `org.gnome.Epiphany.WebAppProvider.Error.Failed`. + --> + <method name="Uninstall"> + <arg type="s" name="desktop_file_id" direction="in" /> + </method> + <!-- + Version: + + The API version number, to be incremented for backwards compatible + changes so clients can determine which features are available. For + backwards incompatible changes, the interface name will change. + --> + <property name="Version" type="u" access="read"/> + </interface> +</node> diff --git a/plugins/epiphany/org.gnome.Software.Plugin.Epiphany.metainfo.xml.in b/plugins/epiphany/org.gnome.Software.Plugin.Epiphany.metainfo.xml.in new file mode 100644 index 0000000..626381f --- /dev/null +++ b/plugins/epiphany/org.gnome.Software.Plugin.Epiphany.metainfo.xml.in @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Epiphany</id> + <extends>org.gnome.Software.desktop</extends> + <name>Web Apps Support</name> + <summary>Run popular web applications in a browser</summary> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>mwleeds_at_protonmail.com</update_contact> +</component> diff --git a/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.c b/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.c new file mode 100644 index 0000000..cea7d6c --- /dev/null +++ b/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.c @@ -0,0 +1,140 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Sundeep Anand <suanand@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This plugin does following.. + * 1. locates the active locale, say, xx + * 2. identifies related langpacks-xx + * 3. tries to add langpack-xx in app list + * 4. logs install information; not to try again + */ + +#include <gnome-software.h> + +#include "gs-plugin-fedora-langpacks.h" + +struct _GsPluginFedoraLangpacks { + GsPlugin parent; + + GHashTable *locale_langpack_map; +}; + +G_DEFINE_TYPE (GsPluginFedoraLangpacks, gs_plugin_fedora_langpacks, GS_TYPE_PLUGIN) + +static void +gs_plugin_fedora_langpacks_init (GsPluginFedoraLangpacks *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + /* this plugin should be fedora specific */ + if (!gs_plugin_check_distro_id (plugin, "fedora")) { + gs_plugin_set_enabled (plugin, FALSE); + return; + } + + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + + /* + * A few language code may have more than one language packs. + * Example: en {en_GB}, pt {pt_BR}, zh {zh_CN, zh_TW, zh_HK} + */ + self->locale_langpack_map = g_hash_table_new (g_str_hash, g_str_equal); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers" + g_hash_table_insert (self->locale_langpack_map, "en_GB", "langpacks-en_GB"); + g_hash_table_insert (self->locale_langpack_map, "pt_BR", "langpacks-pt_BR"); + g_hash_table_insert (self->locale_langpack_map, "zh_CN", "langpacks-zh_CN"); + g_hash_table_insert (self->locale_langpack_map, "zh_TW", "langpacks-zh_TW"); + g_hash_table_insert (self->locale_langpack_map, "zh_HK", "langpacks-zh_HK"); +#pragma GCC diagnostic pop +} + +static void +gs_plugin_fedora_langpacks_dispose (GObject *object) +{ + GsPluginFedoraLangpacks *self = GS_PLUGIN_FEDORA_LANGPACKS (object); + + g_clear_pointer (&self->locale_langpack_map, g_hash_table_unref); + + G_OBJECT_CLASS (gs_plugin_fedora_langpacks_parent_class)->dispose (object); +} + +gboolean +gs_plugin_add_langpacks (GsPlugin *plugin, + GsAppList *list, + const gchar *locale, + GCancellable *cancellable, + GError **error) +{ + GsPluginFedoraLangpacks *self = GS_PLUGIN_FEDORA_LANGPACKS (plugin); + gchar *separator; + const gchar *language_code; + g_autofree gchar *cachefn = NULL; + g_autofree gchar *langpack_pkgname = NULL; + g_auto(GStrv) language_region = NULL; + + /* This plugin may receive user locale in the form as documented in `man 3 setlocale`: + * + * language[_territory][.codeset][@modifier] + * + * e.g. `ja_JP.UTF-8` or `en_GB.iso88591` or `uz_UZ.utf8@cyrillic` or `de_DE@euro` + * Get the locale without codeset and modifier as required for langpacks. + */ + separator = strpbrk (locale, ".@"); + if (separator != NULL) + *separator = '\0'; + + if (g_strrstr (locale, "_") != NULL && + !g_hash_table_lookup (self->locale_langpack_map, locale)) { + /* + * language_code should be the langpack_source_id + * if input language_code is a locale and it doesn't + * not found in locale_langpack_map + */ + language_region = g_strsplit (locale, "_", 2); + language_code = language_region[0]; + } else { + language_code = locale; + } + + /* per-user cache */ + langpack_pkgname = g_strconcat ("langpacks-", language_code, NULL); + cachefn = gs_utils_get_cache_filename ("langpacks", langpack_pkgname, + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + error); + if (cachefn == NULL) + return FALSE; + if (!g_file_test (cachefn, G_FILE_TEST_EXISTS)) { + g_autoptr(GsApp) app = gs_app_new (NULL); + gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (plugin)); + gs_app_set_kind (app, AS_COMPONENT_KIND_LOCALIZATION); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_add_source (app, langpack_pkgname); + gs_app_list_add (list, app); + + /* ensure we do not keep trying to install the langpack */ + if (!g_file_set_contents (cachefn, language_code, -1, error)) + return FALSE; + } + + return TRUE; +} + +static void +gs_plugin_fedora_langpacks_class_init (GsPluginFedoraLangpacksClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gs_plugin_fedora_langpacks_dispose; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_FEDORA_LANGPACKS; +} diff --git a/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.h b/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.h new file mode 100644 index 0000000..2a6a428 --- /dev/null +++ b/plugins/fedora-langpacks/gs-plugin-fedora-langpacks.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_LANGPACKS (gs_plugin_fedora_langpacks_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginFedoraLangpacks, gs_plugin_fedora_langpacks, GS, PLUGIN_FEDORA_LANGPACKS, GsPlugin) + +G_END_DECLS diff --git a/plugins/fedora-langpacks/gs-self-test.c b/plugins/fedora-langpacks/gs-self-test.c new file mode 100644 index 0000000..c56058a --- /dev/null +++ b/plugins/fedora-langpacks/gs-self-test.c @@ -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) 2019 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_fedora_langpacks_func (GsPluginLoader *plugin_loader) +{ + g_autofree gchar *cachefn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + os_release = gs_os_release_new (NULL); + if (g_strcmp0 (gs_os_release_get_id (os_release), "fedora") != 0) { + g_test_skip ("not on fedora"); + return; + } + + /* start with a clean slate */ + cachefn = gs_utils_get_cache_filename ("langpacks", "langpacks-pt_BR", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + &error); + g_assert_no_error (error); + g_unlink (cachefn); + + /* get langpacks result based on locale */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_LANGPACKS, + "search", "pt_BR.UTF-8", + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_nonnull (list); + g_assert_no_error (error); + + /* check if we have just one app in the list */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + + /* check app's source and kind */ + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "langpacks-pt_BR"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_LOCALIZATION); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar * const allowlist[] = { + "fedora-langpacks", + NULL + }; + + gs_test_init (&argc, &argv); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/fedora-langpacks", + plugin_loader, + (GTestDataFunc) gs_plugins_fedora_langpacks_func); + return g_test_run (); +} diff --git a/plugins/fedora-langpacks/meson.build b/plugins/fedora-langpacks/meson.build new file mode 100644 index 0000000..c3dc057 --- /dev/null +++ b/plugins/fedora-langpacks/meson.build @@ -0,0 +1,34 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginFedoraLangpacks"'] + +shared_module( + 'gs_plugin_fedora-langpacks', + sources : 'gs-plugin-fedora-langpacks.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +if get_option('tests') + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + e = executable( + 'gs-self-test-fedora-langpacks', + compiled_schemas, + sources : [ + 'gs-self-test.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + c_args : cargs, + ) + test('gs-self-test-fedora-langpacks', e, suite: ['plugins', 'fedora-langpacks'], env: test_env) +endif 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, +) diff --git a/plugins/flatpak/gs-flatpak-app.c b/plugins/flatpak/gs-flatpak-app.c new file mode 100644 index 0000000..b59515b --- /dev/null +++ b/plugins/flatpak/gs-flatpak-app.c @@ -0,0 +1,190 @@ +/* -*- 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+ + */ + +#include "config.h" + +#include <string.h> + +#include "gs-flatpak-app.h" + +const gchar * +gs_flatpak_app_get_ref_name (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RefName"); +} + +const gchar * +gs_flatpak_app_get_ref_arch (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RefArch"); +} + +const gchar * +gs_flatpak_app_get_commit (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::Commit"); +} + +GsFlatpakAppFileKind +gs_flatpak_app_get_file_kind (GsApp *app) +{ + GVariant *tmp = gs_app_get_metadata_variant (app, "flatpak::FileKind"); + if (tmp == NULL) + return GS_FLATPAK_APP_FILE_KIND_UNKNOWN; + return g_variant_get_uint32 (tmp); +} + +const gchar * +gs_flatpak_app_get_runtime_url (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RuntimeUrl"); +} + +FlatpakRefKind +gs_flatpak_app_get_ref_kind (GsApp *app) +{ + GVariant *tmp = gs_app_get_metadata_variant (app, "flatpak::RefKind"); + if (tmp == NULL) + return FLATPAK_REF_KIND_APP; + return g_variant_get_uint32 (tmp); +} + +const gchar * +gs_flatpak_app_get_ref_kind_as_str (GsApp *app) +{ + FlatpakRefKind ref_kind = gs_flatpak_app_get_ref_kind (app); + if (ref_kind == FLATPAK_REF_KIND_APP) + return "app"; + if (ref_kind == FLATPAK_REF_KIND_RUNTIME) + return "runtime"; + return NULL; +} + +const gchar * +gs_flatpak_app_get_object_id (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::ObjectID"); +} + +const gchar * +gs_flatpak_app_get_repo_gpgkey (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RepoGpgKey"); +} + +const gchar * +gs_flatpak_app_get_repo_url (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RepoUrl"); +} + +gchar * +gs_flatpak_app_get_ref_display (GsApp *app) +{ + const gchar *ref_kind_as_str = gs_flatpak_app_get_ref_kind_as_str (app); + const gchar *ref_name = gs_flatpak_app_get_ref_name (app); + const gchar *ref_arch = gs_flatpak_app_get_ref_arch (app); + const gchar *ref_branch = gs_app_get_branch (app); + + g_return_val_if_fail (ref_kind_as_str != NULL, NULL); + g_return_val_if_fail (ref_name != NULL, NULL); + g_return_val_if_fail (ref_arch != NULL, NULL); + g_return_val_if_fail (ref_branch != NULL, NULL); + + return g_strdup_printf ("%s/%s/%s/%s", + ref_kind_as_str, + ref_name, + ref_arch, + ref_branch); +} + +void +gs_flatpak_app_set_ref_name (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RefName", val); +} + +void +gs_flatpak_app_set_ref_arch (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RefArch", val); +} + +void +gs_flatpak_app_set_commit (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::Commit", val); +} + +void +gs_flatpak_app_set_file_kind (GsApp *app, GsFlatpakAppFileKind file_kind) +{ + g_autoptr(GVariant) tmp = g_variant_new_uint32 (file_kind); + gs_app_set_metadata_variant (app, "flatpak::FileKind", tmp); +} + +void +gs_flatpak_app_set_runtime_url (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RuntimeUrl", val); +} + +void +gs_flatpak_app_set_ref_kind (GsApp *app, FlatpakRefKind ref_kind) +{ + g_autoptr(GVariant) tmp = g_variant_new_uint32 (ref_kind); + gs_app_set_metadata_variant (app, "flatpak::RefKind", tmp); +} + +void +gs_flatpak_app_set_object_id (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::ObjectID", val); +} + +void +gs_flatpak_app_set_repo_gpgkey (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RepoGpgKey", val); +} + +void +gs_flatpak_app_set_repo_url (GsApp *app, const gchar *val) +{ + gs_app_set_metadata (app, "flatpak::RepoUrl", val); +} + +GsApp * +gs_flatpak_app_new (const gchar *id) +{ + return GS_APP (g_object_new (GS_TYPE_APP, "id", id, NULL)); +} + +void +gs_flatpak_app_set_main_app_ref_name (GsApp *app, const gchar *main_app_ref) +{ + gs_app_set_metadata (app, "flatpak::mainApp", main_app_ref); +} + +const gchar * +gs_flatpak_app_get_main_app_ref_name (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::mainApp"); +} + +void +gs_flatpak_app_set_repo_filter (GsApp *app, const gchar *filter) +{ + gs_app_set_metadata (app, "flatpak::RepoFilter", filter); +} + +const gchar * +gs_flatpak_app_get_repo_filter (GsApp *app) +{ + return gs_app_get_metadata_item (app, "flatpak::RepoFilter"); +} diff --git a/plugins/flatpak/gs-flatpak-app.h b/plugins/flatpak/gs-flatpak-app.h new file mode 100644 index 0000000..610c8a8 --- /dev/null +++ b/plugins/flatpak/gs-flatpak-app.h @@ -0,0 +1,65 @@ +/* -*- 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 <gnome-software.h> +#include <flatpak.h> + +G_BEGIN_DECLS + +typedef enum { + GS_FLATPAK_APP_FILE_KIND_UNKNOWN, + GS_FLATPAK_APP_FILE_KIND_REPO, + GS_FLATPAK_APP_FILE_KIND_REF, + GS_FLATPAK_APP_FILE_KIND_BUNDLE, + GS_FLATPAK_APP_FILE_KIND_LAST, +} GsFlatpakAppFileKind; + +GsApp *gs_flatpak_app_new (const gchar *id); + +const gchar *gs_flatpak_app_get_ref_name (GsApp *app); +const gchar *gs_flatpak_app_get_ref_arch (GsApp *app); +FlatpakRefKind gs_flatpak_app_get_ref_kind (GsApp *app); +const gchar *gs_flatpak_app_get_ref_kind_as_str (GsApp *app); +gchar *gs_flatpak_app_get_ref_display (GsApp *app); + +const gchar *gs_flatpak_app_get_commit (GsApp *app); +const gchar *gs_flatpak_app_get_object_id (GsApp *app); +const gchar *gs_flatpak_app_get_repo_gpgkey (GsApp *app); +const gchar *gs_flatpak_app_get_repo_url (GsApp *app); +GsFlatpakAppFileKind gs_flatpak_app_get_file_kind (GsApp *app); +const gchar *gs_flatpak_app_get_runtime_url (GsApp *app); + +void gs_flatpak_app_set_ref_name (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_ref_arch (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_ref_kind (GsApp *app, + FlatpakRefKind ref_kind); + +void gs_flatpak_app_set_commit (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_object_id (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_repo_gpgkey (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_repo_url (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_file_kind (GsApp *app, + GsFlatpakAppFileKind file_kind); +void gs_flatpak_app_set_runtime_url (GsApp *app, + const gchar *val); +void gs_flatpak_app_set_main_app_ref_name (GsApp *app, + const gchar *main_app_ref); +const gchar *gs_flatpak_app_get_main_app_ref_name (GsApp *app); +void gs_flatpak_app_set_repo_filter (GsApp *app, + const gchar *filter); +const gchar *gs_flatpak_app_get_repo_filter (GsApp *app); + +G_END_DECLS diff --git a/plugins/flatpak/gs-flatpak-transaction.c b/plugins/flatpak/gs-flatpak-transaction.c new file mode 100644 index 0000000..7377f6d --- /dev/null +++ b/plugins/flatpak/gs-flatpak-transaction.c @@ -0,0 +1,761 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include "gs-flatpak-app.h" +#include "gs-flatpak-transaction.h" + +struct _GsFlatpakTransaction { + FlatpakTransaction parent_instance; + GHashTable *refhash; /* ref:GsApp */ + GError *first_operation_error; +}; + +enum { + SIGNAL_REF_TO_APP, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, FLATPAK_TYPE_TRANSACTION) + +static void +gs_flatpak_transaction_finalize (GObject *object) +{ + GsFlatpakTransaction *self; + g_return_if_fail (GS_IS_FLATPAK_TRANSACTION (object)); + self = GS_FLATPAK_TRANSACTION (object); + + g_assert (self != NULL); + g_hash_table_unref (self->refhash); + if (self->first_operation_error != NULL) + g_error_free (self->first_operation_error); + + G_OBJECT_CLASS (gs_flatpak_transaction_parent_class)->finalize (object); +} + +GsApp * +gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction, const gchar *ref) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + return g_hash_table_lookup (self->refhash, ref); +} + +static void +gs_flatpak_transaction_add_app_internal (GsFlatpakTransaction *self, GsApp *app) +{ + g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app); + g_hash_table_insert (self->refhash, g_steal_pointer (&ref), g_object_ref (app)); +} + +void +gs_flatpak_transaction_add_app (FlatpakTransaction *transaction, GsApp *app) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + gs_flatpak_transaction_add_app_internal (self, app); + if (gs_app_get_runtime (app) != NULL) + gs_flatpak_transaction_add_app_internal (self, gs_app_get_runtime (app)); +} + +static GsApp * +_ref_to_app (GsFlatpakTransaction *self, const gchar *ref) +{ + GsApp *app = g_hash_table_lookup (self->refhash, ref); + if (app != NULL) + return g_object_ref (app); + g_signal_emit (self, signals[SIGNAL_REF_TO_APP], 0, ref, &app); + + /* Cache the result */ + if (app != NULL) + g_hash_table_insert (self->refhash, g_strdup (ref), g_object_ref (app)); + + return app; +} + +static void +_transaction_operation_set_app (FlatpakTransactionOperation *op, GsApp *app) +{ + g_object_set_data_full (G_OBJECT (op), "GsApp", + g_object_ref (app), (GDestroyNotify) g_object_unref); +} + +static GsApp * +_transaction_operation_get_app (FlatpakTransactionOperation *op) +{ + return g_object_get_data (G_OBJECT (op), "GsApp"); +} + +gboolean +gs_flatpak_transaction_run (FlatpakTransaction *transaction, + GCancellable *cancellable, + GError **error) + +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + g_autoptr(GError) error_local = NULL; + + if (!flatpak_transaction_run (transaction, cancellable, &error_local)) { + /* whole transaction failed; restore the state for all the apps involved */ + g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction); + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + const gchar *ref = flatpak_transaction_operation_get_ref (op); + g_autoptr(GsApp) app = _ref_to_app (self, ref); + if (app == NULL) { + g_warning ("failed to find app for %s", ref); + continue; + } + gs_app_set_state_recover (app); + } + + if (self->first_operation_error != NULL) { + g_propagate_error (error, g_steal_pointer (&self->first_operation_error)); + return FALSE; + } else { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +_transaction_ready (FlatpakTransaction *transaction) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + g_autolist(GObject) ops = NULL; + + /* nothing to do */ + ops = flatpak_transaction_get_operations (transaction); + if (ops == NULL) + return TRUE; // FIXME: error? + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + const gchar *ref = flatpak_transaction_operation_get_ref (op); + g_autoptr(GsApp) app = _ref_to_app (self, ref); + if (app != NULL) { + _transaction_operation_set_app (op, app); + /* if we're updating a component, then mark all the apps + * involved to ensure updating the button state */ + if (flatpak_transaction_operation_get_operation_type (op) == FLATPAK_TRANSACTION_OPERATION_UPDATE) { + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN || + gs_app_get_state (app) == GS_APP_STATE_INSTALLED) + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + } + } + + /* Debug dump. */ + { + GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (op); + g_autoptr(GString) debug_message = g_string_new (""); + + g_string_append_printf (debug_message, + "%s: op %p, app %s (%p), download size %" G_GUINT64_FORMAT ", related-to:", + G_STRFUNC, op, + app ? gs_app_get_unique_id (app) : "?", + app, + flatpak_transaction_operation_get_download_size (op)); + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + g_string_append_printf (debug_message, + "\n ├ %s (%p)", flatpak_transaction_operation_get_ref (related_to_op), related_to_op); + } + g_string_append (debug_message, "\n └ (end)"); + g_debug ("%s", debug_message->str); + } + } + return TRUE; +} + +typedef struct +{ + GsFlatpakTransaction *transaction; /* (owned) */ + FlatpakTransactionOperation *operation; /* (owned) */ + GsApp *app; /* (owned) */ +} ProgressData; + +static void +progress_data_free (ProgressData *data) +{ + g_clear_object (&data->operation); + g_clear_object (&data->app); + g_clear_object (&data->transaction); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ProgressData, progress_data_free) + +static gboolean +op_is_related_to_op (FlatpakTransactionOperation *op, + FlatpakTransactionOperation *root_op) +{ + GPtrArray *related_to_ops; /* (element-type FlatpakTransactionOperation) */ + + if (op == root_op) + return TRUE; + + related_to_ops = flatpak_transaction_operation_get_related_to_ops (op); + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + if (related_to_op == root_op || op_is_related_to_op (related_to_op, root_op)) + return TRUE; + } + + return FALSE; +} + +static guint64 +saturated_uint64_add (guint64 a, guint64 b) +{ + return (a <= G_MAXUINT64 - b) ? a + b : G_MAXUINT64; +} + +/* + * update_progress_for_op: + * @self: a #GsFlatpakTransaction + * @current_progress: progress reporting object + * @ops: results of calling flatpak_transaction_get_operations() on @self, for performance + * @current_op: the #FlatpakTransactionOperation which the @current_progress is + * for; this is the operation currently being run by libflatpak + * @root_op: the #FlatpakTransactionOperation at the root of the operation subtree + * to calculate progress for + * + * Calculate and update the #GsApp:progress for each app associated with + * @root_op in a flatpak transaction. This will include the #GsApp for the app + * being installed (for example), but also the #GsApps for all of its runtimes + * and locales, and any other dependencies of them. + * + * Each #GsApp:progress is calculated based on the sum of the progress of all + * the apps related to that one — so the progress for an app will factor in the + * progress for all its runtimes. + */ +static void +update_progress_for_op (GsFlatpakTransaction *self, + FlatpakTransactionProgress *current_progress, + GList *ops, + FlatpakTransactionOperation *current_op, + FlatpakTransactionOperation *root_op) +{ + g_autoptr(GsApp) root_app = NULL; + guint64 related_prior_download_bytes = 0; + guint64 related_download_bytes = 0; + guint64 current_bytes_transferred = flatpak_transaction_progress_get_bytes_transferred (current_progress); + gboolean seen_current_op = FALSE, seen_root_op = FALSE; + gboolean root_op_skipped = flatpak_transaction_operation_get_is_skipped (root_op); + guint percent; + + /* If @root_op is being skipped and its GsApp isn't being + * installed/removed, don't update the progress on it. It may be that + * @root_op is the runtime of an app and the app is the thing the + * transaction was created for. + */ + if (root_op_skipped) { + /* _transaction_operation_set_app() is only called on non-skipped ops */ + const gchar *ref = flatpak_transaction_operation_get_ref (root_op); + root_app = _ref_to_app (self, ref); + if (root_app == NULL) { + g_warning ("Couldn't find GsApp for transaction operation %s", + flatpak_transaction_operation_get_ref (root_op)); + return; + } + if (gs_app_get_state (root_app) != GS_APP_STATE_INSTALLING && + gs_app_get_state (root_app) != GS_APP_STATE_REMOVING) + return; + } else { + GsApp *unskipped_root_app = _transaction_operation_get_app (root_op); + if (unskipped_root_app == NULL) { + g_warning ("Couldn't find GsApp for transaction operation %s", + flatpak_transaction_operation_get_ref (root_op)); + return; + } + root_app = g_object_ref (unskipped_root_app); + } + + /* This relies on ops in a #FlatpakTransaction being run in the order + * they’re returned by flatpak_transaction_get_operations(), which is true. */ + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = FLATPAK_TRANSACTION_OPERATION (l->data); + guint64 op_download_size = flatpak_transaction_operation_get_download_size (op); + + if (op == current_op) + seen_current_op = TRUE; + if (op == root_op) + seen_root_op = TRUE; + + /* Currently libflatpak doesn't return skipped ops in + * flatpak_transaction_get_operations(), but check just in case. + */ + if (op == root_op && root_op_skipped) + continue; + + if (op_is_related_to_op (op, root_op)) { + /* Saturate instead of overflowing */ + related_download_bytes = saturated_uint64_add (related_download_bytes, op_download_size); + if (!seen_current_op) + related_prior_download_bytes = saturated_uint64_add (related_prior_download_bytes, op_download_size); + } + } + + g_assert (related_prior_download_bytes <= related_download_bytes); + g_assert (seen_root_op || root_op_skipped); + + /* Avoid overflows when converting to percent, at the cost of losing + * some precision in the least significant digits. */ + if (related_prior_download_bytes > G_MAXUINT64 / 100 || + current_bytes_transferred > G_MAXUINT64 / 100) { + related_prior_download_bytes /= 100; + current_bytes_transferred /= 100; + related_download_bytes /= 100; + } + + /* Update the progress of @root_app. */ + if (related_download_bytes > 0) + percent = ((related_prior_download_bytes * 100 / related_download_bytes) + + (current_bytes_transferred * 100 / related_download_bytes)); + else + percent = 0; + + if (gs_app_get_progress (root_app) == 100 || + gs_app_get_progress (root_app) == GS_APP_PROGRESS_UNKNOWN || + gs_app_get_progress (root_app) <= percent) { + gs_app_set_progress (root_app, percent); + } else { + g_warning ("ignoring percentage %u%% -> %u%% as going down on app %s", + gs_app_get_progress (root_app), percent, + gs_app_get_unique_id (root_app)); + } +} + +static void +update_progress_for_op_recurse_up (GsFlatpakTransaction *self, + FlatpakTransactionProgress *progress, + GList *ops, + FlatpakTransactionOperation *current_op, + FlatpakTransactionOperation *root_op) +{ + GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (root_op); + + /* Update progress for @root_op */ + update_progress_for_op (self, progress, ops, current_op, root_op); + + /* Update progress for ops related to @root_op, e.g. apps whose runtime is @root_op */ + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + update_progress_for_op_recurse_up (self, progress, ops, current_op, related_to_op); + } +} + +static void +_transaction_progress_changed_cb (FlatpakTransactionProgress *progress, + gpointer user_data) +{ + ProgressData *data = user_data; + GsApp *app = data->app; + GsFlatpakTransaction *self = data->transaction; + g_autolist(FlatpakTransactionOperation) ops = NULL; + + if (flatpak_transaction_progress_get_is_estimating (progress)) { + /* "Estimating" happens while fetching the metadata, which + * flatpak arbitrarily decides happens during the first 5% of + * each operation. At this point, no more detailed progress + * information is available. */ + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + return; + } + + /* Update the progress on this app, and then do the same for each + * related parent app up the hierarchy. For example, @data->operation + * could be for a runtime which was added to the transaction because of + * an app — so we need to update the progress on the app too. + * + * It’s important to note that a new @data->progress is created by + * libflatpak for each @data->operation, and there are multiple + * operations in a transaction. There is no #FlatpakTransactionProgress + * which represents the progress of the whole transaction. + * + * There may be arbitrary many levels of related-to ops. For example, + * one common situation would be to install an app which needs a new + * runtime, and that runtime needs a locale to be installed, which would + * give three levels of related-to relation: + * locale → runtime → app → (null) + * + * In addition, libflatpak may decide to skip some operations (if they + * turn out to not be necessary). These skipped operations are not + * included in the list returned by flatpak_transaction_get_operations(), + * but they can be accessed via + * flatpak_transaction_operation_get_related_to_ops(), so have to be + * ignored manually. + */ + ops = flatpak_transaction_get_operations (FLATPAK_TRANSACTION (self)); + update_progress_for_op_recurse_up (self, progress, ops, data->operation, data->operation); +} + +static const gchar * +_flatpak_transaction_operation_type_to_string (FlatpakTransactionOperationType ot) +{ + if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL) + return "install"; + if (ot == FLATPAK_TRANSACTION_OPERATION_UPDATE) + return "update"; + if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE) + return "install-bundle"; + if (ot == FLATPAK_TRANSACTION_OPERATION_UNINSTALL) + return "uninstall"; + return NULL; +} + +static void +progress_data_free_closure (gpointer user_data, + GClosure *closure) +{ + progress_data_free (user_data); +} + +static void +_transaction_new_operation (FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation, + FlatpakTransactionProgress *progress) +{ + GsApp *app; + g_autoptr(ProgressData) progress_data = NULL; + + /* find app */ + app = _transaction_operation_get_app (operation); + if (app == NULL) { + FlatpakTransactionOperationType ot; + ot = flatpak_transaction_operation_get_operation_type (operation); + g_warning ("failed to find app for %s during %s", + flatpak_transaction_operation_get_ref (operation), + _flatpak_transaction_operation_type_to_string (ot)); + return; + } + + /* report progress */ + progress_data = g_new0 (ProgressData, 1); + progress_data->transaction = GS_FLATPAK_TRANSACTION (g_object_ref (transaction)); + progress_data->app = g_object_ref (app); + progress_data->operation = g_object_ref (operation); + + g_signal_connect_data (progress, "changed", + G_CALLBACK (_transaction_progress_changed_cb), + g_steal_pointer (&progress_data), + progress_data_free_closure, + 0 /* flags */); + flatpak_transaction_progress_set_update_frequency (progress, 500); /* FIXME? */ + + /* set app status */ + switch (flatpak_transaction_operation_get_operation_type (operation)) { + case FLATPAK_TRANSACTION_OPERATION_INSTALL: + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + break; + case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE: + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL); + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + break; + case FLATPAK_TRANSACTION_OPERATION_UPDATE: + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN || + gs_app_get_state (app) == GS_APP_STATE_INSTALLED) + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + break; + case FLATPAK_TRANSACTION_OPERATION_UNINSTALL: + gs_app_set_state (app, GS_APP_STATE_REMOVING); + break; + default: + break; + } +} + +static gboolean +later_op_also_related (GList *ops, + FlatpakTransactionOperation *current_op, + FlatpakTransactionOperation *related_to_current_op) +{ + /* Here we're determining if anything in @ops which comes after + * @current_op is related to @related_to_current_op and not skipped + * (but all @ops are not skipped so no need to check explicitly) + */ + gboolean found_later_op = FALSE, seen_current_op = FALSE; + for (GList *l = ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + GPtrArray *related_to_ops; + if (current_op == op) { + seen_current_op = TRUE; + continue; + } + if (!seen_current_op) + continue; + + related_to_ops = flatpak_transaction_operation_get_related_to_ops (op); + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + if (related_to_op == related_to_current_op) { + g_assert (flatpak_transaction_operation_get_is_skipped (related_to_op)); + found_later_op = TRUE; + } + } + } + + return found_later_op; +} + +static void +set_skipped_related_apps_to_installed (GsFlatpakTransaction *self, + FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation) +{ + /* It's possible the thing being updated/installed, @operation, is a + * related ref (e.g. extension or runtime) of an app which itself doesn't + * need an update and therefore won't have _transaction_operation_done() + * called for it directly. So we have to set the main app to installed + * here. + */ + g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction); + GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (operation); + + for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) { + FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i); + if (flatpak_transaction_operation_get_is_skipped (related_to_op)) { + const gchar *ref; + g_autoptr(GsApp) related_to_app = NULL; + + /* Check that no later op is also related to related_to_op, in + * which case we want to let that operation finish before setting + * the main app to installed. + */ + if (later_op_also_related (ops, operation, related_to_op)) + continue; + + ref = flatpak_transaction_operation_get_ref (related_to_op); + related_to_app = _ref_to_app (self, ref); + if (related_to_app != NULL) + gs_app_set_state (related_to_app, GS_APP_STATE_INSTALLED); + } + } +} + +static void +_transaction_operation_done (FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation, + const gchar *commit, + FlatpakTransactionResult details) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + + /* invalidate */ + GsApp *app = _transaction_operation_get_app (operation); + if (app == NULL) { + g_warning ("failed to find app for %s", + flatpak_transaction_operation_get_ref (operation)); + return; + } + switch (flatpak_transaction_operation_get_operation_type (operation)) { + case FLATPAK_TRANSACTION_OPERATION_INSTALL: + case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE: + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + set_skipped_related_apps_to_installed (self, transaction, operation); + break; + case FLATPAK_TRANSACTION_OPERATION_UPDATE: + gs_app_set_version (app, gs_app_get_update_version (app)); + gs_app_set_update_details_markup (app, NULL); + gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN); + gs_app_set_update_version (app, NULL); + /* force getting the new runtime */ + gs_app_remove_kudo (app, GS_APP_KUDO_SANDBOXED); + /* downloaded, but not yet installed */ + if (flatpak_transaction_get_no_deploy (transaction)) + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + else + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + set_skipped_related_apps_to_installed (self, transaction, operation); + break; + case FLATPAK_TRANSACTION_OPERATION_UNINSTALL: + /* we don't actually know if this app is re-installable */ + gs_flatpak_app_set_commit (app, NULL); + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + break; + default: + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + break; + } +} + +static gboolean +_transaction_operation_error (FlatpakTransaction *transaction, + FlatpakTransactionOperation *operation, + const GError *error, + FlatpakTransactionErrorDetails detail) +{ + GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction); + FlatpakTransactionOperationType operation_type = flatpak_transaction_operation_get_operation_type (operation); + GsApp *app = _transaction_operation_get_app (operation); + const gchar *ref = flatpak_transaction_operation_get_ref (operation); + + if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_SKIPPED)) { + g_debug ("skipped to %s %s: %s", + _flatpak_transaction_operation_type_to_string (operation_type), + ref, + error->message); + return TRUE; /* continue */ + } + + if (detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) { + g_warning ("failed to %s %s (non fatal): %s", + _flatpak_transaction_operation_type_to_string (operation_type), + ref, + error->message); + return TRUE; /* continue */ + } + + if (self->first_operation_error == NULL) { + g_propagate_error (&self->first_operation_error, + g_error_copy (error)); + if (app != NULL) + gs_utils_error_add_app_id (&self->first_operation_error, app); + } + return FALSE; /* stop */ +} + +static int +_transaction_choose_remote_for_ref (FlatpakTransaction *transaction, + const char *for_ref, + const char *runtime_ref, + const char * const *remotes) +{ + //FIXME: do something smarter + return 0; +} + +static void +_transaction_end_of_lifed (FlatpakTransaction *transaction, + const gchar *ref, + const gchar *reason, + const gchar *rebase) +{ + if (rebase) { + g_message ("%s is end-of-life, in favor of %s", ref, rebase); + } else if (reason) { + g_message ("%s is end-of-life, with reason: %s", ref, reason); + } + //FIXME: show something in the UI +} + +static gboolean +_transaction_end_of_lifed_with_rebase (FlatpakTransaction *transaction, + const gchar *remote, + const gchar *ref, + const gchar *reason, + const gchar *rebased_to_ref, + const gchar **previous_ids) +{ + if (rebased_to_ref) { + g_message ("%s is end-of-life, in favor of %s", ref, rebased_to_ref); + } else if (reason) { + g_message ("%s is end-of-life, with reason: %s", ref, reason); + } + + if (rebased_to_ref && remote) { + g_autoptr(GError) local_error = NULL; + + if (!flatpak_transaction_add_rebase (transaction, remote, rebased_to_ref, + NULL, previous_ids, &local_error) || + !flatpak_transaction_add_uninstall (transaction, ref, &local_error)) { + /* There's no way to make the whole transaction fail on + * this error path, so just print a warning and return + * FALSE, which will cause the operation on the + * end-of-lifed ref not to be skipped. + */ + g_warning ("Failed to rebase %s to %s: %s", ref, rebased_to_ref, local_error->message); + return FALSE; + } + + /* Note: A message about the rename will be shown in the UI + * thanks to code in gs_flatpak_refine_appstream() which + * sets gs_app_set_renamed_from(). + */ + return TRUE; + } + + return FALSE; +} + +static gboolean +_transaction_add_new_remote (FlatpakTransaction *transaction, + FlatpakTransactionRemoteReason reason, + const char *from_id, + const char *remote_name, + const char *url) +{ + /* additional applications */ + if (reason == FLATPAK_TRANSACTION_REMOTE_GENERIC_REPO) { + g_debug ("configuring %s as new generic remote", url); + return TRUE; //FIXME? + } + + /* runtime deps always make sense */ + if (reason == FLATPAK_TRANSACTION_REMOTE_RUNTIME_DEPS) { + g_debug ("configuring %s as new remote for deps", url); + return TRUE; + } + + return FALSE; +} + +static void +gs_flatpak_transaction_class_init (GsFlatpakTransactionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + FlatpakTransactionClass *transaction_class = FLATPAK_TRANSACTION_CLASS (klass); + object_class->finalize = gs_flatpak_transaction_finalize; + transaction_class->ready = _transaction_ready; + transaction_class->add_new_remote = _transaction_add_new_remote; + transaction_class->new_operation = _transaction_new_operation; + transaction_class->operation_done = _transaction_operation_done; + transaction_class->operation_error = _transaction_operation_error; + transaction_class->choose_remote_for_ref = _transaction_choose_remote_for_ref; + transaction_class->end_of_lifed = _transaction_end_of_lifed; + transaction_class->end_of_lifed_with_rebase = _transaction_end_of_lifed_with_rebase; + + signals[SIGNAL_REF_TO_APP] = + g_signal_new ("ref-to-app", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_OBJECT, 1, G_TYPE_STRING); +} + +static void +gs_flatpak_transaction_init (GsFlatpakTransaction *self) +{ + self->refhash = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_object_unref); +} + +FlatpakTransaction * +gs_flatpak_transaction_new (FlatpakInstallation *installation, + GCancellable *cancellable, + GError **error) +{ + GsFlatpakTransaction *self; + self = g_initable_new (GS_TYPE_FLATPAK_TRANSACTION, + cancellable, error, + "installation", installation, + NULL); + if (self == NULL) + return NULL; + return FLATPAK_TRANSACTION (self); +} diff --git a/plugins/flatpak/gs-flatpak-transaction.h b/plugins/flatpak/gs-flatpak-transaction.h new file mode 100644 index 0000000..1cc2a07 --- /dev/null +++ b/plugins/flatpak/gs-flatpak-transaction.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) 2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <flatpak.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FLATPAK_TRANSACTION (gs_flatpak_transaction_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, GS, FLATPAK_TRANSACTION, FlatpakTransaction) + +FlatpakTransaction *gs_flatpak_transaction_new (FlatpakInstallation *installation, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction, + const gchar *ref); +void gs_flatpak_transaction_add_app (FlatpakTransaction *transaction, + GsApp *app); +gboolean gs_flatpak_transaction_run (FlatpakTransaction *transaction, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/plugins/flatpak/gs-flatpak-utils.c b/plugins/flatpak/gs-flatpak-utils.c new file mode 100644 index 0000000..9675810 --- /dev/null +++ b/plugins/flatpak/gs-flatpak-utils.c @@ -0,0 +1,271 @@ +/* -*- 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+ + */ + +#include <config.h> +#include <ostree.h> + +#include <glib/gi18n.h> + +#include "gs-flatpak-app.h" +#include "gs-flatpak.h" +#include "gs-flatpak-utils.h" + +void +gs_flatpak_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; + + /* this are allowed for network ops */ + if (gs_utils_error_convert_gresolver (perror)) + return; + + /* custom to this plugin */ + if (error->domain == FLATPAK_ERROR) { + switch (error->code) { + case FLATPAK_ERROR_ALREADY_INSTALLED: + case FLATPAK_ERROR_NOT_INSTALLED: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case FLATPAK_ERROR_OUT_OF_SPACE: + error->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + case FLATPAK_ERROR_INVALID_REF: + case FLATPAK_ERROR_INVALID_DATA: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else if (error->domain == OSTREE_GPG_ERROR) { + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + } else { + g_warning ("can't reliably fixup error from domain %s: %s", + g_quark_to_string (error->domain), + error->message); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; +} + +GsApp * +gs_flatpak_app_new_from_remote (GsPlugin *plugin, + FlatpakRemote *xremote, + gboolean is_user) +{ + g_autofree gchar *title = NULL; + g_autofree gchar *url = NULL; + g_autofree gchar *filter = NULL; + g_autofree gchar *description = NULL; + g_autofree gchar *comment = NULL; + g_autoptr(GsApp) app = NULL; + + app = gs_flatpak_app_new (flatpak_remote_get_name (xremote)); + gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY); + gs_app_set_state (app, flatpak_remote_get_disabled (xremote) ? + GS_APP_STATE_AVAILABLE : GS_APP_STATE_INSTALLED); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + flatpak_remote_get_name (xremote)); + gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + gs_app_set_management_plugin (app, plugin); + gs_flatpak_app_set_packaging_info (app); + gs_app_set_scope (app, is_user ? AS_COMPONENT_SCOPE_USER : AS_COMPONENT_SCOPE_SYSTEM); + + gs_app_set_metadata (app, "GnomeSoftware::SortKey", "100"); + gs_app_set_metadata (app, "GnomeSoftware::InstallationKind", + is_user ? _("User Installation") : _("System Installation")); + if (!is_user) + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + + /* title */ + title = flatpak_remote_get_title (xremote); + if (title != NULL) { + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, title); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, title); + } + + /* origin_ui on a remote is the repo dialogue section name, + * not the remote title */ + gs_app_set_origin_ui (app, _("Applications")); + + description = flatpak_remote_get_description (xremote); + if (description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, description); + + /* url */ + url = flatpak_remote_get_url (xremote); + if (url != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url); + + filter = flatpak_remote_get_filter (xremote); + if (filter != NULL) + gs_flatpak_app_set_repo_filter (app, filter); + + comment = flatpak_remote_get_comment (xremote); + if (comment != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, comment); + + /* success */ + return g_steal_pointer (&app); +} + +GsApp * +gs_flatpak_app_new_from_repo_file (GFile *file, + GCancellable *cancellable, + GError **error) +{ + gchar *tmp; + g_autofree gchar *basename = NULL; + g_autofree gchar *filename = NULL; + g_autofree gchar *repo_comment = NULL; + g_autofree gchar *repo_default_branch = NULL; + g_autofree gchar *repo_description = NULL; + g_autofree gchar *repo_gpgkey = NULL; + g_autofree gchar *repo_homepage = NULL; + g_autofree gchar *repo_icon = NULL; + g_autofree gchar *repo_id = NULL; + g_autofree gchar *repo_title = NULL; + g_autofree gchar *repo_url = NULL; + g_autofree gchar *repo_filter = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app = NULL; + + /* read the file */ + kf = g_key_file_new (); + filename = g_file_get_path (file); + if (!g_key_file_load_from_file (kf, filename, + G_KEY_FILE_NONE, + &error_local)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "failed to load flatpakrepo: %s", + error_local->message); + return NULL; + } + + /* get the ID from the basename */ + basename = g_file_get_basename (file); + tmp = g_strrstr (basename, "."); + if (tmp != NULL) + *tmp = '\0'; + + /* ensure this is valid for flatpak */ + if (ostree_validate_remote_name (basename, NULL)) { + repo_id = g_steal_pointer (&basename); + } else { + repo_id = g_str_to_ascii (basename, NULL); + + for (guint i = 0; repo_id[i] != '\0'; i++) { + if (!g_ascii_isalnum (repo_id[i])) + repo_id[i] = '_'; + } + } + + /* create source */ + repo_title = g_key_file_get_string (kf, "Flatpak Repo", "Title", NULL); + repo_url = g_key_file_get_string (kf, "Flatpak Repo", "Url", NULL); + if (repo_title == NULL || repo_url == NULL || + repo_title[0] == '\0' || repo_url[0] == '\0') { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "not enough data in file, " + "expected at least Title and Url"); + return NULL; + } + + /* check version */ + if (g_key_file_has_key (kf, "Flatpak Repo", "Version", NULL)) { + guint64 ver = g_key_file_get_uint64 (kf, "Flatpak Repo", "Version", NULL); + if (ver != 1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "unsupported version %" G_GUINT64_FORMAT, ver); + return NULL; + } + } + + /* create source */ + app = gs_flatpak_app_new (repo_id); + gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_REPO); + gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, repo_title); + gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + gs_flatpak_app_set_repo_url (app, repo_url); + gs_app_set_origin_ui (app, repo_title); + gs_app_set_origin_hostname (app, repo_url); + + /* user specified a URL */ + repo_gpgkey = g_key_file_get_string (kf, "Flatpak Repo", "GPGKey", NULL); + if (repo_gpgkey != NULL) { + if (g_str_has_prefix (repo_gpgkey, "http://") || + g_str_has_prefix (repo_gpgkey, "https://")) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Base64 encoded GPGKey required, not URL"); + return NULL; + } + gs_flatpak_app_set_repo_gpgkey (app, repo_gpgkey); + } + + /* optional data */ + repo_homepage = g_key_file_get_string (kf, "Flatpak Repo", "Homepage", NULL); + if (repo_homepage != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, repo_homepage); + repo_comment = g_key_file_get_string (kf, "Flatpak Repo", "Comment", NULL); + if (repo_comment != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, repo_comment); + repo_description = g_key_file_get_string (kf, "Flatpak Repo", "Description", NULL); + if (repo_description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, repo_description); + repo_default_branch = g_key_file_get_string (kf, "Flatpak Repo", "DefaultBranch", NULL); + if (repo_default_branch != NULL) + gs_app_set_branch (app, repo_default_branch); + repo_icon = g_key_file_get_string (kf, "Flatpak Repo", "Icon", NULL); + if (repo_icon != NULL && + (g_str_has_prefix (repo_icon, "http:") || + g_str_has_prefix (repo_icon, "https:"))) { + g_autoptr(GIcon) icon = gs_remote_icon_new (repo_icon); + gs_app_add_icon (app, icon); + } + repo_filter = g_key_file_get_string (kf, "Flatpak Repo", "Filter", NULL); + if (repo_filter != NULL && *repo_filter != '\0') + gs_flatpak_app_set_repo_filter (app, repo_filter); + + /* success */ + return g_steal_pointer (&app); +} + +void +gs_flatpak_app_set_packaging_info (GsApp *app) +{ + g_return_if_fail (GS_IS_APP (app)); + + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_FLATPAK); + gs_app_set_metadata (app, "GnomeSoftware::PackagingBaseCssColor", "accent_color"); + gs_app_set_metadata (app, "GnomeSoftware::PackagingIcon", "flatpak-symbolic"); +} diff --git a/plugins/flatpak/gs-flatpak-utils.h b/plugins/flatpak/gs-flatpak-utils.h new file mode 100644 index 0000000..8275828 --- /dev/null +++ b/plugins/flatpak/gs-flatpak-utils.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 + +G_BEGIN_DECLS + +#include <gnome-software.h> + +void gs_flatpak_error_convert (GError **perror); +GsApp *gs_flatpak_app_new_from_remote (GsPlugin *plugin, + FlatpakRemote *xremote, + gboolean is_user); +GsApp *gs_flatpak_app_new_from_repo_file (GFile *file, + GCancellable *cancellable, + GError **error); +void gs_flatpak_app_set_packaging_info (GsApp *app); + +G_END_DECLS diff --git a/plugins/flatpak/gs-flatpak.c b/plugins/flatpak/gs-flatpak.c new file mode 100644 index 0000000..aac55b1 --- /dev/null +++ b/plugins/flatpak/gs-flatpak.c @@ -0,0 +1,4624 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com> + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/* Notes: + * + * All GsApp's created have management-plugin set to flatpak + * The GsApp:origin is the remote name, e.g. test-repo + * + * Two #FlatpakInstallation objects are kept: `installation_noninteractive` and + * `installation_interactive`. One has flatpak_installation_set_no_interaction() + * set to %TRUE, the other to %FALSE. + * + * This is because multiple #GsFlatpak operations can be ongoing with different + * interactive states (for example, a background refresh operation while the + * user is refining an app in the foreground), but the #FlatpakInstallation + * methods don’t support per-operation interactive state. + * + * Internally, each #FlatpakInstallation will use a separate #FlatpakDir + * pointing to the same repository. Those #FlatpakDirs will lock the repository + * when using it, so parallel operations won’t race. + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <xmlb.h> + +#include "gs-appstream.h" +#include "gs-flatpak-app.h" +#include "gs-flatpak.h" +#include "gs-flatpak-utils.h" + +struct _GsFlatpak { + GObject parent_instance; + GsFlatpakFlags flags; + FlatpakInstallation *installation_noninteractive; /* (owned) */ + FlatpakInstallation *installation_interactive; /* (owned) */ + GPtrArray *installed_refs; /* must be entirely replaced rather than updated internally */ + GMutex installed_refs_mutex; + GHashTable *broken_remotes; + GMutex broken_remotes_mutex; + GFileMonitor *monitor; + AsComponentScope scope; + GsPlugin *plugin; + XbSilo *silo; + GRWLock silo_lock; + gchar *id; + guint changed_id; + GHashTable *app_silos; + GMutex app_silos_mutex; + GHashTable *remote_title; /* gchar *remote name ~> gchar *remote title */ + GMutex remote_title_mutex; + gboolean requires_full_rescan; + gint busy; /* (atomic) */ + gboolean changed_while_busy; +}; + +G_DEFINE_TYPE (GsFlatpak, gs_flatpak, G_TYPE_OBJECT) + +static void +gs_plugin_refine_item_scope (GsFlatpak *self, GsApp *app) +{ + if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) { + gboolean is_user = flatpak_installation_get_is_user (self->installation_noninteractive); + gs_app_set_scope (app, is_user ? AS_COMPONENT_SCOPE_USER : AS_COMPONENT_SCOPE_SYSTEM); + } +} + +static void +gs_flatpak_claim_app (GsFlatpak *self, GsApp *app) +{ + if (!gs_app_has_management_plugin (app, NULL)) + return; + + gs_app_set_management_plugin (app, self->plugin); + gs_flatpak_app_set_packaging_info (app); + + /* only when we have a non-temp object */ + if ((self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY) == 0) { + gs_app_set_scope (app, self->scope); + gs_flatpak_app_set_object_id (app, gs_flatpak_get_id (self)); + } +} + +static void +gs_flatpak_ensure_remote_title (GsFlatpak *self, + gboolean interactive, + GCancellable *cancellable) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->remote_title_mutex); + g_autoptr(GPtrArray) xremotes = NULL; + + if (g_hash_table_size (self->remote_title)) + return; + + xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), cancellable, NULL); + if (xremotes) { + guint ii; + + for (ii = 0; ii < xremotes->len; ii++) { + FlatpakRemote *xremote = g_ptr_array_index (xremotes, ii); + + if (flatpak_remote_get_disabled (xremote) || + !flatpak_remote_get_name (xremote)) + continue; + + g_hash_table_insert (self->remote_title, g_strdup (flatpak_remote_get_name (xremote)), flatpak_remote_get_title (xremote)); + } + } +} + +static void +gs_flatpak_set_app_origin (GsFlatpak *self, + GsApp *app, + const gchar *origin, + FlatpakRemote *xremote, + gboolean interactive, + GCancellable *cancellable) +{ + g_autoptr(GMutexLocker) locker = NULL; + g_autofree gchar *tmp = NULL; + const gchar *title = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (origin != NULL); + + if (xremote) { + tmp = flatpak_remote_get_title (xremote); + title = tmp; + } else { + locker = g_mutex_locker_new (&self->remote_title_mutex); + title = g_hash_table_lookup (self->remote_title, origin); + } + + if (!title) { + g_autoptr(GPtrArray) xremotes = NULL; + + xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), cancellable, NULL); + + if (xremotes) { + guint ii; + + for (ii = 0; ii < xremotes->len; ii++) { + FlatpakRemote *yremote = g_ptr_array_index (xremotes, ii); + + if (flatpak_remote_get_disabled (yremote)) + continue; + + if (g_strcmp0 (flatpak_remote_get_name (yremote), origin) == 0) { + title = flatpak_remote_get_title (yremote); + + if (!locker) + locker = g_mutex_locker_new (&self->remote_title_mutex); + + /* Takes ownership of the 'title' */ + g_hash_table_insert (self->remote_title, g_strdup (origin), (gpointer) title); + break; + } + } + } + } + + if (g_strcmp0 (origin, "flathub-beta") == 0 || + g_strcmp0 (gs_app_get_branch (app), "devel") == 0 || + g_strcmp0 (gs_app_get_branch (app), "master") == 0 || + (gs_app_get_branch (app) && g_str_has_suffix (gs_app_get_branch (app), "beta"))) + gs_app_add_quirk (app, GS_APP_QUIRK_DEVELOPMENT_SOURCE); + + gs_app_set_origin (app, origin); + gs_app_set_origin_ui (app, title); +} + +static void +gs_flatpak_claim_app_list (GsFlatpak *self, + GsAppList *list, + gboolean interactive) +{ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* Do not claim ownership of a wildcard app */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + + if (gs_app_get_origin (app)) + gs_flatpak_set_app_origin (self, app, gs_app_get_origin (app), NULL, interactive, NULL); + + gs_flatpak_claim_app (self, app); + } +} + +static void +gs_flatpak_set_kind_from_flatpak (GsApp *app, FlatpakRef *xref) +{ + if (flatpak_ref_get_kind (xref) == FLATPAK_REF_KIND_APP) { + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + } else if (flatpak_ref_get_kind (xref) == FLATPAK_REF_KIND_RUNTIME) { + const gchar *id = gs_app_get_id (app); + /* this is anything that's not an app, including locales + * sources and debuginfo */ + if (g_str_has_suffix (id, ".Locale")) { + gs_app_set_kind (app, AS_COMPONENT_KIND_LOCALIZATION); + } else if (g_str_has_suffix (id, ".Debug") || + g_str_has_suffix (id, ".Sources") || + g_str_has_prefix (id, "org.freedesktop.Platform.Icontheme.") || + g_str_has_prefix (id, "org.gtk.Gtk3theme.")) { + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + } else { + gs_app_set_kind (app, AS_COMPONENT_KIND_RUNTIME); + } + } +} + +static guint +gs_get_strv_index (const gchar * const *strv, + const gchar *value) +{ + guint ii; + + for (ii = 0; strv[ii]; ii++) { + if (g_str_equal (strv[ii], value)) + break; + } + + return ii; +} + +static GsAppPermissions * +perms_from_metadata (GKeyFile *keyfile) +{ + char **strv; + char *str; + GsAppPermissions *permissions = gs_app_permissions_new (); + GsAppPermissionsFlags flags = GS_APP_PERMISSIONS_FLAGS_UNKNOWN; + + strv = g_key_file_get_string_list (keyfile, "Context", "sockets", NULL, NULL); + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "system-bus")) + flags |= GS_APP_PERMISSIONS_FLAGS_SYSTEM_BUS; + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "session-bus")) + flags |= GS_APP_PERMISSIONS_FLAGS_SESSION_BUS; + if (strv != NULL && + !g_strv_contains ((const gchar * const*)strv, "fallback-x11") && + g_strv_contains ((const gchar * const*)strv, "x11")) + flags |= GS_APP_PERMISSIONS_FLAGS_X11; + g_strfreev (strv); + + strv = g_key_file_get_string_list (keyfile, "Context", "devices", NULL, NULL); + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "all")) + flags |= GS_APP_PERMISSIONS_FLAGS_DEVICES; + g_strfreev (strv); + + strv = g_key_file_get_string_list (keyfile, "Context", "shared", NULL, NULL); + if (strv != NULL && g_strv_contains ((const gchar * const*)strv, "network")) + flags |= GS_APP_PERMISSIONS_FLAGS_NETWORK; + g_strfreev (strv); + + strv = g_key_file_get_string_list (keyfile, "Context", "filesystems", NULL, NULL); + if (strv != NULL) { + const struct { + const gchar *key; + GsAppPermissionsFlags perm; + } filesystems_access[] = { + /* Reference: https://docs.flatpak.org/en/latest/flatpak-command-reference.html#idm45858571325264 */ + { "home", GS_APP_PERMISSIONS_FLAGS_HOME_FULL }, + { "home:rw", GS_APP_PERMISSIONS_FLAGS_HOME_FULL }, + { "home:ro", GS_APP_PERMISSIONS_FLAGS_HOME_READ }, + { "~", GS_APP_PERMISSIONS_FLAGS_HOME_FULL }, + { "~:rw", GS_APP_PERMISSIONS_FLAGS_HOME_FULL }, + { "~:ro", GS_APP_PERMISSIONS_FLAGS_HOME_READ }, + { "host", GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL }, + { "host:rw", GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL }, + { "host:ro", GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_READ }, + { "xdg-download", GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL }, + { "xdg-download:rw", GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL }, + { "xdg-download:ro", GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_READ }, + { "xdg-data/flatpak/overrides:create", GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX } + }; + guint filesystems_hits = 0; + guint strv_len = g_strv_length (strv); + + for (guint i = 0; i < G_N_ELEMENTS (filesystems_access); i++) { + guint index = gs_get_strv_index ((const gchar * const *) strv, filesystems_access[i].key); + if (index < strv_len) { + flags |= filesystems_access[i].perm; + filesystems_hits++; + /* Mark it as used */ + strv[index][0] = '\0'; + } + } + + if ((flags & GS_APP_PERMISSIONS_FLAGS_HOME_FULL) != 0) + flags = flags & ~GS_APP_PERMISSIONS_FLAGS_HOME_READ; + if ((flags & GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL) != 0) + flags = flags & ~GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_READ; + if ((flags & GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL) != 0) + flags = flags & ~GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_READ; + + if (strv_len > filesystems_hits) { + /* Cover those not being part of the above filesystem_access array */ + const struct { + const gchar *prefix; + const gchar *title; + const gchar *title_subdir; + } filesystems_other[] = { + /* Reference: https://docs.flatpak.org/en/latest/flatpak-command-reference.html#idm45858571325264 */ + { "/", NULL, N_("System folder %s") }, + { "home/", NULL, N_("Home subfolder %s") }, + { "~/", NULL, N_("Home subfolder %s") }, + { "host-os", N_("Host system folders"), NULL }, + { "host-etc", N_("Host system configuration from /etc"), NULL }, + { "xdg-desktop", N_("Desktop folder"), N_("Desktop subfolder %s") }, + { "xdg-documents", N_("Documents folder"), N_("Documents subfolder %s") }, + { "xdg-music", N_("Music folder"), N_("Music subfolder %s") }, + { "xdg-pictures", N_("Pictures folder"), N_("Pictures subfolder %s") }, + { "xdg-public-share", N_("Public Share folder"), N_("Public Share subfolder %s") }, + { "xdg-videos", N_("Videos folder"), N_("Videos subfolder %s") }, + { "xdg-templates", N_("Templates folder"), N_("Templates subfolder %s") }, + { "xdg-cache", N_("User cache folder"), N_("User cache subfolder %s") }, + { "xdg-config", N_("User configuration folder"), N_("User configuration subfolder %s") }, + { "xdg-data", N_("User data folder"), N_("User data subfolder %s") }, + { "xdg-run", N_("User runtime folder"), N_("User runtime subfolder %s") } + }; + + flags |= GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_OTHER; + + for (guint j = 0; strv[j]; j++) { + gchar *perm = strv[j]; + gboolean is_readonly; + gchar *colon; + guint i; + + /* Already handled by the flags */ + if (!perm[0]) + continue; + + is_readonly = g_str_has_suffix (perm, ":ro"); + colon = strrchr (perm, ':'); + /* modifiers are ":ro", ":rw", ":create", where ":create" is ":rw" + create + and ":rw" is default; treat ":create" as ":rw" */ + if (colon) { + /* Completeness check */ + if (!g_str_equal (colon, ":ro") && + !g_str_equal (colon, ":rw") && + !g_str_equal (colon, ":create")) + g_debug ("Unknown filesystem permission modifier '%s' from '%s'", colon, perm); + /* cut it off */ + *colon = '\0'; + } + + for (i = 0; i < G_N_ELEMENTS (filesystems_other); i++) { + if (g_str_has_prefix (perm, filesystems_other[i].prefix)) { + g_autofree gchar *title_tmp = NULL; + const gchar *slash, *title = NULL; + slash = strchr (perm, '/'); + /* Catch and ignore invalid permission definitions */ + if (slash && filesystems_other[i].title_subdir != NULL) { + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wformat-nonliteral" + title_tmp = g_strdup_printf ( + _(filesystems_other[i].title_subdir), + slash + (slash == perm ? 0 : 1)); + #pragma GCC diagnostic pop + title = title_tmp; + } else if (!slash && filesystems_other[i].title != NULL) { + title = _(filesystems_other[i].title); + } + if (title != NULL) { + if (is_readonly) + gs_app_permissions_add_filesystem_read (permissions, title); + else + gs_app_permissions_add_filesystem_full (permissions, title); + } + break; + } + } + + /* Nothing matched, use a generic entry */ + if (i == G_N_ELEMENTS (filesystems_other)) { + g_autofree gchar *title = g_strdup_printf (_("Filesystem access to %s"), perm); + if (is_readonly) + gs_app_permissions_add_filesystem_read (permissions, title); + else + gs_app_permissions_add_filesystem_full (permissions, title); + } + } + } + } + g_strfreev (strv); + + str = g_key_file_get_string (keyfile, "Session Bus Policy", "ca.desrt.dconf", NULL); + if (str != NULL && g_str_equal (str, "talk")) + flags |= GS_APP_PERMISSIONS_FLAGS_SETTINGS; + g_free (str); + + if (!(flags & GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX)) { + str = g_key_file_get_string (keyfile, "Session Bus Policy", "org.freedesktop.Flatpak", NULL); + if (str != NULL && g_str_equal (str, "talk")) + flags |= GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX; + g_free (str); + } + + if (!(flags & GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX)) { + str = g_key_file_get_string (keyfile, "Session Bus Policy", "org.freedesktop.impl.portal.PermissionStore", NULL); + if (str != NULL && g_str_equal (str, "talk")) + flags |= GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX; + g_free (str); + } + + /* no permissions set */ + if (flags == GS_APP_PERMISSIONS_FLAGS_UNKNOWN) + flags = GS_APP_PERMISSIONS_FLAGS_NONE; + + gs_app_permissions_set_flags (permissions, flags); + gs_app_permissions_seal (permissions); + + return permissions; +} + +static void +gs_flatpak_set_update_permissions (GsFlatpak *self, + GsApp *app, + FlatpakInstalledRef *xref, + gboolean interactive, + GCancellable *cancellable) +{ + g_autoptr(GBytes) old_bytes = NULL; + g_autoptr(GKeyFile) old_keyfile = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GsAppPermissions) additional_permissions = gs_app_permissions_new (); + g_autoptr(GError) error_local = NULL; + + old_bytes = flatpak_installed_ref_load_metadata (FLATPAK_INSTALLED_REF (xref), NULL, NULL); + old_keyfile = g_key_file_new (); + g_key_file_load_from_data (old_keyfile, + g_bytes_get_data (old_bytes, NULL), + g_bytes_get_size (old_bytes), + 0, NULL); + + bytes = flatpak_installation_fetch_remote_metadata_sync (gs_flatpak_get_installation (self, interactive), + gs_app_get_origin (app), + FLATPAK_REF (xref), + cancellable, + &error_local); + if (bytes == NULL) { + g_debug ("Failed to get metadata for remote ‘%s’: %s", + gs_app_get_origin (app), error_local->message); + g_clear_error (&error_local); + gs_app_permissions_set_flags (additional_permissions, GS_APP_PERMISSIONS_FLAGS_UNKNOWN); + } else { + g_autoptr(GsAppPermissions) old_permissions = NULL; + g_autoptr(GsAppPermissions) new_permissions = NULL; + const GPtrArray *new_paths; + + keyfile = g_key_file_new (); + g_key_file_load_from_data (keyfile, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes), + 0, NULL); + + old_permissions = perms_from_metadata (old_keyfile); + new_permissions = perms_from_metadata (keyfile); + + gs_app_permissions_set_flags (additional_permissions, + gs_app_permissions_get_flags (new_permissions) & + ~gs_app_permissions_get_flags (old_permissions)); + + new_paths = gs_app_permissions_get_filesystem_read (new_permissions); + for (guint i = 0; new_paths && i < new_paths->len; i++) { + const gchar *new_path = g_ptr_array_index (new_paths, i); + if (!gs_app_permissions_contains_filesystem_read (old_permissions, new_path)) + gs_app_permissions_add_filesystem_read (additional_permissions, new_path); + } + + new_paths = gs_app_permissions_get_filesystem_full (new_permissions); + for (guint i = 0; new_paths && i < new_paths->len; i++) { + const gchar *new_path = g_ptr_array_index (new_paths, i); + if (!gs_app_permissions_contains_filesystem_full (old_permissions, new_path)) + gs_app_permissions_add_filesystem_full (additional_permissions, new_path); + } + } + + /* no new permissions set */ + if (gs_app_permissions_get_flags (additional_permissions) == GS_APP_PERMISSIONS_FLAGS_UNKNOWN) + gs_app_permissions_set_flags (additional_permissions, GS_APP_PERMISSIONS_FLAGS_NONE); + + gs_app_permissions_seal (additional_permissions); + gs_app_set_update_permissions (app, additional_permissions); + + if (gs_app_permissions_get_flags (additional_permissions) != GS_APP_PERMISSIONS_FLAGS_NONE) + gs_app_add_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS); + else + gs_app_remove_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS); +} + +static void +gs_flatpak_set_metadata (GsFlatpak *self, GsApp *app, FlatpakRef *xref) +{ + g_autofree gchar *ref_tmp = flatpak_ref_format_ref (FLATPAK_REF (xref)); + guint64 installed_size = 0, download_size = 0; + + /* core */ + gs_flatpak_claim_app (self, app); + gs_app_set_branch (app, flatpak_ref_get_branch (xref)); + gs_app_add_source (app, ref_tmp); + gs_plugin_refine_item_scope (self, app); + + /* flatpak specific */ + gs_flatpak_app_set_ref_kind (app, flatpak_ref_get_kind (xref)); + gs_flatpak_app_set_ref_name (app, flatpak_ref_get_name (xref)); + gs_flatpak_app_set_ref_arch (app, flatpak_ref_get_arch (xref)); + gs_flatpak_app_set_commit (app, flatpak_ref_get_commit (xref)); + + /* map the flatpak kind to the gnome-software kind */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN || + gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC) { + gs_flatpak_set_kind_from_flatpak (app, xref); + } + + if (FLATPAK_IS_REMOTE_REF (xref) && flatpak_remote_ref_get_eol (FLATPAK_REMOTE_REF (xref)) != NULL) + gs_app_set_metadata (app, "GnomeSoftware::EolReason", flatpak_remote_ref_get_eol (FLATPAK_REMOTE_REF (xref))); + else if (FLATPAK_IS_INSTALLED_REF (xref) && flatpak_installed_ref_get_eol (FLATPAK_INSTALLED_REF (xref)) != NULL) + gs_app_set_metadata (app, "GnomeSoftware::EolReason", flatpak_installed_ref_get_eol (FLATPAK_INSTALLED_REF (xref))); + + if (FLATPAK_IS_REMOTE_REF (xref)) { + installed_size = flatpak_remote_ref_get_installed_size (FLATPAK_REMOTE_REF (xref)); + download_size = flatpak_remote_ref_get_download_size (FLATPAK_REMOTE_REF (xref)); + } else if (FLATPAK_IS_INSTALLED_REF (xref)) { + installed_size = flatpak_installed_ref_get_installed_size (FLATPAK_INSTALLED_REF (xref)); + } + + gs_app_set_size_installed (app, (installed_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, installed_size); + gs_app_set_size_download (app, (download_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, download_size); +} + +static GsApp * +gs_flatpak_create_app (GsFlatpak *self, + const gchar *origin, + FlatpakRef *xref, + FlatpakRemote *xremote, + gboolean interactive, + GCancellable *cancellable) +{ + GsApp *app_cached; + g_autoptr(GsApp) app = NULL; + + /* create a temp GsApp */ + app = gs_app_new (flatpak_ref_get_name (xref)); + gs_flatpak_set_metadata (self, app, xref); + if (origin != NULL) { + gs_flatpak_set_app_origin (self, app, origin, xremote, interactive, cancellable); + + if (!(self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY)) { + /* return the ref'd cached copy, only if the origin is known */ + app_cached = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cached != NULL) + return app_cached; + } + } + + /* fallback values */ + if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME) { + g_autoptr(GIcon) icon = NULL; + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, + flatpak_ref_get_name (FLATPAK_REF (xref))); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + "Framework for applications"); + gs_app_set_version (app, flatpak_ref_get_branch (FLATPAK_REF (xref))); + icon = g_themed_icon_new ("system-component-runtime"); + gs_app_add_icon (app, icon); + } + + /* Don't add NULL origin apps to the cache. If the app is later set to + * origin x the cache may return it as a match for origin y since the cache + * hash table uses as_utils_data_id_equal() as the equal func and a NULL + * origin becomes a "*" in gs_utils_build_unique_id(). + */ + if (origin != NULL && !(self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY)) + gs_plugin_cache_add (self->plugin, NULL, app); + + /* no existing match, just steal the temp object */ + return g_steal_pointer (&app); +} + +static GsApp * +gs_flatpak_create_source (GsFlatpak *self, FlatpakRemote *xremote) +{ + GsApp *app_cached; + g_autoptr(GsApp) app = NULL; + + /* create a temp GsApp */ + app = gs_flatpak_app_new_from_remote (self->plugin, xremote, + flatpak_installation_get_is_user (self->installation_noninteractive)); + gs_flatpak_claim_app (self, app); + + /* we already have one, returned the ref'd cached copy */ + app_cached = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cached != NULL) + return app_cached; + + /* no existing match, just steal the temp object */ + gs_plugin_cache_add (self->plugin, NULL, app); + return g_steal_pointer (&app); +} + +static void +gs_flatpak_invalidate_silo (GsFlatpak *self) +{ + g_rw_lock_writer_lock (&self->silo_lock); + if (self->silo != NULL) + xb_silo_invalidate (self->silo); + g_rw_lock_writer_unlock (&self->silo_lock); +} + +static void +gs_flatpak_internal_data_changed (GsFlatpak *self) +{ + g_autoptr(GMutexLocker) locker = NULL; + + /* drop the installed refs cache */ + locker = g_mutex_locker_new (&self->installed_refs_mutex); + g_clear_pointer (&self->installed_refs, g_ptr_array_unref); + g_clear_pointer (&locker, g_mutex_locker_free); + + /* drop the remote title cache */ + locker = g_mutex_locker_new (&self->remote_title_mutex); + g_hash_table_remove_all (self->remote_title); + g_clear_pointer (&locker, g_mutex_locker_free); + + /* give all the repos a second chance */ + locker = g_mutex_locker_new (&self->broken_remotes_mutex); + g_hash_table_remove_all (self->broken_remotes); + g_clear_pointer (&locker, g_mutex_locker_free); + + gs_flatpak_invalidate_silo (self); + + self->requires_full_rescan = TRUE; +} + +static gboolean +gs_flatpak_claim_changed_idle_cb (gpointer user_data) +{ + GsFlatpak *self = user_data; + + gs_flatpak_internal_data_changed (self); + gs_plugin_cache_invalidate (self->plugin); + gs_plugin_reload (self->plugin); + + return G_SOURCE_REMOVE; +} + +static void +gs_plugin_flatpak_changed_cb (GFileMonitor *monitor, + GFile *child, + GFile *other_file, + GFileMonitorEvent event_type, + GsFlatpak *self) +{ + if (gs_flatpak_get_busy (self)) { + self->changed_while_busy = TRUE; + } else { + gs_flatpak_claim_changed_idle_cb (self); + } +} + +static gboolean +gs_flatpak_add_flatpak_keyword_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) + gs_appstream_component_add_keyword (bn, "flatpak"); + return TRUE; +} + +static gboolean +gs_flatpak_fix_id_desktop_suffix_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_auto(GStrv) split = NULL; + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + g_autoptr(XbBuilderNode) bundle = xb_builder_node_get_child (bn, "bundle", NULL); + if (id == NULL || bundle == NULL) + return TRUE; + split = g_strsplit (xb_builder_node_get_text (bundle), "/", -1); + if (g_strv_length (split) != 4) + return TRUE; + if (g_strcmp0 (xb_builder_node_get_text (id), split[1]) != 0) { + g_debug ("fixing up <id>%s</id> to %s", + xb_builder_node_get_text (id), split[1]); + gs_appstream_component_add_provide (bn, xb_builder_node_get_text (id)); + xb_builder_node_set_text (id, split[1], -1); + } + } + return TRUE; +} + +static gboolean +gs_flatpak_add_bundle_tag_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const char *app_ref = (char *)user_data; + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + g_autoptr(XbBuilderNode) bundle = xb_builder_node_get_child (bn, "bundle", NULL); + if (id == NULL || bundle != NULL) + return TRUE; + g_debug ("adding <bundle> tag for %s", app_ref); + xb_builder_node_insert_text (bn, "bundle", app_ref, "type", "flatpak", NULL); + } + return TRUE; +} + +static gboolean +gs_flatpak_fix_metadata_tag_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) metadata = xb_builder_node_get_child (bn, "metadata", NULL); + if (metadata != NULL) + xb_builder_node_set_element (metadata, "custom"); + } + return TRUE; +} + +static gboolean +gs_flatpak_set_origin_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const char *remote_name = (char *)user_data; + if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) { + xb_builder_node_set_attr (bn, "origin", + remote_name); + } + return TRUE; +} + +static gboolean +gs_flatpak_filter_default_branch_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const gchar *default_branch = (const gchar *) user_data; + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) bc = xb_builder_node_get_child (bn, "bundle", NULL); + g_auto(GStrv) split = NULL; + if (bc == NULL) { + g_debug ("no bundle for component"); + return TRUE; + } + split = g_strsplit (xb_builder_node_get_text (bc), "/", -1); + if (split == NULL || g_strv_length (split) != 4) + return TRUE; + if (g_strcmp0 (split[3], default_branch) != 0) { + g_debug ("not adding app with branch %s as filtering to %s", + split[3], default_branch); + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE); + } + } + return TRUE; +} + +static gboolean +gs_flatpak_filter_noenumerate_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const gchar *main_ref = (const gchar *) user_data; + + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) bc = xb_builder_node_get_child (bn, "bundle", NULL); + if (bc == NULL) { + g_debug ("no bundle for component"); + return TRUE; + } + if (g_strcmp0 (xb_builder_node_get_text (bc), main_ref) != 0) { + g_debug ("not adding app %s as filtering to %s", + xb_builder_node_get_text (bc), main_ref); + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE); + } + } + return TRUE; +} + +#if LIBXMLB_CHECK_VERSION(0,3,0) +static gboolean +gs_flatpak_tokenize_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + const gchar * const elements_to_tokenize[] = { + "id", + "keyword", + "launchable", + "mimetype", + "name", + "summary", + NULL }; + if (xb_builder_node_get_element (bn) != NULL && + g_strv_contains (elements_to_tokenize, xb_builder_node_get_element (bn))) + xb_builder_node_tokenize_text (bn); + return TRUE; +} +#endif + +static void +fixup_flatpak_appstream_xml (XbBuilderSource *source, + const char *origin) +{ + g_autoptr(XbBuilderFixup) fixup1 = NULL; + g_autoptr(XbBuilderFixup) fixup2 = NULL; + g_autoptr(XbBuilderFixup) fixup3 = NULL; +#if LIBXMLB_CHECK_VERSION(0,3,0) + g_autoptr(XbBuilderFixup) fixup5 = NULL; +#endif + + /* add the flatpak search keyword */ + fixup1 = xb_builder_fixup_new ("AddKeywordFlatpak", + gs_flatpak_add_flatpak_keyword_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup1, 2); + xb_builder_source_add_fixup (source, fixup1); + + /* ensure the <id> matches the flatpak ref ID */ + fixup2 = xb_builder_fixup_new ("FixIdDesktopSuffix", + gs_flatpak_fix_id_desktop_suffix_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup2, 2); + xb_builder_source_add_fixup (source, fixup2); + + /* Fixup <metadata> to <custom> for appstream versions >= 0.9 */ + fixup3 = xb_builder_fixup_new ("FixMetadataTag", + gs_flatpak_fix_metadata_tag_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup3, 2); + xb_builder_source_add_fixup (source, fixup3); + +#if LIBXMLB_CHECK_VERSION(0,3,0) + fixup5 = xb_builder_fixup_new ("TextTokenize", + gs_flatpak_tokenize_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup5, 2); + xb_builder_source_add_fixup (source, fixup5); +#endif + + if (origin != NULL) { + g_autoptr(XbBuilderFixup) fixup4 = NULL; + + /* override the *AppStream* origin */ + fixup4 = xb_builder_fixup_new ("SetOrigin", + gs_flatpak_set_origin_cb, + g_strdup (origin), g_free); + xb_builder_fixup_set_max_depth (fixup4, 1); + xb_builder_source_add_fixup (source, fixup4); + } +} + +static gboolean +gs_flatpak_refresh_appstream_remote (GsFlatpak *self, + const gchar *remote_name, + gboolean interactive, + GCancellable *cancellable, + GError **error); + +static gboolean +gs_flatpak_add_apps_from_xremote (GsFlatpak *self, + XbBuilder *builder, + FlatpakRemote *xremote, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *appstream_dir_fn = NULL; + g_autofree gchar *appstream_fn = NULL; + g_autofree gchar *icon_prefix = NULL; + g_autofree gchar *default_branch = NULL; + g_autoptr(GFile) appstream_dir = NULL; + g_autoptr(GFile) file_xml = NULL; + g_autoptr(GSettings) settings = NULL; + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + const gchar *remote_name = flatpak_remote_get_name (xremote); + gboolean did_refresh = FALSE; + + /* get the AppStream data location */ + appstream_dir = flatpak_remote_get_appstream_dir (xremote, NULL); + if (appstream_dir == NULL) { + g_autoptr(GError) error_local = NULL; + g_debug ("no appstream dir for %s, trying refresh...", + remote_name); + + if (!gs_flatpak_refresh_appstream_remote (self, remote_name, interactive, cancellable, &error_local)) { + g_debug ("Failed to refresh appstream data for '%s': %s", remote_name, error_local->message); + if (g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED)) { + g_autoptr(GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&self->broken_remotes_mutex); + + /* don't try to fetch this again until refresh() */ + g_hash_table_insert (self->broken_remotes, + g_strdup (remote_name), + GUINT_TO_POINTER (1)); + } + return TRUE; + } + + appstream_dir = flatpak_remote_get_appstream_dir (xremote, NULL); + if (appstream_dir == NULL) { + g_debug ("no appstream dir for %s even after refresh, skipping", + remote_name); + return TRUE; + } + + did_refresh = TRUE; + } + + /* load the file into a temp silo */ + appstream_dir_fn = g_file_get_path (appstream_dir); + appstream_fn = g_build_filename (appstream_dir_fn, "appstream.xml.gz", NULL); + if (!g_file_test (appstream_fn, G_FILE_TEST_EXISTS)) { + g_autoptr(GError) error_local = NULL; + g_debug ("no appstream metadata found for '%s' (file: %s), %s", + remote_name, + appstream_fn, + did_refresh ? "skipping" : "trying refresh..."); + if (did_refresh) + return TRUE; + + if (!gs_flatpak_refresh_appstream_remote (self, remote_name, interactive, cancellable, &error_local)) { + g_debug ("Failed to refresh appstream data for '%s': %s", remote_name, error_local->message); + if (g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED)) { + g_autoptr(GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&self->broken_remotes_mutex); + + /* don't try to fetch this again until refresh() */ + g_hash_table_insert (self->broken_remotes, + g_strdup (remote_name), + GUINT_TO_POINTER (1)); + } + return TRUE; + } + + if (!g_file_test (appstream_fn, G_FILE_TEST_EXISTS)) { + g_debug ("no appstream metadata found for '%s', even after refresh (file: %s), skipping", + remote_name, + appstream_fn); + return TRUE; + } + } + + /* add source */ + file_xml = g_file_new_for_path (appstream_fn); + if (!xb_builder_source_load_file (source, file_xml, + XB_BUILDER_SOURCE_FLAG_WATCH_FILE | + XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT, + cancellable, + error)) + return FALSE; + + fixup_flatpak_appstream_xml (source, remote_name); + + /* add metadata */ + icon_prefix = g_build_filename (appstream_dir_fn, "icons", NULL); + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", as_component_scope_to_string (self->scope), NULL); + xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL); + xb_builder_source_set_info (source, info); + + /* only add the specific app for noenumerate=true */ + if (flatpak_remote_get_noenumerate (xremote)) { + g_autofree gchar *main_ref = NULL; + + main_ref = flatpak_remote_get_main_ref (xremote); + + if (main_ref != NULL) { + g_autoptr(XbBuilderFixup) fixup = NULL; + fixup = xb_builder_fixup_new ("FilterNoEnumerate", + gs_flatpak_filter_noenumerate_cb, + g_strdup (main_ref), + g_free); + xb_builder_fixup_set_max_depth (fixup, 2); + xb_builder_source_add_fixup (source, fixup); + } + } + + /* do we want to filter to the default branch */ + settings = g_settings_new ("org.gnome.software"); + default_branch = flatpak_remote_get_default_branch (xremote); + if (g_settings_get_boolean (settings, "filter-default-branch") && + default_branch != NULL) { + g_autoptr(XbBuilderFixup) fixup = NULL; + fixup = xb_builder_fixup_new ("FilterDefaultbranch", + gs_flatpak_filter_default_branch_cb, + flatpak_remote_get_default_branch (xremote), + g_free); + xb_builder_fixup_set_max_depth (fixup, 2); + xb_builder_source_add_fixup (source, fixup); + } + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static GInputStream * +gs_plugin_appstream_load_desktop_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *xml = NULL; + g_autoptr(AsComponent) cpt = as_component_new (); + g_autoptr(AsContext) actx = as_context_new (); + g_autoptr(GBytes) bytes = NULL; + gboolean ret; + + bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error); + if (bytes == NULL) + return NULL; + + as_component_set_id (cpt, xb_builder_source_ctx_get_filename (ctx)); + ret = as_component_load_from_bytes (cpt, + actx, + AS_FORMAT_KIND_DESKTOP_ENTRY, + bytes, + error); + if (!ret) + return NULL; + xml = as_component_to_xml_data (cpt, actx, error); + if (xml == NULL) + return NULL; + + return g_memory_input_stream_new_from_data (g_steal_pointer (&xml), (gssize) -1, g_free); +} + +static gboolean +gs_flatpak_load_desktop_fn (GsFlatpak *self, + XbBuilder *builder, + const gchar *filename, + const gchar *icon_prefix, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbBuilderFixup) fixup = NULL; + + /* add support for desktop files */ + xb_builder_source_add_adapter (source, "application/x-desktop", + gs_plugin_appstream_load_desktop_cb, NULL, NULL); + + /* add the flatpak search keyword */ + fixup = xb_builder_fixup_new ("AddKeywordFlatpak", + gs_flatpak_add_flatpak_keyword_cb, + self, NULL); + xb_builder_fixup_set_max_depth (fixup, 2); + xb_builder_source_add_fixup (source, fixup); + + /* set the component metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", as_component_scope_to_string (self->scope), NULL); + xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL); + xb_builder_source_set_info (source, info); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static void +gs_flatpak_rescan_installed (GsFlatpak *self, + XbBuilder *builder, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GFile) path = NULL; + g_autoptr(GDir) dir = NULL; + g_autofree gchar *path_str = NULL; + g_autofree gchar *path_exports = NULL; + g_autofree gchar *path_apps = NULL; + + /* add all installed desktop files */ + path = flatpak_installation_get_path (self->installation_noninteractive); + path_str = g_file_get_path (path); + path_exports = g_build_filename (path_str, "exports", NULL); + path_apps = g_build_filename (path_exports, "share", "applications", NULL); + dir = g_dir_open (path_apps, 0, NULL); + if (dir == NULL) + return; + while ((fn = g_dir_read_name (dir)) != NULL) { + g_autofree gchar *filename = NULL; + g_autoptr(GError) error_local = NULL; + + /* ignore */ + if (g_strcmp0 (fn, "mimeinfo.cache") == 0) + continue; + + /* parse desktop files */ + filename = g_build_filename (path_apps, fn, NULL); + if (!gs_flatpak_load_desktop_fn (self, + builder, + filename, + path_exports, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } +} + +static gboolean +gs_flatpak_rescan_appstream_store (GsFlatpak *self, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + const gchar *const *locales = g_get_language_names (); + g_autofree gchar *blobfn = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GPtrArray) xremotes = NULL; + g_autoptr(GRWLockReaderLocker) reader_locker = NULL; + g_autoptr(GRWLockWriterLocker) writer_locker = NULL; + g_autoptr(XbBuilder) builder = NULL; + g_autoptr(GMainContext) old_thread_default = NULL; + + reader_locker = g_rw_lock_reader_locker_new (&self->silo_lock); + /* everything is okay */ + if (self->silo != NULL && xb_silo_is_valid (self->silo)) + return TRUE; + g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free); + + /* drat! silo needs regenerating */ + writer_locker = g_rw_lock_writer_locker_new (&self->silo_lock); + g_clear_object (&self->silo); + + /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */ + old_thread_default = g_main_context_ref_thread_default (); + if (old_thread_default == g_main_context_default ()) + g_clear_pointer (&old_thread_default, g_main_context_unref); + if (old_thread_default != NULL) + g_main_context_pop_thread_default (old_thread_default); + builder = xb_builder_new (); + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + g_clear_pointer (&old_thread_default, g_main_context_unref); + + /* verbose profiling */ + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + xb_builder_set_profile_flags (builder, + XB_SILO_PROFILE_FLAG_XPATH | + XB_SILO_PROFILE_FLAG_DEBUG); + } + + /* add current locales */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + /* go through each remote adding metadata */ + xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), + cancellable, + error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + g_autoptr(GError) error_local = NULL; + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + if (flatpak_remote_get_disabled (xremote)) + continue; + g_debug ("found remote %s", + flatpak_remote_get_name (xremote)); + if (!gs_flatpak_add_apps_from_xremote (self, builder, xremote, interactive, cancellable, &error_local)) { + g_debug ("Failed to add apps from remote ‘%s’; skipping: %s", + flatpak_remote_get_name (xremote), error_local->message); + if (g_cancellable_set_error_if_cancelled (cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + } + } + + /* add any installed files without AppStream info */ + gs_flatpak_rescan_installed (self, builder, cancellable, error); + + /* create per-user cache */ + blobfn = gs_utils_get_cache_filename (gs_flatpak_get_id (self), + "components.xmlb", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + error); + if (blobfn == NULL) + return FALSE; + file = g_file_new_for_path (blobfn); + g_debug ("ensuring %s", blobfn); + + /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */ + old_thread_default = g_main_context_ref_thread_default (); + if (old_thread_default == g_main_context_default ()) + g_clear_pointer (&old_thread_default, g_main_context_unref); + if (old_thread_default != NULL) + g_main_context_pop_thread_default (old_thread_default); + + self->silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID | + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + cancellable, error); + + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + + if (self->silo == NULL) + return FALSE; + + /* success */ + return TRUE; +} + +static gboolean +gs_flatpak_rescan_app_data (GsFlatpak *self, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + if (self->requires_full_rescan) { + gboolean res = gs_flatpak_refresh (self, 60, interactive, cancellable, error); + if (res) + self->requires_full_rescan = FALSE; + else + gs_flatpak_internal_data_changed (self); + return res; + } + + if (!gs_flatpak_rescan_appstream_store (self, interactive, cancellable, error)) { + gs_flatpak_internal_data_changed (self); + return FALSE; + } + + return TRUE; +} + +/* Returns with a read lock held on @self->silo_lock on success. + The *locker should be NULL when being called. */ +static gboolean +ensure_flatpak_silo_with_locker (GsFlatpak *self, + GRWLockReaderLocker **locker, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + /* should not hold the lock when called */ + g_return_val_if_fail (*locker == NULL, FALSE); + + /* ensure valid */ + if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error)) + return FALSE; + + *locker = g_rw_lock_reader_locker_new (&self->silo_lock); + + while (self->silo == NULL) { + g_clear_pointer (locker, g_rw_lock_reader_locker_free); + + if (!gs_flatpak_rescan_appstream_store (self, interactive, cancellable, error)) { + gs_flatpak_internal_data_changed (self); + return FALSE; + } + + /* At this point either rescan_appstream_store() returned an error or it successfully + * initialised self->silo. There is the possibility that another thread will invalidate + * the silo before we regain the lock. If so, we’ll have to rescan again. */ + *locker = g_rw_lock_reader_locker_new (&self->silo_lock); + } + + return TRUE; +} + +gboolean +gs_flatpak_setup (GsFlatpak *self, GCancellable *cancellable, GError **error) +{ + /* watch for changes */ + self->monitor = flatpak_installation_create_monitor (self->installation_noninteractive, + cancellable, + error); + if (self->monitor == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + self->changed_id = + g_signal_connect (self->monitor, "changed", + G_CALLBACK (gs_plugin_flatpak_changed_cb), self); + + /* success */ + return TRUE; +} + +typedef struct { + GsPlugin *plugin; + GsApp *app; +} GsFlatpakProgressHelper; + +static void +gs_flatpak_progress_helper_free (GsFlatpakProgressHelper *phelper) +{ + g_object_unref (phelper->plugin); + if (phelper->app != NULL) + g_object_unref (phelper->app); + g_slice_free (GsFlatpakProgressHelper, phelper); +} + +static GsFlatpakProgressHelper * +gs_flatpak_progress_helper_new (GsPlugin *plugin, GsApp *app) +{ + GsFlatpakProgressHelper *phelper; + phelper = g_slice_new0 (GsFlatpakProgressHelper); + phelper->plugin = g_object_ref (plugin); + if (app != NULL) + phelper->app = g_object_ref (app); + return phelper; +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsFlatpakProgressHelper, gs_flatpak_progress_helper_free) + +static void +gs_flatpak_progress_cb (const gchar *status, + guint progress, + gboolean estimating, + gpointer user_data) +{ + GsFlatpakProgressHelper *phelper = (GsFlatpakProgressHelper *) user_data; + GsPluginStatus plugin_status = GS_PLUGIN_STATUS_DOWNLOADING; + + if (phelper->app != NULL) { + if (estimating) + gs_app_set_progress (phelper->app, GS_APP_PROGRESS_UNKNOWN); + else + gs_app_set_progress (phelper->app, progress); + + switch (gs_app_get_state (phelper->app)) { + case GS_APP_STATE_INSTALLING: + plugin_status = GS_PLUGIN_STATUS_INSTALLING; + break; + case GS_APP_STATE_REMOVING: + plugin_status = GS_PLUGIN_STATUS_REMOVING; + break; + default: + break; + } + } + gs_plugin_status_update (phelper->plugin, phelper->app, plugin_status); +} + +static gboolean +gs_flatpak_refresh_appstream_remote (GsFlatpak *self, + const gchar *remote_name, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *str = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (self->plugin)); + g_autoptr(GsFlatpakProgressHelper) phelper = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + g_autoptr(GError) error_local = NULL; + + /* TRANSLATORS: status text when downloading new metadata */ + str = g_strdup_printf (_("Getting flatpak metadata for %s…"), remote_name); + gs_app_set_summary_missing (app_dl, str); + gs_plugin_status_update (self->plugin, app_dl, GS_PLUGIN_STATUS_DOWNLOADING); + + if (!flatpak_installation_update_remote_sync (installation, + remote_name, + cancellable, + &error_local)) { + g_debug ("Failed to update metadata for remote %s: %s", + remote_name, error_local->message); + gs_flatpak_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + phelper = gs_flatpak_progress_helper_new (self->plugin, app_dl); + if (!flatpak_installation_update_appstream_full_sync (installation, + remote_name, + NULL, /* arch */ + gs_flatpak_progress_cb, + phelper, + NULL, /* out_changed */ + cancellable, + error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* success */ + gs_app_set_progress (app_dl, 100); + return TRUE; +} + +static gboolean +gs_flatpak_refresh_appstream (GsFlatpak *self, + guint64 cache_age_secs, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + gboolean ret; + g_autoptr(GPtrArray) xremotes = NULL; + + /* get remotes */ + xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), + cancellable, + error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + const gchar *remote_name; + guint64 tmp; + g_autoptr(GError) error_local = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFile) file_timestamp = NULL; + g_autofree gchar *appstream_fn = NULL; + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(GMutexLocker) locker = NULL; + + /* not enabled */ + if (flatpak_remote_get_disabled (xremote)) + continue; + + remote_name = flatpak_remote_get_name (xremote); + locker = g_mutex_locker_new (&self->broken_remotes_mutex); + + /* skip known-broken repos */ + if (g_hash_table_lookup (self->broken_remotes, remote_name) != NULL) { + g_debug ("skipping known broken remote: %s", remote_name); + continue; + } + + g_clear_pointer (&locker, g_mutex_locker_free); + + /* is the timestamp new enough */ + file_timestamp = flatpak_remote_get_appstream_timestamp (xremote, NULL); + tmp = gs_utils_get_file_age (file_timestamp); + if (tmp < cache_age_secs) { + g_autofree gchar *fn = g_file_get_path (file_timestamp); + g_debug ("%s is only %" G_GUINT64_FORMAT " seconds old, so ignoring refresh", + fn, tmp); + continue; + } + + /* download new data */ + g_debug ("%s is %" G_GUINT64_FORMAT " seconds old, so downloading new data", + remote_name, tmp); + ret = gs_flatpak_refresh_appstream_remote (self, + remote_name, + interactive, + cancellable, + &error_local); + if (!ret) { + g_autoptr(GsPluginEvent) event = NULL; + if (g_error_matches (error_local, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED)) { + g_debug ("Failed to get AppStream metadata: %s", + error_local->message); + + locker = g_mutex_locker_new (&self->broken_remotes_mutex); + + /* don't try to fetch this again until refresh() */ + g_hash_table_insert (self->broken_remotes, + g_strdup (remote_name), + GUINT_TO_POINTER (1)); + continue; + } + + /* allow the plugin loader to decide if this should be + * shown the user, possibly only for interactive jobs */ + gs_flatpak_error_convert (&error_local); + event = gs_plugin_event_new ("error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (self->plugin, event); + continue; + } + + /* add the new AppStream repo to the shared silo */ + file = flatpak_remote_get_appstream_dir (xremote, NULL); + appstream_fn = g_file_get_path (file); + g_debug ("using AppStream metadata found at: %s", appstream_fn); + } + + /* ensure the AppStream silo is up to date */ + if (!gs_flatpak_rescan_appstream_store (self, interactive, cancellable, error)) { + gs_flatpak_internal_data_changed (self); + return FALSE; + } + + return TRUE; +} + +static void +gs_flatpak_set_metadata_installed (GsFlatpak *self, + GsApp *app, + FlatpakInstalledRef *xref, + gboolean interactive, + GCancellable *cancellable) +{ + const gchar *appdata_version; + guint64 mtime; + guint64 size_installed; + g_autofree gchar *metadata_fn = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileInfo) info = NULL; + + /* for all types */ + gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref)); + if (gs_app_get_metadata_item (app, "GnomeSoftware::Creator") == NULL) { + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (self->plugin)); + } + + /* get the last time the app was updated */ + metadata_fn = g_build_filename (flatpak_installed_ref_get_deploy_dir (xref), + "..", + "active", + "metadata", + NULL); + file = g_file_new_for_path (metadata_fn); + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, NULL); + if (info != NULL) { + mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + gs_app_set_install_date (app, mtime); + } + + /* If it's a runtime, check if the main-app info should be set. Note that + * checking the app for AS_COMPONENT_KIND_RUNTIME is not good enough because it + * could be e.g. AS_COMPONENT_KIND_LOCALIZATION and still be a runtime from + * Flatpak's perspective. + */ + if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME && + gs_flatpak_app_get_main_app_ref_name (app) == NULL) { + g_autoptr(GError) error = NULL; + g_autoptr(GKeyFile) metadata_file = NULL; + metadata_file = g_key_file_new (); + if (g_key_file_load_from_file (metadata_file, metadata_fn, + G_KEY_FILE_NONE, &error)) { + g_autofree gchar *main_app = g_key_file_get_string (metadata_file, + "ExtensionOf", + "ref", NULL); + if (main_app != NULL) + gs_flatpak_app_set_main_app_ref_name (app, main_app); + } else { + g_warning ("Error loading the metadata file for '%s': %s", + gs_app_get_unique_id (app), error->message); + } + } + + /* this is faster than resolving */ + if (gs_app_get_origin (app) == NULL) + gs_flatpak_set_app_origin (self, app, flatpak_installed_ref_get_origin (xref), NULL, interactive, cancellable); + + /* this is faster than flatpak_installation_fetch_remote_size_sync() */ + size_installed = flatpak_installed_ref_get_installed_size (xref); + gs_app_set_size_installed (app, (size_installed != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, size_installed); + + appdata_version = flatpak_installed_ref_get_appdata_version (xref); + if (appdata_version != NULL) + gs_app_set_version (app, appdata_version); +} + +static GsApp * +gs_flatpak_create_installed (GsFlatpak *self, + FlatpakInstalledRef *xref, + FlatpakRemote *xremote, + gboolean interactive, + GCancellable *cancellable) +{ + g_autoptr(GsApp) app = NULL; + const gchar *origin; + + g_return_val_if_fail (xref != NULL, NULL); + + /* create new object */ + origin = flatpak_installed_ref_get_origin (xref); + app = gs_flatpak_create_app (self, origin, FLATPAK_REF (xref), xremote, interactive, cancellable); + + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + gs_flatpak_set_metadata_installed (self, app, xref, interactive, cancellable); + return g_steal_pointer (&app); +} + +gboolean +gs_flatpak_add_installed (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xrefs = NULL; + + /* get apps and runtimes */ + xrefs = flatpak_installation_list_installed_refs (gs_flatpak_get_installation (self, interactive), + cancellable, error); + if (xrefs == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + gs_flatpak_ensure_remote_title (self, interactive, cancellable); + + for (guint i = 0; i < xrefs->len; i++) { + FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i); + g_autoptr(GsApp) app = gs_flatpak_create_installed (self, xref, NULL, interactive, cancellable); + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_flatpak_add_sources (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xrefs = NULL; + g_autoptr(GPtrArray) xremotes = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + /* refresh */ + if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error)) + return FALSE; + + /* get installed apps and runtimes */ + xrefs = flatpak_installation_list_installed_refs (installation, + cancellable, + error); + if (xrefs == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* get available remotes */ + xremotes = flatpak_installation_list_remotes (installation, + cancellable, + error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(GsApp) app = NULL; + + /* apps installed from bundles add their own remote that only + * can be used for updating that app only -- so hide them */ + if (flatpak_remote_get_noenumerate (xremote)) + continue; + + /* create app */ + app = gs_flatpak_create_source (self, xremote); + gs_app_list_add (list, app); + + /* add related apps, i.e. what was installed from there */ + for (guint j = 0; j < xrefs->len; j++) { + FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, j); + g_autoptr(GsApp) related = NULL; + + /* only apps */ + if (flatpak_ref_get_kind (FLATPAK_REF (xref)) != FLATPAK_REF_KIND_APP) + continue; + if (g_strcmp0 (flatpak_installed_ref_get_origin (xref), + flatpak_remote_get_name (xremote)) != 0) + continue; + related = gs_flatpak_create_installed (self, xref, xremote, interactive, cancellable); + gs_app_add_related (app, related); + } + } + return TRUE; +} + +GsApp * +gs_flatpak_find_source_by_url (GsFlatpak *self, + const gchar *url, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xremotes = NULL; + + g_return_val_if_fail (url != NULL, NULL); + + xremotes = flatpak_installation_list_remotes (gs_flatpak_get_installation (self, interactive), cancellable, error); + if (xremotes == NULL) + return NULL; + for (guint i = 0; i < xremotes->len; i++) { + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autofree gchar *url_tmp = flatpak_remote_get_url (xremote); + if (g_strcmp0 (url, url_tmp) == 0) + return gs_flatpak_create_source (self, xremote); + } + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "cannot find %s", url); + return NULL; +} + +/* transfer full */ +GsApp * +gs_flatpak_ref_to_app (GsFlatpak *self, + const gchar *ref, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xremotes = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + g_return_val_if_fail (ref != NULL, NULL); + + g_mutex_lock (&self->installed_refs_mutex); + + if (self->installed_refs == NULL) { + self->installed_refs = flatpak_installation_list_installed_refs (installation, + cancellable, error); + + if (self->installed_refs == NULL) { + g_mutex_unlock (&self->installed_refs_mutex); + gs_flatpak_error_convert (error); + return NULL; + } + } + + for (guint i = 0; i < self->installed_refs->len; i++) { + g_autoptr(FlatpakInstalledRef) xref = g_object_ref (g_ptr_array_index (self->installed_refs, i)); + g_autofree gchar *ref_tmp = flatpak_ref_format_ref (FLATPAK_REF (xref)); + if (g_strcmp0 (ref, ref_tmp) == 0) { + g_mutex_unlock (&self->installed_refs_mutex); + return gs_flatpak_create_installed (self, xref, NULL, interactive, cancellable); + } + } + + g_mutex_unlock (&self->installed_refs_mutex); + + /* look at each remote xref */ + xremotes = flatpak_installation_list_remotes (installation, + cancellable, error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + for (guint i = 0; i < xremotes->len; i++) { + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) refs_remote = NULL; + + /* disabled */ + if (flatpak_remote_get_disabled (xremote)) + continue; + refs_remote = flatpak_installation_list_remote_refs_sync (installation, + flatpak_remote_get_name (xremote), + cancellable, + &error_local); + if (refs_remote == NULL) { + g_debug ("failed to list refs in '%s': %s", + flatpak_remote_get_name (xremote), + error_local->message); + continue; + } + for (guint j = 0; j < refs_remote->len; j++) { + FlatpakRef *xref = g_ptr_array_index (refs_remote, j); + g_autofree gchar *ref_tmp = flatpak_ref_format_ref (xref); + if (g_strcmp0 (ref, ref_tmp) == 0) { + const gchar *origin = flatpak_remote_get_name (xremote); + return gs_flatpak_create_app (self, origin, xref, xremote, interactive, cancellable); + } + } + } + + /* nothing found */ + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "cannot find %s", ref); + return NULL; +} + +/* This is essentially the inverse of gs_flatpak_app_new_from_repo_file() */ +static void +gs_flatpak_update_remote_from_app (GsFlatpak *self, + FlatpakRemote *xremote, + GsApp *app) +{ + const gchar *gpg_key; + const gchar *branch; + const gchar *title, *homepage, *comment, *description; + const gchar *filter; + GPtrArray *icons; + + flatpak_remote_set_disabled (xremote, FALSE); + + flatpak_remote_set_url (xremote, gs_flatpak_app_get_repo_url (app)); + flatpak_remote_set_noenumerate (xremote, FALSE); + + title = gs_app_get_name (app); + if (title != NULL) + flatpak_remote_set_title (xremote, title); + + /* decode GPG key if set */ + gpg_key = gs_flatpak_app_get_repo_gpgkey (app); + if (gpg_key != NULL) { + gsize data_len = 0; + g_autofree guchar *data = NULL; + g_autoptr(GBytes) bytes = NULL; + data = g_base64_decode (gpg_key, &data_len); + bytes = g_bytes_new (data, data_len); + flatpak_remote_set_gpg_verify (xremote, TRUE); + flatpak_remote_set_gpg_key (xremote, bytes); + } else { + flatpak_remote_set_gpg_verify (xremote, FALSE); + } + + /* default branch */ + branch = gs_app_get_branch (app); + if (branch != NULL) + flatpak_remote_set_default_branch (xremote, branch); + + /* optional data */ + homepage = gs_app_get_url (app, AS_URL_KIND_HOMEPAGE); + if (homepage != NULL) + flatpak_remote_set_homepage (xremote, homepage); + + comment = gs_app_get_summary (app); + if (comment != NULL) + flatpak_remote_set_comment (xremote, comment); + + description = gs_app_get_description (app); + if (description != NULL) + flatpak_remote_set_description (xremote, description); + + icons = gs_app_get_icons (app); + for (guint i = 0; icons != NULL && i < icons->len; i++) { + GIcon *icon = g_ptr_array_index (icons, i); + + if (GS_IS_REMOTE_ICON (icon)) { + flatpak_remote_set_icon (xremote, + gs_remote_icon_get_uri (GS_REMOTE_ICON (icon))); + break; + } + } + + /* With the other fields, we always want to add as much information as + * we can to the @xremote. With the filter, though, we want to drop it + * if no filter is set on the @app. Importing an updated flatpakrepo + * file is one of the methods for switching from (for example) filtered + * flathub to unfiltered flathub. So if @app doesn’t have a filter set, + * clear it on the @xremote (i.e. don’t check for NULL). */ + filter = gs_flatpak_app_get_repo_filter (app); + flatpak_remote_set_filter (xremote, filter); +} + +static FlatpakRemote * +gs_flatpak_create_new_remote (GsFlatpak *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRemote) xremote = NULL; + + /* create a new remote */ + xremote = flatpak_remote_new (gs_app_get_id (app)); + gs_flatpak_update_remote_from_app (self, xremote, app); + + return g_steal_pointer (&xremote); +} + +/* @is_install is %TRUE if the repo is being installed, or %FALSE if it’s being + * enabled. If it’s being enabled, no properties apart from enabled/disabled + * should be modified. */ +gboolean +gs_flatpak_app_install_source (GsFlatpak *self, + GsApp *app, + gboolean is_install, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRemote) xremote = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + xremote = flatpak_installation_get_remote_by_name (installation, + gs_app_get_id (app), + cancellable, NULL); + if (xremote != NULL) { + /* if the remote already exists, just enable it and update it */ + g_debug ("modifying existing remote %s", flatpak_remote_get_name (xremote)); + flatpak_remote_set_disabled (xremote, FALSE); + + if (is_install && + gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REPO) { + gs_flatpak_update_remote_from_app (self, xremote, app); + } + } else if (!is_install) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, "Cannot enable flatpak remote '%s', remote not found", gs_app_get_id (app)); + } else { + /* create a new remote */ + xremote = gs_flatpak_create_new_remote (self, app, cancellable, error); + } + + /* install it */ + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + if (!flatpak_installation_modify_remote (installation, + xremote, + cancellable, + error)) { + gs_flatpak_error_convert (error); + g_prefix_error (error, "cannot modify remote: "); + gs_app_set_state_recover (app); + gs_flatpak_internal_data_changed (self); + return FALSE; + } + + /* Mark the internal cache as obsolete. */ + gs_flatpak_internal_data_changed (self); + + /* success */ + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + gs_plugin_repository_changed (self->plugin, app); + + return TRUE; +} + +static GsApp * +get_main_app_of_related (GsFlatpak *self, + GsApp *related_app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakInstalledRef) ref = NULL; + const gchar *ref_name; + g_auto(GStrv) app_tokens = NULL; + FlatpakRefKind ref_kind = FLATPAK_REF_KIND_RUNTIME; + + ref_name = gs_flatpak_app_get_main_app_ref_name (related_app); + if (ref_name == NULL) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "%s doesn't have a main app set to it.", + gs_app_get_unique_id (related_app)); + return NULL; + } + + app_tokens = g_strsplit (ref_name, "/", -1); + if (g_strv_length (app_tokens) != 4) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "The main app of %s has an invalid name: %s", + gs_app_get_unique_id (related_app), ref_name); + return NULL; + } + + /* get the right ref kind for the main app */ + if (g_strcmp0 (app_tokens[0], "app") == 0) + ref_kind = FLATPAK_REF_KIND_APP; + + /* this function only returns G_IO_ERROR_NOT_FOUND when the metadata file + * is missing, but if that's the case then things should have broken before + * this point */ + ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive), + ref_kind, + app_tokens[1], + app_tokens[2], + app_tokens[3], + cancellable, + error); + if (ref == NULL) + return NULL; + + return gs_flatpak_create_installed (self, ref, NULL, interactive, cancellable); +} + +static GsApp * +get_real_app_for_update (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + GsApp *main_app = NULL; + g_autoptr(GError) error_local = NULL; + + if (gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_RUNTIME) + main_app = get_main_app_of_related (self, app, interactive, cancellable, &error_local); + + if (main_app == NULL) { + /* not all runtimes are extensions, and in that case we get the + * not-found error, so we only report other types of errors */ + if (error_local != NULL && + !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_flatpak_error_convert (error); + return NULL; + } + + main_app = g_object_ref (app); + } else { + g_debug ("Related extension app %s of main app %s is updatable, so " + "setting the latter's state instead.", gs_app_get_unique_id (app), + gs_app_get_unique_id (main_app)); + gs_app_set_state (main_app, GS_APP_STATE_UPDATABLE_LIVE); + /* Make sure the 'app' is not forgotten, it'll be added into the transaction later */ + gs_app_add_related (main_app, app); + } + + return main_app; +} + +gboolean +gs_flatpak_add_updates (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) xrefs = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + /* ensure valid */ + if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error)) + return FALSE; + + /* get all the updatable apps and runtimes */ + xrefs = flatpak_installation_list_installed_refs_for_update (installation, + cancellable, + error); + if (xrefs == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + gs_flatpak_ensure_remote_title (self, interactive, cancellable); + + /* look at each installed xref */ + for (guint i = 0; i < xrefs->len; i++) { + FlatpakInstalledRef *xref = g_ptr_array_index (xrefs, i); + const gchar *commit; + const gchar *latest_commit; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GsApp) main_app = NULL; + + /* check the application has already been downloaded */ + commit = flatpak_ref_get_commit (FLATPAK_REF (xref)); + latest_commit = flatpak_installed_ref_get_latest_commit (xref); + app = gs_flatpak_create_installed (self, xref, NULL, interactive, cancellable); + main_app = get_real_app_for_update (self, app, interactive, cancellable, &error_local); + if (main_app == NULL) { + g_debug ("Couldn't get the main app for updatable app extension %s: " + "%s; adding the app itself to the updates list...", + gs_app_get_unique_id (app), error_local->message); + g_clear_error (&error_local); + main_app = g_object_ref (app); + } + + /* if for some reason the app is already getting updated, then + * don't change its state */ + if (gs_app_get_state (main_app) != GS_APP_STATE_INSTALLING) + gs_app_set_state (main_app, GS_APP_STATE_UPDATABLE_LIVE); + + /* set updatable state on the extension too, as it will have + * its state updated to installing then installed later on */ + if (gs_app_get_state (app) != GS_APP_STATE_INSTALLING) + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + + /* already downloaded */ + if (latest_commit && g_strcmp0 (commit, latest_commit) != 0) { + g_debug ("%s has a downloaded update %s->%s", + flatpak_ref_get_name (FLATPAK_REF (xref)), + commit, latest_commit); + gs_app_set_update_details_markup (main_app, NULL); + gs_app_set_update_version (main_app, NULL); + gs_app_set_update_urgency (main_app, AS_URGENCY_KIND_UNKNOWN); + gs_app_set_size_download (main_app, GS_SIZE_TYPE_VALID, 0); + + /* needs download */ + } else { + guint64 download_size = 0; + g_debug ("%s needs update", + flatpak_ref_get_name (FLATPAK_REF (xref))); + + /* get the current download size */ + if (gs_app_get_size_download (main_app, NULL) != GS_SIZE_TYPE_VALID) { + if (!flatpak_installation_fetch_remote_size_sync (installation, + gs_app_get_origin (app), + FLATPAK_REF (xref), + &download_size, + NULL, + cancellable, + &error_local)) { + g_warning ("failed to get download size: %s", + error_local->message); + g_clear_error (&error_local); + gs_app_set_size_download (main_app, GS_SIZE_TYPE_UNKNOWABLE, 0); + } else { + gs_app_set_size_download (main_app, GS_SIZE_TYPE_VALID, download_size); + } + } + } + gs_flatpak_set_update_permissions (self, main_app, xref, interactive, cancellable); + gs_app_list_add (list, main_app); + } + + /* success */ + return TRUE; +} + +gboolean +gs_flatpak_refresh (GsFlatpak *self, + guint64 cache_age_secs, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + /* give all the repos a second chance */ + g_mutex_lock (&self->broken_remotes_mutex); + g_hash_table_remove_all (self->broken_remotes); + g_mutex_unlock (&self->broken_remotes_mutex); + + /* manually drop the cache in both installation instances; + * it's needed to have them both agree on the content. */ + if (!flatpak_installation_drop_caches (gs_flatpak_get_installation (self, FALSE), + cancellable, + error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + if (!flatpak_installation_drop_caches (gs_flatpak_get_installation (self, TRUE), + cancellable, + error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* drop the installed refs cache */ + g_mutex_lock (&self->installed_refs_mutex); + g_clear_pointer (&self->installed_refs, g_ptr_array_unref); + g_mutex_unlock (&self->installed_refs_mutex); + + /* manually do this in case we created the first appstream file */ + gs_flatpak_invalidate_silo (self); + + /* update AppStream metadata */ + if (!gs_flatpak_refresh_appstream (self, cache_age_secs, interactive, cancellable, error)) + return FALSE; + + /* success */ + return TRUE; +} + +static gboolean +gs_plugin_refine_item_origin_hostname (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRemote) xremote = NULL; + g_autofree gchar *url = NULL; + g_autoptr(GError) error_local = NULL; + + /* already set */ + if (gs_app_get_origin_hostname (app) != NULL) + return TRUE; + + /* no origin */ + if (gs_app_get_origin (app) == NULL) + return TRUE; + + /* get the remote */ + xremote = flatpak_installation_get_remote_by_name (gs_flatpak_get_installation (self, interactive), + gs_app_get_origin (app), + cancellable, + &error_local); + if (xremote == NULL) { + if (g_error_matches (error_local, + FLATPAK_ERROR, + FLATPAK_ERROR_REMOTE_NOT_FOUND)) { + /* if the user deletes the -origin remote for a locally + * installed flatpakref file then we should just show + * 'localhost' and not return an error */ + gs_app_set_origin_hostname (app, ""); + return TRUE; + } + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_flatpak_error_convert (error); + return FALSE; + } + url = flatpak_remote_get_url (xremote); + if (url == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no URL for remote %s", + flatpak_remote_get_name (xremote)); + return FALSE; + } + gs_app_set_origin_hostname (app, url); + return TRUE; +} + +static gboolean +gs_refine_item_metadata (GsFlatpak *self, + GsApp *app, + GError **error) +{ + g_autoptr(FlatpakRef) xref = NULL; + + /* already set */ + if (gs_flatpak_app_get_ref_name (app) != NULL) + return TRUE; + + /* not a valid type */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) + return TRUE; + + /* AppStream sets the source to appname/arch/branch, if this isn't set + * we can't break out the fields */ + if (gs_app_get_source_default (app) == NULL) { + g_autofree gchar *tmp = gs_app_to_string (app); + g_warning ("no source set by appstream for %s: %s", + gs_plugin_get_name (self->plugin), tmp); + return TRUE; + } + + /* parse the ref */ + xref = flatpak_ref_parse (gs_app_get_source_default (app), error); + if (xref == NULL) { + gs_flatpak_error_convert (error); + g_prefix_error (error, "failed to parse '%s': ", + gs_app_get_source_default (app)); + return FALSE; + } + gs_flatpak_set_metadata (self, app, xref); + + /* success */ + return TRUE; +} + +static gboolean +gs_plugin_refine_item_origin (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *ref_display = NULL; + g_autoptr(GPtrArray) xremotes = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + /* already set */ + if (gs_app_get_origin (app) != NULL) + return TRUE; + + /* not applicable */ + if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE_LOCAL) + return TRUE; + + /* ensure metadata exists */ + if (!gs_refine_item_metadata (self, app, error)) + return FALSE; + + /* find list of remotes */ + ref_display = gs_flatpak_app_get_ref_display (app); + g_debug ("looking for a remote for %s", ref_display); + xremotes = flatpak_installation_list_remotes (installation, + cancellable, error); + if (xremotes == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < xremotes->len; i++) { + const gchar *remote_name; + FlatpakRemote *xremote = g_ptr_array_index (xremotes, i); + g_autoptr(FlatpakRemoteRef) xref = NULL; + g_autoptr(GError) error_local = NULL; + + /* not enabled */ + if (flatpak_remote_get_disabled (xremote)) + continue; + + /* sync */ + remote_name = flatpak_remote_get_name (xremote); + g_debug ("looking at remote %s", remote_name); + xref = flatpak_installation_fetch_remote_ref_sync (installation, + remote_name, + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + cancellable, + &error_local); + if (xref != NULL) { + g_debug ("found remote %s", remote_name); + gs_flatpak_set_app_origin (self, app, remote_name, xremote, interactive, cancellable); + gs_flatpak_app_set_commit (app, flatpak_ref_get_commit (FLATPAK_REF (xref))); + gs_plugin_refine_item_scope (self, app); + return TRUE; + } + g_debug ("%s failed to find remote %s: %s", + ref_display, remote_name, error_local->message); + } + + /* not found */ + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "%s not found in any remote", + ref_display); + return FALSE; +} + +static FlatpakRef * +gs_flatpak_create_fake_ref (GsApp *app, GError **error) +{ + FlatpakRef *xref; + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s/%s/%s/%s", + gs_flatpak_app_get_ref_kind_as_str (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app)); + xref = flatpak_ref_parse (id, error); + if (xref == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + return xref; +} + +/* the _unlocked() version doesn't call gs_flatpak_rescan_app_data, + * in order to avoid taking the writer lock on self->silo_lock */ +static gboolean +gs_flatpak_refine_app_state_unlocked (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakInstalledRef) ref = NULL; + g_autoptr(GPtrArray) installed_refs = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + /* already found */ + if (gs_app_get_state (app) != GS_APP_STATE_UNKNOWN) + return TRUE; + + /* need broken out metadata */ + if (!gs_refine_item_metadata (self, app, error)) + return FALSE; + + /* ensure origin set */ + if (!gs_plugin_refine_item_origin (self, app, interactive, cancellable, error)) + return FALSE; + + /* find the app using the origin and the ID */ + g_mutex_lock (&self->installed_refs_mutex); + + if (self->installed_refs == NULL) { + self->installed_refs = flatpak_installation_list_installed_refs (installation, + cancellable, error); + + if (self->installed_refs == NULL) { + g_mutex_unlock (&self->installed_refs_mutex); + gs_flatpak_error_convert (error); + return FALSE; + } + } + + installed_refs = g_ptr_array_ref (self->installed_refs); + + for (guint i = 0; i < installed_refs->len; i++) { + FlatpakInstalledRef *ref_tmp = g_ptr_array_index (installed_refs, i); + const gchar *origin = flatpak_installed_ref_get_origin (ref_tmp); + const gchar *name = flatpak_ref_get_name (FLATPAK_REF (ref_tmp)); + const gchar *arch = flatpak_ref_get_arch (FLATPAK_REF (ref_tmp)); + const gchar *branch = flatpak_ref_get_branch (FLATPAK_REF (ref_tmp)); + if (g_strcmp0 (origin, gs_app_get_origin (app)) == 0 && + g_strcmp0 (name, gs_flatpak_app_get_ref_name (app)) == 0 && + g_strcmp0 (arch, gs_flatpak_app_get_ref_arch (app)) == 0 && + g_strcmp0 (branch, gs_app_get_branch (app)) == 0) { + ref = g_object_ref (ref_tmp); + break; + } + } + g_mutex_unlock (&self->installed_refs_mutex); + if (ref != NULL) { + g_debug ("marking %s as installed with flatpak", + gs_app_get_unique_id (app)); + gs_flatpak_set_metadata_installed (self, app, ref, interactive, cancellable); + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + /* flatpak only allows one installed app to be launchable */ + if (flatpak_installed_ref_get_is_current (ref)) { + gs_app_remove_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } else { + g_debug ("%s is not current, and therefore not launchable", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } + return TRUE; + } + + /* anything not installed just check the remote is still present */ + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN && + gs_app_get_origin (app) != NULL) { + g_autoptr(FlatpakRemote) xremote = NULL; + xremote = flatpak_installation_get_remote_by_name (installation, + gs_app_get_origin (app), + cancellable, NULL); + if (xremote != NULL) { + if (flatpak_remote_get_disabled (xremote)) { + g_debug ("%s is available with flatpak " + "but %s is disabled", + gs_app_get_unique_id (app), + flatpak_remote_get_name (xremote)); + gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE); + } else { + g_debug ("marking %s as available with flatpak", + gs_app_get_unique_id (app)); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + } + } else { + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + g_debug ("failed to find %s remote %s for %s", + self->id, + gs_app_get_origin (app), + gs_app_get_unique_id (app)); + } + } + + /* success */ + return TRUE; +} + +gboolean +gs_flatpak_refine_app_state (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + /* ensure valid */ + if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error)) + return FALSE; + + return gs_flatpak_refine_app_state_unlocked (self, app, interactive, cancellable, error); +} + +static GsApp * +gs_flatpak_create_runtime (GsFlatpak *self, + GsApp *parent, + const gchar *runtime, + gboolean interactive, + GCancellable *cancellable) +{ + g_autofree gchar *source = NULL; + g_auto(GStrv) split = NULL; + g_autoptr(GsApp) app_cache = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) local_error = NULL; + const gchar *origin; + + /* get the name/arch/branch */ + split = g_strsplit (runtime, "/", -1); + if (g_strv_length (split) != 3) + return NULL; + + /* create the complete GsApp from the single string */ + app = gs_app_new (split[0]); + gs_flatpak_claim_app (self, app); + source = g_strdup_printf ("runtime/%s", runtime); + gs_app_add_source (app, source); + gs_app_set_kind (app, AS_COMPONENT_KIND_RUNTIME); + gs_app_set_branch (app, split[2]); + + origin = gs_app_get_origin (parent); + if (origin != NULL) { + g_autoptr(FlatpakRemoteRef) xref = NULL; + + xref = flatpak_installation_fetch_remote_ref_sync (gs_flatpak_get_installation (self, interactive), + origin, + FLATPAK_REF_KIND_RUNTIME, + gs_app_get_id (app), + gs_flatpak_app_get_ref_arch (parent), + gs_app_get_branch (app), + cancellable, + NULL); + + /* Prefer runtime from the same origin as the parent application */ + if (xref) + gs_app_set_origin (app, origin); + } + + /* search in the cache */ + app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cache != NULL && + g_strcmp0 (gs_flatpak_app_get_ref_name (app_cache), split[0]) == 0 && + g_strcmp0 (gs_flatpak_app_get_ref_arch (app_cache), split[1]) == 0 && + g_strcmp0 (gs_app_get_branch (app_cache), split[2]) == 0) { + /* since the cached runtime can have been created somewhere else + * (we're using a global cache), we need to make sure that a + * source is set */ + if (gs_app_get_source_default (app_cache) == NULL) + gs_app_add_source (app_cache, source); + return g_steal_pointer (&app_cache); + } else { + g_clear_object (&app_cache); + } + + /* if the app is per-user we can also use the installed system runtime */ + if (gs_app_get_scope (parent) == AS_COMPONENT_SCOPE_USER) { + gs_app_set_scope (app, AS_COMPONENT_SCOPE_UNKNOWN); + app_cache = gs_plugin_cache_lookup (self->plugin, gs_app_get_unique_id (app)); + if (app_cache != NULL && + g_strcmp0 (gs_flatpak_app_get_ref_name (app_cache), split[0]) == 0 && + g_strcmp0 (gs_flatpak_app_get_ref_arch (app_cache), split[1]) == 0 && + g_strcmp0 (gs_app_get_branch (app_cache), split[2]) == 0) { + return g_steal_pointer (&app_cache); + } else { + g_clear_object (&app_cache); + } + } + + /* set superclassed app properties */ + gs_flatpak_app_set_ref_kind (app, FLATPAK_REF_KIND_RUNTIME); + gs_flatpak_app_set_ref_name (app, split[0]); + gs_flatpak_app_set_ref_arch (app, split[1]); + + if (!gs_flatpak_refine_app_state_unlocked (self, app, interactive, NULL, &local_error)) + g_debug ("Failed to refine state for runtime '%s': %s", gs_app_get_unique_id (app), local_error->message); + + /* save in the cache */ + gs_plugin_cache_add (self->plugin, NULL, app); + return g_steal_pointer (&app); +} + +static gboolean +gs_flatpak_set_app_metadata (GsFlatpak *self, + GsApp *app, + const gchar *data, + gsize length, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + gboolean secure = TRUE; + g_autofree gchar *name = NULL; + g_autofree gchar *runtime = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app_runtime = NULL; + g_autoptr(GsAppPermissions) permissions = NULL; + g_auto(GStrv) shared = NULL; + g_auto(GStrv) sockets = NULL; + g_auto(GStrv) filesystems = NULL; + + kf = g_key_file_new (); + if (!g_key_file_load_from_data (kf, data, length, G_KEY_FILE_NONE, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + name = g_key_file_get_string (kf, "Application", "name", error); + if (name == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + gs_flatpak_app_set_ref_name (app, name); + runtime = g_key_file_get_string (kf, "Application", "runtime", error); + if (runtime == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + shared = g_key_file_get_string_list (kf, "Context", "shared", NULL, NULL); + if (shared != NULL) { + /* SHM isn't secure enough */ + if (g_strv_contains ((const gchar * const *) shared, "ipc")) + secure = FALSE; + } + sockets = g_key_file_get_string_list (kf, "Context", "sockets", NULL, NULL); + if (sockets != NULL) { + /* X11 isn't secure enough */ + if (g_strv_contains ((const gchar * const *) sockets, "x11")) + secure = FALSE; + } + filesystems = g_key_file_get_string_list (kf, "Context", "filesystems", NULL, NULL); + if (filesystems != NULL) { + /* secure apps should be using portals */ + if (g_strv_contains ((const gchar * const *) filesystems, "home")) + secure = FALSE; + } + + permissions = perms_from_metadata (kf); + gs_app_set_permissions (app, permissions); + /* this is actually quite hard to achieve */ + if (secure) + gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED_SECURE); + + /* create runtime */ + app_runtime = gs_flatpak_create_runtime (self, app, runtime, interactive, cancellable); + if (app_runtime != NULL) { + gs_plugin_refine_item_scope (self, app_runtime); + gs_app_set_runtime (app, app_runtime); + } + + /* we always get this, but it's a low bar... */ + gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED); + + return TRUE; +} + +static GBytes * +gs_flatpak_fetch_remote_metadata (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GBytes) data = NULL; + g_autoptr(FlatpakRef) xref = NULL; + g_autoptr(GError) local_error = NULL; + + /* no origin */ + if (gs_app_get_origin (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no origin set when getting metadata for %s", + gs_app_get_unique_id (app)); + return NULL; + } + + /* fetch from the server */ + xref = gs_flatpak_create_fake_ref (app, error); + if (xref == NULL) + return NULL; + data = flatpak_installation_fetch_remote_metadata_sync (gs_flatpak_get_installation (self, interactive), + gs_app_get_origin (app), + xref, + cancellable, + &local_error); + if (data == NULL) { + if (g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_REF_NOT_FOUND) && + !gs_plugin_get_network_available (self->plugin)) { + local_error->code = GS_PLUGIN_ERROR_NO_NETWORK; + local_error->domain = GS_PLUGIN_ERROR; + } else { + gs_flatpak_error_convert (&local_error); + } + g_propagate_error (error, g_steal_pointer (&local_error)); + return NULL; + } + return g_steal_pointer (&data); +} + +static gboolean +gs_plugin_refine_item_metadata (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + const gchar *str; + gsize len = 0; + g_autofree gchar *contents = NULL; + g_autofree gchar *installation_path_str = NULL; + g_autofree gchar *install_path = NULL; + g_autoptr(GBytes) data = NULL; + g_autoptr(GFile) installation_path = NULL; + + /* not applicable */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) + return TRUE; + if (gs_flatpak_app_get_ref_kind (app) != FLATPAK_REF_KIND_APP) + return TRUE; + + /* already done */ + if (gs_app_has_kudo (app, GS_APP_KUDO_SANDBOXED)) + return TRUE; + + /* this is quicker than doing network IO */ + installation_path = flatpak_installation_get_path (self->installation_noninteractive); + installation_path_str = g_file_get_path (installation_path); + install_path = g_build_filename (installation_path_str, + gs_flatpak_app_get_ref_kind_as_str (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + "active", + "metadata", + NULL); + if (g_file_test (install_path, G_FILE_TEST_EXISTS)) { + if (!g_file_get_contents (install_path, &contents, &len, error)) + return FALSE; + str = contents; + } else { + data = gs_flatpak_fetch_remote_metadata (self, app, interactive, + cancellable, + error); + if (data == NULL) + return FALSE; + str = g_bytes_get_data (data, &len); + } + + /* parse key file */ + if (!gs_flatpak_set_app_metadata (self, app, str, len, interactive, cancellable, error)) + return FALSE; + return TRUE; +} + +static FlatpakInstalledRef * +gs_flatpak_get_installed_ref (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + FlatpakInstalledRef *ref; + ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive), + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + cancellable, + error); + if (ref == NULL) + gs_flatpak_error_convert (error); + return ref; +} + +static gboolean +gs_flatpak_prune_addons_list (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) addons_list = NULL; + g_autoptr(GPtrArray) installed_related_refs = NULL; + g_autoptr(GPtrArray) remote_related_refs = NULL; + g_autofree gchar *ref = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + g_autoptr(GError) error_local = NULL; + + addons_list = gs_app_dup_addons (app); + if (addons_list == NULL || gs_app_list_length (addons_list) == 0) + return TRUE; + + if (gs_app_get_origin (app) == NULL) + return TRUE; + + /* return early if the addons haven't been refined */ + for (guint i = 0; i < gs_app_list_length (addons_list); i++) { + GsApp *app_addon = gs_app_list_index (addons_list, i); + + if (gs_flatpak_app_get_ref_name (app_addon) == NULL || + gs_flatpak_app_get_ref_arch (app_addon) == NULL || + gs_app_get_branch (app_addon) == NULL) + return TRUE; + } + + /* return early if the API we need isn't available */ +#if !FLATPAK_CHECK_VERSION(1,11,1) + if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED) + return TRUE; +#endif + + ref = g_strdup_printf ("%s/%s/%s/%s", + gs_flatpak_app_get_ref_kind_as_str (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app)); + + /* Find installed related refs in case the app is installed */ + installed_related_refs = flatpak_installation_list_installed_related_refs_sync (installation, + gs_app_get_origin (app), + ref, + cancellable, + &error_local); + if (installed_related_refs == NULL && + !g_error_matches (error_local, + FLATPAK_ERROR, + FLATPAK_ERROR_NOT_INSTALLED)) { + gs_flatpak_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + g_clear_error (&error_local); + +#if FLATPAK_CHECK_VERSION(1,11,1) + /* Find remote related refs that match the installed version in case the app is installed */ + remote_related_refs = flatpak_installation_list_remote_related_refs_for_installed_sync (installation, + gs_app_get_origin (app), + ref, + cancellable, + &error_local); + if (remote_related_refs == NULL && + !g_error_matches (error_local, + FLATPAK_ERROR, + FLATPAK_ERROR_NOT_INSTALLED)) { + gs_flatpak_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + g_clear_error (&error_local); +#endif + + /* Find remote related refs in case the app is not installed */ + if (remote_related_refs == NULL) { + remote_related_refs = flatpak_installation_list_remote_related_refs_sync (installation, + gs_app_get_origin (app), + ref, + cancellable, + &error_local); + /* don't make the error fatal in case we're offline */ + if (error_local != NULL) + g_debug ("failed to list remote related refs of %s: %s", + gs_app_get_unique_id (app), error_local->message); + } + + g_clear_error (&error_local); + + /* For each addon, if it is neither installed nor available, hide it + * since it may be intended for a different version of the app. We + * don't want to show both org.videolan.VLC.Plugin.bdj//3-19.08 and + * org.videolan.VLC.Plugin.bdj//3-20.08 in the UI; only one will work + * for the installed app + */ + for (guint i = 0; i < gs_app_list_length (addons_list); i++) { + GsApp *app_addon = gs_app_list_index (addons_list, i); + gboolean found = FALSE; + g_autofree char *addon_ref = NULL; + + addon_ref = g_strdup_printf ("%s/%s/%s/%s", + gs_flatpak_app_get_ref_kind_as_str (app_addon), + gs_flatpak_app_get_ref_name (app_addon), + gs_flatpak_app_get_ref_arch (app_addon), + gs_app_get_branch (app_addon)); + for (guint j = 0; installed_related_refs && j < installed_related_refs->len; j++) { + FlatpakRelatedRef *rel = g_ptr_array_index (installed_related_refs, j); + g_autofree char *rel_ref = flatpak_ref_format_ref (FLATPAK_REF (rel)); + if (g_strcmp0 (addon_ref, rel_ref) == 0) + found = TRUE; + } + for (guint j = 0; remote_related_refs && j < remote_related_refs->len; j++) { + FlatpakRelatedRef *rel = g_ptr_array_index (remote_related_refs, j); + g_autofree char *rel_ref = flatpak_ref_format_ref (FLATPAK_REF (rel)); + if (g_strcmp0 (addon_ref, rel_ref) == 0) + found = TRUE; + } + + if (!found) { + gs_app_add_quirk (app_addon, GS_APP_QUIRK_HIDE_EVERYWHERE); + g_debug ("hiding %s since it's not related to %s", + addon_ref, gs_app_get_unique_id (app)); + } else { + gs_app_remove_quirk (app_addon, GS_APP_QUIRK_HIDE_EVERYWHERE); + g_debug ("unhiding %s since it's related to %s", + addon_ref, gs_app_get_unique_id (app)); + } + } + return TRUE; +} + +static guint64 +gs_flatpak_get_app_directory_size (GsApp *app, + const gchar *subdir_name, + GCancellable *cancellable) +{ + g_autofree gchar *filename = NULL; + filename = g_build_filename (g_get_home_dir (), ".var", "app", gs_app_get_id (app), subdir_name, NULL); + return gs_utils_get_file_size (filename, NULL, NULL, cancellable); +} + +static gboolean +gs_plugin_refine_item_size (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + gboolean ret; + guint64 download_size = 0; + guint64 installed_size = 0; + GsSizeType size_type = GS_SIZE_TYPE_UNKNOWABLE; + + /* not applicable */ + if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE_LOCAL) + return TRUE; + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) + return TRUE; + + /* already set */ + if (gs_app_is_installed (app)) { + /* only care about the installed size if the app is installed */ + if (gs_app_get_size_installed (app, NULL) == GS_SIZE_TYPE_VALID) + return TRUE; + } else { + if (gs_app_get_size_installed (app, NULL) == GS_SIZE_TYPE_VALID && + gs_app_get_size_download (app, NULL) == GS_SIZE_TYPE_VALID) + return TRUE; + } + + /* need runtime */ + if (!gs_plugin_refine_item_metadata (self, app, interactive, cancellable, error)) + return FALSE; + + /* calculate the platform size too if the app is not installed */ + if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE && + gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_APP) { + GsApp *app_runtime; + + /* is the app_runtime already installed? */ + app_runtime = gs_app_get_runtime (app); + if (!gs_flatpak_refine_app_state_unlocked (self, + app_runtime, + interactive, + cancellable, + error)) + return FALSE; + if (gs_app_get_state (app_runtime) == GS_APP_STATE_INSTALLED) { + g_debug ("runtime %s is already installed, so not adding size", + gs_app_get_unique_id (app_runtime)); + } else { + if (!gs_plugin_refine_item_size (self, + app_runtime, + interactive, + cancellable, + error)) + return FALSE; + } + } + + /* just get the size of the app */ + if (!gs_plugin_refine_item_origin (self, app, interactive, + cancellable, error)) + return FALSE; + + /* if the app is installed we use the ref to fetch the installed size + * and ignore the download size as this is faster */ + if (gs_app_is_installed (app)) { + g_autoptr(FlatpakInstalledRef) xref = NULL; + xref = gs_flatpak_get_installed_ref (self, app, interactive, cancellable, error); + if (xref == NULL) + return FALSE; + installed_size = flatpak_installed_ref_get_installed_size (xref); + size_type = (installed_size > 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWABLE; + } else { + g_autoptr(FlatpakRef) xref = NULL; + g_autoptr(GError) error_local = NULL; + + /* no origin */ + if (gs_app_get_origin (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no origin set for %s", + gs_app_get_unique_id (app)); + return FALSE; + } + xref = gs_flatpak_create_fake_ref (app, error); + if (xref == NULL) + return FALSE; + ret = flatpak_installation_fetch_remote_size_sync (gs_flatpak_get_installation (self, interactive), + gs_app_get_origin (app), + xref, + &download_size, + &installed_size, + cancellable, + &error_local); + + if (!ret) { + /* This can happen when the remote is filtered */ + g_debug ("libflatpak failed to return application size: %s", error_local->message); + g_clear_error (&error_local); + } else { + size_type = GS_SIZE_TYPE_VALID; + } + } + + gs_app_set_size_installed (app, size_type, installed_size); + gs_app_set_size_download (app, size_type, download_size); + + return TRUE; +} + +static void +gs_flatpak_refine_appstream_release (XbNode *component, GsApp *app) +{ + const gchar *version; + + /* get first release */ + version = xb_node_query_attr (component, "releases/release", "version", NULL); + if (version == NULL) + return; + switch (gs_app_get_state (app)) { + case GS_APP_STATE_INSTALLED: + case GS_APP_STATE_AVAILABLE: + case GS_APP_STATE_AVAILABLE_LOCAL: + gs_app_set_version (app, version); + break; + case GS_APP_STATE_UPDATABLE: + case GS_APP_STATE_UPDATABLE_LIVE: + gs_app_set_update_version (app, version); + break; + default: + g_debug ("%s is not installed, so ignoring version of %s", + gs_app_get_unique_id (app), version); + break; + } +} + +/* This function is like gs_flatpak_refine_appstream(), but takes gzip + * compressed appstream data as a GBytes and assumes they are already uniquely + * tied to the app (and therefore app ID alone can be used to find the right + * component). + */ +static gboolean +gs_flatpak_refine_appstream_from_bytes (GsFlatpak *self, + GsApp *app, + const char *origin, /* (nullable) */ + FlatpakInstalledRef *installed_ref, /* (nullable) */ + GBytes *appstream_gz, + GsPluginRefineFlags flags, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + const gchar *const *locales = g_get_language_names (); + g_autofree gchar *xpath = NULL; + g_autoptr(XbBuilder) builder = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbNode) component_node = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + g_autoptr(XbBuilderFixup) bundle_fixup = NULL; + g_autoptr(GBytes) appstream = NULL; + g_autoptr(GInputStream) stream_data = NULL; + g_autoptr(GInputStream) stream_gz = NULL; + g_autoptr(GZlibDecompressor) decompressor = NULL; + g_autoptr(GMainContext) old_thread_default = NULL; + + /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */ + old_thread_default = g_main_context_ref_thread_default (); + if (old_thread_default == g_main_context_default ()) + g_clear_pointer (&old_thread_default, g_main_context_unref); + if (old_thread_default != NULL) + g_main_context_pop_thread_default (old_thread_default); + builder = xb_builder_new (); + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + g_clear_pointer (&old_thread_default, g_main_context_unref); + + /* add current locales */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + /* decompress data */ + decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP); + stream_gz = g_memory_input_stream_new_from_bytes (appstream_gz); + if (stream_gz == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "unable to decompress appstream data"); + return FALSE; + } + stream_data = g_converter_input_stream_new (stream_gz, + G_CONVERTER (decompressor)); + + appstream = g_input_stream_read_bytes (stream_data, + 0x100000, /* 1Mb */ + cancellable, + error); + if (appstream == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* build silo */ + if (!xb_builder_source_load_bytes (source, appstream, + XB_BUILDER_SOURCE_FLAG_NONE, + error)) + return FALSE; + + /* Appdata from flatpak_installed_ref_load_appdata() may be missing the + * <bundle> tag but for this function we know it's the right component. + */ + bundle_fixup = xb_builder_fixup_new ("AddBundle", + gs_flatpak_add_bundle_tag_cb, + gs_flatpak_app_get_ref_display (app), g_free); + xb_builder_fixup_set_max_depth (bundle_fixup, 2); + xb_builder_source_add_fixup (source, bundle_fixup); + + fixup_flatpak_appstream_xml (source, origin); + + /* add metadata */ + if (installed_ref != NULL) { + g_autoptr(XbBuilderNode) info = NULL; + g_autofree char *icon_prefix = NULL; + + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", as_component_scope_to_string (self->scope), NULL); + icon_prefix = g_build_filename (flatpak_installed_ref_get_deploy_dir (installed_ref), + "files", "share", "app-info", "icons", "flatpak", NULL); + xb_builder_node_insert_text (info, "icon-prefix", icon_prefix, NULL); + xb_builder_source_set_info (source, info); + } + + xb_builder_import_source (builder, source); + + /* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */ + old_thread_default = g_main_context_ref_thread_default (); + if (old_thread_default == g_main_context_default ()) + g_clear_pointer (&old_thread_default, g_main_context_unref); + if (old_thread_default != NULL) + g_main_context_pop_thread_default (old_thread_default); + + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + cancellable, + error); + + if (old_thread_default != NULL) + g_main_context_push_thread_default (old_thread_default); + + if (silo == NULL) + return FALSE; + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + g_autofree gchar *xml = NULL; + xml = xb_silo_export (silo, + XB_NODE_EXPORT_FLAG_FORMAT_INDENT | + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE, + NULL); + g_debug ("showing AppStream data: %s", xml); + } + + /* check for sanity */ + n = xb_silo_query_first (silo, "components/component", NULL); + if (n == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no apps found in AppStream data"); + return FALSE; + } + + /* find app */ + xpath = g_strdup_printf ("components/component/id[text()='%s']/..", + gs_flatpak_app_get_ref_name (app)); + component_node = xb_silo_query_first (silo, xpath, NULL); + if (component_node == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "application %s not found", + gs_flatpak_app_get_ref_name (app)); + return FALSE; + } + + /* copy details from AppStream to app */ + if (!gs_appstream_refine_app (self->plugin, app, silo, component_node, flags, error)) + return FALSE; + + if (gs_app_get_origin (app)) + gs_flatpak_set_app_origin (self, app, gs_app_get_origin (app), NULL, interactive, cancellable); + + /* use the default release as the version number */ + gs_flatpak_refine_appstream_release (component_node, app); + + /* save the silo so it can be used for searches */ + { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->app_silos_mutex); + g_hash_table_replace (self->app_silos, + gs_flatpak_app_get_ref_display (app), + g_steal_pointer (&silo)); + } + + return TRUE; +} + +static XbNode * +get_renamed_component (GsFlatpak *self, + GsApp *app, + XbSilo *silo, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + const gchar *origin = gs_app_get_origin (app); + const gchar *renamed_to; +#if LIBXMLB_CHECK_VERSION(0, 3, 0) + g_autoptr(XbQuery) query = NULL; + g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT (); +#else + g_autofree gchar *xpath = NULL; + g_autofree gchar *source_safe = NULL; +#endif + g_autoptr(FlatpakRemoteRef) remote_ref = NULL; + g_autoptr(XbNode) component = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + remote_ref = flatpak_installation_fetch_remote_ref_sync (installation, + origin, + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + cancellable, error); + if (remote_ref == NULL) + return NULL; + + renamed_to = flatpak_remote_ref_get_eol_rebase (remote_ref); + if (renamed_to == NULL) + return NULL; + +#if LIBXMLB_CHECK_VERSION(0, 3, 0) + query = xb_silo_lookup_query (silo, "components[@origin=?]/component/bundle[@type='flatpak'][text()=?]/.."); + xb_value_bindings_bind_str (xb_query_context_get_bindings (&context), 0, origin, NULL); + xb_value_bindings_bind_str (xb_query_context_get_bindings (&context), 1, renamed_to, NULL); + component = xb_silo_query_first_with_context (silo, query, &context, NULL); +#else + source_safe = xb_string_escape (renamed_to); + xpath = g_strdup_printf ("components[@origin='%s']/component/bundle[@type='flatpak'][text()='%s']/..", + origin, source_safe); + component = xb_silo_query_first (silo, xpath, NULL); +#endif + + /* Get the previous name so it can be displayed in the UI */ + if (component != NULL) { + g_autoptr(FlatpakInstalledRef) installed_ref = NULL; + const gchar *installed_name = NULL; + + installed_ref = flatpak_installation_get_installed_ref (installation, + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + cancellable, error); + if (installed_ref != NULL) + installed_name = flatpak_installed_ref_get_appdata_name (installed_ref); + if (installed_name != NULL) + gs_app_set_renamed_from (app, installed_name); + } + + return g_steal_pointer (&component); +} + +/* Returns %TRUE if @error exists and is set to G_IO_ERROR_CANCELLED */ +static inline gboolean +propagate_cancelled_error (GError **dest, + GError **error) +{ + g_assert (error != NULL); + + if (*error && g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_propagate_error (dest, g_steal_pointer (error)); + return TRUE; + } + + return FALSE; +} + +static gboolean +gs_flatpak_refine_appstream (GsFlatpak *self, + GsApp *app, + XbSilo *silo, + GsPluginRefineFlags flags, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + const gchar *origin = gs_app_get_origin (app); + const gchar *source = gs_app_get_source_default (app); + g_autofree gchar *source_safe = NULL; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(XbNode) component = NULL; + + if (origin == NULL || source == NULL) + return TRUE; + + /* find using source and origin */ + source_safe = xb_string_escape (source); + xpath = g_strdup_printf ("components[@origin='%s']/component/bundle[@type='flatpak'][text()='%s']/..", + origin, source_safe); + component = xb_silo_query_first (silo, xpath, &error_local); + + if (propagate_cancelled_error (error, &error_local)) + return FALSE; + + /* Ensure the gs_flatpak_app_get_ref_*() metadata are set */ + gs_refine_item_metadata (self, app, NULL); + + /* If the app was renamed, use the appstream data from the new name; + * usually it will not exist under the old name */ + if (component == NULL && gs_flatpak_app_get_ref_kind (app) == FLATPAK_REF_KIND_APP) { + g_autoptr(GError) renamed_component_error = NULL; + + component = get_renamed_component (self, app, silo, + interactive, + cancellable, + &renamed_component_error); + + if (propagate_cancelled_error (error, &renamed_component_error)) + return FALSE; + } + + if (component == NULL) { + g_autoptr(FlatpakInstalledRef) installed_ref = NULL; + g_autoptr(GBytes) appstream_gz = NULL; + + g_debug ("no match for %s: %s", xpath, error_local->message); + + g_clear_error (&error_local); + + /* For apps installed from .flatpak bundles there may not be any remote + * appstream data in @silo for it, so use the appstream data from + * within the app. + */ + installed_ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive), + gs_flatpak_app_get_ref_kind (app), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + cancellable, + &error_local); + + if (installed_ref == NULL) + return !propagate_cancelled_error (error, &error_local); /* the app may not be installed */ + + appstream_gz = flatpak_installed_ref_load_appdata (installed_ref, + cancellable, + &error_local); + if (appstream_gz == NULL) + return !propagate_cancelled_error (error, &error_local); + + g_debug ("using installed appdata for %s", gs_flatpak_app_get_ref_name (app)); + return gs_flatpak_refine_appstream_from_bytes (self, + app, + flatpak_installed_ref_get_origin (installed_ref), + installed_ref, + appstream_gz, + flags, + interactive, + cancellable, error); + } + + if (!gs_appstream_refine_app (self->plugin, app, silo, component, flags, error)) + return FALSE; + + /* use the default release as the version number */ + gs_flatpak_refine_appstream_release (component, app); + return TRUE; +} + +static gboolean +gs_flatpak_refine_app_unlocked (GsFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + gboolean interactive, + GRWLockReaderLocker **locker, + GCancellable *cancellable, + GError **error) +{ + GsAppState old_state = gs_app_get_state (app); + g_autoptr(GError) local_error = NULL; + + /* not us */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK) + return TRUE; + + g_clear_pointer (locker, g_rw_lock_reader_locker_free); + + if (!ensure_flatpak_silo_with_locker (self, locker, interactive, cancellable, error)) + return FALSE; + + /* always do AppStream properties */ + if (!gs_flatpak_refine_appstream (self, app, self->silo, flags, interactive, cancellable, error)) + return FALSE; + + /* AppStream sets the source to appname/arch/branch */ + if (!gs_refine_item_metadata (self, app, error)) { + g_prefix_error (error, "failed to get metadata: "); + return FALSE; + } + + /* check the installed state */ + if (!gs_flatpak_refine_app_state_unlocked (self, app, interactive, cancellable, error)) { + g_prefix_error (error, "failed to get state: "); + return FALSE; + } + + /* hide any addons that aren't for this app */ + if (!gs_flatpak_prune_addons_list (self, app, interactive, cancellable, &local_error)) { + g_warning ("failed to prune addons: %s", local_error->message); + g_clear_error (&local_error); + } + + /* scope is fast, do unconditionally */ + if (gs_app_get_state (app) != GS_APP_STATE_AVAILABLE_LOCAL) + gs_plugin_refine_item_scope (self, app); + + /* if the state was changed, perhaps set the version from the release */ + if (old_state != gs_app_get_state (app)) { + if (!gs_flatpak_refine_appstream (self, app, self->silo, flags, interactive, cancellable, error)) + return FALSE; + } + + /* version fallback */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) { + if (gs_app_get_version (app) == NULL) { + const gchar *branch; + branch = gs_app_get_branch (app); + gs_app_set_version (app, branch); + } + } + + /* size */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) { + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_refine_item_size (self, app, interactive, + cancellable, &error_local)) { + if (g_error_matches (error_local, GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_NETWORK)) { + g_debug ("failed to get size while " + "refining app %s: %s", + gs_app_get_unique_id (app), + error_local->message); + } else { + g_prefix_error (&error_local, "failed to get size: "); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + } + + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE_DATA) != 0 && + gs_app_is_installed (app) && + gs_app_get_kind (app) != AS_COMPONENT_KIND_RUNTIME) { + if (gs_app_get_size_cache_data (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_cache_data (app, GS_SIZE_TYPE_VALID, + gs_flatpak_get_app_directory_size (app, "cache", cancellable)); + if (gs_app_get_size_user_data (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_user_data (app, GS_SIZE_TYPE_VALID, + gs_flatpak_get_app_directory_size (app, "config", cancellable) + + gs_flatpak_get_app_directory_size (app, "data", cancellable)); + + if (g_cancellable_is_cancelled (cancellable)) { + gs_app_set_size_cache_data (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + gs_app_set_size_user_data (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + } + } + + /* origin-hostname */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) { + if (!gs_plugin_refine_item_origin_hostname (self, app, interactive, + cancellable, + error)) { + g_prefix_error (error, "failed to get origin-hostname: "); + return FALSE; + } + } + + /* permissions */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME || + flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) { + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_refine_item_metadata (self, app, interactive, + cancellable, &error_local)) { + if (!gs_plugin_get_network_available (self->plugin) && + g_error_matches (error_local, GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_NETWORK)) { + g_debug ("failed to get permissions while " + "refining app %s: %s", + gs_app_get_unique_id (app), + error_local->message); + } else { + g_prefix_error (&error_local, "failed to read permissions from app '%s' metadata: ", gs_app_get_unique_id (app)); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + } + + if (gs_app_get_origin (app)) + gs_flatpak_set_app_origin (self, app, gs_app_get_origin (app), NULL, interactive, cancellable); + + return TRUE; +} + +void +gs_flatpak_refine_addons (GsFlatpak *self, + GsApp *parent_app, + GsPluginRefineFlags flags, + GsAppState state, + gboolean interactive, + GCancellable *cancellable) +{ + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GsAppList) addons = NULL; + g_autoptr(GString) errors = NULL; + guint ii, sz; + + addons = gs_app_dup_addons (parent_app); + sz = addons ? gs_app_list_length (addons) : 0; + + for (ii = 0; ii < sz; ii++) { + GsApp *addon = gs_app_list_index (addons, ii); + g_autoptr(GError) local_error = NULL; + + if (state != gs_app_get_state (addon)) + continue; + + /* To have refined also the state */ + gs_app_set_state (addon, GS_APP_STATE_UNKNOWN); + + if (!gs_flatpak_refine_app_unlocked (self, addon, flags, interactive, &locker, cancellable, &local_error)) { + if (errors) + g_string_append_c (errors, '\n'); + else + errors = g_string_new (NULL); + g_string_append_printf (errors, _("Failed to refine addon ‘%s’: %s"), + gs_app_get_name (addon), local_error->message); + } + } + + if (errors) { + g_autoptr(GsPluginEvent) event = NULL; + g_autoptr(GError) error_local = g_error_new_literal (GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + errors->str); + + event = gs_plugin_event_new ("error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (self->plugin, event); + } +} + +gboolean +gs_flatpak_refine_app (GsFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GRWLockReaderLocker) locker = NULL; + + /* ensure valid */ + if (!gs_flatpak_rescan_app_data (self, interactive, cancellable, error)) + return FALSE; + + return gs_flatpak_refine_app_unlocked (self, app, flags, interactive, &locker, cancellable, error); +} + +gboolean +gs_flatpak_refine_wildcard (GsFlatpak *self, GsApp *app, + GsAppList *list, GsPluginRefineFlags refine_flags, + gboolean interactive, + GCancellable *cancellable, GError **error) +{ + const gchar *id; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) components = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + + /* not enough info to find */ + id = gs_app_get_id (app); + if (id == NULL) + return TRUE; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + /* find all apps when matching any prefixes */ + xpath = g_strdup_printf ("components/component/id[text()='%s']/..", id); + components = xb_silo_query (self->silo, xpath, 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; + } + + gs_flatpak_ensure_remote_title (self, interactive, cancellable); + + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + g_autoptr(GsApp) new = NULL; + new = gs_appstream_create_app (self->plugin, self->silo, component, error); + if (new == NULL) + return FALSE; + gs_flatpak_claim_app (self, new); + if (!gs_flatpak_refine_app_unlocked (self, new, refine_flags, interactive, &locker, cancellable, error)) + return FALSE; + gs_app_subsume_metadata (new, app); + gs_app_list_add (list, new); + } + + /* success */ + return TRUE; +} + +gboolean +gs_flatpak_launch (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + /* launch the app */ + if (!flatpak_installation_launch (gs_flatpak_get_installation (self, interactive), + gs_flatpak_app_get_ref_name (app), + gs_flatpak_app_get_ref_arch (app), + gs_app_get_branch (app), + NULL, + cancellable, + error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + return TRUE; +} + +gboolean +gs_flatpak_app_remove_source (GsFlatpak *self, + GsApp *app, + gboolean is_remove, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakRemote) xremote = NULL; + gboolean success; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + /* find the remote */ + xremote = flatpak_installation_get_remote_by_name (installation, + gs_app_get_id (app), + cancellable, error); + if (xremote == NULL) { + gs_flatpak_error_convert (error); + g_prefix_error (error, + "flatpak source %s not found: ", + gs_app_get_id (app)); + return FALSE; + } + + /* remove */ + gs_app_set_state (app, GS_APP_STATE_REMOVING); + if (is_remove) { + success = flatpak_installation_remove_remote (installation, gs_app_get_id (app), cancellable, error); + } else { + gboolean was_disabled = flatpak_remote_get_disabled (xremote); + flatpak_remote_set_disabled (xremote, TRUE); + success = flatpak_installation_modify_remote (installation, xremote, cancellable, error); + if (!success) + flatpak_remote_set_disabled (xremote, was_disabled); + } + + if (!success) { + gs_flatpak_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* invalidate cache */ + gs_flatpak_invalidate_silo (self); + + gs_app_set_state (app, is_remove ? GS_APP_STATE_UNAVAILABLE : GS_APP_STATE_AVAILABLE); + + gs_plugin_repository_changed (self->plugin, app); + + return TRUE; +} + +GsApp * +gs_flatpak_file_to_app_bundle (GsFlatpak *self, + GFile *file, + gboolean unrefined, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GBytes) appstream_gz = NULL; + g_autoptr(GBytes) icon_data64 = NULL, icon_data128 = NULL; + g_autoptr(GBytes) metadata = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(FlatpakBundleRef) xref_bundle = NULL; + + /* load bundle */ + xref_bundle = flatpak_bundle_ref_new (file, error); + if (xref_bundle == NULL) { + gs_flatpak_error_convert (error); + g_prefix_error (error, "error loading bundle: "); + return NULL; + } + + /* load metadata */ + app = gs_flatpak_create_app (self, NULL, FLATPAK_REF (xref_bundle), NULL, interactive, cancellable); + if (unrefined) + return g_steal_pointer (&app); + + gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_BUNDLE); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL); + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, flatpak_bundle_ref_get_installed_size (xref_bundle)); + gs_flatpak_set_metadata (self, app, FLATPAK_REF (xref_bundle)); + metadata = flatpak_bundle_ref_get_metadata (xref_bundle); + if (!gs_flatpak_set_app_metadata (self, app, + g_bytes_get_data (metadata, NULL), + g_bytes_get_size (metadata), + interactive, + cancellable, + error)) + return NULL; + + /* load AppStream */ + appstream_gz = flatpak_bundle_ref_get_appstream (xref_bundle); + if (appstream_gz != NULL) { + if (!gs_flatpak_refine_appstream_from_bytes (self, app, NULL, NULL, + appstream_gz, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID, + interactive, + cancellable, error)) + return NULL; + } else { + g_warning ("no appstream metadata in file"); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + gs_flatpak_app_get_ref_name (app)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, + "A flatpak application"); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, ""); + } + + /* Load icons. Currently flatpak only supports exactly 64px or 128px + * icons in bundles. */ + icon_data64 = flatpak_bundle_ref_get_icon (xref_bundle, 64); + if (icon_data64 != NULL) { + g_autoptr(GIcon) icon = g_bytes_icon_new (icon_data64); + gs_icon_set_width (icon, 64); + gs_icon_set_height (icon, 64); + gs_app_add_icon (app, icon); + } + + icon_data128 = flatpak_bundle_ref_get_icon (xref_bundle, 128); + if (icon_data128 != NULL) { + g_autoptr(GIcon) icon = g_bytes_icon_new (icon_data128); + gs_icon_set_width (icon, 128); + gs_icon_set_height (icon, 128); + gs_app_add_icon (app, icon); + } + + /* Fallback */ + if (icon_data64 == NULL && icon_data128 == NULL) { + g_autoptr(GIcon) icon = g_themed_icon_new ("system-component-application"); + gs_app_add_icon (app, icon); + } + + /* not quite true: this just means we can update this specific app */ + if (flatpak_bundle_ref_get_origin (xref_bundle)) + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); + + /* success */ + return g_steal_pointer (&app); +} + +static gboolean +_txn_abort_on_ready (FlatpakTransaction *transaction) +{ + return FALSE; +} + +static gboolean +_txn_add_new_remote (FlatpakTransaction *transaction, + FlatpakTransactionRemoteReason reason, + const char *from_id, + const char *remote_name, + const char *url) +{ + return TRUE; +} + +static int +_txn_choose_remote_for_ref (FlatpakTransaction *transaction, + const char *for_ref, + const char *runtime_ref, + const char * const *remotes) +{ + /* This transaction is just for displaying the app not installing it so + * this choice shouldn't matter */ + return 0; +} + +GsApp * +gs_flatpak_file_to_app_ref (GsFlatpak *self, + GFile *file, + gboolean unrefined, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + GsApp *runtime; + const gchar *const *locales = g_get_language_names (); + const gchar *remote_name; + gboolean is_runtime, success; + gsize len = 0; + GList *txn_ops; +#if !FLATPAK_CHECK_VERSION(1,13,1) + guint64 app_installed_size = 0, app_download_size = 0; +#endif + g_autofree gchar *contents = NULL; + g_autoptr(FlatpakTransaction) transaction = NULL; + g_autoptr(FlatpakRef) parsed_ref = NULL; + g_autoptr(FlatpakRemoteRef) remote_ref = NULL; + g_autoptr(FlatpakRemote) xremote = NULL; + g_autoptr(GBytes) ref_file_data = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + g_autofree gchar *origin_url = NULL; + g_autofree gchar *ref_comment = NULL; + g_autofree gchar *ref_description = NULL; + g_autofree gchar *ref_homepage = NULL; + g_autofree gchar *ref_icon = NULL; + g_autofree gchar *ref_title = NULL; + g_autofree gchar *ref_name = NULL; + g_autofree gchar *ref_branch = NULL; + FlatpakInstallation *installation = gs_flatpak_get_installation (self, interactive); + + /* add current locales */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + /* get file data */ + if (!g_file_load_contents (file, + cancellable, + &contents, + &len, + NULL, + error)) { + gs_utils_error_convert_gio (error); + return NULL; + } + + /* load the file */ + kf = g_key_file_new (); + if (!g_key_file_load_from_data (kf, contents, len, G_KEY_FILE_NONE, error)) { + gs_utils_error_convert_gio (error); + return NULL; + } + + /* check version */ + if (g_key_file_has_key (kf, "Flatpak Ref", "Version", NULL)) { + guint64 ver = g_key_file_get_uint64 (kf, "Flatpak Ref", "Version", NULL); + if (ver != 1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "unsupported version %" G_GUINT64_FORMAT, ver); + return NULL; + } + } + + /* get name, branch, kind */ + ref_name = g_key_file_get_string (kf, "Flatpak Ref", "Name", error); + if (ref_name == NULL) { + gs_utils_error_convert_gio (error); + return NULL; + } + if (g_key_file_has_key (kf, "Flatpak Ref", "Branch", NULL)) { + ref_branch = g_key_file_get_string (kf, "Flatpak Ref", "Branch", error); + if (ref_branch == NULL) { + gs_utils_error_convert_gio (error); + return NULL; + } + } else { + ref_branch = g_strdup ("master"); + } + if (g_key_file_has_key (kf, "Flatpak Ref", "IsRuntime", NULL)) { + is_runtime = g_key_file_get_boolean (kf, "Flatpak Ref", "IsRuntime", error); + if (error != NULL && *error != NULL) { + gs_utils_error_convert_gio (error); + return NULL; + } + } else { + is_runtime = FALSE; + } + + if (unrefined) { + /* Note: we don't support non-default arch here but it's not a + * regression since we never have for a flatpakref + */ + g_autofree char *app_ref = g_strdup_printf ("%s/%s/%s/%s", + is_runtime ? "runtime" : "app", + ref_name, + flatpak_get_default_arch (), + ref_branch); + parsed_ref = flatpak_ref_parse (app_ref, error); + if (parsed_ref == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + + /* early return */ + app = gs_flatpak_create_app (self, NULL, parsed_ref, NULL, interactive, cancellable); + return g_steal_pointer (&app); + } + + /* Add the remote (to the temporary installation) but abort the + * transaction before it installs the app + */ + transaction = flatpak_transaction_new_for_installation (installation, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + flatpak_transaction_set_no_interaction (transaction, TRUE); + g_signal_connect (transaction, "ready-pre-auth", G_CALLBACK (_txn_abort_on_ready), NULL); + g_signal_connect (transaction, "add-new-remote", G_CALLBACK (_txn_add_new_remote), NULL); + g_signal_connect (transaction, "choose-remote-for-ref", G_CALLBACK (_txn_choose_remote_for_ref), NULL); + ref_file_data = g_bytes_new (contents, len); + if (!flatpak_transaction_add_install_flatpakref (transaction, ref_file_data, error)) { + gs_flatpak_error_convert (error); + return NULL; + } + success = flatpak_transaction_run (transaction, cancellable, &error_local); + g_assert (!success); /* aborted in _txn_abort_on_ready */ + + /* We don't check for FLATPAK_ERROR_ALREADY_INSTALLED here because it's + * a temporary installation + */ + if (!g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_ABORTED)) { + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_flatpak_error_convert (error); + return NULL; + } + + g_clear_error (&error_local); + + /* find the operation for the flatpakref */ + txn_ops = flatpak_transaction_get_operations (transaction); + for (GList *l = txn_ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + const char *op_ref = flatpak_transaction_operation_get_ref (op); + parsed_ref = flatpak_ref_parse (op_ref, error); + if (parsed_ref == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + if (g_strcmp0 (flatpak_ref_get_name (parsed_ref), ref_name) != 0) { + g_clear_object (&parsed_ref); + } else { + remote_name = flatpak_transaction_operation_get_remote (op); + g_debug ("auto-created remote name: %s", remote_name); +#if !FLATPAK_CHECK_VERSION(1,13,1) + app_download_size = flatpak_transaction_operation_get_download_size (op); + app_installed_size = flatpak_transaction_operation_get_installed_size (op); +#endif + break; + } + } + g_assert (parsed_ref != NULL); + g_list_free_full (g_steal_pointer (&txn_ops), g_object_unref); + +#if FLATPAK_CHECK_VERSION(1,13,1) + /* fetch remote ref */ + remote_ref = flatpak_installation_fetch_remote_ref_sync (installation, + remote_name, + flatpak_ref_get_kind (parsed_ref), + flatpak_ref_get_name (parsed_ref), + flatpak_ref_get_arch (parsed_ref), + flatpak_ref_get_branch (parsed_ref), + cancellable, + error); + if (remote_ref == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + app = gs_flatpak_create_app (self, remote_name, FLATPAK_REF (remote_ref), NULL, interactive, cancellable); +#else + app = gs_flatpak_create_app (self, remote_name, parsed_ref, NULL, interactive, cancellable); + gs_app_set_size_download (app, (app_download_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, app_download_size); + gs_app_set_size_installed (app, (app_installed_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, app_installed_size); +#endif + + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); + gs_flatpak_app_set_file_kind (app, GS_FLATPAK_APP_FILE_KIND_REF); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + + runtime = gs_app_get_runtime (app); + if (runtime != NULL) { + g_autofree char *runtime_ref = gs_flatpak_app_get_ref_display (runtime); + if (gs_app_get_state (runtime) == GS_APP_STATE_UNKNOWN) { + g_autofree gchar *uri = NULL; + /* the new runtime is available from the RuntimeRepo */ + uri = g_key_file_get_string (kf, "Flatpak Ref", "RuntimeRepo", NULL); + gs_flatpak_app_set_runtime_url (runtime, uri); + } + + /* find the operation for the runtime to set its size data. Since this + * is all happening on a tmp installation, it won't be available later + * during the refine step + */ + txn_ops = flatpak_transaction_get_operations (transaction); + for (GList *l = txn_ops; l != NULL; l = l->next) { + FlatpakTransactionOperation *op = l->data; + const char *op_ref = flatpak_transaction_operation_get_ref (op); + if (g_strcmp0 (runtime_ref, op_ref) == 0) { + guint64 installed_size = 0, download_size = 0; + download_size = flatpak_transaction_operation_get_download_size (op); + gs_app_set_size_download (runtime, (download_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, download_size); + installed_size = flatpak_transaction_operation_get_installed_size (op); + gs_app_set_size_installed (runtime, (installed_size != 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, installed_size); + break; + } + } + g_list_free_full (g_steal_pointer (&txn_ops), g_object_unref); + } + + /* use the data from the flatpakref file as a fallback */ + ref_title = g_key_file_get_string (kf, "Flatpak Ref", "Title", NULL); + if (ref_title != NULL) + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, ref_title); + ref_comment = g_key_file_get_string (kf, "Flatpak Ref", "Comment", NULL); + if (ref_comment != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, ref_comment); + ref_description = g_key_file_get_string (kf, "Flatpak Ref", "Description", NULL); + if (ref_description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, ref_description); + ref_homepage = g_key_file_get_string (kf, "Flatpak Ref", "Homepage", NULL); + if (ref_homepage != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, ref_homepage); + ref_icon = g_key_file_get_string (kf, "Flatpak Ref", "Icon", NULL); + if (ref_icon != NULL && + (g_str_has_prefix (ref_icon, "http:") || + g_str_has_prefix (ref_icon, "https:"))) { + g_autoptr(GIcon) icon = gs_remote_icon_new (ref_icon); + gs_app_add_icon (app, icon); + } + + /* set the origin data */ + xremote = flatpak_installation_get_remote_by_name (installation, + remote_name, + cancellable, + error); + if (xremote == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + origin_url = flatpak_remote_get_url (xremote); + if (origin_url == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no URL for remote %s", + flatpak_remote_get_name (xremote)); + return NULL; + } + gs_app_set_origin_hostname (app, origin_url); + + /* get the new appstream data (nonfatal for failure) */ + if (!gs_flatpak_refresh_appstream_remote (self, remote_name, interactive, + cancellable, &error_local)) { + g_autoptr(GsPluginEvent) event = NULL; + + gs_flatpak_error_convert (&error_local); + + event = gs_plugin_event_new ("app", app, + "error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (self->plugin, event); + g_clear_error (&error_local); + } + + /* get this now, as it's not going to be available at install time */ + if (!gs_plugin_refine_item_metadata (self, app, interactive, cancellable, error)) + return NULL; + + /* parse it */ + if (!gs_flatpak_add_apps_from_xremote (self, builder, xremote, interactive, cancellable, error)) + return NULL; + + /* build silo */ + /* No need to change the thread-default main context because the silo + * doesn’t live beyond this function */ + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + cancellable, + error); + if (silo == NULL) + return NULL; + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + g_autofree gchar *xml = NULL; + xml = xb_silo_export (silo, + XB_NODE_EXPORT_FLAG_FORMAT_INDENT | + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE, + NULL); + g_debug ("showing AppStream data: %s", xml); + } + + /* get extra AppStream data if available */ + if (!gs_flatpak_refine_appstream (self, app, silo, + GS_PLUGIN_REFINE_FLAGS_MASK, + interactive, + cancellable, + error)) + return NULL; + + /* success */ + return g_steal_pointer (&app); +} + +gboolean +gs_flatpak_search (GsFlatpak *self, + const gchar * const *values, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GMutexLocker) app_silo_locker = NULL; + g_autoptr(GPtrArray) silos_to_remove = g_ptr_array_new (); + GHashTableIter iter; + gpointer key, value; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + if (!gs_appstream_search (self->plugin, self->silo, values, list_tmp, + cancellable, error)) + return FALSE; + + gs_flatpak_ensure_remote_title (self, interactive, cancellable); + + gs_flatpak_claim_app_list (self, list_tmp, interactive); + gs_app_list_add_list (list, list_tmp); + + /* Also search silos from installed apps which were missing from self->silo */ + app_silo_locker = g_mutex_locker_new (&self->app_silos_mutex); + g_hash_table_iter_init (&iter, self->app_silos); + while (g_hash_table_iter_next (&iter, &key, &value)) { + g_autoptr(XbSilo) app_silo = g_object_ref (value); + g_autoptr(GsAppList) app_list_tmp = gs_app_list_new (); + const char *app_ref = (char *)key; + g_autoptr(FlatpakInstalledRef) installed_ref = NULL; + g_auto(GStrv) split = NULL; + FlatpakRefKind kind; + + /* Ignore any silos of apps that have since been removed. + * FIXME: can we use self->installed_refs here? */ + split = g_strsplit (app_ref, "/", -1); + g_assert (g_strv_length (split) == 4); + if (g_strcmp0 (split[0], "app") == 0) + kind = FLATPAK_REF_KIND_APP; + else + kind = FLATPAK_REF_KIND_RUNTIME; + installed_ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive), + kind, + split[1], + split[2], + split[3], + NULL, NULL); + if (installed_ref == NULL) { + g_ptr_array_add (silos_to_remove, (gpointer) app_ref); + continue; + } + + if (!gs_appstream_search (self->plugin, app_silo, values, app_list_tmp, + cancellable, error)) + return FALSE; + + gs_flatpak_claim_app_list (self, app_list_tmp, interactive); + gs_app_list_add_list (list, app_list_tmp); + } + + for (guint i = 0; i < silos_to_remove->len; i++) { + const char *silo = g_ptr_array_index (silos_to_remove, i); + g_hash_table_remove (self->app_silos, silo); + } + + return TRUE; +} + +gboolean +gs_flatpak_search_developer_apps (GsFlatpak *self, + const gchar * const *values, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GMutexLocker) app_silo_locker = NULL; + g_autoptr(GPtrArray) silos_to_remove = g_ptr_array_new (); + GHashTableIter iter; + gpointer key, value; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + if (!gs_appstream_search_developer_apps (self->plugin, self->silo, values, list_tmp, + cancellable, error)) + return FALSE; + + gs_flatpak_ensure_remote_title (self, interactive, cancellable); + + gs_flatpak_claim_app_list (self, list_tmp, interactive); + gs_app_list_add_list (list, list_tmp); + + /* Also search silos from installed apps which were missing from self->silo */ + app_silo_locker = g_mutex_locker_new (&self->app_silos_mutex); + g_hash_table_iter_init (&iter, self->app_silos); + while (g_hash_table_iter_next (&iter, &key, &value)) { + g_autoptr(XbSilo) app_silo = g_object_ref (value); + g_autoptr(GsAppList) app_list_tmp = gs_app_list_new (); + const char *app_ref = (char *)key; + g_autoptr(FlatpakInstalledRef) installed_ref = NULL; + g_auto(GStrv) split = NULL; + FlatpakRefKind kind; + + /* Ignore any silos of apps that have since been removed. + * FIXME: can we use self->installed_refs here? */ + split = g_strsplit (app_ref, "/", -1); + g_assert (g_strv_length (split) == 4); + if (g_strcmp0 (split[0], "app") == 0) + kind = FLATPAK_REF_KIND_APP; + else + kind = FLATPAK_REF_KIND_RUNTIME; + installed_ref = flatpak_installation_get_installed_ref (gs_flatpak_get_installation (self, interactive), + kind, + split[1], + split[2], + split[3], + NULL, NULL); + if (installed_ref == NULL) { + g_ptr_array_add (silos_to_remove, (gpointer) app_ref); + continue; + } + + if (!gs_appstream_search_developer_apps (self->plugin, app_silo, values, app_list_tmp, + cancellable, error)) + return FALSE; + + gs_flatpak_claim_app_list (self, app_list_tmp, interactive); + gs_app_list_add_list (list, app_list_tmp); + } + + for (guint i = 0; i < silos_to_remove->len; i++) { + const char *silo = g_ptr_array_index (silos_to_remove, i); + g_hash_table_remove (self->app_silos, silo); + } + + return TRUE; +} + +gboolean +gs_flatpak_add_category_apps (GsFlatpak *self, + GsCategory *category, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + return gs_appstream_add_category_apps (self->plugin, self->silo, + category, list, + cancellable, error); +} + +gboolean +gs_flatpak_refine_category_sizes (GsFlatpak *self, + GPtrArray *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + return gs_appstream_refine_category_sizes (self->silo, list, cancellable, error); +} + +gboolean +gs_flatpak_add_popular (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + if (!gs_appstream_add_popular (self->silo, list_tmp, + cancellable, error)) + return FALSE; + + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +gboolean +gs_flatpak_add_featured (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + if (!gs_appstream_add_featured (self->silo, list_tmp, + cancellable, error)) + return FALSE; + + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +gboolean +gs_flatpak_add_deployment_featured (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + const gchar *const *deployments, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + return gs_appstream_add_deployment_featured (self->silo, deployments, list, cancellable, error); +} + +gboolean +gs_flatpak_add_alternates (GsFlatpak *self, + GsApp *app, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + if (!gs_appstream_add_alternates (self->silo, app, list_tmp, + cancellable, error)) + return FALSE; + + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +gboolean +gs_flatpak_add_recent (GsFlatpak *self, + GsAppList *list, + guint64 age, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + if (!gs_appstream_add_recent (self->plugin, self->silo, list_tmp, age, + cancellable, error)) + return FALSE; + + gs_flatpak_claim_app_list (self, list_tmp, interactive); + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +gboolean +gs_flatpak_url_to_app (GsFlatpak *self, + GsAppList *list, + const gchar *url, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!ensure_flatpak_silo_with_locker (self, &locker, interactive, cancellable, error)) + return FALSE; + + if (!gs_appstream_url_to_app (self->plugin, self->silo, list_tmp, url, cancellable, error)) + return FALSE; + + gs_flatpak_claim_app_list (self, list_tmp, interactive); + gs_app_list_add_list (list, list_tmp); + + return TRUE; +} + +const gchar * +gs_flatpak_get_id (GsFlatpak *self) +{ + if (self->id == NULL) { + GString *str = g_string_new ("flatpak"); + g_string_append_printf (str, "-%s", + as_component_scope_to_string (self->scope)); + if (flatpak_installation_get_id (self->installation_noninteractive) != NULL) { + g_string_append_printf (str, "-%s", + flatpak_installation_get_id (self->installation_noninteractive)); + } + if (self->flags & GS_FLATPAK_FLAG_IS_TEMPORARY) + g_string_append (str, "-temp"); + self->id = g_string_free (str, FALSE); + } + return self->id; +} + +AsComponentScope +gs_flatpak_get_scope (GsFlatpak *self) +{ + return self->scope; +} + +FlatpakInstallation * +gs_flatpak_get_installation (GsFlatpak *self, + gboolean interactive) +{ + return interactive ? self->installation_interactive : self->installation_noninteractive; +} + +static void +gs_flatpak_finalize (GObject *object) +{ + GsFlatpak *self; + g_return_if_fail (GS_IS_FLATPAK (object)); + self = GS_FLATPAK (object); + + if (self->changed_id > 0) { + g_signal_handler_disconnect (self->monitor, self->changed_id); + self->changed_id = 0; + } + if (self->silo != NULL) + g_object_unref (self->silo); + if (self->monitor != NULL) + g_object_unref (self->monitor); + + g_free (self->id); + g_object_unref (self->installation_noninteractive); + g_object_unref (self->installation_interactive); + g_clear_pointer (&self->installed_refs, g_ptr_array_unref); + g_mutex_clear (&self->installed_refs_mutex); + g_object_unref (self->plugin); + g_hash_table_unref (self->broken_remotes); + g_mutex_clear (&self->broken_remotes_mutex); + g_rw_lock_clear (&self->silo_lock); + g_hash_table_unref (self->app_silos); + g_mutex_clear (&self->app_silos_mutex); + g_clear_pointer (&self->remote_title, g_hash_table_unref); + g_mutex_clear (&self->remote_title_mutex); + + G_OBJECT_CLASS (gs_flatpak_parent_class)->finalize (object); +} + +static void +gs_flatpak_class_init (GsFlatpakClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_flatpak_finalize; +} + +static void +gs_flatpak_init (GsFlatpak *self) +{ + /* XbSilo needs external locking as we destroy the silo and build a new + * one when something changes */ + g_rw_lock_init (&self->silo_lock); + + g_mutex_init (&self->installed_refs_mutex); + self->installed_refs = NULL; + g_mutex_init (&self->broken_remotes_mutex); + self->broken_remotes = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + self->app_silos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + g_mutex_init (&self->app_silos_mutex); + self->remote_title = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_mutex_init (&self->remote_title_mutex); +} + +GsFlatpak * +gs_flatpak_new (GsPlugin *plugin, FlatpakInstallation *installation, GsFlatpakFlags flags) +{ + GsFlatpak *self; + g_autoptr(GFile) path = NULL; + gboolean is_user; + + path = flatpak_installation_get_path (installation); + is_user = flatpak_installation_get_is_user (installation); + + self = g_object_new (GS_TYPE_FLATPAK, NULL); + + self->installation_noninteractive = g_object_ref (installation); + flatpak_installation_set_no_interaction (self->installation_noninteractive, TRUE); + + /* Cloning it should never fail as the repo should already exist on disk. */ + self->installation_interactive = flatpak_installation_new_for_path (path, is_user, NULL, NULL); + g_assert (self->installation_interactive != NULL); + flatpak_installation_set_no_interaction (self->installation_interactive, FALSE); + + self->scope = is_user ? AS_COMPONENT_SCOPE_USER : AS_COMPONENT_SCOPE_SYSTEM; + self->plugin = g_object_ref (plugin); + self->flags = flags; + return GS_FLATPAK (self); +} + +void +gs_flatpak_set_busy (GsFlatpak *self, + gboolean busy) +{ + g_return_if_fail (GS_IS_FLATPAK (self)); + + if (busy) { + g_atomic_int_inc (&self->busy); + } else { + g_return_if_fail (g_atomic_int_get (&self->busy) > 0); + if (g_atomic_int_dec_and_test (&self->busy)) { + if (self->changed_while_busy) { + self->changed_while_busy = FALSE; + g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, gs_flatpak_claim_changed_idle_cb, + g_object_ref (self), g_object_unref); + } + } + } +} + +gboolean +gs_flatpak_get_busy (GsFlatpak *self) +{ + g_return_val_if_fail (GS_IS_FLATPAK (self), FALSE); + return g_atomic_int_get (&self->busy) > 0; +} diff --git a/plugins/flatpak/gs-flatpak.h b/plugins/flatpak/gs-flatpak.h new file mode 100644 index 0000000..b3f8a13 --- /dev/null +++ b/plugins/flatpak/gs-flatpak.h @@ -0,0 +1,184 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com> + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <flatpak.h> + +G_BEGIN_DECLS + +#define GS_TYPE_FLATPAK (gs_flatpak_get_type ()) + +G_DECLARE_FINAL_TYPE (GsFlatpak, gs_flatpak, GS, FLATPAK, GObject) + +typedef enum { + GS_FLATPAK_FLAG_NONE = 0, + GS_FLATPAK_FLAG_IS_TEMPORARY = 1 << 0, + GS_FLATPAK_FLAG_LAST /*< skip >*/ +} GsFlatpakFlags; + +GsFlatpak *gs_flatpak_new (GsPlugin *plugin, + FlatpakInstallation *installation, + GsFlatpakFlags flags); +FlatpakInstallation *gs_flatpak_get_installation (GsFlatpak *self, + gboolean interactive); + +GsApp *gs_flatpak_ref_to_app (GsFlatpak *self, + const gchar *ref, + gboolean interactive, + GCancellable *cancellable, + GError **error); + +AsComponentScope gs_flatpak_get_scope (GsFlatpak *self); +const gchar *gs_flatpak_get_id (GsFlatpak *self); +gboolean gs_flatpak_setup (GsFlatpak *self, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_installed (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_sources (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_updates (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refresh (GsFlatpak *self, + guint64 cache_age_secs, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refine_app (GsFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + gboolean interactive, + GCancellable *cancellable, + GError **error); +void gs_flatpak_refine_addons (GsFlatpak *self, + GsApp *parent_app, + GsPluginRefineFlags flags, + GsAppState state, + gboolean interactive, + GCancellable *cancellable); +gboolean gs_flatpak_refine_app_state (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refine_wildcard (GsFlatpak *self, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags flags, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_launch (GsFlatpak *self, + GsApp *app, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_app_remove_source (GsFlatpak *self, + GsApp *app, + gboolean is_remove, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_app_install_source (GsFlatpak *self, + GsApp *app, + gboolean is_install, + gboolean interactive, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_file_to_app_ref (GsFlatpak *self, + GFile *file, + gboolean unrefined, + gboolean interactive, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_file_to_app_bundle (GsFlatpak *self, + GFile *file, + gboolean unrefined, + gboolean interactive, + GCancellable *cancellable, + GError **error); +GsApp *gs_flatpak_find_source_by_url (GsFlatpak *self, + const gchar *name, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_search (GsFlatpak *self, + const gchar * const *values, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_search_developer_apps(GsFlatpak *self, + const gchar * const *values, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_refine_category_sizes(GsFlatpak *self, + GPtrArray *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_category_apps (GsFlatpak *self, + GsCategory *category, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_popular (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_featured (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_deployment_featured + (GsFlatpak *self, + GsAppList *list, + gboolean interactive, + const gchar *const *deployments, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_alternates (GsFlatpak *self, + GsApp *app, + GsAppList *list, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_add_recent (GsFlatpak *self, + GsAppList *list, + guint64 age, + gboolean interactive, + GCancellable *cancellable, + GError **error); +gboolean gs_flatpak_url_to_app (GsFlatpak *self, + GsAppList *list, + const gchar *url, + gboolean interactive, + GCancellable *cancellable, + GError **error); +void gs_flatpak_set_busy (GsFlatpak *self, + gboolean busy); +gboolean gs_flatpak_get_busy (GsFlatpak *self); + +G_END_DECLS diff --git a/plugins/flatpak/gs-plugin-flatpak.c b/plugins/flatpak/gs-plugin-flatpak.c new file mode 100644 index 0000000..7c893ef --- /dev/null +++ b/plugins/flatpak/gs-plugin-flatpak.c @@ -0,0 +1,2326 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Joaquim Rocha <jrocha@endlessm.com> + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/* + * SECTION: + * Exposes flatpaks from the user and system repositories. + * + * All GsApp's created have management-plugin set to flatpak + * Some GsApp's created have have flatpak::kind of app or runtime + * The GsApp:origin is the remote name, e.g. test-repo + * + * The plugin has a worker thread which all operations are delegated to, as the + * libflatpak API is entirely synchronous (and thread-safe). * Message passing + * to the worker thread is by gs_worker_thread_queue(). + * + * FIXME: It may speed things up in future to have one worker thread *per* + * `FlatpakInstallation`, all operating in parallel. + */ + +#include <config.h> + +#include <flatpak.h> +#include <glib/gi18n.h> +#include <gnome-software.h> + +#include "gs-appstream.h" +#include "gs-flatpak-app.h" +#include "gs-flatpak.h" +#include "gs-flatpak-transaction.h" +#include "gs-flatpak-utils.h" +#include "gs-metered.h" +#include "gs-worker-thread.h" + +#include "gs-plugin-flatpak.h" + +struct _GsPluginFlatpak +{ + GsPlugin parent; + + GsWorkerThread *worker; /* (owned) */ + + GPtrArray *installations; /* (element-type GsFlatpak) (owned); may be NULL before setup or after shutdown */ + gboolean has_system_helper; + const gchar *destdir_for_tests; +}; + +G_DEFINE_TYPE (GsPluginFlatpak, gs_plugin_flatpak, GS_TYPE_PLUGIN) + +#define assert_in_worker(self) \ + g_assert (gs_worker_thread_is_in_worker_context (self->worker)) + +/* Work around flatpak_transaction_get_no_interaction() not existing before + * flatpak 1.13.0. */ +#if !FLATPAK_CHECK_VERSION(1,13,0) +#define flatpak_transaction_get_no_interaction(transaction) \ + GPOINTER_TO_INT (g_object_get_data (G_OBJECT (transaction), "flatpak-no-interaction")) +#define flatpak_transaction_set_no_interaction(transaction, no_interaction) \ + G_STMT_START { \ + FlatpakTransaction *ftsni_transaction = (transaction); \ + gboolean ftsni_no_interaction = (no_interaction); \ + (flatpak_transaction_set_no_interaction) (ftsni_transaction, ftsni_no_interaction); \ + g_object_set_data (G_OBJECT (ftsni_transaction), "flatpak-no-interaction", GINT_TO_POINTER (ftsni_no_interaction)); \ + } G_STMT_END +#endif /* flatpak < 1.13.0 */ + +static void +gs_plugin_flatpak_dispose (GObject *object) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (object); + + g_clear_pointer (&self->installations, g_ptr_array_unref); + g_clear_object (&self->worker); + + G_OBJECT_CLASS (gs_plugin_flatpak_parent_class)->dispose (object); +} + +static void +gs_plugin_flatpak_init (GsPluginFlatpak *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + self->installations = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + /* getting app properties from appstream is quicker */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + + /* like appstream, we need the icon plugin to load cached icons into pixbufs */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); + + /* prioritize over packages */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "packagekit"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "rpm-ostree"); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Flatpak"); + + /* used for self tests */ + self->destdir_for_tests = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"); +} + +static gboolean +_as_component_scope_is_compatible (AsComponentScope scope1, AsComponentScope scope2) +{ + if (scope1 == AS_COMPONENT_SCOPE_UNKNOWN) + return TRUE; + if (scope2 == AS_COMPONENT_SCOPE_UNKNOWN) + return TRUE; + return scope1 == scope2; +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) + gs_app_set_management_plugin (app, plugin); +} + +static gboolean +gs_plugin_flatpak_add_installation (GsPluginFlatpak *self, + FlatpakInstallation *installation, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsFlatpak) flatpak = NULL; + + /* create and set up */ + flatpak = gs_flatpak_new (GS_PLUGIN (self), installation, GS_FLATPAK_FLAG_NONE); + if (!gs_flatpak_setup (flatpak, cancellable, error)) + return FALSE; + g_debug ("successfully set up %s", gs_flatpak_get_id (flatpak)); + + /* add objects that set up correctly */ + g_ptr_array_add (self->installations, g_steal_pointer (&flatpak)); + return TRUE; +} + +static void +gs_plugin_flatpak_report_warning (GsPlugin *plugin, + GError **error) +{ + g_autoptr(GsPluginEvent) event = NULL; + g_assert (error != NULL); + if (*error != NULL && (*error)->domain != GS_PLUGIN_ERROR) + gs_flatpak_error_convert (error); + + event = gs_plugin_event_new ("error", *error, + NULL); + gs_plugin_event_add_flag (event, + GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); +} + +static gint +get_priority_for_interactivity (gboolean interactive) +{ + return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW; +} + +static void setup_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + + g_debug ("Flatpak version: %d.%d.%d", + FLATPAK_MAJOR_VERSION, + FLATPAK_MINOR_VERSION, + FLATPAK_MICRO_VERSION); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_setup_async); + + /* Shouldn’t end up setting up twice */ + g_assert (self->installations == NULL || self->installations->len == 0); + + /* Start up a worker thread to process all the plugin’s function calls. */ + self->worker = gs_worker_thread_new ("gs-plugin-flatpak"); + + /* Queue a job to find and set up the installations. */ + gs_worker_thread_queue (self->worker, G_PRIORITY_DEFAULT, + setup_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +setup_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + GsPlugin *plugin = GS_PLUGIN (self); + g_autoptr(GPtrArray) installations = NULL; + const gchar *action_id = "org.freedesktop.Flatpak.appstream-update"; + g_autoptr(GError) permission_error = NULL; + g_autoptr(GPermission) permission = NULL; + + assert_in_worker (self); + + /* if we can't update the AppStream database system-wide don't even + * pull the data as we can't do anything with it */ + permission = gs_utils_get_permission (action_id, NULL, &permission_error); + if (permission == NULL) { + g_debug ("no permission for %s: %s", action_id, permission_error->message); + g_clear_error (&permission_error); + } else { + self->has_system_helper = g_permission_get_allowed (permission) || + g_permission_get_can_acquire (permission); + } + + /* if we're not just running the tests */ + if (self->destdir_for_tests == NULL) { + g_autoptr(GError) error_local = NULL; + g_autoptr(FlatpakInstallation) installation = NULL; + + /* include the system installations */ + if (self->has_system_helper) { + installations = flatpak_get_system_installations (cancellable, + &error_local); + + if (installations == NULL) { + gs_plugin_flatpak_report_warning (plugin, &error_local); + g_clear_error (&error_local); + } + } + + /* include the user installation */ + installation = flatpak_installation_new_user (cancellable, + &error_local); + if (installation == NULL) { + /* if some error happened, report it as an event, but + * do not return it, otherwise it will disable the whole + * plugin (meaning that support for Flatpak will not be + * possible even if a system installation is working) */ + gs_plugin_flatpak_report_warning (plugin, &error_local); + } else { + if (installations == NULL) + installations = g_ptr_array_new_with_free_func (g_object_unref); + + g_ptr_array_add (installations, g_steal_pointer (&installation)); + } + } else { + g_autoptr(GError) error_local = NULL; + + /* use the test installation */ + g_autofree gchar *full_path = g_build_filename (self->destdir_for_tests, + "flatpak", + NULL); + g_autoptr(GFile) file = g_file_new_for_path (full_path); + g_autoptr(FlatpakInstallation) installation = NULL; + g_debug ("using custom flatpak path %s", full_path); + installation = flatpak_installation_new_for_path (file, TRUE, + cancellable, + &error_local); + if (installation == NULL) { + gs_flatpak_error_convert (&error_local); + g_task_return_error (task, g_steal_pointer (&error_local)); + return; + } + + installations = g_ptr_array_new_with_free_func (g_object_unref); + g_ptr_array_add (installations, g_steal_pointer (&installation)); + } + + /* add the installations */ + for (guint i = 0; installations != NULL && i < installations->len; i++) { + g_autoptr(GError) error_local = NULL; + + FlatpakInstallation *installation = g_ptr_array_index (installations, i); + if (!gs_plugin_flatpak_add_installation (self, + installation, + cancellable, + &error_local)) { + gs_plugin_flatpak_report_warning (plugin, + &error_local); + continue; + } + } + + /* when no installation has been loaded, return the error so the + * plugin gets disabled */ + if (self->installations->len == 0) { + g_task_return_new_error (task, + GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Failed to load any Flatpak installations"); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_flatpak_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_flatpak_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_shutdown_async); + + /* Stop the worker thread. */ + gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task)); +} + +static void +shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginFlatpak *self = g_task_get_source_object (task); + g_autoptr(GsWorkerThread) worker = NULL; + g_autoptr(GError) local_error = NULL; + + worker = g_steal_pointer (&self->worker); + + if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Clear the flatpak installations */ + g_ptr_array_set_size (self->installations, 0); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_flatpak_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + if (!gs_flatpak_add_sources (flatpak, list, interactive, cancellable, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + g_autoptr(GError) local_error = NULL; + if (!gs_flatpak_add_updates (flatpak, list, interactive, cancellable, &local_error)) + g_debug ("Failed to get updates for '%s': %s", gs_flatpak_get_id (flatpak), local_error->message); + } + gs_plugin_cache_lookup_by_state (plugin, list, GS_APP_STATE_INSTALLING); + return TRUE; +} + +static void refresh_metadata_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_refresh_metadata_async); + g_task_set_task_data (task, gs_plugin_refresh_metadata_data_new (cache_age_secs, flags), (GDestroyNotify) gs_plugin_refresh_metadata_data_free); + + /* Queue a job to get the installed apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refresh_metadata_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refresh_metadata_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + GsPluginRefreshMetadataData *data = task_data; + gboolean interactive = (data->flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE); + + assert_in_worker (self); + + for (guint i = 0; i < self->installations->len; i++) { + g_autoptr(GError) local_error = NULL; + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + + if (!gs_flatpak_refresh (flatpak, data->cache_age_secs, interactive, cancellable, &local_error)) + g_debug ("Failed to refresh metadata for '%s': %s", gs_flatpak_get_id (flatpak), local_error->message); + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_flatpak_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static GsFlatpak * +gs_plugin_flatpak_get_handler (GsPluginFlatpak *self, + GsApp *app) +{ + const gchar *object_id; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, GS_PLUGIN (self))) + return NULL; + + /* specified an explicit name */ + object_id = gs_flatpak_app_get_object_id (app); + if (object_id != NULL) { + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + if (g_strcmp0 (gs_flatpak_get_id (flatpak), object_id) == 0) + return flatpak; + } + } + + /* find a scope that matches */ + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + if (_as_component_scope_is_compatible (gs_flatpak_get_scope (flatpak), + gs_app_get_scope (app))) + return flatpak; + } + return NULL; +} + +static gboolean +gs_plugin_flatpak_refine_app (GsPluginFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + GsFlatpak *flatpak = NULL; + + /* not us */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_FLATPAK) { + g_debug ("%s not a package, ignoring", gs_app_get_unique_id (app)); + return TRUE; + } + + /* we have to look for the app in all GsFlatpak stores */ + if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) { + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak_tmp = g_ptr_array_index (self->installations, i); + g_autoptr(GError) error_local = NULL; + if (gs_flatpak_refine_app_state (flatpak_tmp, app, interactive, + cancellable, &error_local)) { + flatpak = flatpak_tmp; + break; + } else { + g_debug ("%s", error_local->message); + } + } + } else { + flatpak = gs_plugin_flatpak_get_handler (self, app); + } + if (flatpak == NULL) + return TRUE; + return gs_flatpak_refine_app (flatpak, app, flags, interactive, cancellable, error); +} + + +static gboolean +refine_app (GsPluginFlatpak *self, + GsApp *app, + GsPluginRefineFlags flags, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, GS_PLUGIN (self))) + return TRUE; + + /* get the runtime first */ + if (!gs_plugin_flatpak_refine_app (self, app, flags, interactive, cancellable, error)) + return FALSE; + + /* the runtime might be installed in a different scope */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME) { + GsApp *runtime = gs_app_get_runtime (app); + if (runtime != NULL) { + if (!gs_plugin_flatpak_refine_app (self, runtime, + flags, + interactive, + cancellable, + error)) { + return FALSE; + } + } + } + return TRUE; +} + +static void refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_refine_async); + + /* Queue a job to refine the apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refine_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + GsPluginRefineData *data = task_data; + GsAppList *list = data->list; + GsPluginRefineFlags flags = data->flags; + gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE); + g_autoptr(GsAppList) app_list = NULL; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (self, app, flags, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + + /* Refine wildcards. + * + * Use a copy of the list for the loop because a function called + * on the plugin may affect the list which can lead to problems + * (e.g. inserting an app in the list on every call results in + * an infinite loop) */ + app_list = gs_app_list_copy (list); + + for (guint j = 0; j < gs_app_list_length (app_list); j++) { + GsApp *app = gs_app_list_index (app_list, j); + + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + + if (!gs_flatpak_refine_wildcard (flatpak, app, list, flags, interactive, + cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_flatpak_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (GS_PLUGIN_FLATPAK (plugin), app); + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + if (flatpak == NULL) + return TRUE; + + return gs_flatpak_launch (flatpak, app, interactive, cancellable, error); +} + +/* ref full */ +static GsApp * +gs_plugin_flatpak_find_app_by_ref (GsPluginFlatpak *self, + const gchar *ref, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_debug ("finding ref %s", ref); + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak_tmp = g_ptr_array_index (self->installations, i); + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error_local = NULL; + + app = gs_flatpak_ref_to_app (flatpak_tmp, ref, interactive, cancellable, &error_local); + if (app == NULL) { + g_debug ("%s", error_local->message); + continue; + } + g_debug ("found ref=%s->%s", ref, gs_app_get_unique_id (app)); + return g_steal_pointer (&app); + } + return NULL; +} + +/* ref full */ +static GsApp * +_ref_to_app (FlatpakTransaction *transaction, + const gchar *ref, + GsPluginFlatpak *self) +{ + g_return_val_if_fail (GS_IS_FLATPAK_TRANSACTION (transaction), NULL); + g_return_val_if_fail (ref != NULL, NULL); + g_return_val_if_fail (GS_IS_PLUGIN_FLATPAK (self), NULL); + + /* search through each GsFlatpak */ + return gs_plugin_flatpak_find_app_by_ref (self, ref, + gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE), + NULL, NULL); +} + +static void +_group_apps_by_installation_recurse (GsPluginFlatpak *self, + GsAppList *list, + GHashTable *applist_by_flatpaks) +{ + if (!list) + return; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GsFlatpak *flatpak = gs_plugin_flatpak_get_handler (self, app); + if (flatpak != NULL) { + GsAppList *list_tmp = g_hash_table_lookup (applist_by_flatpaks, flatpak); + GsAppList *related_list; + if (list_tmp == NULL) { + list_tmp = gs_app_list_new (); + g_hash_table_insert (applist_by_flatpaks, + g_object_ref (flatpak), + list_tmp); + } + gs_app_list_add (list_tmp, app); + + /* Add also related apps, which can be those recognized for update, + while the 'app' is already up to date. */ + related_list = gs_app_get_related (app); + _group_apps_by_installation_recurse (self, related_list, applist_by_flatpaks); + } + } +} + +/* + * Returns: (transfer full) (element-type GsFlatpak GsAppList): + * a map from GsFlatpak to non-empty lists of apps from @list associated + * with that installation. + */ +static GHashTable * +_group_apps_by_installation (GsPluginFlatpak *self, + GsAppList *list) +{ + g_autoptr(GHashTable) applist_by_flatpaks = NULL; + + /* list of apps to be handled by each flatpak installation */ + applist_by_flatpaks = g_hash_table_new_full (g_direct_hash, g_direct_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) g_object_unref); + + /* put each app into the correct per-GsFlatpak list */ + _group_apps_by_installation_recurse (self, list, applist_by_flatpaks); + + return g_steal_pointer (&applist_by_flatpaks); +} + +typedef struct { + FlatpakTransaction *transaction; + guint id; +} BasicAuthData; + +static void +basic_auth_data_free (BasicAuthData *data) +{ + g_object_unref (data->transaction); + g_slice_free (BasicAuthData, data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(BasicAuthData, basic_auth_data_free) + +static void +_basic_auth_cb (const gchar *user, const gchar *password, gpointer user_data) +{ + g_autoptr(BasicAuthData) data = user_data; + + g_debug ("Submitting basic auth data"); + + /* NULL user aborts the basic auth request */ + flatpak_transaction_complete_basic_auth (data->transaction, data->id, user, password, NULL /* options */); +} + +static gboolean +_basic_auth_start (FlatpakTransaction *transaction, + const char *remote, + const char *realm, + GVariant *options, + guint id, + GsPlugin *plugin) +{ + BasicAuthData *data; + + if (flatpak_transaction_get_no_interaction (transaction)) + return FALSE; + + data = g_slice_new0 (BasicAuthData); + data->transaction = g_object_ref (transaction); + data->id = id; + + g_debug ("Login required remote %s (realm %s)\n", remote, realm); + gs_plugin_basic_auth_start (plugin, remote, realm, G_CALLBACK (_basic_auth_cb), data); + return TRUE; +} + +static gboolean +_webflow_start (FlatpakTransaction *transaction, + const char *remote, + const char *url, + GVariant *options, + guint id, + GsPlugin *plugin) +{ + const char *browser; + g_autoptr(GError) error_local = NULL; + + if (flatpak_transaction_get_no_interaction (transaction)) + return FALSE; + + g_debug ("Authentication required for remote '%s'", remote); + + /* Allow hard overrides with $BROWSER */ + browser = g_getenv ("BROWSER"); + if (browser != NULL) { + const char *args[3] = { NULL, url, NULL }; + args[0] = browser; + if (!g_spawn_async (NULL, (char **)args, NULL, G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, &error_local)) { + g_autoptr(GsPluginEvent) event = NULL; + + g_warning ("Failed to start browser %s: %s", browser, error_local->message); + + gs_flatpak_error_convert (&error_local); + + event = gs_plugin_event_new ("error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + + return FALSE; + } + } else { + if (!g_app_info_launch_default_for_uri (url, NULL, &error_local)) { + g_autoptr(GsPluginEvent) event = NULL; + + g_warning ("Failed to show url: %s", error_local->message); + + gs_flatpak_error_convert (&error_local); + + event = gs_plugin_event_new ("error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + + return FALSE; + } + } + + g_debug ("Waiting for browser..."); + + return TRUE; +} + +static void +_webflow_done (FlatpakTransaction *transaction, + GVariant *options, + guint id, + GsPlugin *plugin) +{ + g_debug ("Browser done"); +} + +static FlatpakTransaction * +_build_transaction (GsPlugin *plugin, GsFlatpak *flatpak, + gboolean interactive, + GCancellable *cancellable, GError **error) +{ + FlatpakInstallation *installation; + g_autoptr(FlatpakInstallation) installation_clone = NULL; + g_autoptr(FlatpakTransaction) transaction = NULL; + + installation = gs_flatpak_get_installation (flatpak, interactive); + + installation_clone = g_object_ref (installation); + + /* create transaction */ + transaction = gs_flatpak_transaction_new (installation_clone, cancellable, error); + if (transaction == NULL) { + g_prefix_error (error, "failed to build transaction: "); + gs_flatpak_error_convert (error); + return NULL; + } + + /* Let flatpak know if it is a background operation */ + flatpak_transaction_set_no_interaction (transaction, !interactive); + + /* connect up signals */ + g_signal_connect (transaction, "ref-to-app", + G_CALLBACK (_ref_to_app), plugin); + g_signal_connect (transaction, "basic-auth-start", + G_CALLBACK (_basic_auth_start), plugin); + g_signal_connect (transaction, "webflow-start", + G_CALLBACK (_webflow_start), plugin); + g_signal_connect (transaction, "webflow-done", + G_CALLBACK (_webflow_done), plugin); + + /* use system installations as dependency sources for user installations */ + flatpak_transaction_add_default_dependency_sources (transaction); + + return g_steal_pointer (&transaction); +} + +static void +remove_schedule_entry (gpointer schedule_entry_handle) +{ + g_autoptr(GError) error_local = NULL; + + if (!gs_metered_remove_from_download_scheduler (schedule_entry_handle, NULL, &error_local)) + g_warning ("Failed to remove schedule entry: %s", error_local->message); +} + +gboolean +gs_plugin_download (GsPlugin *plugin, GsAppList *list, + GCancellable *cancellable, GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GHashTable) applist_by_flatpaks = NULL; + GHashTableIter iter; + gpointer key, value; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + /* build and run transaction for each flatpak installation */ + applist_by_flatpaks = _group_apps_by_installation (self, list); + g_hash_table_iter_init (&iter, applist_by_flatpaks); + while (g_hash_table_iter_next (&iter, &key, &value)) { + GsFlatpak *flatpak = GS_FLATPAK (key); + GsAppList *list_tmp = GS_APP_LIST (value); + g_autoptr(FlatpakTransaction) transaction = NULL; + gpointer schedule_entry_handle = NULL; + + g_assert (GS_IS_FLATPAK (flatpak)); + g_assert (list_tmp != NULL); + g_assert (gs_app_list_length (list_tmp) > 0); + + if (!interactive) { + g_autoptr(GError) error_local = NULL; + + if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, &schedule_entry_handle, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + /* build and run non-deployed transaction */ + transaction = _build_transaction (plugin, flatpak, interactive, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + flatpak_transaction_set_no_deploy (transaction, TRUE); + + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + g_autofree gchar *ref = NULL; + g_autoptr(GError) error_local = NULL; + + ref = gs_flatpak_app_get_ref_display (app); + if (flatpak_transaction_add_update (transaction, ref, NULL, NULL, &error_local)) + continue; + + /* Errors about missing remotes are not fatal, as that’s + * a not-uncommon situation. */ + if (g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_REMOTE_NOT_FOUND)) { + g_autoptr(GsPluginEvent) event = NULL; + + g_warning ("Skipping update for ‘%s’: %s", ref, error_local->message); + + gs_flatpak_error_convert (&error_local); + + event = gs_plugin_event_new ("error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + } else { + gs_flatpak_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + + if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { + gs_flatpak_error_convert (error); + remove_schedule_entry (schedule_entry_handle); + return FALSE; + } + + remove_schedule_entry (schedule_entry_handle); + + /* Traverse over the GsAppList again and set that the update has been already downloaded + * for the apps. */ + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + gs_app_set_is_update_downloaded (app, TRUE); + } + } + + return TRUE; +} + +static void +gs_flatpak_cover_addons_in_transaction (GsPlugin *plugin, + FlatpakTransaction *transaction, + GsApp *parent_app, + GsAppState state) +{ + g_autoptr(GsAppList) addons = NULL; + g_autoptr(GString) errors = NULL; + guint ii, sz; + + g_return_if_fail (transaction != NULL); + g_return_if_fail (GS_IS_APP (parent_app)); + + addons = gs_app_dup_addons (parent_app); + sz = addons ? gs_app_list_length (addons) : 0; + + for (ii = 0; ii < sz; ii++) { + GsApp *addon = gs_app_list_index (addons, ii); + g_autoptr(GError) local_error = NULL; + + if (state == GS_APP_STATE_INSTALLING && gs_app_get_to_be_installed (addon)) { + g_autofree gchar *ref = NULL; + + ref = gs_flatpak_app_get_ref_display (addon); + if (flatpak_transaction_add_install (transaction, gs_app_get_origin (addon), ref, NULL, &local_error)) { + gs_app_set_state (addon, state); + } else { + if (errors) + g_string_append_c (errors, '\n'); + else + errors = g_string_new (NULL); + g_string_append_printf (errors, _("Failed to add to install for addon ‘%s’: %s"), + gs_app_get_name (addon), local_error->message); + } + } else if (state == GS_APP_STATE_REMOVING && gs_app_get_state (addon) == GS_APP_STATE_INSTALLED) { + g_autofree gchar *ref = NULL; + + ref = gs_flatpak_app_get_ref_display (addon); + if (flatpak_transaction_add_uninstall (transaction, ref, &local_error)) { + gs_app_set_state (addon, state); + } else { + if (errors) + g_string_append_c (errors, '\n'); + else + errors = g_string_new (NULL); + g_string_append_printf (errors, _("Failed to add to uninstall for addon ‘%s’: %s"), + gs_app_get_name (addon), local_error->message); + } + } + } + + if (errors) { + g_autoptr(GsPluginEvent) event = NULL; + g_autoptr(GError) error_local = g_error_new_literal (GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + errors->str); + + event = gs_plugin_event_new ("error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + } +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + GsFlatpak *flatpak; + g_autoptr(FlatpakTransaction) transaction = NULL; + g_autofree gchar *ref = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + /* not supported */ + flatpak = gs_plugin_flatpak_get_handler (self, app); + if (flatpak == NULL) + return TRUE; + + /* is a source, handled by dedicated function */ + g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE); + + /* build and run transaction */ + transaction = _build_transaction (plugin, flatpak, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE), cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add to the transaction cache for quick look up -- other unrelated + * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ + gs_flatpak_transaction_add_app (transaction, app); + + ref = gs_flatpak_app_get_ref_display (app); + if (!flatpak_transaction_add_uninstall (transaction, ref, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + gs_flatpak_cover_addons_in_transaction (plugin, transaction, app, GS_APP_STATE_REMOVING); + + /* run transaction */ + gs_app_set_state (app, GS_APP_STATE_REMOVING); + if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { + gs_flatpak_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* get any new state */ + gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWN, 0); + gs_app_set_size_installed (app, GS_SIZE_TYPE_UNKNOWN, 0); + + if (!gs_flatpak_refresh (flatpak, G_MAXUINT, interactive, cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!gs_flatpak_refine_app (flatpak, app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID, + interactive, + cancellable, error)) { + g_prefix_error (error, "failed to run refine for %s: ", ref); + gs_flatpak_error_convert (error); + return FALSE; + } + + gs_flatpak_refine_addons (flatpak, + app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID, + GS_APP_STATE_REMOVING, + interactive, + cancellable); + + return TRUE; +} + +static gboolean +app_has_local_source (GsApp *app) +{ + const gchar *url = gs_app_get_origin_hostname (app); + + if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE) + return TRUE; + + if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF && + g_strcmp0 (url, "localhost") == 0) + return TRUE; + + return FALSE; +} + +static void +gs_plugin_flatpak_ensure_scope (GsPlugin *plugin, + GsApp *app) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + + if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) { + g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software"); + + /* get the new GsFlatpak for handling of local files */ + gs_app_set_scope (app, g_settings_get_boolean (settings, "install-bundles-system-wide") ? + AS_COMPONENT_SCOPE_SYSTEM : AS_COMPONENT_SCOPE_USER); + if (!self->has_system_helper) { + g_info ("no flatpak system helper is available, using user"); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER); + } + if (self->destdir_for_tests != NULL) { + g_debug ("in self tests, using user"); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER); + } + } +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + GsFlatpak *flatpak; + g_autoptr(FlatpakTransaction) transaction = NULL; + g_autoptr(GError) error_local = NULL; + gpointer schedule_entry_handle = NULL; + gboolean already_installed = FALSE; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + /* queue for install if installation needs the network */ + if (!app_has_local_source (app) && + !gs_plugin_get_network_available (plugin)) { + gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL); + return TRUE; + } + + /* set the app scope */ + gs_plugin_flatpak_ensure_scope (plugin, app); + + /* not supported */ + flatpak = gs_plugin_flatpak_get_handler (self, app); + if (flatpak == NULL) + return TRUE; + + /* is a source, handled by dedicated function */ + g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE); + + /* build */ + transaction = _build_transaction (plugin, flatpak, interactive, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add to the transaction cache for quick look up -- other unrelated + * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ + gs_flatpak_transaction_add_app (transaction, app); + + /* add flatpakref */ + if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF) { + GFile *file = gs_app_get_local_file (app); + g_autoptr(GBytes) blob = NULL; + if (file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no local file set for bundle %s", + gs_app_get_unique_id (app)); + return FALSE; + } + blob = g_file_load_bytes (file, cancellable, NULL, error); + if (blob == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!flatpak_transaction_add_install_flatpakref (transaction, blob, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add bundle */ + } else if (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE) { + GFile *file = gs_app_get_local_file (app); + if (file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no local file set for bundle %s", + gs_app_get_unique_id (app)); + return FALSE; + } + if (!flatpak_transaction_add_install_bundle (transaction, file, + NULL, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + + /* add normal ref */ + } else { + g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app); + if (!flatpak_transaction_add_install (transaction, + gs_app_get_origin (app), + ref, NULL, &error_local)) { + /* Somehow, the app might already be installed. */ + if (g_error_matches (error_local, FLATPAK_ERROR, + FLATPAK_ERROR_ALREADY_INSTALLED)) { + already_installed = TRUE; + g_clear_error (&error_local); + } else { + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_flatpak_error_convert (error); + return FALSE; + } + } + } + + gs_flatpak_cover_addons_in_transaction (plugin, transaction, app, GS_APP_STATE_INSTALLING); + + if (!interactive) { + /* FIXME: Add additional details here, especially the download + * size bounds (using `size-minimum` and `size-maximum`, both + * type `t`). */ + if (!gs_metered_block_app_on_download_scheduler (app, &schedule_entry_handle, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + /* run transaction */ + if (!already_installed) { + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + if (!gs_flatpak_transaction_run (transaction, cancellable, &error_local)) { + /* Somehow, the app might already be installed. */ + if (g_error_matches (error_local, FLATPAK_ERROR, + FLATPAK_ERROR_ALREADY_INSTALLED)) { + already_installed = TRUE; + g_clear_error (&error_local); + } else { + if (g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_REF_NOT_FOUND)) { + const gchar *origin = gs_app_get_origin (app); + if (origin != NULL) { + g_autoptr(FlatpakRemote) remote = NULL; + remote = flatpak_installation_get_remote_by_name (gs_flatpak_get_installation (flatpak, interactive), + origin, cancellable, NULL); + if (remote != NULL) { + g_autofree gchar *filter = flatpak_remote_get_filter (remote); + if (filter != NULL && *filter != '\0') { + /* It's a filtered remote, create a user friendly error message for it */ + g_autoptr(GError) error_tmp = NULL; + g_set_error (&error_tmp, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + _("Remote “%s” doesn't allow install of “%s”, possibly due to its filter. Remove the filter and repeat the install. Detailed error: %s"), + flatpak_remote_get_title (remote), + gs_app_get_name (app), + error_local->message); + g_clear_error (&error_local); + error_local = g_steal_pointer (&error_tmp); + } + } + } + } + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_flatpak_error_convert (error); + gs_app_set_state_recover (app); + remove_schedule_entry (schedule_entry_handle); + return FALSE; + } + } + } + + if (already_installed) { + /* Set the app back to UNKNOWN so that refining it gets all the right details. */ + g_debug ("App %s is already installed", gs_app_get_unique_id (app)); + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + } + + remove_schedule_entry (schedule_entry_handle); + + /* get any new state */ + if (!gs_flatpak_refresh (flatpak, G_MAXUINT, interactive, cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + if (!gs_flatpak_refine_app (flatpak, app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID, + interactive, + cancellable, error)) { + g_prefix_error (error, "failed to run refine for %s: ", + gs_app_get_unique_id (app)); + gs_flatpak_error_convert (error); + return FALSE; + } + + gs_flatpak_refine_addons (flatpak, + app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID, + GS_APP_STATE_INSTALLING, + interactive, + cancellable); + + return TRUE; +} + +static gboolean +gs_plugin_flatpak_update (GsPlugin *plugin, + GsFlatpak *flatpak, + GsAppList *list_tmp, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FlatpakTransaction) transaction = NULL; + gboolean is_update_downloaded = TRUE; + gpointer schedule_entry_handle = NULL; + + if (!interactive) { + g_autoptr(GError) error_local = NULL; + + if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, &schedule_entry_handle, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + /* build and run transaction */ + transaction = _build_transaction (plugin, flatpak, interactive, cancellable, error); + if (transaction == NULL) { + gs_flatpak_error_convert (error); + return FALSE; + } + + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + g_autofree gchar *ref = NULL; + g_autoptr(GError) error_local = NULL; + + ref = gs_flatpak_app_get_ref_display (app); + if (flatpak_transaction_add_update (transaction, ref, NULL, NULL, error)) { + /* add to the transaction cache for quick look up -- other unrelated + * refs will be matched using gs_plugin_flatpak_find_app_by_ref() */ + gs_flatpak_transaction_add_app (transaction, app); + + continue; + } + + /* Errors about missing remotes are not fatal, as that’s + * a not-uncommon situation. */ + if (g_error_matches (error_local, FLATPAK_ERROR, FLATPAK_ERROR_REMOTE_NOT_FOUND)) { + g_autoptr(GsPluginEvent) event = NULL; + + g_warning ("Skipping update for ‘%s’: %s", ref, error_local->message); + + gs_flatpak_error_convert (&error_local); + + event = gs_plugin_event_new ("error", error_local, + NULL); + gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING); + gs_plugin_report_event (plugin, event); + } else { + gs_flatpak_error_convert (&error_local); + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } + + /* run transaction */ + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + + /* If all apps' update are previously downloaded and available locally, + * FlatpakTransaction should run with no-pull flag. This is the case + * for apps' autoupdates. */ + is_update_downloaded &= gs_app_get_is_update_downloaded (app); + } + + if (is_update_downloaded) { + flatpak_transaction_set_no_pull (transaction, TRUE); + } + + /* automatically clean up unused EOL runtimes when updating */ + flatpak_transaction_set_include_unused_uninstall_ops (transaction, TRUE); + + if (!gs_flatpak_transaction_run (transaction, cancellable, error)) { + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + gs_app_set_state_recover (app); + } + gs_flatpak_error_convert (error); + remove_schedule_entry (schedule_entry_handle); + return FALSE; + } else { + /* Reset the state to have it updated */ + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + } + } + + remove_schedule_entry (schedule_entry_handle); + gs_plugin_updates_changed (plugin); + + /* get any new state */ + if (!gs_flatpak_refresh (flatpak, G_MAXUINT, interactive, cancellable, error)) { + gs_flatpak_error_convert (error); + return FALSE; + } + for (guint i = 0; i < gs_app_list_length (list_tmp); i++) { + GsApp *app = gs_app_list_index (list_tmp, i); + g_autofree gchar *ref = NULL; + + ref = gs_flatpak_app_get_ref_display (app); + if (!gs_flatpak_refine_app (flatpak, app, + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + interactive, + cancellable, error)) { + g_prefix_error (error, "failed to run refine for %s: ", ref); + gs_flatpak_error_convert (error); + return FALSE; + } + } + return TRUE; +} + +gboolean +gs_plugin_update (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GHashTable) applist_by_flatpaks = NULL; + GHashTableIter iter; + gpointer key, value; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + /* build and run transaction for each flatpak installation */ + applist_by_flatpaks = _group_apps_by_installation (self, list); + g_hash_table_iter_init (&iter, applist_by_flatpaks); + while (g_hash_table_iter_next (&iter, &key, &value)) { + GsFlatpak *flatpak = GS_FLATPAK (key); + GsAppList *list_tmp = GS_APP_LIST (value); + gboolean success; + + g_assert (GS_IS_FLATPAK (flatpak)); + g_assert (list_tmp != NULL); + g_assert (gs_app_list_length (list_tmp) > 0); + + gs_flatpak_set_busy (flatpak, TRUE); + success = gs_plugin_flatpak_update (plugin, flatpak, list_tmp, interactive, cancellable, error); + gs_flatpak_set_busy (flatpak, FALSE); + if (!success) + return FALSE; + } + return TRUE; +} + +static GsApp * +gs_plugin_flatpak_file_to_app_repo (GsPluginFlatpak *self, + GFile *file, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsApp) app = NULL; + + /* parse the repo file */ + app = gs_flatpak_app_new_from_repo_file (file, cancellable, error); + if (app == NULL) + return NULL; + + /* already exists */ + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + g_autoptr(GError) error_local = NULL; + g_autoptr(GsApp) app_tmp = NULL; + app_tmp = gs_flatpak_find_source_by_url (flatpak, + gs_flatpak_app_get_repo_url (app), + interactive, + cancellable, &error_local); + if (app_tmp == NULL) { + g_debug ("%s", error_local->message); + continue; + } + if (g_strcmp0 (gs_flatpak_app_get_repo_filter (app), gs_flatpak_app_get_repo_filter (app_tmp)) != 0) + continue; + return g_steal_pointer (&app_tmp); + } + + /* this is new */ + gs_app_set_management_plugin (app, GS_PLUGIN (self)); + return g_steal_pointer (&app); +} + +static GsFlatpak * +gs_plugin_flatpak_create_temporary (GsPluginFlatpak *self, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *installation_path = NULL; + g_autoptr(FlatpakInstallation) installation = NULL; + g_autoptr(GFile) installation_file = NULL; + + /* create new per-user installation in a cache dir */ + installation_path = gs_utils_get_cache_filename ("flatpak", + "installation-tmp", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_ENSURE_EMPTY | + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + error); + if (installation_path == NULL) + return NULL; + installation_file = g_file_new_for_path (installation_path); + installation = flatpak_installation_new_for_path (installation_file, + TRUE, /* user */ + cancellable, + error); + if (installation == NULL) { + gs_flatpak_error_convert (error); + return NULL; + } + return gs_flatpak_new (GS_PLUGIN (self), installation, GS_FLATPAK_FLAG_IS_TEMPORARY); +} + +static GsApp * +gs_plugin_flatpak_file_to_app_bundle (GsPluginFlatpak *self, + GFile *file, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *ref = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_tmp = NULL; + g_autoptr(GsFlatpak) flatpak_tmp = NULL; + + /* only use the temporary GsFlatpak to avoid the auth dialog */ + flatpak_tmp = gs_plugin_flatpak_create_temporary (self, cancellable, error); + if (flatpak_tmp == NULL) + return NULL; + + /* First make a quick GsApp to get the ref */ + app = gs_flatpak_file_to_app_bundle (flatpak_tmp, file, TRUE /* unrefined */, + interactive, cancellable, error); + if (app == NULL) + return NULL; + + /* is this already installed or available in a configured remote */ + ref = gs_flatpak_app_get_ref_display (app); + app_tmp = gs_plugin_flatpak_find_app_by_ref (self, ref, interactive, cancellable, NULL); + if (app_tmp != NULL) + return g_steal_pointer (&app_tmp); + + /* If not installed/available, make a fully refined GsApp */ + g_clear_object (&app); + app = gs_flatpak_file_to_app_bundle (flatpak_tmp, file, FALSE /* unrefined */, + interactive, cancellable, error); + if (app == NULL) + return NULL; + + /* force this to be 'any' scope for installation */ + gs_app_set_scope (app, AS_COMPONENT_SCOPE_UNKNOWN); + + /* this is new */ + return g_steal_pointer (&app); +} + +static GsApp * +gs_plugin_flatpak_file_to_app_ref (GsPluginFlatpak *self, + GFile *file, + gboolean interactive, + GCancellable *cancellable, + GError **error) +{ + GsApp *runtime; + g_autofree gchar *ref = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_tmp = NULL; + g_autoptr(GsFlatpak) flatpak_tmp = NULL; + + /* only use the temporary GsFlatpak to avoid the auth dialog */ + flatpak_tmp = gs_plugin_flatpak_create_temporary (self, cancellable, error); + if (flatpak_tmp == NULL) + return NULL; + + /* First make a quick GsApp to get the ref */ + app = gs_flatpak_file_to_app_ref (flatpak_tmp, file, TRUE /* unrefined */, + interactive, cancellable, error); + if (app == NULL) + return NULL; + + /* is this already installed or available in a configured remote */ + ref = gs_flatpak_app_get_ref_display (app); + app_tmp = gs_plugin_flatpak_find_app_by_ref (self, ref, interactive, cancellable, NULL); + if (app_tmp != NULL) + return g_steal_pointer (&app_tmp); + + /* If not installed/available, make a fully refined GsApp */ + g_clear_object (&app); + app = gs_flatpak_file_to_app_ref (flatpak_tmp, file, FALSE /* unrefined */, + interactive, cancellable, error); + if (app == NULL) + return NULL; + + /* force this to be 'any' scope for installation */ + gs_app_set_scope (app, AS_COMPONENT_SCOPE_UNKNOWN); + + /* do we have a system runtime available */ + runtime = gs_app_get_runtime (app); + if (runtime != NULL) { + g_autoptr(GsApp) runtime_tmp = NULL; + g_autofree gchar *runtime_ref = gs_flatpak_app_get_ref_display (runtime); + runtime_tmp = gs_plugin_flatpak_find_app_by_ref (self, + runtime_ref, + interactive, + cancellable, + NULL); + if (runtime_tmp != NULL) { + gs_app_set_runtime (app, runtime_tmp); + } else { + /* the new runtime is available from the RuntimeRepo */ + if (gs_flatpak_app_get_runtime_url (runtime) != NULL) + gs_app_set_state (runtime, GS_APP_STATE_AVAILABLE); + } + } + + /* this is new */ + return g_steal_pointer (&app); +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autofree gchar *content_type = NULL; + g_autoptr(GsApp) app = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + const gchar *mimetypes_bundle[] = { + "application/vnd.flatpak", + NULL }; + const gchar *mimetypes_repo[] = { + "application/vnd.flatpak.repo", + NULL }; + const gchar *mimetypes_ref[] = { + "application/vnd.flatpak.ref", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (g_strv_contains (mimetypes_bundle, content_type)) { + app = gs_plugin_flatpak_file_to_app_bundle (self, file, interactive, + cancellable, error); + if (app == NULL) + return FALSE; + } else if (g_strv_contains (mimetypes_repo, content_type)) { + app = gs_plugin_flatpak_file_to_app_repo (self, file, interactive, + cancellable, error); + if (app == NULL) + return FALSE; + } else if (g_strv_contains (mimetypes_ref, content_type)) { + app = gs_plugin_flatpak_file_to_app_ref (self, file, interactive, + cancellable, error); + if (app == NULL) + return FALSE; + } + if (app != NULL) { + GsApp *runtime = gs_app_get_runtime (app); + /* Ensure the origin for the runtime is set */ + if (runtime != NULL && gs_app_get_origin (runtime) == NULL) { + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_flatpak_refine_app (self, runtime, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN, interactive, cancellable, &error_local)) + g_debug ("Failed to refine runtime: %s", error_local->message); + } + gs_app_list_add (list, app); + } + return TRUE; +} + +static void refine_categories_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_refine_categories_async (GsPlugin *plugin, + GPtrArray *list, + GsPluginRefineCategoriesFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_categories_data_new_task (plugin, list, flags, + cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_refine_categories_async); + + /* All we actually do is add the sizes of each category. If that’s + * not been requested, avoid queueing a worker job. */ + if (!(flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_SIZE)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* Queue a job to get the apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refine_categories_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +refine_categories_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + g_autoptr(GRWLockReaderLocker) locker = NULL; + GsPluginRefineCategoriesData *data = task_data; + gboolean interactive = (data->flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + + if (!gs_flatpak_refine_category_sizes (flatpak, data->list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_flatpak_refine_categories_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_list_apps_async (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE); + + task = gs_plugin_list_apps_data_new_task (plugin, query, flags, + cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_list_apps_async); + + /* Queue a job to get the apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + list_apps_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + g_autoptr(GsAppList) list = gs_app_list_new (); + GsPluginListAppsData *data = task_data; + gboolean interactive = (data->flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE); + GDateTime *released_since = NULL; + GsAppQueryTristate is_curated = GS_APP_QUERY_TRISTATE_UNSET; + GsAppQueryTristate is_featured = GS_APP_QUERY_TRISTATE_UNSET; + GsCategory *category = NULL; + GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET; + guint64 age_secs = 0; + const gchar * const *deployment_featured = NULL; + const gchar *const *developers = NULL; + const gchar * const *keywords = NULL; + GsApp *alternate_of = NULL; + const gchar *provides_tag = NULL; + GsAppQueryProvidesType provides_type = GS_APP_QUERY_PROVIDES_UNKNOWN; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (data->query != NULL) { + released_since = gs_app_query_get_released_since (data->query); + is_curated = gs_app_query_get_is_curated (data->query); + is_featured = gs_app_query_get_is_featured (data->query); + category = gs_app_query_get_category (data->query); + is_installed = gs_app_query_get_is_installed (data->query); + deployment_featured = gs_app_query_get_deployment_featured (data->query); + developers = gs_app_query_get_developers (data->query); + keywords = gs_app_query_get_keywords (data->query); + alternate_of = gs_app_query_get_alternate_of (data->query); + provides_type = gs_app_query_get_provides (data->query, &provides_tag); + } + + if (released_since != NULL) { + g_autoptr(GDateTime) now = g_date_time_new_now_local (); + age_secs = g_date_time_difference (now, released_since) / G_TIME_SPAN_SECOND; + } + + /* Currently only support a subset of query properties, and only one set at once. + * Also don’t currently support GS_APP_QUERY_TRISTATE_FALSE. */ + if ((released_since == NULL && + is_curated == GS_APP_QUERY_TRISTATE_UNSET && + is_featured == GS_APP_QUERY_TRISTATE_UNSET && + category == NULL && + is_installed == GS_APP_QUERY_TRISTATE_UNSET && + deployment_featured == NULL && + developers == NULL && + keywords == NULL && + alternate_of == NULL && + provides_tag == NULL) || + is_curated == GS_APP_QUERY_TRISTATE_FALSE || + is_featured == GS_APP_QUERY_TRISTATE_FALSE || + is_installed == GS_APP_QUERY_TRISTATE_FALSE || + gs_app_query_get_n_properties_set (data->query) != 1) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported query"); + return; + } + + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + const gchar * const provides_tag_strv[2] = { provides_tag, NULL }; + + if (released_since != NULL && + !gs_flatpak_add_recent (flatpak, list, age_secs, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (is_curated != GS_APP_QUERY_TRISTATE_UNSET && + !gs_flatpak_add_popular (flatpak, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (is_featured != GS_APP_QUERY_TRISTATE_UNSET && + !gs_flatpak_add_featured (flatpak, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (category != NULL && + !gs_flatpak_add_category_apps (flatpak, category, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (is_installed != GS_APP_QUERY_TRISTATE_UNSET && + !gs_flatpak_add_installed (flatpak, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (deployment_featured != NULL && + !gs_flatpak_add_deployment_featured (flatpak, list, interactive, deployment_featured, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (developers != NULL && + !gs_flatpak_search_developer_apps (flatpak, developers, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (keywords != NULL && + !gs_flatpak_search (flatpak, keywords, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (alternate_of != NULL && + !gs_flatpak_add_alternates (flatpak, alternate_of, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* The @provides_type is deliberately ignored here, as flatpak + * wants to try and match anything. This could be changed in + * future. */ + if (provides_tag != NULL && + provides_type != GS_APP_QUERY_PROVIDES_UNKNOWN && + !gs_flatpak_search (flatpak, provides_tag_strv, list, interactive, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + } + + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); +} + +static GsAppList * +gs_plugin_flatpak_list_apps_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + for (guint i = 0; i < self->installations->len; i++) { + GsFlatpak *flatpak = g_ptr_array_index (self->installations, i); + if (!gs_flatpak_url_to_app (flatpak, list, url, interactive, cancellable, error)) + return FALSE; + } + return TRUE; +} + +static void install_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_install_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_install_repository_async); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* is a source */ + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + install_repository_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +install_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + GsFlatpak *flatpak; + GsPluginManageRepositoryData *data = task_data; + gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + /* queue for install if installation needs the network */ + if (!app_has_local_source (data->repository) && + !gs_plugin_get_network_available (GS_PLUGIN (self))) { + gs_app_set_state (data->repository, GS_APP_STATE_QUEUED_FOR_INSTALL); + g_task_return_boolean (task, TRUE); + return; + } + + gs_plugin_flatpak_ensure_scope (GS_PLUGIN (self), data->repository); + + flatpak = gs_plugin_flatpak_get_handler (self, data->repository); + if (flatpak == NULL) { + g_task_return_boolean (task, TRUE); + return; + } + + if (gs_flatpak_app_install_source (flatpak, data->repository, TRUE, interactive, cancellable, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_flatpak_install_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void remove_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_remove_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_remove_repository_async); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* is a source */ + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + remove_repository_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +remove_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + GsFlatpak *flatpak; + GsPluginManageRepositoryData *data = task_data; + gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + flatpak = gs_plugin_flatpak_get_handler (self, data->repository); + if (flatpak == NULL) { + g_task_return_boolean (task, TRUE); + return; + } + + if (gs_flatpak_app_remove_source (flatpak, data->repository, TRUE, interactive, cancellable, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_flatpak_remove_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void enable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_enable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_enable_repository_async); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* is a source */ + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + enable_repository_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +enable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + GsFlatpak *flatpak; + GsPluginManageRepositoryData *data = task_data; + gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + flatpak = gs_plugin_flatpak_get_handler (self, data->repository); + if (flatpak == NULL) { + g_task_return_boolean (task, TRUE); + return; + } + + if (gs_flatpak_app_install_source (flatpak, data->repository, FALSE, interactive, cancellable, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_flatpak_enable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void disable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_flatpak_disable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_flatpak_disable_repository_async); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* is a source */ + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + disable_repository_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +disable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginFlatpak *self = GS_PLUGIN_FLATPAK (source_object); + GsFlatpak *flatpak; + GsPluginManageRepositoryData *data = task_data; + gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + flatpak = gs_plugin_flatpak_get_handler (self, data->repository); + if (flatpak == NULL) { + g_task_return_boolean (task, TRUE); + return; + } + + if (gs_flatpak_app_remove_source (flatpak, data->repository, FALSE, interactive, cancellable, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_flatpak_disable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_flatpak_class_init (GsPluginFlatpakClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_flatpak_dispose; + + plugin_class->setup_async = gs_plugin_flatpak_setup_async; + plugin_class->setup_finish = gs_plugin_flatpak_setup_finish; + plugin_class->shutdown_async = gs_plugin_flatpak_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_flatpak_shutdown_finish; + plugin_class->refine_async = gs_plugin_flatpak_refine_async; + plugin_class->refine_finish = gs_plugin_flatpak_refine_finish; + plugin_class->list_apps_async = gs_plugin_flatpak_list_apps_async; + plugin_class->list_apps_finish = gs_plugin_flatpak_list_apps_finish; + plugin_class->refresh_metadata_async = gs_plugin_flatpak_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_flatpak_refresh_metadata_finish; + plugin_class->install_repository_async = gs_plugin_flatpak_install_repository_async; + plugin_class->install_repository_finish = gs_plugin_flatpak_install_repository_finish; + plugin_class->remove_repository_async = gs_plugin_flatpak_remove_repository_async; + plugin_class->remove_repository_finish = gs_plugin_flatpak_remove_repository_finish; + plugin_class->enable_repository_async = gs_plugin_flatpak_enable_repository_async; + plugin_class->enable_repository_finish = gs_plugin_flatpak_enable_repository_finish; + plugin_class->disable_repository_async = gs_plugin_flatpak_disable_repository_async; + plugin_class->disable_repository_finish = gs_plugin_flatpak_disable_repository_finish; + plugin_class->refine_categories_async = gs_plugin_flatpak_refine_categories_async; + plugin_class->refine_categories_finish = gs_plugin_flatpak_refine_categories_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_FLATPAK; +} diff --git a/plugins/flatpak/gs-plugin-flatpak.h b/plugins/flatpak/gs-plugin-flatpak.h new file mode 100644 index 0000000..8426156 --- /dev/null +++ b/plugins/flatpak/gs-plugin-flatpak.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_FLATPAK (gs_plugin_flatpak_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginFlatpak, gs_plugin_flatpak, GS, PLUGIN_FLATPAK, GsPlugin) + +G_END_DECLS diff --git a/plugins/flatpak/gs-self-test.c b/plugins/flatpak/gs-self-test.c new file mode 100644 index 0000000..6f4bd7f --- /dev/null +++ b/plugins/flatpak/gs-self-test.c @@ -0,0 +1,2003 @@ +/* -*- 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) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> + +#include "gnome-software-private.h" + +#include "gs-flatpak-app.h" + +#include "gs-test.h" + +const gchar * const allowlist[] = { + "appstream", + "flatpak", + "icons", + NULL +}; + +static gboolean +gs_flatpak_test_write_repo_file (const gchar *fn, const gchar *testdir, GFile **file_out, GError **error) +{ + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *path = NULL; + + /* create file */ + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + g_string_append (str, "[Flatpak Repo]\n"); + g_string_append (str, "Title=foo-bar\n"); + g_string_append (str, "Comment=Longer one line comment\n"); + g_string_append (str, "Description=Longer multiline comment that " + "does into detail.\n"); + g_string_append (str, "DefaultBranch=master\n"); + g_string_append_printf (str, "Url=%s\n", testdir_repourl); + g_string_append (str, "Homepage=http://foo.bar\n"); + + path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), fn, NULL); + *file_out = g_file_new_for_path (path); + + return g_file_set_contents (path, str->str, -1, error); +} + +static gboolean +gs_flatpak_test_write_ref_file (const gchar *filename, const gchar *url, const gchar *runtimerepo, GFile **file_out, GError **error) +{ + g_autoptr(GString) str = g_string_new (NULL); + g_autofree gchar *path = NULL; + + g_return_val_if_fail (filename != NULL, FALSE); + g_return_val_if_fail (url != NULL, FALSE); + g_return_val_if_fail (runtimerepo != NULL, FALSE); + + g_string_append (str, "[Flatpak Ref]\n"); + g_string_append (str, "Title=Chiron\n"); + g_string_append (str, "Name=org.test.Chiron\n"); + g_string_append (str, "Branch=master\n"); + g_string_append_printf (str, "Url=%s\n", url); + g_string_append (str, "IsRuntime=false\n"); + g_string_append (str, "Comment=Single line synopsis\n"); + g_string_append (str, "Description=A Testing Application\n"); + g_string_append (str, "Icon=https://getfedora.org/static/images/fedora-logotext.png\n"); + g_string_append_printf (str, "RuntimeRepo=%s\n", runtimerepo); + + path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), filename, NULL); + *file_out = g_file_new_for_path (path); + + return g_file_set_contents (path, str->str, -1, error); +} + +/* create duplicate file as if downloaded in firefox */ +static void +gs_plugins_flatpak_repo_non_ascii_func (GsPluginLoader *plugin_loader) +{ + const gchar *fn = "example (1)….flatpakrepo"; + gboolean ret; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get a resolvable */ + testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir == NULL) + return; + + ret = gs_flatpak_test_write_repo_file (fn, testdir, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpstr (gs_app_get_unique_id (app), ==, "*/*/*/example__1____/master"); +} + +static void +gs_plugins_flatpak_repo_func (GsPluginLoader *plugin_loader) +{ + const gchar *group_name = "remote \"example\""; + const gchar *root = NULL; + const gchar *fn = "example.flatpakrepo"; + gboolean ret; + g_autofree gchar *config_fn = NULL; + g_autofree gchar *remote_url = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GIcon) icon = NULL; + g_autoptr(GsPlugin) management_plugin = NULL; + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* get a resolvable */ + testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + + /* create file */ + ret = gs_flatpak_test_write_repo_file (fn, testdir, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* load local file */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_REPOSITORY); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app), ==, "example"); + management_plugin = gs_app_dup_management_plugin (app); + g_assert_nonnull (management_plugin); + g_assert_cmpstr (gs_plugin_get_name (management_plugin), ==, "flatpak"); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://foo.bar"); + g_assert_cmpstr (gs_app_get_name (app), ==, "foo-bar"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Longer one line comment"); + g_assert_cmpstr (gs_app_get_description (app), ==, + "Longer multiline comment that does into detail."); + g_assert_true (gs_app_get_local_file (app) != NULL); + icon = gs_app_get_icon_for_size (app, 64, 1, NULL); + g_assert_nonnull (icon); + + /* now install the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + + /* check config file was updated */ + root = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"); + config_fn = g_build_filename (root, "flatpak", "repo", "config", NULL); + kf = g_key_file_new (); + ret = g_key_file_load_from_file (kf, config_fn, 0, &error); + g_assert_no_error (error); + g_assert_true (ret); + + g_assert_true (g_key_file_has_group (kf, "core")); + g_assert_true (g_key_file_has_group (kf, group_name)); + g_assert_true (!g_key_file_get_boolean (kf, group_name, "gpg-verify", NULL)); + + /* check the URL was unmangled */ + remote_url = g_key_file_get_string (kf, group_name, "url", &error); + g_assert_no_error (error); + g_assert_cmpstr (remote_url, ==, testdir_repourl); + + /* try again, check state is correct */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app2 = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app2 != NULL); + g_assert_cmpint (gs_app_get_state (app2), ==, GS_APP_STATE_INSTALLED); + + /* disable repo */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_DISABLE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + + /* enable repo */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_ENABLE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + + /* remove it */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNAVAILABLE); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); +} + +static void +progress_notify_cb (GObject *obj, GParamSpec *pspec, gpointer user_data) +{ + gboolean *seen_unknown = user_data; + GsApp *app = GS_APP (obj); + + if (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN) + *seen_unknown = TRUE; +} + +static void +gs_plugins_flatpak_app_with_runtime_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *runtime; + const gchar *root; + gboolean ret; + gint kf_remote_repo_version; + g_autofree gchar *changed_fn = NULL; + g_autofree gchar *config_fn = NULL; + g_autofree gchar *desktop_fn = NULL; + g_autofree gchar *kf_remote_url = NULL; + g_autofree gchar *metadata_fn = NULL; + g_autofree gchar *repodir_fn = NULL; + g_autofree gchar *runtime_fn = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GKeyFile) kf1 = g_key_file_new (); + g_autoptr(GKeyFile) kf2 = g_key_file_new (); + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsAppList) list_all = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + gulong signal_id; + gboolean seen_unknown; + GsPlugin *plugin; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* no files to use */ + repodir_fn = gs_test_get_filename (TESTDATADIR, "app-with-runtime/repo"); + if (repodir_fn == NULL || + !g_file_test (repodir_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* check changed file exists */ + root = g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"); + changed_fn = g_build_filename (root, "flatpak", ".changed", NULL); + g_assert_true (g_file_test (changed_fn, G_FILE_TEST_IS_REGULAR)); + + /* check repo is set up */ + config_fn = g_build_filename (root, "flatpak", "repo", "config", NULL); + ret = g_key_file_load_from_file (kf1, config_fn, G_KEY_FILE_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + kf_remote_repo_version = g_key_file_get_integer (kf1, "core", "repo_version", &error); + g_assert_no_error (error); + g_assert_cmpint (kf_remote_repo_version, ==, 1); + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak"); + gs_app_set_management_plugin (app_source, plugin); + gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, testdir_repourl); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED); + + /* check remote was set up */ + ret = g_key_file_load_from_file (kf2, config_fn, G_KEY_FILE_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + kf_remote_url = g_key_file_get_string (kf2, "remote \"test\"", "url", &error); + g_assert_no_error (error); + g_assert_cmpstr (kf_remote_url, !=, NULL); + + /* check the source now exists */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 1); + app = gs_app_list_index (sources, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "test"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_REPOSITORY); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64, + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* all the apps should have the flatpak keyword */ + g_object_unref (plugin_job); + + keywords[0] = "flatpak"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + list_all = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (list_all != NULL); + g_assert_cmpint (gs_app_list_length (list_all), ==, 2); + + /* find available application */ + g_object_unref (plugin_job); + + keywords[0] = "Bingo"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpint ((gint64) gs_app_get_kudos (app), ==, + GS_APP_KUDO_MY_LANGUAGE | + GS_APP_KUDO_HAS_KEYWORDS | + GS_APP_KUDO_HI_DPI_ICON | + GS_APP_KUDO_SANDBOXED_SECURE | + GS_APP_KUDO_SANDBOXED); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL); + g_assert_cmpint (gs_app_get_update_urgency (app), ==, AS_URGENCY_KIND_UNKNOWN); + + /* check runtime */ + runtime = gs_app_get_runtime (app); + g_assert_true (runtime != NULL); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* install, also installing runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN || + gs_app_get_progress (app) == 100); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED); + + /* check the application exists in the right places */ + metadata_fn = g_build_filename (root, + "flatpak", + "app", + "org.test.Chiron", + "current", + "active", + "metadata", + NULL); + g_assert_true (g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR)); + desktop_fn = g_build_filename (root, + "flatpak", + "app", + "org.test.Chiron", + "current", + "active", + "export", + "share", + "applications", + "org.test.Chiron.desktop", + NULL); + g_assert_true (g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR)); + + /* check the runtime was installed as well */ + runtime_fn = g_build_filename (root, + "flatpak", + "runtime", + "org.test.Runtime", + "x86_64", + "master", + "active", + "files", + "share", + "libtest", + "README", + NULL); + g_assert_true (g_file_test (runtime_fn, G_FILE_TEST_IS_REGULAR)); + + /* remove the application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED); + g_assert_true (!g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR)); + g_assert_true (!g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR)); + + /* install again, to check whether the progress gets initialized; + * since installation happens in another thread, we have to monitor all + * changes to the progress and see if we see the one we want */ + seen_unknown = (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN); + signal_id = g_signal_connect (app, "notify::progress", + G_CALLBACK (progress_notify_cb), &seen_unknown); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + + /* progress should be set to unknown right before installing */ + while (!seen_unknown) + g_main_context_iteration (NULL, TRUE); + g_assert_true (seen_unknown); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN || + gs_app_get_progress (app) == 100); + g_signal_handler_disconnect (app, signal_id); + + /* remove the application */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED); + g_assert_true (!g_file_test (metadata_fn, G_FILE_TEST_IS_REGULAR)); + g_assert_true (!g_file_test (desktop_fn, G_FILE_TEST_IS_REGULAR)); + + /* remove the remote (fail, as the runtime is still installed) */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED); + g_assert_true (!ret); + g_clear_error (&error); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE); +} + +static void +gs_plugins_flatpak_app_missing_runtime_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + gboolean ret; + g_autofree gchar *repodir_fn = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPlugin *plugin; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* no files to use */ + repodir_fn = gs_test_get_filename (TESTDATADIR, "app-missing-runtime/repo"); + if (repodir_fn == NULL || + !g_file_test (repodir_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "app-missing-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak"); + gs_app_set_management_plugin (app_source, plugin); + gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, testdir_repourl); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64, + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + + keywords[0] = "Bingo"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + + /* install, also installing runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED); + g_assert_true (!ret); + g_clear_error (&error); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE); +} + +static void +update_app_progress_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data) +{ + g_debug ("progress now %u%%", gs_app_get_progress (app)); + if (user_data != NULL) { + guint *tmp = (guint *) user_data; + (*tmp)++; + } +} + +static void +update_app_state_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data) +{ + GsAppState state = gs_app_get_state (app); + g_debug ("state now %s", gs_app_state_to_string (state)); + if (state == GS_APP_STATE_INSTALLING) { + gboolean *tmp = (gboolean *) user_data; + *tmp = TRUE; + } +} + +static gboolean +update_app_action_delay_cb (gpointer user_data) +{ + GMainLoop *loop = (GMainLoop *) user_data; + g_main_loop_quit (loop); + return FALSE; +} + +static void +update_app_action_finish_sync (GObject *source, GAsyncResult *res, gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + gboolean ret; + g_autoptr(GError) error = NULL; + ret = gs_plugin_loader_job_action_finish (plugin_loader, res, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_timeout_add_seconds (5, update_app_action_delay_cb, user_data); +} + +static void +gs_plugins_flatpak_runtime_repo_func (GsPluginLoader *plugin_loader) +{ + GsApp *app_source; + GsApp *runtime; + const gchar *fn_ref = "test.flatpakref"; + const gchar *fn_repo = "test.flatpakrepo"; + gboolean ret; + g_autoptr(GFile) fn_repo_file = NULL; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE); + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) sources2 = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* write a flatpakrepo file */ + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir, &fn_repo_file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (fn_repo_file); + testdir2 = gs_test_get_filename (TESTDATADIR, "app-missing-runtime"); + if (testdir2 == NULL) + return; + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + ret = gs_flatpak_test_write_ref_file (fn_ref, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app), + "user/flatpak/*/org.test.Chiron/master")); + g_assert_true (gs_app_get_local_file (app) != NULL); + + /* get runtime */ + runtime = gs_app_get_runtime (app); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, + NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 0); + + /* install, which will install the runtime from the new remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + NULL, + update_app_action_finish_sync, + loop); + g_main_loop_run (loop); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources2 != NULL); + g_assert_cmpint (gs_app_list_length (sources2), ==, 1); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNKNOWN); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* remove the remote */ + app_source = gs_app_list_index (sources2, 0); + g_assert_true (app_source != NULL); + g_assert_cmpstr (gs_app_get_unique_id (app_source), ==, "user/flatpak/*/test/*"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE); +} + +/* same as gs_plugins_flatpak_runtime_repo_func, but this time manually + * installing the flatpakrepo BEFORE the flatpakref is installed */ +static void +gs_plugins_flatpak_runtime_repo_redundant_func (GsPluginLoader *plugin_loader) +{ + GsApp *app_source; + GsApp *runtime; + const gchar *fn_ref = "test.flatpakref"; + const gchar *fn_repo = "test.flatpakrepo"; + gboolean ret; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFile) file_repo = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_src = NULL; + g_autoptr(GsAppList) sources2 = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* write a flatpakrepo file */ + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir, &file_repo, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file_repo, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app_src = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app_src != NULL); + g_assert_cmpint (gs_app_get_kind (app_src), ==, AS_COMPONENT_KIND_REPOSITORY); + g_assert_cmpint (gs_app_get_state (app_src), ==, GS_APP_STATE_AVAILABLE_LOCAL); + g_assert_cmpstr (gs_app_get_id (app_src), ==, "test"); + g_assert_cmpstr (gs_app_get_unique_id (app_src), ==, "*/*/*/test/master"); + g_assert_true (gs_app_get_local_file (app_src) != NULL); + + /* install the source manually */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_src, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);; + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_src), ==, GS_APP_STATE_INSTALLED); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (file_repo); + testdir2 = gs_test_get_filename (TESTDATADIR, "app-missing-runtime"); + if (testdir2 == NULL) + return; + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + ret = gs_flatpak_test_write_ref_file (fn_ref, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app), + "user/flatpak/*/org.test.Chiron/master")); + g_assert_true (gs_app_get_local_file (app) != NULL); + + /* get runtime */ + runtime = gs_app_get_runtime (app); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, + NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 1); /* repo */ + + /* install, which will NOT install the runtime from the RuntimeRemote, + * but from the existing test repo */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED); + + /* check the number of sources */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources2 != NULL); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNKNOWN); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* remove the remote */ + app_source = gs_app_list_index (sources2, 0); + g_assert_true (app_source != NULL); + g_assert_cmpstr (gs_app_get_unique_id (app_source), ==, "user/flatpak/*/test/*"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE); +} + +static void +gs_plugins_flatpak_broken_remote_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + const gchar *fn = "test.flatpakref"; + const gchar *fn_repo = "test.flatpakrepo"; + g_autoptr(GFile) fn_repo_file = NULL; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + g_autofree gchar *testdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPlugin *plugin; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* add a remote with only the runtime in */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak"); + gs_app_set_management_plugin (app_source, plugin); + gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, "file:///wont/work"); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED); + + /* write a flatpakrepo file (the flatpakref below must have a RuntimeRepo= + * to avoid a warning) */ + testdir2 = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir2 == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir2, &fn_repo_file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (fn_repo_file); + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + ret = gs_flatpak_test_write_ref_file (fn, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* convert it to a GsApp */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app), + "user/flatpak/test/org.test.Chiron/master")); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_description (app), ==, "Long description."); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (gs_app_get_local_file (app) != NULL); + + /* remove source */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +flatpak_bundle_or_ref_helper (GsPluginLoader *plugin_loader, + gboolean is_bundle) +{ + GsApp *app_tmp; + GsApp *runtime; + gboolean ret; + GsPluginRefineFlags refine_flags; + g_autofree gchar *fn = NULL; + g_autofree gchar *testdir = NULL; + g_autofree gchar *testdir_repourl = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) search1 = NULL; + g_autoptr(GsAppList) search2 = NULL; + g_autoptr(GsAppList) sources = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsPlugin *plugin; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* add a remote with only the runtime in */ + app_source = gs_flatpak_app_new ("test"); + testdir = gs_test_get_filename (TESTDATADIR, "only-runtime"); + if (testdir == NULL) + return; + testdir_repourl = g_strdup_printf ("file://%s/repo", testdir); + gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak"); + gs_app_set_management_plugin (app_source, plugin); + gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE); + gs_flatpak_app_set_repo_url (app_source, testdir_repourl); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refresh_metadata_new (0, + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + + keywords[0] = "runtime"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak runtime */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + runtime = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (runtime), ==, "org.test.Runtime"); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* install the runtime ahead of time */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED); + + if (is_bundle) { + /* find the flatpak bundle file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron.flatpak"); + g_assert_true (fn != NULL); + file = g_file_new_for_path (fn); + refine_flags = GS_PLUGIN_REFINE_FLAGS_NONE; + } else { + const gchar *fn_repo = "test.flatpakrepo"; + g_autoptr(GFile) fn_repo_file = NULL; + g_autofree gchar *fn_repourl = NULL; + g_autofree gchar *testdir2 = NULL; + g_autofree gchar *testdir2_repourl = NULL; + + /* write a flatpakrepo file (the flatpakref below must have a RuntimeRepo= + * to avoid a warning) */ + testdir2 = gs_test_get_filename (TESTDATADIR, "app-with-runtime"); + if (testdir2 == NULL) + return; + ret = gs_flatpak_test_write_repo_file (fn_repo, testdir2, &fn_repo_file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* write a flatpakref file */ + fn_repourl = g_file_get_uri (fn_repo_file); + testdir2_repourl = g_strdup_printf ("file://%s/repo", testdir2); + fn = g_strdup ("test.flatpakref"); + ret = gs_flatpak_test_write_ref_file (fn, testdir2_repourl, fn_repourl, &file, &error); + g_assert_no_error (error); + g_assert_true (ret); + + refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME; + } + + /* Wait for the flatpak changes to be delivered through the file + monitor notifications, which will cleanup plugin cache. */ + g_usleep (G_USEC_PER_SEC); + + /* convert it to a GsApp */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", refine_flags, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (app != NULL); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DESKTOP_APP); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (gs_app_get_local_file (app) != NULL); + if (is_bundle) { + /* Note: The origin is set to "flatpak" here because an origin remote + * won't be created until the app is installed. + */ + g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app), + "user/flatpak/flatpak/org.test.Chiron/master")); + g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_BUNDLE); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE_LOCAL); + } else { + g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app), + "user/flatpak/test/org.test.Chiron/master")); + g_assert_true (gs_flatpak_app_get_file_kind (app) == GS_FLATPAK_APP_FILE_KIND_REF); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_description (app), ==, "Long description."); + } + + /* get runtime */ + runtime = gs_app_get_runtime (app); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/master"); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_INSTALLED); + + /* install */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL); + + /* search for the application */ + g_object_unref (plugin_job); + + keywords[0] = "chiron"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + search1 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (search1 != NULL); + g_assert_cmpint (gs_app_list_length (search1), ==, 1); + app_tmp = gs_app_list_index (search1, 0); + g_assert_cmpstr (gs_app_get_id (app_tmp), ==, "org.test.Chiron"); + + /* convert it to a GsApp again, and get the installed thing */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + NULL); + app2 = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (app2 != NULL); + g_assert_cmpint (gs_app_get_state (app2), ==, GS_APP_STATE_INSTALLED); + if (is_bundle) { + g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app2), + "user/flatpak/chiron-origin/org.test.Chiron/master")); + } else { + /* Note: the origin is now test-1 because that remote was created from the + * RuntimeRepo= setting + */ + g_assert_true (as_utils_data_id_equal (gs_app_get_unique_id (app2), + "user/flatpak/test-1/org.test.Chiron/master")); + } + + /* remove app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app2, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove source */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + if (!is_bundle) { + /* remove remote added by RuntimeRepo= in flatpakref */ + g_autoptr(GsApp) runtime_source = gs_flatpak_app_new ("test-1"); + gs_app_set_kind (runtime_source, AS_COMPONENT_KIND_REPOSITORY); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak"); + gs_app_set_management_plugin (runtime_source, plugin); + gs_app_set_state (runtime_source, GS_APP_STATE_INSTALLED); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (runtime_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + } + + /* there should be no sources now */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, NULL); + sources = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (sources != NULL); + g_assert_cmpint (gs_app_list_length (sources), ==, 0); + + /* there should be no matches now */ + g_object_unref (plugin_job); + + keywords[0] = "chiron"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + search2 = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (search2 != NULL); + g_assert_cmpint (gs_app_list_length (search2), ==, 0); +} + +static void +gs_plugins_flatpak_ref_func (GsPluginLoader *plugin_loader) +{ + flatpak_bundle_or_ref_helper (plugin_loader, FALSE); +} + +static void +gs_plugins_flatpak_bundle_func (GsPluginLoader *plugin_loader) +{ + flatpak_bundle_or_ref_helper (plugin_loader, TRUE); +} + +static void +gs_plugins_flatpak_count_signal_cb (GsPluginLoader *plugin_loader, guint *cnt) +{ + if (cnt != NULL) + (*cnt)++; +} + +static void +gs_plugins_flatpak_app_update_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *app_tmp; + GsApp *runtime; + gboolean got_progress_installing = FALSE; + gboolean ret; + guint notify_progress_id; + guint notify_state_id; + guint pending_app_changed_cnt = 0; + guint pending_apps_changed_id; + guint progress_cnt = 0; + guint updates_changed_cnt = 0; + guint updates_changed_id; + g_autofree gchar *repodir1_fn = NULL; + g_autofree gchar *repodir2_fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsApp) old_runtime = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_updates = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE); + g_autofree gchar *repo_path = NULL; + g_autofree gchar *repo_url = NULL; + GsPlugin *plugin; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* no flatpak, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "flatpak")) + return; + + /* no files to use */ + repodir1_fn = gs_test_get_filename (TESTDATADIR, "app-with-runtime/repo"); + if (repodir1_fn == NULL || + !g_file_test (repodir1_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + repodir2_fn = gs_test_get_filename (TESTDATADIR, "app-update/repo"); + if (repodir2_fn == NULL || + !g_file_test (repodir2_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* add indirection so we can switch this after install */ + repo_path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), "repo", NULL); + unlink (repo_path); + g_assert_true (symlink (repodir1_fn, repo_path) == 0); + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak"); + gs_app_set_management_plugin (app_source, plugin); + gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE); + repo_url = g_strdup_printf ("file://%s", repo_path); + gs_flatpak_app_set_repo_url (app_source, repo_url); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64, + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + + keywords[0] = "Bingo"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (list != NULL); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + + /* install, also installing runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL); + + /* switch to the new repo */ + g_assert_true (unlink (repo_path) == 0); + g_assert_true (symlink (repodir2_fn, repo_path) == 0); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refresh_metadata_new (0, /* force now */ + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* get the updates list */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + list_updates = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (list_updates != NULL); + + /* make sure there is one entry */ + g_assert_cmpint (gs_app_list_length (list_updates), ==, 1); + for (guint i = 0; i < gs_app_list_length (list_updates); i++) { + app_tmp = gs_app_list_index (list_updates, i); + g_debug ("got update %s", gs_app_get_unique_id (app_tmp)); + } + + /* check that the runtime is not the update's one */ + old_runtime = gs_app_get_runtime (app); + g_assert_true (old_runtime != NULL); + g_object_ref (old_runtime); + g_assert_cmpstr (gs_app_get_branch (old_runtime), !=, "new_master"); + + /* use the returned app, which can be a different object instance from previously */ + app = gs_app_list_lookup (list_updates, "*/flatpak/test/org.test.Chiron/*"); + g_assert_nonnull (app); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE_LIVE); + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, "Version 1.2.4:\nThis is best."); + g_assert_cmpstr (gs_app_get_update_version (app), ==, "1.2.4"); + + /* care about signals */ + pending_apps_changed_id = + g_signal_connect (plugin_loader, "pending-apps-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &pending_app_changed_cnt); + updates_changed_id = + g_signal_connect (plugin_loader, "updates-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &updates_changed_cnt); + notify_state_id = + g_signal_connect (app, "notify::state", + G_CALLBACK (update_app_state_notify_cb), + &got_progress_installing); + notify_progress_id = + g_signal_connect (app, "notify::progress", + G_CALLBACK (update_app_progress_notify_cb), + &progress_cnt); + + /* use a mainloop so we get the events in the default context */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + NULL, + update_app_action_finish_sync, + loop); + g_main_loop_run (loop); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.4"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, NULL); + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, NULL); + g_assert_true (gs_app_get_progress (app) == GS_APP_PROGRESS_UNKNOWN || + gs_app_get_progress (app) == 100); + g_assert_true (got_progress_installing); + //g_assert_cmpint (progress_cnt, >, 20); //FIXME: bug in OSTree + g_assert_cmpint (pending_app_changed_cnt, ==, 0); + g_assert_cmpint (updates_changed_cnt, ==, 1); + + /* check that the app's runtime has changed */ + runtime = gs_app_get_runtime (app); + g_assert_true (runtime != NULL); + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/new_master"); + g_assert_true (old_runtime != runtime); + g_assert_cmpstr (gs_app_get_branch (runtime), ==, "new_master"); + g_assert_true (gs_app_get_state (runtime) == GS_APP_STATE_INSTALLED); + + /* no longer care */ + g_signal_handler_disconnect (plugin_loader, pending_apps_changed_id); + g_signal_handler_disconnect (plugin_loader, updates_changed_id); + g_signal_handler_disconnect (app, notify_state_id); + g_signal_handler_disconnect (app, notify_progress_id); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the old_runtime */ + g_assert_cmpstr (gs_app_get_unique_id (old_runtime), ==, "user/flatpak/test/org.test.Runtime/master"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", old_runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the runtime */ + g_assert_cmpstr (gs_app_get_unique_id (runtime), ==, "user/flatpak/test/org.test.Runtime/new_master"); + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE); +} + +static void +gs_plugins_flatpak_runtime_extension_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + GsApp *runtime; + GsApp *app_tmp; + gboolean got_progress_installing = FALSE; + gboolean ret; + guint notify_progress_id; + guint notify_state_id; + guint pending_app_changed_cnt = 0; + guint pending_apps_changed_id; + guint progress_cnt = 0; + guint updates_changed_cnt = 0; + guint updates_changed_id; + g_autofree gchar *repodir1_fn = NULL; + g_autofree gchar *repodir2_fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_source = NULL; + g_autoptr(GsApp) extension = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_updates = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE); + g_autofree gchar *repo_path = NULL; + g_autofree gchar *repo_url = NULL; + GsPlugin *plugin; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_test_reinitialise_plugin_loader (plugin_loader, allowlist, NULL); + + /* no flatpak, abort */ + g_assert_true (gs_plugin_loader_get_enabled (plugin_loader, "flatpak")); + + /* no files to use */ + repodir1_fn = gs_test_get_filename (TESTDATADIR, "app-extension/repo"); + if (repodir1_fn == NULL || + !g_file_test (repodir1_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + repodir2_fn = gs_test_get_filename (TESTDATADIR, "app-extension-update/repo"); + if (repodir2_fn == NULL || + !g_file_test (repodir2_fn, G_FILE_TEST_EXISTS)) { + g_test_skip ("no flatpak test repo"); + return; + } + + /* add indirection so we can switch this after install */ + repo_path = g_build_filename (g_getenv ("GS_SELF_TEST_FLATPAK_DATADIR"), "repo", NULL); + g_assert_cmpint (symlink (repodir1_fn, repo_path), ==, 0); + + /* add a remote */ + app_source = gs_flatpak_app_new ("test"); + gs_app_set_kind (app_source, AS_COMPONENT_KIND_REPOSITORY); + plugin = gs_plugin_loader_find_plugin (plugin_loader, "flatpak"); + gs_app_set_management_plugin (app_source, plugin); + gs_app_set_state (app_source, GS_APP_STATE_AVAILABLE); + repo_url = g_strdup_printf ("file://%s", repo_path); + gs_flatpak_app_set_repo_url (app_source, repo_url); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_INSTALLED); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refresh_metadata_new (G_MAXUINT64, + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + + /* find available application */ + g_object_unref (plugin_job); + + keywords[0] = "Bingo"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (list); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + + /* install, also installing runtime and suggested extensions */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + + /* check if the extension was installed */ + extension = gs_plugin_loader_app_create (plugin_loader, + "user/flatpak/*/org.test.Chiron.Extension/master", + NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (extension); + + g_assert_cmpint (gs_app_get_state (extension), ==, GS_APP_STATE_INSTALLED); + + /* switch to the new repo (to get the update) */ + g_assert_cmpint (unlink (repo_path), ==, 0); + g_assert_cmpint (symlink (repodir2_fn, repo_path), ==, 0); + + /* refresh the appstream metadata */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_refresh_metadata_new (0, /* force now */ + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* get the updates list */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + list_updates = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (list_updates); + + g_assert_cmpint (gs_app_list_length (list_updates), ==, 1); + for (guint i = 0; i < gs_app_list_length (list_updates); i++) { + app_tmp = gs_app_list_index (list_updates, i); + g_debug ("got update %s", gs_app_get_unique_id (app_tmp)); + } + + /* check that the extension has no update */ + app_tmp = gs_app_list_lookup (list_updates, "*/flatpak/test/org.test.Chiron.Extension/*"); + g_assert_null (app_tmp); + + /* check that the app has an update (it's affected by the extension's update) */ + app = gs_app_list_lookup (list_updates, "*/flatpak/test/org.test.Chiron/*"); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UPDATABLE_LIVE); + + /* care about signals */ + pending_apps_changed_id = + g_signal_connect (plugin_loader, "pending-apps-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &pending_app_changed_cnt); + updates_changed_id = + g_signal_connect (plugin_loader, "updates-changed", + G_CALLBACK (gs_plugins_flatpak_count_signal_cb), + &updates_changed_cnt); + notify_state_id = + g_signal_connect (app, "notify::state", + G_CALLBACK (update_app_state_notify_cb), + &got_progress_installing); + notify_progress_id = + g_signal_connect (app, "notify::progress", + G_CALLBACK (update_app_progress_notify_cb), + &progress_cnt); + + /* use a mainloop so we get the events in the default context */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (plugin_loader, plugin_job, + NULL, + update_app_action_finish_sync, + loop); + g_main_loop_run (loop); + gs_test_flush_main_context (); + + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.2.3"); + g_assert_true (got_progress_installing); + g_assert_cmpint (pending_app_changed_cnt, ==, 0); + + /* The install refreshes GsApp-s cache, thus re-get the extension */ + g_clear_object (&extension); + extension = gs_plugin_loader_app_create (plugin_loader, + "user/flatpak/*/org.test.Chiron.Extension/master", + NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (extension); + + /* check the extension's state after the update */ + g_assert_cmpint (gs_app_get_state (extension), ==, GS_APP_STATE_INSTALLED); + + /* no longer care */ + g_signal_handler_disconnect (plugin_loader, pending_apps_changed_id); + g_signal_handler_disconnect (plugin_loader, updates_changed_id); + g_signal_handler_disconnect (app, notify_state_id); + g_signal_handler_disconnect (app, notify_progress_id); + + g_clear_object (&list); + /* Reload the 'app', as it could change due to repo change */ + g_object_unref (plugin_job); + + keywords[0] = "Bingo"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + g_clear_object (&query); + + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (list); + + /* make sure there is one entry, the flatpak app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.test.Chiron"); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + + /* getting the runtime for later removal */ + runtime = gs_app_get_runtime (app); + g_assert_nonnull (runtime); + + /* remove the app */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* remove the runtime */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", runtime, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (runtime), ==, GS_APP_STATE_AVAILABLE); + + /* remove the remote */ + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_manage_repository_new (app_source, GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (gs_app_get_state (app_source), ==, GS_APP_STATE_UNAVAILABLE); + + /* verify that the extension has been removed by the app's removal */ + g_assert_false (gs_app_is_installed (extension)); +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *xml = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ + g_content_type_set_mime_dirs (NULL); + + /* Similarly, add the system-wide icon theme path before it’s + * overwritten by %G_TEST_OPTION_ISOLATE_DIRS. */ + gs_test_expose_icon_theme_paths (); + + gs_test_init (&argc, &argv); + g_setenv ("GS_XMLB_VERBOSE", "1", TRUE); + g_setenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD", "1", TRUE); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-flatpak-test-XXXXXX", NULL); + g_assert_true (tmp_root != NULL); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + g_setenv ("GS_SELF_TEST_FLATPAK_DATADIR", tmp_root, TRUE); + + /* allow dist'ing with no gnome-software installed */ + if (g_getenv ("GS_SELF_TEST_SKIP_ALL") != NULL) + return 0; + + xml = g_strdup ("<?xml version=\"1.0\"?>\n" + "<components version=\"0.9\">\n" + " <component type=\"desktop\">\n" + " <id>zeus.desktop</id>\n" + " <name>Zeus</name>\n" + " <summary>A teaching application</summary>\n" + " </component>\n" + "</components>\n"); + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-with-runtime", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_app_with_runtime_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-missing-runtime", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_app_missing_runtime_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/ref", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_ref_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/bundle", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_bundle_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/broken-remote", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_broken_remote_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/runtime-repo", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_runtime_repo_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/runtime-repo-redundant", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_runtime_repo_redundant_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-runtime-extension", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_runtime_extension_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/app-update-runtime", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_app_update_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/repo", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_repo_func); + g_test_add_data_func ("/gnome-software/plugins/flatpak/repo{non-ascii}", + plugin_loader, + (GTestDataFunc) gs_plugins_flatpak_repo_non_ascii_func); + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/flatpak/meson.build b/plugins/flatpak/meson.build new file mode 100644 index 0000000..6a0baed --- /dev/null +++ b/plugins/flatpak/meson.build @@ -0,0 +1,63 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginFlatpak"'] +deps = [ + plugin_libs, + flatpak, + libxmlb, + ostree, +] + +if get_option('mogwai') + deps += mogwai_schedule_client +endif + +shared_module( + 'gs_plugin_flatpak', + sources : [ + 'gs-flatpak-app.c', + 'gs-flatpak.c', + 'gs-flatpak-transaction.c', + 'gs-flatpak-utils.c', + 'gs-plugin-flatpak.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : deps, +) +metainfo = 'org.gnome.Software.Plugin.Flatpak.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +if get_option('tests') + subdir('tests') + + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_build_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-flatpak', + compiled_schemas, + sources : [ + 'gs-flatpak-app.c', + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : deps, + c_args : cargs, + ) + test('gs-self-test-flatpak', e, suite: ['plugins', 'flatpak'], env: test_env, timeout : 120) +endif diff --git a/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in b/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in new file mode 100644 index 0000000..44d6d03 --- /dev/null +++ b/plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Flatpak</id> + <extends>org.gnome.Software.desktop</extends> + <name>Flatpak Support</name> + <summary>Flatpak is a framework for desktop applications on Linux</summary> + <url type="homepage">http://flatpak.org/</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>richard_at_hughsie.com</update_contact> +</component> diff --git a/plugins/flatpak/tests/app-extension-update/.gitignore b/plugins/flatpak/tests/app-extension-update/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore new file mode 100644 index 0000000..db00ec8 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/.gitignore @@ -0,0 +1 @@ +files/share/app-info diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/.empty diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README new file mode 100644 index 0000000..a0b9703 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/libtest/README @@ -0,0 +1 @@ +UPDATED!
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml new file mode 100644 index 0000000..d884539 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2017 Endless Mobile, Inc. + Author: Joaquim Rocha <jrocha@endlessm.com> +--> +<component type="runtime"> + <id>org.test.Chiron.Extension</id> + <metadata_license>CC0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron App Extension</name> + <summary>Test extension for flatpak self tests</summary> +</component> + diff --git a/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata new file mode 100644 index 0000000..d81f8f9 --- /dev/null +++ b/plugins/flatpak/tests/app-extension-update/org.test.Chiron.Extension/metadata @@ -0,0 +1,6 @@ +[Runtime] +name=org.test.Chiron.Extension +sdk=org.test.Runtime/x86_64/master + +[ExtensionOf] +ref=app/org.test.Chiron/x86_64/master diff --git a/plugins/flatpak/tests/app-extension/.gitignore b/plugins/flatpak/tests/app-extension/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-extension/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore new file mode 100644 index 0000000..db00ec8 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/.gitignore @@ -0,0 +1 @@ +files/share/app-info diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/.empty diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/libtest/README diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml new file mode 100644 index 0000000..d884539 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/files/share/metainfo/org.test.Chiron.Extension.metainfo.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2017 Endless Mobile, Inc. + Author: Joaquim Rocha <jrocha@endlessm.com> +--> +<component type="runtime"> + <id>org.test.Chiron.Extension</id> + <metadata_license>CC0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron App Extension</name> + <summary>Test extension for flatpak self tests</summary> +</component> + diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata new file mode 100644 index 0000000..d81f8f9 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron.Extension/metadata @@ -0,0 +1,6 @@ +[Runtime] +name=org.test.Chiron.Extension +sdk=org.test.Runtime/x86_64/master + +[ExtensionOf] +ref=app/org.test.Chiron/x86_64/master diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore new file mode 100644 index 0000000..fea15c0 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/.gitignore @@ -0,0 +1,2 @@ +export +files/share/app-info diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh new file mode 100755 index 0000000..e61d501 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/bin/chiron.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello world" diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml new file mode 100644 index 0000000..0d912a8 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="desktop"> + <id>org.test.Chiron.desktop</id> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description><p>Long description.</p></description> + <url type="homepage">http://127.0.0.1/</url> + <releases> + <release date="2014-12-15" version="1.2.3"> + <description><p>This is better.</p></description> + </release> + </releases> +</component> diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop new file mode 100644 index 0000000..2fbdf95 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/applications/org.test.Chiron.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +Name=Chiron +Exec=chiron.sh +Icon=org.test.Chiron +Keywords=Bingo; diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png Binary files differnew file mode 100644 index 0000000..0c38f2f --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png diff --git a/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata b/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata new file mode 100644 index 0000000..45b76d6 --- /dev/null +++ b/plugins/flatpak/tests/app-extension/org.test.Chiron/metadata @@ -0,0 +1,10 @@ +[Application] +name=org.test.Chiron +runtime=org.test.Runtime/x86_64/master +command=chiron.sh + +[Extension org.test.Chiron.Extension] +directory=share/extension +subdirectories=true +version=master +autodelete=true diff --git a/plugins/flatpak/tests/app-missing-runtime/.gitignore b/plugins/flatpak/tests/app-missing-runtime/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-missing-runtime/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron b/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron new file mode 120000 index 0000000..d9384e4 --- /dev/null +++ b/plugins/flatpak/tests/app-missing-runtime/org.test.Chiron @@ -0,0 +1 @@ +../app-with-runtime/org.test.Chiron/
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-update/.gitignore b/plugins/flatpak/tests/app-update/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-update/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore new file mode 100644 index 0000000..fea15c0 --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/.gitignore @@ -0,0 +1,2 @@ +export +files/share/app-info diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh new file mode 100644 index 0000000..dfed21c --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/bin/chiron.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello world, with upgrades" diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml new file mode 100644 index 0000000..74eb9db --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="desktop"> + <id>org.test.Chiron</id> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description><p>Long description.</p></description> + <url type="homepage">http://127.0.0.1/</url> + <releases> + <release date="2015-02-13" version="1.2.4"> + <description><p>This is best.</p></description> + </release> + <release date="2014-12-15" version="1.2.3"> + <description><p>This is better.</p></description> + </release> + </releases> +</component> diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop new file mode 120000 index 0000000..2b06818 --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/applications/org.test.Chiron.desktop @@ -0,0 +1 @@ +../../../../../app-missing-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png new file mode 120000 index 0000000..9c37986 --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png @@ -0,0 +1 @@ +../../../../../../../../app-missing-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png
\ No newline at end of file diff --git a/plugins/flatpak/tests/app-update/org.test.Chiron/metadata b/plugins/flatpak/tests/app-update/org.test.Chiron/metadata new file mode 100644 index 0000000..1de0ab8 --- /dev/null +++ b/plugins/flatpak/tests/app-update/org.test.Chiron/metadata @@ -0,0 +1,4 @@ +[Application] +name=org.test.Chiron +runtime=org.test.Runtime/x86_64/new_master +command=chiron.sh diff --git a/plugins/flatpak/tests/app-with-runtime/.gitignore b/plugins/flatpak/tests/app-with-runtime/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore new file mode 100644 index 0000000..fea15c0 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/.gitignore @@ -0,0 +1,2 @@ +export +files/share/app-info diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh new file mode 100755 index 0000000..e61d501 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/bin/chiron.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello world" diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml new file mode 100644 index 0000000..58af082 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/appdata/org.test.Chiron.appdata.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="desktop"> + <id>org.test.Chiron</id> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description><p>Long description.</p></description> + <url type="homepage">http://127.0.0.1/</url> + <releases> + <release date="2014-12-15" version="1.2.3"> + <description><p>This is better.</p></description> + </release> + </releases> +</component> diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop new file mode 100644 index 0000000..b744766 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/applications/org.test.Chiron.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Chiron +Exec=chiron.sh +Icon=org.test.Chiron +Keywords=Bingo; +X-Flatpak=org.test.Chiron diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png Binary files differnew file mode 100644 index 0000000..0c38f2f --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/files/share/icons/hicolor/128x128/apps/org.test.Chiron.png diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata new file mode 100644 index 0000000..ce57357 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Chiron/metadata @@ -0,0 +1,4 @@ +[Application] +name=org.test.Chiron +runtime=org.test.Runtime/x86_64/master +command=chiron.sh diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/files/.empty diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata new file mode 100644 index 0000000..16f0fa1 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/metadata @@ -0,0 +1,3 @@ +[Runtime] +name=org.test.Runtime +sdk=org.test.Runtime/x86_64/master diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore new file mode 100644 index 0000000..3600b9c --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/.gitignore @@ -0,0 +1 @@ +app-info diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/libtest/README diff --git a/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml new file mode 100644 index 0000000..5d68c60 --- /dev/null +++ b/plugins/flatpak/tests/app-with-runtime/org.test.Runtime/usr/share/metainfo/org.test.Runtime.metainfo.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2017 Richard Hughes <richard@hughsie.com> --> +<component type="runtime"> + <id>org.test.Runtime</id> + <metadata_license>CC0</metadata_license> + <project_license>GPL-2.0+</project_license> + <name>Test runtime</name> + <summary>Test runtime for flatpak self tests</summary> +</component> + diff --git a/plugins/flatpak/tests/build.py b/plugins/flatpak/tests/build.py new file mode 100755 index 0000000..6c6a8dd --- /dev/null +++ b/plugins/flatpak/tests/build.py @@ -0,0 +1,125 @@ +#!/usr/bin/python3 + +import subprocess +import os +import shutil +import configparser + +def build_flatpak(appid, srcdir, repodir, branch='master', cleanrepodir=True): + print('Building %s from %s into %s' % (appid, srcdir, repodir)) + + # delete repodir + if cleanrepodir and os.path.exists(repodir): + print("Deleting %s" % repodir) + shutil.rmtree(repodir) + + # delete exportdir + exportdir = os.path.join(srcdir, appid, 'export') + if os.path.exists(exportdir): + print("Deleting %s" % exportdir) + shutil.rmtree(exportdir) + + metadata_path = os.path.join(srcdir, appid, 'metadata') + metadata = configparser.ConfigParser() + metadata.read(metadata_path) + is_runtime = True if 'Runtime' in metadata.sections() else False + is_extension = True if 'ExtensionOf' in metadata.sections() else False + + # runtimes have different defaults + if is_runtime and not is_extension: + prefix = 'usr' + else: + prefix = 'files' + + # finish the build + argv = ['flatpak', 'build-finish'] + argv.append(os.path.join(srcdir, appid)) + subprocess.call(argv) + + # compose AppStream data + argv = ['appstream-compose'] + argv.append('--origin=flatpak') + argv.append('--basename=%s' % appid) + argv.append('--prefix=%s' % os.path.join(srcdir, appid, prefix)) + argv.append('--output-dir=%s' % os.path.join(srcdir, appid, prefix, 'share/app-info/xmls')) + argv.append(appid) + subprocess.call(argv) + + # export into repo + argv = ['flatpak', 'build-export'] + argv.append(repodir) + argv.append(os.path.join(srcdir, appid)) + argv.append(branch) + argv.append('--update-appstream') + argv.append('--timestamp=2016-09-15T01:02:03') + if is_runtime: + argv.append('--runtime') + subprocess.call(argv) + +def build_flatpak_bundle(appid, srcdir, repodir, filename, branch='master'): + argv = ['flatpak', 'build-bundle'] + argv.append(repodir) + argv.append(filename) + argv.append(appid) + argv.append(branch) + subprocess.call(argv) + +def copy_repo(srcdir, destdir): + srcdir_repo = os.path.join(srcdir, 'repo') + destdir_repo = os.path.join(destdir, 'repo') + print("Copying %s to %s" % (srcdir_repo, destdir_repo)) + if os.path.exists(destdir_repo): + shutil.rmtree(destdir_repo) + shutil.copytree(srcdir_repo, destdir_repo) + +# normal app with runtime in same remote +build_flatpak('org.test.Chiron', + 'app-with-runtime', + 'app-with-runtime/repo') +build_flatpak('org.test.Runtime', + 'app-with-runtime', + 'app-with-runtime/repo', + cleanrepodir=False) + +# build a flatpak bundle for the app +build_flatpak_bundle('org.test.Chiron', + 'app-with-runtime', + 'app-with-runtime/repo', + 'chiron.flatpak') + +# app referencing runtime that cannot be found +build_flatpak('org.test.Chiron', + 'app-with-runtime', + 'app-missing-runtime/repo') + +# app with an update +build_flatpak('org.test.Runtime', + 'app-with-runtime', + 'app-update/repo', + branch='new_master', + cleanrepodir=True) +build_flatpak('org.test.Chiron', + 'app-update', + 'app-update/repo', + cleanrepodir=False) + +# just a runtime present +build_flatpak('org.test.Runtime', + 'only-runtime', + 'only-runtime/repo') + +# app with an extension +copy_repo('only-runtime', 'app-extension') +build_flatpak('org.test.Chiron', + 'app-extension', + 'app-extension/repo', + cleanrepodir=False) +build_flatpak('org.test.Chiron.Extension', + 'app-extension', + 'app-extension/repo', + cleanrepodir=False) +copy_repo('app-extension', 'app-extension-update') +build_flatpak('org.test.Chiron.Extension', + 'app-extension-update', + 'app-extension-update/repo', + cleanrepodir=False) diff --git a/plugins/flatpak/tests/chiron.flatpak b/plugins/flatpak/tests/chiron.flatpak Binary files differnew file mode 100644 index 0000000..ce038e9 --- /dev/null +++ b/plugins/flatpak/tests/chiron.flatpak diff --git a/plugins/flatpak/tests/flatpakrepos.tar.gz b/plugins/flatpak/tests/flatpakrepos.tar.gz Binary files differnew file mode 100644 index 0000000..f8bcfde --- /dev/null +++ b/plugins/flatpak/tests/flatpakrepos.tar.gz diff --git a/plugins/flatpak/tests/meson.build b/plugins/flatpak/tests/meson.build new file mode 100644 index 0000000..9e48b00 --- /dev/null +++ b/plugins/flatpak/tests/meson.build @@ -0,0 +1,34 @@ +tar = find_program('tar') +custom_target( + 'flatpak-self-test-data', + input : 'flatpakrepos.tar.gz', + output : 'done', + command : [ + tar, + '--no-same-owner', + '--directory=' + meson.current_build_dir(), + '-xf', '@INPUT@', + ], + build_by_default : true, +) + +custom_target( + 'flatpak-self-test-bundle', + output : 'flatpakrepos.tar.gz', + command : [ + tar, + '-czf', '@OUTPUT@', + 'app-missing-runtime/repo/', + 'app-update/repo/', + 'app-with-runtime/repo/', + 'only-runtime/repo/', + 'app-extension/repo', + 'app-extension-update/repo', + ], +) + +configure_file( + input : 'chiron.flatpak', + output : 'chiron.flatpak', + copy : true, +) diff --git a/plugins/flatpak/tests/only-runtime/.gitignore b/plugins/flatpak/tests/only-runtime/.gitignore new file mode 100644 index 0000000..f606d5e --- /dev/null +++ b/plugins/flatpak/tests/only-runtime/.gitignore @@ -0,0 +1 @@ +repo diff --git a/plugins/flatpak/tests/only-runtime/org.test.Runtime b/plugins/flatpak/tests/only-runtime/org.test.Runtime new file mode 120000 index 0000000..eb7054c --- /dev/null +++ b/plugins/flatpak/tests/only-runtime/org.test.Runtime @@ -0,0 +1 @@ +../app-with-runtime/org.test.Runtime/
\ No newline at end of file diff --git a/plugins/fwupd/gs-fwupd-app.c b/plugins/fwupd/gs-fwupd-app.c new file mode 100644 index 0000000..c27f5a5 --- /dev/null +++ b/plugins/fwupd/gs-fwupd-app.c @@ -0,0 +1,309 @@ +/* -*- 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+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-fwupd-app.h" + +const gchar * +gs_fwupd_app_get_device_id (GsApp *app) +{ + return gs_app_get_metadata_item (app, "fwupd::DeviceID"); +} + +const gchar * +gs_fwupd_app_get_update_uri (GsApp *app) +{ + return gs_app_get_metadata_item (app, "fwupd::UpdateID"); +} + +gboolean +gs_fwupd_app_get_is_locked (GsApp *app) +{ + GVariant *tmp = gs_app_get_metadata_variant (app, "fwupd::IsLocked"); + if (tmp == NULL) + return FALSE; + return g_variant_get_boolean (tmp); +} + +void +gs_fwupd_app_set_device_id (GsApp *app, const gchar *device_id) +{ + gs_app_set_metadata (app, "fwupd::DeviceID", device_id); +} + +void +gs_fwupd_app_set_update_uri (GsApp *app, const gchar *update_uri) +{ + gs_app_set_metadata (app, "fwupd::UpdateID", update_uri); +} + +void +gs_fwupd_app_set_is_locked (GsApp *app, gboolean is_locked) +{ + g_autoptr(GVariant) tmp = g_variant_new_boolean (is_locked); + gs_app_set_metadata_variant (app, "fwupd::IsLocked", tmp); +} + +void +gs_fwupd_app_set_from_device (GsApp *app, FwupdDevice *dev) +{ + GPtrArray *guids; + + /* something can be done */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_UPDATABLE)) + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + + /* only can be applied in systemd-offline */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_ONLY_OFFLINE)) + gs_app_set_metadata (app, "fwupd::OnlyOffline", ""); + + + /* reboot required to apply update */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + + /* is removable */ + if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_INTERNAL)) + gs_app_add_quirk (app, GS_APP_QUIRK_REMOVABLE_HARDWARE); + + guids = fwupd_device_get_guids (dev); + if (guids->len > 0) { + g_autofree gchar *guid_str = NULL; + g_auto(GStrv) tmp = g_new0 (gchar *, guids->len + 1); + for (guint i = 0; i < guids->len; i++) + tmp[i] = g_strdup (g_ptr_array_index (guids, i)); + guid_str = g_strjoinv (",", tmp); + gs_app_set_metadata (app, "fwupd::Guid", guid_str); + } + if (fwupd_device_get_name (dev) != NULL) { + g_autofree gchar *vendor_name = NULL; + if (fwupd_device_get_vendor (dev) == NULL || + g_str_has_prefix (fwupd_device_get_name (dev), + fwupd_device_get_vendor (dev))) { + vendor_name = g_strdup (fwupd_device_get_name (dev)); + } else { + vendor_name = g_strdup_printf ("%s %s", + fwupd_device_get_vendor (dev), + fwupd_device_get_name (dev)); + } + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, vendor_name); + } + if (fwupd_device_get_summary (dev) != NULL) { + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + fwupd_device_get_summary (dev)); + } + if (fwupd_device_get_version (dev) != NULL) { + gs_app_set_version (app, fwupd_device_get_version (dev)); + } + if (fwupd_device_get_created (dev) != 0) + gs_app_set_install_date (app, fwupd_device_get_created (dev)); + if (fwupd_device_get_description (dev) != NULL) { + g_autofree gchar *tmp = NULL; + tmp = as_markup_convert_simple (fwupd_device_get_description (dev), NULL); + if (tmp != NULL) + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, tmp); + } + + /* needs action */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER)) + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION); + else + gs_app_remove_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION); +} + +static gchar * +gs_fwupd_release_get_name (FwupdRelease *release) +{ + const gchar *name = fwupd_release_get_name (release); + GPtrArray *cats = fwupd_release_get_categories (release); + + for (guint i = 0; i < cats->len; i++) { + const gchar *cat = g_ptr_array_index (cats, i); + if (g_strcmp0 (cat, "X-Device") == 0) { + /* TRANSLATORS: a specific part of hardware, + * the first %s is the device name, e.g. 'Unifying Receiver` */ + return g_strdup_printf (_("%s Device Update"), name); + } + if (g_strcmp0 (cat, "X-System") == 0) { + /* TRANSLATORS: the entire system, e.g. all internal devices, + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s System Update"), name); + } + if (g_strcmp0 (cat, "X-EmbeddedController") == 0) { + /* TRANSLATORS: the EC is typically the keyboard controller chip, + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Embedded Controller Update"), name); + } + if (g_strcmp0 (cat, "X-ManagementEngine") == 0) { + /* TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s ME Update"), name); + } + if (g_strcmp0 (cat, "X-CorporateManagementEngine") == 0) { + /* TRANSLATORS: ME stands for Management Engine (with Intel AMT), + * where the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Corporate ME Update"), name); + } + if (g_strcmp0 (cat, "X-ConsumerManagementEngine") == 0) { + /* TRANSLATORS: ME stands for Management Engine, where + * the first %s is the device name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Consumer ME Update"), name); + } + if (g_strcmp0 (cat, "X-Controller") == 0) { + /* TRANSLATORS: the controller is a device that has other devices + * plugged into it, for example ThunderBolt, FireWire or USB, + * the first %s is the device name, e.g. 'Intel ThunderBolt` */ + return g_strdup_printf (_("%s Controller Update"), name); + } + if (g_strcmp0 (cat, "X-ThunderboltController") == 0) { + /* TRANSLATORS: the Thunderbolt controller is a device that + * has other high speed Thunderbolt devices plugged into it; + * the first %s is the system name, e.g. 'ThinkPad P50` */ + return g_strdup_printf (_("%s Thunderbolt Controller Update"), name); + } + if (g_strcmp0 (cat, "X-CpuMicrocode") == 0) { + /* TRANSLATORS: the CPU microcode is firmware loaded onto the CPU + * at system bootup */ + return g_strdup_printf (_("%s CPU Microcode Update"), name); + } + if (g_strcmp0 (cat, "X-Configuration") == 0) { + /* TRANSLATORS: configuration refers to hardware state, + * e.g. a security database or a default power value */ + return g_strdup_printf (_("%s Configuration Update"), name); + } + if (g_strcmp0 (cat, "X-Battery") == 0) { + /* TRANSLATORS: battery refers to the system power source */ + return g_strdup_printf (_("%s Battery Update"), name); + } + if (g_strcmp0 (cat, "X-Camera") == 0) { + /* TRANSLATORS: camera can refer to the laptop internal + * camera in the bezel or external USB webcam */ + return g_strdup_printf (_("%s Camera Update"), name); + } + if (g_strcmp0 (cat, "X-TPM") == 0) { + /* TRANSLATORS: TPM refers to a Trusted Platform Module */ + return g_strdup_printf (_("%s TPM Update"), name); + } + if (g_strcmp0 (cat, "X-Touchpad") == 0) { + /* TRANSLATORS: TouchPad refers to a flat input device */ + return g_strdup_printf (_("%s Touchpad Update"), name); + } + if (g_strcmp0 (cat, "X-Mouse") == 0) { + /* TRANSLATORS: Mouse refers to a handheld input device */ + return g_strdup_printf (_("%s Mouse Update"), name); + } + if (g_strcmp0 (cat, "X-Keyboard") == 0) { + /* TRANSLATORS: Keyboard refers to an input device for typing */ + return g_strdup_printf (_("%s Keyboard Update"), name); + } + if (g_strcmp0 (cat, "X-StorageController") == 0) { + /* TRANSLATORS: Storage Controller is typically a RAID or SAS adapter */ + return g_strdup_printf (_("%s Storage Controller Update"), name); + } + if (g_strcmp0 (cat, "X-NetworkInterface") == 0) { + /* TRANSLATORS: Network Interface refers to the physical + * PCI card, not the logical wired connection */ + return g_strdup_printf (_("%s Network Interface Update"), name); + } + if (g_strcmp0 (cat, "X-VideoDisplay") == 0) { + /* TRANSLATORS: Video Display refers to the laptop internal display or + * external monitor */ + return g_strdup_printf (_("%s Display Update"), name); + } + if (g_strcmp0 (cat, "X-BaseboardManagementController") == 0) { + /* TRANSLATORS: BMC refers to baseboard management controller which + * is the device that updates all the other firmware on the system */ + return g_strdup_printf (_("%s BMC Update"), name); + } + if (g_strcmp0 (cat, "X-UsbReceiver") == 0) { + /* TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth + * device that stays in the USB port so the wireless peripheral works */ + return g_strdup_printf (_("%s USB Receiver Update"), name); + } + } + + /* default fallback */ + return g_strdup (name); +} + +static AsUrgencyKind +gs_fwupd_release_urgency_to_as_urgency_kind (FwupdReleaseUrgency urgency) +{ + switch (urgency) { + case FWUPD_RELEASE_URGENCY_LOW: + return AS_URGENCY_KIND_LOW; + case FWUPD_RELEASE_URGENCY_MEDIUM: + return AS_URGENCY_KIND_MEDIUM; + case FWUPD_RELEASE_URGENCY_HIGH: + return AS_URGENCY_KIND_HIGH; + case FWUPD_RELEASE_URGENCY_CRITICAL: + return AS_URGENCY_KIND_CRITICAL; + case FWUPD_RELEASE_URGENCY_UNKNOWN: + default: + return AS_URGENCY_KIND_UNKNOWN; + } +} + +void +gs_fwupd_app_set_from_release (GsApp *app, FwupdRelease *rel) +{ + GPtrArray *locations = fwupd_release_get_locations (rel); + + if (fwupd_release_get_name (rel) != NULL) { + g_autofree gchar *tmp = gs_fwupd_release_get_name (rel); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, tmp); + } + if (fwupd_release_get_summary (rel) != NULL) { + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, + fwupd_release_get_summary (rel)); + } + if (fwupd_release_get_homepage (rel) != NULL) { + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + fwupd_release_get_homepage (rel)); + } + if (fwupd_release_get_size (rel) != 0) { + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, 0); + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, fwupd_release_get_size (rel)); + } + if (fwupd_release_get_version (rel) != NULL) + gs_app_set_update_version (app, fwupd_release_get_version (rel)); + if (fwupd_release_get_license (rel) != NULL) { + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, + fwupd_release_get_license (rel)); + } + if (locations->len > 0) { + const gchar *uri = g_ptr_array_index (locations, 0); + /* typically the first URI will be the main HTTP mirror, and we + * don't have the capability to use an IPFS/IPNS URL anyway */ + gs_app_set_origin_hostname (app, uri); + gs_fwupd_app_set_update_uri (app, uri); + } + if (fwupd_release_get_description (rel) != NULL) { + g_autofree gchar *tmp = NULL; + tmp = as_markup_convert_simple (fwupd_release_get_description (rel), NULL); + if (tmp != NULL) + gs_app_set_update_details_text (app, tmp); + } + if (fwupd_release_get_detach_image (rel) != NULL) { + g_autoptr(AsScreenshot) ss = as_screenshot_new (); + g_autoptr(AsImage) im = as_image_new (); + as_image_set_kind (im, AS_IMAGE_KIND_SOURCE); + as_image_set_url (im, fwupd_release_get_detach_image (rel)); + as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_DEFAULT); + as_screenshot_add_image (ss, im); + if (fwupd_release_get_detach_caption (rel) != NULL) + as_screenshot_set_caption (ss, fwupd_release_get_detach_caption (rel), NULL); + gs_app_set_action_screenshot (app, ss); + } + + gs_app_set_update_urgency (app, gs_fwupd_release_urgency_to_as_urgency_kind (fwupd_release_get_urgency (rel))); +} diff --git a/plugins/fwupd/gs-fwupd-app.h b/plugins/fwupd/gs-fwupd-app.h new file mode 100644 index 0000000..5a3e9f2 --- /dev/null +++ b/plugins/fwupd/gs-fwupd-app.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) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <fwupd.h> + +G_BEGIN_DECLS + +const gchar *gs_fwupd_app_get_device_id (GsApp *app); +const gchar *gs_fwupd_app_get_update_uri (GsApp *app); +gboolean gs_fwupd_app_get_is_locked (GsApp *app); + +void gs_fwupd_app_set_device_id (GsApp *app, + const gchar *device_id); +void gs_fwupd_app_set_update_uri (GsApp *app, + const gchar *update_uri); +void gs_fwupd_app_set_is_locked (GsApp *app, + gboolean is_locked); +void gs_fwupd_app_set_from_device (GsApp *app, + FwupdDevice *dev); +void gs_fwupd_app_set_from_release (GsApp *app, + FwupdRelease *rel); + +G_END_DECLS diff --git a/plugins/fwupd/gs-plugin-fwupd.c b/plugins/fwupd/gs-plugin-fwupd.c new file mode 100644 index 0000000..b5d99b7 --- /dev/null +++ b/plugins/fwupd/gs-plugin-fwupd.c @@ -0,0 +1,1414 @@ +/* -*- 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) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <fwupd.h> +#include <fcntl.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> + +#include <gnome-software.h> + +#include "gs-fwupd-app.h" +#include "gs-metered.h" + +#include "gs-plugin-fwupd.h" + +/* + * SECTION: + * Queries for new firmware and schedules it to be installed as required. + * + * This plugin calls UpdatesChanged() if any updatable devices are + * added or removed or if a device has been updated live. + * + * Since fwupd is a daemon accessible over D-Bus, this plugin basically + * translates every job into one or more D-Bus calls, and all the real work is + * done in the fwupd daemon. FIXME: This means the plugin can therefore execute + * entirely in the main thread, making asynchronous D-Bus calls, once all the + * vfuncs have been ported. + */ + +struct _GsPluginFwupd { + GsPlugin parent; + + FwupdClient *client; + GsApp *app_current; + GsApp *cached_origin; +}; + +G_DEFINE_TYPE (GsPluginFwupd, gs_plugin_fwupd, GS_TYPE_PLUGIN) + +static void +gs_plugin_fwupd_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* already correct */ + if (error->domain == GS_PLUGIN_ERROR) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; + + /* custom to this plugin */ + if (error->domain == FWUPD_ERROR) { + switch (error->code) { + case FWUPD_ERROR_ALREADY_PENDING: + case FWUPD_ERROR_INVALID_FILE: + case FWUPD_ERROR_NOT_SUPPORTED: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case FWUPD_ERROR_AUTH_FAILED: + error->code = GS_PLUGIN_ERROR_AUTH_INVALID; + break; + case FWUPD_ERROR_SIGNATURE_INVALID: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case FWUPD_ERROR_AC_POWER_REQUIRED: + error->code = GS_PLUGIN_ERROR_AC_POWER_REQUIRED; + break; + case FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW: + error->code = GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW; + 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; +} + +static void +gs_plugin_fwupd_init (GsPluginFwupd *self) +{ + self->client = fwupd_client_new (); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (GS_PLUGIN (self), "org.gnome.Software.Plugin.Fwupd"); +} + +static void +gs_plugin_fwupd_dispose (GObject *object) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (object); + + g_clear_object (&self->cached_origin); + g_clear_object (&self->client); + + G_OBJECT_CLASS (gs_plugin_fwupd_parent_class)->dispose (object); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_FIRMWARE) + gs_app_set_management_plugin (app, plugin); +} + +static void +gs_plugin_fwupd_changed_cb (FwupdClient *client, GsPlugin *plugin) +{ +} + +static void +gs_plugin_fwupd_device_changed_cb (FwupdClient *client, + FwupdDevice *dev, + GsPlugin *plugin) +{ + /* limit number of UI refreshes */ + if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_SUPPORTED)) { + g_debug ("%s changed (not supported) so ignoring", + fwupd_device_get_id (dev)); + return; + } + + /* If the flag is set the device matches something in the + * metadata as therefor is worth refreshing the update list */ + g_debug ("%s changed (supported) so reloading", + fwupd_device_get_id (dev)); + gs_plugin_updates_changed (plugin); +} + +static void +gs_plugin_fwupd_notify_percentage_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (user_data); + + /* nothing in progress */ + if (self->app_current == NULL) { + g_debug ("fwupd percentage: %u%%", + fwupd_client_get_percentage (self->client)); + return; + } + g_debug ("fwupd percentage for %s: %u%%", + gs_app_get_unique_id (self->app_current), + fwupd_client_get_percentage (self->client)); + gs_app_set_progress (self->app_current, + fwupd_client_get_percentage (self->client)); +} + +static void +gs_plugin_fwupd_notify_status_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (user_data); + + /* nothing in progress */ + if (self->app_current == NULL) { + g_debug ("fwupd status: %s", + fwupd_status_to_string (fwupd_client_get_status (self->client))); + return; + } + + g_debug ("fwupd status for %s: %s", + gs_app_get_unique_id (self->app_current), + fwupd_status_to_string (fwupd_client_get_status (self->client))); + switch (fwupd_client_get_status (self->client)) { + case FWUPD_STATUS_DECOMPRESSING: + case FWUPD_STATUS_DEVICE_RESTART: + case FWUPD_STATUS_DEVICE_WRITE: + case FWUPD_STATUS_DEVICE_VERIFY: + gs_app_set_state (self->app_current, GS_APP_STATE_INSTALLING); + break; + case FWUPD_STATUS_IDLE: + g_clear_object (&self->app_current); + break; + default: + break; + } +} + +static gchar * +gs_plugin_fwupd_get_file_checksum (const gchar *filename, + GChecksumType checksum_type, + GError **error) +{ + gsize len; + g_autofree gchar *data = NULL; + + if (!g_file_get_contents (filename, &data, &len, error)) { + gs_utils_error_convert_gio (error); + return NULL; + } + return g_compute_checksum_for_data (checksum_type, (const guchar *)data, len); +} + +static void setup_connect_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void setup_features_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_fwupd_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_fwupd_setup_async); + + /* connect a proxy */ + fwupd_client_connect_async (self->client, cancellable, setup_connect_cb, + g_steal_pointer (&task)); +} + +static void +setup_connect_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginFwupd *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GError) local_error = NULL; + + if (!fwupd_client_connect_finish (self->client, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* send our implemented feature set */ + fwupd_client_set_feature_flags_async (self->client, + FWUPD_FEATURE_FLAG_UPDATE_ACTION | + FWUPD_FEATURE_FLAG_DETACH_ACTION, + cancellable, setup_features_cb, + g_steal_pointer (&task)); +} + +static void +setup_features_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginFwupd *self = g_task_get_source_object (task); + GsPlugin *plugin = GS_PLUGIN (self); + g_autoptr(GError) local_error = NULL; + + if (!fwupd_client_set_feature_flags_finish (self->client, result, &local_error)) + g_debug ("Failed to set front-end features: %s", local_error->message); + g_clear_error (&local_error); + + /* we know the runtime daemon version now */ + fwupd_client_set_user_agent_for_package (self->client, PACKAGE_NAME, PACKAGE_VERSION); + if (!fwupd_client_ensure_networking (self->client, &local_error)) { + gs_plugin_fwupd_error_convert (&local_error); + g_prefix_error (&local_error, "Failed to setup networking: "); + g_task_return_error (task, g_steal_pointer (&local_error)); + 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_bundle_kind (self->cached_origin, AS_BUNDLE_KIND_CABINET); + 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); + + /* register D-Bus errors */ + fwupd_error_quark (); + g_signal_connect (self->client, "changed", + G_CALLBACK (gs_plugin_fwupd_changed_cb), plugin); + g_signal_connect (self->client, "device-added", + G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin); + g_signal_connect (self->client, "device-removed", + G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin); + g_signal_connect (self->client, "device-changed", + G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin); + g_signal_connect (self->client, "notify::percentage", + G_CALLBACK (gs_plugin_fwupd_notify_percentage_cb), self); + g_signal_connect (self->client, "notify::status", + G_CALLBACK (gs_plugin_fwupd_notify_status_cb), self); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_fwupd_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static GsApp * +gs_plugin_fwupd_new_app_from_device (GsPlugin *plugin, FwupdDevice *dev) +{ + FwupdRelease *rel = fwupd_device_get_release_default (dev); + GsApp *app; + g_autofree gchar *id = NULL; + g_autoptr(GIcon) icon = NULL; + + /* older versions of fwups didn't record this for historical devices */ + if (fwupd_release_get_appstream_id (rel) == NULL) + return NULL; + + /* get from cache */ + id = gs_utils_build_unique_id (AS_COMPONENT_SCOPE_SYSTEM, + AS_BUNDLE_KIND_UNKNOWN, + NULL, /* origin */ + fwupd_release_get_appstream_id (rel), + NULL); + app = gs_plugin_cache_lookup (plugin, id); + if (app == NULL) { + app = gs_app_new (id); + gs_plugin_cache_add (plugin, id, app); + } + + /* default stuff */ + gs_app_set_kind (app, AS_COMPONENT_KIND_FIRMWARE); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_CABINET); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + gs_app_set_management_plugin (app, plugin); + gs_app_add_category (app, "System"); + gs_fwupd_app_set_device_id (app, fwupd_device_get_id (dev)); + + /* create icon */ + icon = g_themed_icon_new ("system-component-firmware"); + gs_app_add_icon (app, icon); + gs_fwupd_app_set_from_device (app, dev); + gs_fwupd_app_set_from_release (app, rel); + + if (fwupd_release_get_appstream_id (rel) != NULL) + gs_app_set_id (app, fwupd_release_get_appstream_id (rel)); + + /* the same as we have already */ + if (g_strcmp0 (fwupd_device_get_version (dev), + fwupd_release_get_version (rel)) == 0) { + g_warning ("same firmware version as installed"); + } + + return app; +} + +static gchar * +gs_plugin_fwupd_build_device_id (FwupdDevice *dev) +{ + g_autofree gchar *tmp = g_strdup (fwupd_device_get_id (dev)); + g_strdelimit (tmp, "/", '_'); + return g_strdup_printf ("org.fwupd.%s.device", tmp); +} + +static GsApp * +gs_plugin_fwupd_new_app_from_device_raw (GsPlugin *plugin, FwupdDevice *device) +{ + GPtrArray *icons; + g_autofree gchar *id = NULL; + g_autoptr(GsApp) app = NULL; + + /* create a GsApp based on the device, not the release */ + id = gs_plugin_fwupd_build_device_id (device); + app = gs_app_new (id); + gs_app_set_kind (app, AS_COMPONENT_KIND_FIRMWARE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + gs_app_set_version (app, fwupd_device_get_version (device)); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_name (device)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_summary (device)); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_description (device)); + gs_app_set_origin (app, fwupd_device_get_vendor (device)); + gs_fwupd_app_set_device_id (app, fwupd_device_get_id (device)); + gs_app_set_management_plugin (app, plugin); + + /* create icon */ + icons = fwupd_device_get_icons (device); + for (guint j = 0; j < icons->len; j++) { + const gchar *icon_str = g_ptr_array_index (icons, j); + g_autoptr(GIcon) icon = NULL; + if (g_str_has_prefix (icon_str, "/")) { + g_autoptr(GFile) icon_file = g_file_new_for_path (icon_str); + icon = g_file_icon_new (icon_file); + } else { + icon = g_themed_icon_new (icon_str); + } + gs_app_add_icon (app, icon); + } + return g_steal_pointer (&app); +} + +static GsApp * +gs_plugin_fwupd_new_app (GsPlugin *plugin, FwupdDevice *dev, GError **error) +{ + FwupdRelease *rel = fwupd_device_get_release_default (dev); + GPtrArray *checksums; + GPtrArray *locations = fwupd_release_get_locations (rel); + const gchar *update_uri = NULL; + g_autofree gchar *basename = NULL; + g_autofree gchar *filename_cache = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + + /* update unsupported */ + app = gs_plugin_fwupd_new_app_from_device (plugin, dev); + if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE_LIVE) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "%s [%s] cannot be updated", + gs_app_get_name (app), gs_app_get_id (app)); + return NULL; + } + + /* some missing */ + if (gs_app_get_id (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "fwupd: No id for firmware"); + return NULL; + } + if (gs_app_get_version (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "fwupd: No version! for %s!", gs_app_get_id (app)); + return NULL; + } + if (gs_app_get_update_version (app) == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "fwupd: No update-version! for %s!", gs_app_get_id (app)); + return NULL; + } + checksums = fwupd_release_get_checksums (rel); + if (checksums->len == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_SECURITY, + "%s [%s] (%s) has no checksums, ignoring as unsafe", + gs_app_get_name (app), + gs_app_get_id (app), + gs_app_get_update_version (app)); + return NULL; + } + + /* typically the first URI will be the main HTTP mirror, and we + * don't have the capability to use an IPFS/IPNS URL anyway */ + if (locations->len > 0) + update_uri = g_ptr_array_index (locations, 0); + + if (update_uri == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no location available for %s [%s]", + gs_app_get_name (app), gs_app_get_id (app)); + return NULL; + } + + /* does the firmware already exist in the cache? */ + basename = g_path_get_basename (update_uri); + filename_cache = gs_utils_get_cache_filename ("fwupd", + basename, + GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY, + error); + if (filename_cache == NULL) + return NULL; + + /* delete the file if the checksum does not match */ + if (g_file_test (filename_cache, G_FILE_TEST_EXISTS)) { + const gchar *checksum_tmp = NULL; + g_autofree gchar *checksum = NULL; + + /* we can migrate to something better than SHA1 when the LVFS + * starts producing metadata with multiple hash types */ + checksum_tmp = fwupd_checksum_get_by_kind (checksums, + G_CHECKSUM_SHA1); + if (checksum_tmp == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "No valid checksum for %s", + filename_cache); + } + checksum = gs_plugin_fwupd_get_file_checksum (filename_cache, + G_CHECKSUM_SHA1, + error); + if (checksum == NULL) + return NULL; + if (g_strcmp0 (checksum_tmp, checksum) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "%s does not match checksum, expected %s got %s", + filename_cache, checksum_tmp, checksum); + g_unlink (filename_cache); + return NULL; + } + } + + /* already downloaded, so overwrite */ + if (g_file_test (filename_cache, G_FILE_TEST_EXISTS)) + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0); + + /* actually add the application */ + file = g_file_new_for_path (filename_cache); + gs_app_set_local_file (app, file); + return g_steal_pointer (&app); +} + +gboolean +gs_plugin_add_updates_historical (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + g_autoptr(GError) error_local = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(FwupdDevice) dev = NULL; + + /* get historical updates */ + dev = fwupd_client_get_results (self->client, + FWUPD_DEVICE_ID_ANY, + cancellable, + &error_local); + if (dev == NULL) { + if (g_error_matches (error_local, + FWUPD_ERROR, + FWUPD_ERROR_NOTHING_TO_DO)) + return TRUE; + if (g_error_matches (error_local, + FWUPD_ERROR, + FWUPD_ERROR_NOT_FOUND)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + + /* parse */ + app = gs_plugin_fwupd_new_app_from_device (plugin, dev); + if (app == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "failed to build result for %s", + fwupd_device_get_id (dev)); + return FALSE; + } + gs_app_list_add (list, app); + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) devices = NULL; + + /* get current list of updates */ + devices = fwupd_client_get_devices (self->client, cancellable, &error_local); + if (devices == NULL) { + if (g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { + g_debug ("no devices (%s)", error_local->message); + return TRUE; + } + g_debug ("Failed to get devices: %s", error_local->message); + return TRUE; + } + for (guint i = 0; i < devices->len; i++) { + FwupdDevice *dev = g_ptr_array_index (devices, i); + FwupdRelease *rel_newest; + g_autoptr(GError) error_local2 = NULL; + g_autoptr(GPtrArray) rels = NULL; + g_autoptr(GsApp) app = NULL; + + /* locked device that needs unlocking */ + if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_LOCKED)) { + app = gs_plugin_fwupd_new_app_from_device_raw (plugin, dev); + gs_fwupd_app_set_is_locked (app, TRUE); + gs_app_list_add (list, app); + continue; + } + + /* not going to have results, so save a D-Bus round-trip */ + if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_SUPPORTED)) + continue; + + /* get the releases for this device and filter for validity */ + rels = fwupd_client_get_upgrades (self->client, + fwupd_device_get_id (dev), + cancellable, &error_local2); + if (rels == NULL) { + if (g_error_matches (error_local2, + FWUPD_ERROR, + FWUPD_ERROR_NOTHING_TO_DO)) { + g_debug ("no updates for %s", fwupd_device_get_id (dev)); + continue; + } + if (g_error_matches (error_local2, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED)) { + g_debug ("not supported for %s", fwupd_device_get_id (dev)); + continue; + } + g_warning ("failed to get upgrades for %s: %s]", + fwupd_device_get_id (dev), + error_local2->message); + continue; + } + + /* normal device update */ + rel_newest = g_ptr_array_index (rels, 0); + fwupd_device_add_release (dev, rel_newest); + app = gs_plugin_fwupd_new_app (plugin, dev, &error_local2); + if (app == NULL) { + g_debug ("%s", error_local2->message); + continue; + } + + /* add update descriptions for all releases inbetween */ + if (rels->len > 1) { + g_autoptr(GString) update_desc = g_string_new (NULL); + for (guint j = 0; j < rels->len; j++) { + FwupdRelease *rel = g_ptr_array_index (rels, j); + g_autofree gchar *desc = NULL; + if (fwupd_release_get_description (rel) == NULL) + continue; + desc = as_markup_convert_simple (fwupd_release_get_description (rel), NULL); + if (desc == NULL) + continue; + g_string_append_printf (update_desc, + "Version %s:\n%s\n\n", + fwupd_release_get_version (rel), + desc); + } + if (update_desc->len > 2) { + g_string_truncate (update_desc, update_desc->len - 2); + gs_app_set_update_details_text (app, update_desc->str); + } + } + gs_app_list_add (list, app); + } + return TRUE; +} + +static gboolean +remote_cache_is_expired (FwupdRemote *remote, + guint64 cache_age_secs) +{ + /* check cache age */ + if (cache_age_secs > 0) { + guint64 age = fwupd_remote_get_age (remote); + if (age < cache_age_secs) { + g_debug ("fwupd remote is only %" G_GUINT64_FORMAT " seconds old, so ignoring refresh", age); + return FALSE; + } + } + + return TRUE; +} + +typedef struct { + /* Input data. */ + guint64 cache_age_secs; + + /* In-progress state. */ + guint n_operations_pending; + GError *error; /* (owned) (nullable) */ +} RefreshMetadataData; + +static void +refresh_metadata_data_free (RefreshMetadataData *data) +{ + g_clear_error (&data->error); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (RefreshMetadataData, refresh_metadata_data_free) + +static void get_remotes_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void refresh_remote_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_refresh_metadata_op (GTask *task); + +static void +gs_plugin_fwupd_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(RefreshMetadataData) data = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_fwupd_refresh_metadata_async); + + data = g_new0 (RefreshMetadataData, 1); + data->cache_age_secs = cache_age_secs; + g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) refresh_metadata_data_free); + + /* get the list of enabled remotes */ + fwupd_client_get_remotes_async (self->client, cancellable, get_remotes_cb, g_steal_pointer (&task)); +} + +static void +get_remotes_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + FwupdClient *client = FWUPD_CLIENT (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + RefreshMetadataData *data = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) remotes = NULL; + + remotes = fwupd_client_get_remotes_finish (client, result, &error_local); + + if (remotes == NULL) { + g_debug ("No remotes found: %s", error_local ? error_local->message : "Unknown error"); + if (g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || + g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { + g_task_return_boolean (task, TRUE); + return; + } + + gs_plugin_fwupd_error_convert (&error_local); + g_task_return_error (task, g_steal_pointer (&error_local)); + return; + } + + /* Refresh each of the remotes in parallel. Keep the pending operation + * count incremented until all operations have been started, so that + * the overall operation doesn’t complete too early. */ + data->n_operations_pending = 1; + + for (guint i = 0; i < remotes->len; i++) { + FwupdRemote *remote = g_ptr_array_index (remotes, i); + + if (!fwupd_remote_get_enabled (remote)) + continue; + if (fwupd_remote_get_kind (remote) != FWUPD_REMOTE_KIND_DOWNLOAD) + continue; + if (!remote_cache_is_expired (remote, data->cache_age_secs)) + continue; + + data->n_operations_pending++; + fwupd_client_refresh_remote_async (client, remote, cancellable, + refresh_remote_cb, g_object_ref (task)); + } + + finish_refresh_metadata_op (task); +} + +static void +refresh_remote_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + FwupdClient *client = FWUPD_CLIENT (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + RefreshMetadataData *data = g_task_get_task_data (task); + g_autoptr(GError) local_error = NULL; + + if (!fwupd_client_refresh_remote_finish (client, result, &local_error)) { + gs_plugin_fwupd_error_convert (&local_error); + if (data->error == NULL) + data->error = g_steal_pointer (&local_error); + else + g_debug ("Another remote refresh error: %s", local_error->message); + } + + finish_refresh_metadata_op (task); +} + +static void +finish_refresh_metadata_op (GTask *task) +{ + RefreshMetadataData *data = g_task_get_task_data (task); + + g_assert (data->n_operations_pending > 0); + data->n_operations_pending--; + + if (data->n_operations_pending == 0) { + if (data->error != NULL) + g_task_return_error (task, g_steal_pointer (&data->error)); + else + g_task_return_boolean (task, TRUE); + } +} + +static gboolean +gs_plugin_fwupd_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +gs_plugin_fwupd_install (GsPluginFwupd *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + const gchar *device_id; + FwupdInstallFlags install_flags = 0; + GFile *local_file; + g_autofree gchar *filename = NULL; + gboolean downloaded_to_cache = FALSE; + g_autoptr(FwupdDevice) dev = NULL; + g_autoptr(GError) error_local = NULL; + + /* not set */ + local_file = gs_app_get_local_file (app); + if (local_file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "not enough data for fwupd %s", + filename); + return FALSE; + } + + /* file does not yet exist */ + filename = g_file_get_path (local_file); + if (!g_file_query_exists (local_file, cancellable)) { + const gchar *uri = gs_fwupd_app_get_update_uri (app); + g_autoptr(GFile) file = g_file_new_for_path (filename); + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + if (!fwupd_client_download_file (self->client, + uri, file, + FWUPD_CLIENT_DOWNLOAD_FLAG_NONE, + cancellable, + error)) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + + downloaded_to_cache = TRUE; + } + + /* limit to single device? */ + device_id = gs_fwupd_app_get_device_id (app); + if (device_id == NULL) + device_id = FWUPD_DEVICE_ID_ANY; + + /* set the last object */ + g_set_object (&self->app_current, app); + + /* only offline supported */ + if (gs_app_get_metadata_item (app, "fwupd::OnlyOffline") != NULL) + install_flags |= FWUPD_INSTALL_FLAG_OFFLINE; + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + if (!fwupd_client_install (self->client, device_id, + filename, install_flags, + cancellable, error)) { + gs_plugin_fwupd_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* delete the file from the cache */ + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + if (downloaded_to_cache) { + if (!g_file_delete (local_file, cancellable, error)) + return FALSE; + } + + /* does the device have an update message */ + dev = fwupd_client_get_device_by_id (self->client, device_id, + cancellable, &error_local); + if (dev == NULL) { + /* NOTE: this is probably entirely fine; some devices do not + * re-enumerate until replugged manually or the machine is + * rebooted -- and the metadata to know that is only available + * in a too-new-to-depend-on fwupd version */ + g_debug ("failed to find device after install: %s", error_local->message); + } else { + if (fwupd_device_get_update_message (dev) != NULL) { + g_autoptr(AsScreenshot) ss = as_screenshot_new (); + + /* image is optional */ + if (fwupd_device_get_update_image (dev) != NULL) { + g_autoptr(AsImage) im = as_image_new (); + as_image_set_kind (im, AS_IMAGE_KIND_SOURCE); + as_image_set_url (im, fwupd_device_get_update_image (dev)); + as_screenshot_add_image (ss, im); + } + + /* caption is required */ + as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_DEFAULT); + as_screenshot_set_caption (ss, fwupd_device_get_update_message (dev), NULL); + gs_app_set_action_screenshot (app, ss); + + /* require the dialog */ + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION); + } + } + + /* success */ + return TRUE; +} + +static void +gs_plugin_fwupd_modify_source_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(GTask) task = user_data; + GsPluginFwupd *self = g_task_get_source_object (task); + GsApp *repository = g_task_get_task_data (task); + + if (!fwupd_client_modify_remote_finish (FWUPD_CLIENT (source_object), result, &local_error)) { + gs_app_set_state_recover (repository); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (gs_app_get_state (repository) == GS_APP_STATE_INSTALLING) + gs_app_set_state (repository, GS_APP_STATE_INSTALLED); + else if (gs_app_get_state (repository) == GS_APP_STATE_REMOVING) + gs_app_set_state (repository, GS_APP_STATE_AVAILABLE); + + gs_plugin_repository_changed (GS_PLUGIN (self), repository); + + g_task_return_boolean (task, TRUE); +} + +static void +gs_plugin_fwupd_modify_source_async (GsPluginFwupd *self, + GsApp *repository, + gboolean enabled, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + const gchar *remote_id; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, g_object_ref (repository), g_object_unref); + g_task_set_source_tag (task, gs_plugin_fwupd_modify_source_async); + + if (!gs_app_has_management_plugin (repository, GS_PLUGIN (self))) { + g_task_return_boolean (task, TRUE); + return; + } + + /* source -> remote */ + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + remote_id = gs_app_get_metadata_item (repository, "fwupd::remote-id"); + if (remote_id == NULL) { + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "not enough data for fwupd %s", + gs_app_get_unique_id (repository)); + return; + } + gs_app_set_state (repository, enabled ? + GS_APP_STATE_INSTALLING : GS_APP_STATE_REMOVING); + fwupd_client_modify_remote_async (self->client, + remote_id, + "Enabled", + enabled ? "true" : "false", + cancellable, + gs_plugin_fwupd_modify_source_ready_cb, + g_steal_pointer (&task)); +} + +static gboolean +gs_plugin_fwupd_modify_source_finish (GsPluginFwupd *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* source -> remote, handled by dedicated function */ + g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE); + + /* firmware */ + return gs_plugin_fwupd_install (self, app, cancellable, error); +} + +gboolean +gs_plugin_download_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + GFile *local_file; + g_autofree gchar *filename = NULL; + gpointer schedule_entry_handle = NULL; + g_autoptr(GError) error_local = NULL; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* not set */ + local_file = gs_app_get_local_file (app); + if (local_file == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "not enough data for fwupd %s", + filename); + return FALSE; + } + + /* file does not yet exist */ + filename = g_file_get_path (local_file); + if (!g_file_query_exists (local_file, cancellable)) { + const gchar *uri = gs_fwupd_app_get_update_uri (app); + g_autoptr(GFile) file = g_file_new_for_path (filename); + gboolean download_success; + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { + if (!gs_metered_block_app_on_download_scheduler (app, &schedule_entry_handle, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + download_success = fwupd_client_download_file (self->client, + uri, file, + FWUPD_CLIENT_DOWNLOAD_FLAG_NONE, + cancellable, + error); + if (!download_success) + gs_plugin_fwupd_error_convert (error); + + if (!gs_metered_remove_from_download_scheduler (schedule_entry_handle, NULL, &error_local)) + g_warning ("Failed to remove schedule entry: %s", error_local->message); + + if (!download_success) + return FALSE; + } + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0); + return TRUE; +} + +gboolean +gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* locked devices need unlocking, rather than installing */ + if (gs_fwupd_app_get_is_locked (app)) { + const gchar *device_id; + device_id = gs_fwupd_app_get_device_id (app); + if (device_id == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "not enough data for fwupd unlock"); + return FALSE; + } + if (!fwupd_client_unlock (self->client, device_id, + cancellable, error)) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + return TRUE; + } + + /* update means install */ + if (!gs_plugin_fwupd_install (self, app, cancellable, error)) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + return TRUE; +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + g_autofree gchar *content_type = NULL; + g_autofree gchar *filename = NULL; + g_autoptr(GPtrArray) devices = NULL; + const gchar *mimetypes[] = { + "application/vnd.ms-cab-compressed", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (!g_strv_contains (mimetypes, content_type)) + return TRUE; + + /* get results */ + filename = g_file_get_path (file); + devices = fwupd_client_get_details (self->client, + filename, + cancellable, + error); + if (devices == NULL) { + gs_plugin_fwupd_error_convert (error); + return FALSE; + } + for (guint i = 0; i < devices->len; i++) { + FwupdDevice *dev = g_ptr_array_index (devices, i); + g_autoptr(GsApp) app = NULL; + + /* create each app */ + app = gs_plugin_fwupd_new_app_from_device (plugin, dev); + + /* we *might* have no update view for local files */ + gs_app_set_version (app, gs_app_get_update_version (app)); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, + gs_app_get_update_details_markup (app)); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + g_autoptr(GPtrArray) remotes = NULL; + + /* find all remotes */ + remotes = fwupd_client_get_remotes (self->client, cancellable, error); + if (remotes == NULL) + return FALSE; + for (guint i = 0; i < remotes->len; i++) { + FwupdRemote *remote = g_ptr_array_index (remotes, i); + g_autofree gchar *id = NULL; + g_autoptr(GsApp) app = NULL; + + /* ignore these, they're built in */ + if (fwupd_remote_get_kind (remote) != FWUPD_REMOTE_KIND_DOWNLOAD) + continue; + + /* create something that we can use to enable/disable */ + id = g_strdup_printf ("org.fwupd.%s.remote", fwupd_remote_get_id (remote)); + app = gs_app_new (id); + gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_state (app, fwupd_remote_get_enabled (remote) ? + GS_APP_STATE_INSTALLED : GS_APP_STATE_AVAILABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + fwupd_remote_get_title (remote)); + gs_app_set_agreement (app, fwupd_remote_get_agreement (remote)); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, + fwupd_remote_get_metadata_uri (remote)); + gs_app_set_metadata (app, "fwupd::remote-id", + fwupd_remote_get_id (remote)); + gs_app_set_management_plugin (app, plugin); + gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "fwupd"); + gs_app_set_metadata (app, "GnomeSoftware::SortKey", "800"); + gs_app_set_origin_ui (app, _("Firmware")); + gs_app_list_add (list, app); + } + return TRUE; +} + +static void +gs_plugin_fwupd_enable_repository_remote_refresh_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GError) local_error = NULL; + + if (!fwupd_client_refresh_remote_finish (FWUPD_CLIENT (source_object), result, &local_error)) + g_debug ("Failed to refresh remote after enable: %s", local_error ? local_error->message : "Unknown error"); + + /* Silently ignore refresh errors */ + g_task_return_boolean (task, TRUE); +} + +static void +gs_plugin_fwupd_enable_repository_get_remotes_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GError) local_error = NULL; + g_autoptr(GPtrArray) remotes = NULL; + GsPluginFwupd *self = GS_PLUGIN_FWUPD (g_task_get_source_object (task)); + GsApp *repository = g_task_get_task_data (task); + const gchar *remote_id; + guint cache_age = 1; + + remotes = fwupd_client_get_remotes_finish (FWUPD_CLIENT (source_object), result, &local_error); + if (remotes == NULL) { + g_debug ("No remotes found after remote enable: %s", local_error ? local_error->message : "Unknown error"); + /* Silently ignore refresh errors */ + g_task_return_boolean (task, TRUE); + return; + } + + remote_id = gs_app_get_metadata_item (repository, "fwupd::remote-id"); + g_assert (remote_id != NULL); + + for (guint i = 0; i < remotes->len; i++) { + FwupdRemote *remote = g_ptr_array_index (remotes, i); + if (g_strcmp0 (remote_id, fwupd_remote_get_id (remote)) == 0) { + if (fwupd_remote_get_enabled (remote) && + fwupd_remote_get_kind (remote) != FWUPD_REMOTE_KIND_LOCAL && + !remote_cache_is_expired (remote, cache_age)) { + GCancellable *cancellable = g_task_get_cancellable (task); + fwupd_client_refresh_remote_async (self->client, remote, cancellable, + gs_plugin_fwupd_enable_repository_remote_refresh_ready_cb, + g_steal_pointer (&task)); + return; + } + break; + } + } + + g_task_return_boolean (task, TRUE); +} + +static void +gs_plugin_fwupd_enable_repository_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(GError) local_error = NULL; + GsPluginFwupd *self = GS_PLUGIN_FWUPD (g_task_get_source_object (task)); + GCancellable *cancellable = g_task_get_cancellable (task); + + if (!gs_plugin_fwupd_modify_source_finish (self, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* This can fail silently, it's only to update necessary caches, to provide + * up-to-date information after the successful repository enable/install. */ + fwupd_client_get_remotes_async (self->client, + cancellable, + gs_plugin_fwupd_enable_repository_get_remotes_ready_cb, + g_steal_pointer (&task)); +} + +static void +gs_plugin_fwupd_enable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, g_object_ref (repository), g_object_unref); + g_task_set_source_tag (task, gs_plugin_fwupd_enable_repository_async); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + gs_plugin_fwupd_modify_source_async (self, repository, TRUE, cancellable, + gs_plugin_fwupd_enable_repository_ready_cb, g_steal_pointer (&task)); +} + +static gboolean +gs_plugin_fwupd_enable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_fwupd_disable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_fwupd_disable_repository_async); + g_task_return_boolean (task, TRUE); + return; + } + + gs_plugin_fwupd_modify_source_async (self, repository, FALSE, cancellable, callback, user_data); +} + +static gboolean +gs_plugin_fwupd_disable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin); + return gs_plugin_fwupd_modify_source_finish (self, result, error); +} + +static void +gs_plugin_fwupd_class_init (GsPluginFwupdClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_fwupd_dispose; + + plugin_class->setup_async = gs_plugin_fwupd_setup_async; + plugin_class->setup_finish = gs_plugin_fwupd_setup_finish; + plugin_class->refresh_metadata_async = gs_plugin_fwupd_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_fwupd_refresh_metadata_finish; + plugin_class->enable_repository_async = gs_plugin_fwupd_enable_repository_async; + plugin_class->enable_repository_finish = gs_plugin_fwupd_enable_repository_finish; + plugin_class->disable_repository_async = gs_plugin_fwupd_disable_repository_async; + plugin_class->disable_repository_finish = gs_plugin_fwupd_disable_repository_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_FWUPD; +} diff --git a/plugins/fwupd/gs-plugin-fwupd.h b/plugins/fwupd/gs-plugin-fwupd.h new file mode 100644 index 0000000..6114814 --- /dev/null +++ b/plugins/fwupd/gs-plugin-fwupd.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_FWUPD (gs_plugin_fwupd_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginFwupd, gs_plugin_fwupd, GS, PLUGIN_FWUPD, GsPlugin) + +G_END_DECLS diff --git a/plugins/fwupd/gs-self-test.c b/plugins/fwupd/gs-self-test.c new file mode 100644 index 0000000..f02a9f0 --- /dev/null +++ b/plugins/fwupd/gs-self-test.c @@ -0,0 +1,111 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <fwupd.h> + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_fwupd_func (GsPluginLoader *plugin_loader) +{ + g_autofree gchar *fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + GsSizeType size_download_type; + guint64 size_download_bytes; + + /* no fwupd, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "fwupd")) { + g_test_skip ("not enabled"); + return; + } + + /* load local file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron-0.2.cab"); + g_assert_nonnull (fn); + file = g_file_new_for_path (fn); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert_nonnull (app); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_FIRMWARE); + g_assert_nonnull (gs_app_get_license (app)); + g_assert_true (gs_app_has_category (app, "System")); + g_assert_cmpstr (gs_app_get_id (app), ==, "com.test.chiron.firmware"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_version (app), ==, "0.2"); + size_download_type = gs_app_get_size_download (app, &size_download_bytes); + g_assert_cmpint (size_download_type, ==, GS_SIZE_TYPE_VALID); + g_assert_cmpuint (size_download_bytes, ==, 32784); + g_assert_cmpstr (gs_app_get_description (app), ==, + "This is the first paragraph in the example " + "cab file.\n\nThis is the second paragraph."); +#if FWUPD_CHECK_VERSION(1, 7, 1) && !FWUPD_CHECK_VERSION(1, 8, 0) + /* Changes introduced in fwupd commit d3706e0e0b0fc210796da839b84ac391f7a251f8 and + removed for 1.8.0 with https://github.com/fwupd/fwupd/commit/0eeaad76ec79562ea3790bb377d847d5be02182f */ + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, + "Some of the platform secrets may be invalidated when " + "updating this firmware. Please ensure you have the " + "volume recovery key before continuing.\n\nLatest " + "firmware release."); +#else + g_assert_cmpstr (gs_app_get_update_details_markup (app), ==, + "Latest firmware release."); +#endif + + /* seems wrong, but this is only set if the update is available */ + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNKNOWN); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar * const allowlist[] = { + "fwupd", + NULL + }; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ + g_content_type_set_mime_dirs (NULL); + + gs_test_init (&argc, &argv); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/fwupd", + plugin_loader, + (GTestDataFunc) gs_plugins_fwupd_func); + + return g_test_run (); +} diff --git a/plugins/fwupd/meson.build b/plugins/fwupd/meson.build new file mode 100644 index 0000000..8ed030b --- /dev/null +++ b/plugins/fwupd/meson.build @@ -0,0 +1,54 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginFwupd"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +deps = [ + plugin_libs, + fwupd, +] + +if get_option('mogwai') + deps += [mogwai_schedule_client] +endif + +shared_module( + 'gs_plugin_fwupd', + sources : [ + 'gs-fwupd-app.c', + 'gs-plugin-fwupd.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : deps, +) +metainfo = 'org.gnome.Software.Plugin.Fwupd.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-fwupd', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : deps, + c_args : cargs, + ) + test('gs-self-test-fwupd', e, suite: ['plugins', 'fwupd'], env: test_env) +endif diff --git a/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in b/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in new file mode 100644 index 0000000..3f2067d --- /dev/null +++ b/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Fwupd</id> + <extends>org.gnome.Software.desktop</extends> + <name>Firmware Upgrade Support</name> + <summary>Provides support for firmware upgrades</summary> + <url type="homepage">http://www.fwupd.org</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>richard_at_hughsie.com</update_contact> +</component> diff --git a/plugins/fwupd/tests/build-cab.sh b/plugins/fwupd/tests/build-cab.sh new file mode 100755 index 0000000..ea7bed6 --- /dev/null +++ b/plugins/fwupd/tests/build-cab.sh @@ -0,0 +1,4 @@ +gcab --create chiron-0.2.cab \ + firmware.dfu \ + firmware.dfu.asc \ + firmware.metainfo.xml diff --git a/plugins/fwupd/tests/chiron-0.2.cab b/plugins/fwupd/tests/chiron-0.2.cab Binary files differnew file mode 100644 index 0000000..6618361 --- /dev/null +++ b/plugins/fwupd/tests/chiron-0.2.cab diff --git a/plugins/fwupd/tests/firmware.dfu b/plugins/fwupd/tests/firmware.dfu Binary files differnew file mode 100644 index 0000000..50f00c0 --- /dev/null +++ b/plugins/fwupd/tests/firmware.dfu diff --git a/plugins/fwupd/tests/firmware.dfu.asc b/plugins/fwupd/tests/firmware.dfu.asc new file mode 100644 index 0000000..0ea79a7 --- /dev/null +++ b/plugins/fwupd/tests/firmware.dfu.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v2.0.14 (GNU/Linux) + +iQEcBAABAgAGBQJWUy6EAAoJEEim2A5FOLrCxokIAJJtLVkuXZHgEu2C2Eq9jGrR +FZ9/z9XtsMgs33teLdmYUAQwvnNIbtIb6z7JViP8llCREP8y2fH+1OjrOOdtuS/A +bIJ0r40c9wYeH97ZcXBdHZiYVEFO+etbMBUg5ifuRO5VPjD9H1NqL05Wx9kUg/1T +a1fwgHopXR0T4jYcg5aijp3mdgfmg4boIklDaRV/g2c93W+0VhDZ2h5sKwBxxlFS +TrptclTMCvRYmVvL1CDOsBtgzu3jGo03wV9rcnSKzeBWvINcvlRLdS0ejlPaRYDK +MUY4MBVz3fDW1vFsqLpU80XMOYk0bxtQqQ2MsrlXWp9qazB+A6mC7kOnJQfx0yI= +=A3W8 +-----END PGP SIGNATURE----- diff --git a/plugins/fwupd/tests/firmware.metainfo.xml b/plugins/fwupd/tests/firmware.metainfo.xml new file mode 100644 index 0000000..d942fd8 --- /dev/null +++ b/plugins/fwupd/tests/firmware.metainfo.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> --> +<component type="firmware"> + <id>com.test.chiron.firmware</id> + <name>Chiron</name> + <summary>Single line synopsis</summary> + <description> + <p> + This is the first paragraph in the example cab file. + </p> + <p> + This is the second paragraph. + </p> + </description> + <provides> + <firmware type="flashed">fd9f37b4-36fb-5245-86a8-4d5993bb153b</firmware> + </provides> + <url type="homepage">http://127.0.0.1/</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <developer_name>ACME Corp</developer_name> + <releases> + <release urgency="medium" version="0.2" timestamp="1447353015"> + <checksum target="content" filename="firmware.dfu"/> + <description> + <p>Latest firmware release.</p> + </description> + </release> + </releases> +</component> diff --git a/plugins/malcontent/gs-plugin-malcontent.c b/plugins/malcontent/gs-plugin-malcontent.c new file mode 100644 index 0000000..d8825a9 --- /dev/null +++ b/plugins/malcontent/gs-plugin-malcontent.c @@ -0,0 +1,480 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2018-2019 Endless Mobile + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> +#include <libmalcontent/malcontent.h> +#include <string.h> +#include <math.h> + +#include "gs-plugin-malcontent.h" +#include "gs-plugin-private.h" + +/* + * SECTION: + * Adds the %GS_APP_QUIRK_PARENTAL_FILTER and + * %GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE quirks to applications if they + * contravene the effective user’s current parental controls policy. + * + * Specifically, %GS_APP_QUIRK_PARENTAL_FILTER will be added if an app’s OARS + * rating is too extreme for the current parental controls OARS policy. + * %GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE will be added if the app is listed on + * the current parental controls blocklist. + * + * Parental controls policy is loaded using libmalcontent. + * + * This plugin is ordered after flatpak and appstream as it uses OARS data from + * them. + * + * Limiting access to applications by not allowing them to be launched by + * gnome-software is only one part of a wider approach to parental controls. + * In order to guarantee users do not have access to applications they shouldn’t + * have access to, an LSM (such as AppArmor) needs to be used. That complements, + * rather than substitutes for, filtering in user visible UIs. + */ + +struct _GsPluginMalcontent { + GsPlugin parent; + + GMutex mutex; /* protects @app_filter **/ + MctManager *manager; /* (owned) */ + gulong manager_app_filter_changed_id; + MctAppFilter *app_filter; /* (mutex) (owned) (nullable) */ +}; + +G_DEFINE_TYPE (GsPluginMalcontent, gs_plugin_malcontent, GS_TYPE_PLUGIN) + +/* Convert an #MctAppFilterOarsValue to an #AsContentRatingValue. This is + * actually a trivial cast, since the types are defined the same; but throw in + * a static assertion to be sure. */ +static AsContentRatingValue +convert_app_filter_oars_value (MctAppFilterOarsValue filter_value) +{ + G_STATIC_ASSERT (AS_CONTENT_RATING_VALUE_LAST == MCT_APP_FILTER_OARS_VALUE_INTENSE + 1); + + return (AsContentRatingValue) filter_value; +} + +static gboolean +app_is_expected_to_have_content_rating (GsApp *app) +{ + if (gs_app_has_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE)) + return FALSE; + + 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: + return FALSE; + case AS_COMPONENT_KIND_UNKNOWN: + case AS_COMPONENT_KIND_DESKTOP_APP: + case AS_COMPONENT_KIND_WEB_APP: + case AS_COMPONENT_KIND_CONSOLE_APP: + default: + break; + } + + return TRUE; +} + +/* Check whether the OARS rating for @app is as, or less, extreme than the + * user’s preferences in @app_filter. If so (i.e. if the app is suitable for + * this user to use), return %TRUE; otherwise return %FALSE. + * + * The #AsContentRating in @app may be %NULL if no OARS ratings are provided for + * the app. If so, we have to assume the most restrictive ratings. However, if + * @rating is provided but is empty, we assume that every section in it has + * value %AS_CONTENT_RATING_VALUE_NONE. See + * https://github.com/hughsie/oars/blob/HEAD/specification/oars-1.1.md */ +static gboolean +app_is_content_rating_appropriate (GsApp *app, MctAppFilter *app_filter) +{ + g_autoptr(AsContentRating) rating = gs_app_dup_content_rating (app); /* (nullable) */ + g_autofree const gchar **oars_sections = mct_app_filter_get_oars_sections (app_filter); + AsContentRatingValue default_rating_value; + + if (rating == NULL && !app_is_expected_to_have_content_rating (app)) { + /* Some apps, such as flatpak runtimes, are not expected to have + * content ratings. */ + return TRUE; + } else if (rating == NULL) { + g_debug ("No OARS ratings provided for ‘%s’: assuming most extreme", + gs_app_get_unique_id (app)); + default_rating_value = AS_CONTENT_RATING_VALUE_INTENSE; + } else { + default_rating_value = AS_CONTENT_RATING_VALUE_NONE; + } + + for (gsize i = 0; oars_sections[i] != NULL; i++) { + AsContentRatingValue rating_value; + MctAppFilterOarsValue filter_value; + + filter_value = mct_app_filter_get_oars_value (app_filter, oars_sections[i]); + + if (rating != NULL) + rating_value = as_content_rating_get_value (rating, oars_sections[i]); + else + rating_value = AS_CONTENT_RATING_VALUE_UNKNOWN; + + if (rating_value == AS_CONTENT_RATING_VALUE_UNKNOWN) + rating_value = default_rating_value; + + if (filter_value == MCT_APP_FILTER_OARS_VALUE_UNKNOWN) + continue; + else if (convert_app_filter_oars_value (filter_value) < rating_value) + return FALSE; + } + + return TRUE; +} + +static gboolean +app_is_parentally_blocklisted (GsApp *app, MctAppFilter *app_filter) +{ + const gchar *desktop_id; + g_autoptr(GAppInfo) appinfo = NULL; + + desktop_id = gs_app_get_id (app); + if (desktop_id == NULL) + return FALSE; + appinfo = G_APP_INFO (gs_utils_get_desktop_app_info (desktop_id)); + if (appinfo == NULL) + return FALSE; + + return !mct_app_filter_is_appinfo_allowed (app_filter, appinfo); +} + +static gboolean +app_set_parental_quirks (GsPluginMalcontent *self, + GsApp *app, + MctAppFilter *app_filter) +{ + /* note that both quirks can be set on an app at the same time, and they + * have slightly different meanings */ + gboolean filtered = FALSE; + + /* check the OARS ratings to see if this app should be installable */ + if (!app_is_content_rating_appropriate (app, app_filter)) { + g_debug ("Filtering ‘%s’: app OARS rating is too extreme for this user", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER); + filtered = TRUE; + } else { + gs_app_remove_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER); + } + + /* check the app blocklist to see if this app should be launchable */ + if (app_is_parentally_blocklisted (app, app_filter)) { + g_debug ("Filtering ‘%s’: app is blocklisted for this user", + gs_app_get_unique_id (app)); + gs_app_add_quirk (app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE); + filtered = TRUE; + } else { + gs_app_remove_quirk (app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE); + } + + return filtered; +} + +static void +reload_app_filter_async (GsPluginMalcontent *self, + gboolean interactive, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + /* Refresh the app filter. This causes a D-Bus request. */ + mct_manager_get_app_filter_async (self->manager, + getuid (), + interactive ? MCT_GET_APP_FILTER_FLAGS_INTERACTIVE : MCT_GET_APP_FILTER_FLAGS_NONE, + cancellable, + callback, + user_data); +} + +static gboolean +reload_app_filter_finish (GsPluginMalcontent *self, + GAsyncResult *result, + GError **error) +{ + g_autoptr(MctAppFilter) new_app_filter = NULL; + g_autoptr(MctAppFilter) old_app_filter = NULL; + + new_app_filter = mct_manager_get_app_filter_finish (self->manager, + result, + error); + + /* on failure, keep the old app filter around since it might be more + * useful than nothing */ + if (new_app_filter == NULL) + return FALSE; + + { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + old_app_filter = g_steal_pointer (&self->app_filter); + self->app_filter = g_steal_pointer (&new_app_filter); + } + + return TRUE; +} + +static void reload_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +app_filter_changed_cb (MctManager *manager, + guint64 user_id, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (user_data); + + if (user_id != getuid ()) + return; + + /* The user’s app filter has changed, which means that different + * apps could be filtered from before. Reload everything to be + * sure of re-filtering correctly. */ + g_debug ("Reloading due to app filter changing for user %" G_GUINT64_FORMAT, user_id); + reload_app_filter_async (self, FALSE, NULL, reload_cb, g_object_ref (self)); +} + +static void +reload_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GsPluginMalcontent) self = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + if (reload_app_filter_finish (self, result, &local_error)) + gs_plugin_reload (GS_PLUGIN (self)); + else + g_warning ("Failed to reload changed app filter: %s", local_error->message); +} + +static void +gs_plugin_malcontent_init (GsPluginMalcontent *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + /* need application IDs and content ratings */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "flatpak"); + + /* set plugin name; it’s not a loadable plugin, but this is descriptive and harmless */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Malcontent"); +} + +static void get_app_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_malcontent_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_malcontent_setup_async); + + self->manager = mct_manager_new (gs_plugin_get_system_bus_connection (plugin)); + self->manager_app_filter_changed_id = g_signal_connect (self->manager, + "app-filter-changed", + (GCallback) app_filter_changed_cb, + self); + + mct_manager_get_app_filter_async (self->manager, getuid (), + /* FIXME: Should this be unconditionally interactive? */ + MCT_GET_APP_FILTER_FLAGS_INTERACTIVE, cancellable, + get_app_filter_cb, + g_steal_pointer (&task)); +} + +static void +get_app_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginMalcontent *self = g_task_get_source_object (task); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + g_autoptr(GError) local_error = NULL; + + self->app_filter = mct_manager_get_app_filter_finish (self->manager, result, &local_error); + if (self->app_filter == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_malcontent_setup_finish (GsPlugin *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +refine_app_locked (GsPluginMalcontent *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* not valid */ + if (gs_app_get_id (app) == NULL) + return TRUE; + + /* Filter by various parental filters. The filter can’t be %NULL, + * otherwise setup() would have failed and the plugin would have been + * disabled. */ + g_assert (self->app_filter != NULL); + + app_set_parental_quirks (self, app, self->app_filter); + + return TRUE; +} + +static void +gs_plugin_malcontent_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->mutex); + g_autoptr(GError) local_error = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_malcontent_refine_async); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app_locked (self, app, 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_malcontent_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_malcontent_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_malcontent_refresh_metadata_async); + + reload_app_filter_async (self, + (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE), + cancellable, + refresh_metadata_cb, + g_steal_pointer (&task)); +} + +static void +refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginMalcontent *self = g_task_get_source_object (task); + g_autoptr(GError) local_error = NULL; + + if (reload_app_filter_finish (self, result, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_malcontent_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_malcontent_dispose (GObject *object) +{ + GsPluginMalcontent *self = GS_PLUGIN_MALCONTENT (object); + + g_clear_pointer (&self->app_filter, mct_app_filter_unref); + if (self->manager != NULL && self->manager_app_filter_changed_id != 0) { + g_signal_handler_disconnect (self->manager, + self->manager_app_filter_changed_id); + self->manager_app_filter_changed_id = 0; + } + g_clear_object (&self->manager); + + G_OBJECT_CLASS (gs_plugin_malcontent_parent_class)->dispose (object); +} + +static void +gs_plugin_malcontent_class_init (GsPluginMalcontentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_malcontent_dispose; + + plugin_class->setup_async = gs_plugin_malcontent_setup_async; + plugin_class->setup_finish = gs_plugin_malcontent_setup_finish; + plugin_class->refine_async = gs_plugin_malcontent_refine_async; + plugin_class->refine_finish = gs_plugin_malcontent_refine_finish; + plugin_class->refresh_metadata_async = gs_plugin_malcontent_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_malcontent_refresh_metadata_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_MALCONTENT; +} diff --git a/plugins/malcontent/gs-plugin-malcontent.h b/plugins/malcontent/gs-plugin-malcontent.h new file mode 100644 index 0000000..11f9a4a --- /dev/null +++ b/plugins/malcontent/gs-plugin-malcontent.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_MALCONTENT (gs_plugin_malcontent_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginMalcontent, gs_plugin_malcontent, GS, PLUGIN_MALCONTENT, GsPlugin) + +G_END_DECLS diff --git a/plugins/malcontent/meson.build b/plugins/malcontent/meson.build new file mode 100644 index 0000000..149fde6 --- /dev/null +++ b/plugins/malcontent/meson.build @@ -0,0 +1,14 @@ +c_args = ['-DG_LOG_DOMAIN="GsPluginMalcontent"'] + +shared_module( + 'gs_plugin_malcontent', + sources : 'gs-plugin-malcontent.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : c_args, + dependencies : [ plugin_libs, malcontent ], +) diff --git a/plugins/meson.build b/plugins/meson.build new file mode 100644 index 0000000..711b488 --- /dev/null +++ b/plugins/meson.build @@ -0,0 +1,43 @@ +plugin_dir = join_paths(gs_private_libdir, 'plugins-' + gs_plugin_api_version) +plugin_libs = [ + appstream, + gio_unix, + json_glib, + libsoup, + libgnomesoftware_dep, +] + +subdir('core') +subdir('dpkg') +subdir('dummy') +subdir('fedora-langpacks') +subdir('fedora-pkgdb-collections') + +if get_option('eos_updater') + subdir('eos-updater') +endif +if get_option('flatpak') + subdir('flatpak') +endif +if get_option('fwupd') + subdir('fwupd') +endif +if get_option('gudev') + subdir('modalias') +endif +if get_option('malcontent') + subdir('malcontent') +endif +if get_option('packagekit') + subdir('packagekit') +endif +subdir('repos') +if get_option('rpm_ostree') + subdir('rpm-ostree') +endif +if get_option('snap') + subdir('snap') +endif +if get_option('webapps') + subdir('epiphany') +endif diff --git a/plugins/modalias/gs-plugin-modalias.c b/plugins/modalias/gs-plugin-modalias.c new file mode 100644 index 0000000..9efbcf1 --- /dev/null +++ b/plugins/modalias/gs-plugin-modalias.c @@ -0,0 +1,199 @@ +/* -*- 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+ + */ + +#include <config.h> + +#include <fnmatch.h> +#include <gudev/gudev.h> + +#include <gnome-software.h> + +#include "gs-plugin-modalias.h" + +struct _GsPluginModalias { + GsPlugin parent; + + GUdevClient *client; + GPtrArray *devices; +}; + +G_DEFINE_TYPE (GsPluginModalias, gs_plugin_modalias, GS_TYPE_PLUGIN) + +static void +gs_plugin_modalias_uevent_cb (GUdevClient *client, + const gchar *action, + GUdevDevice *device, + gpointer user_data) +{ + GsPluginModalias *self = GS_PLUGIN_MODALIAS (user_data); + + if (g_strcmp0 (action, "add") == 0 || + g_strcmp0 (action, "remove") == 0) { + g_debug ("invalidating devices as '%s' sent action '%s'", + g_udev_device_get_sysfs_path (device), + action); + g_ptr_array_set_size (self->devices, 0); + } +} + +static void +gs_plugin_modalias_init (GsPluginModalias *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); + + self->devices = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + self->client = g_udev_client_new (NULL); + g_signal_connect (self->client, "uevent", + G_CALLBACK (gs_plugin_modalias_uevent_cb), self); +} + +static void +gs_plugin_modalias_dispose (GObject *object) +{ + GsPluginModalias *self = GS_PLUGIN_MODALIAS (object); + + g_clear_object (&self->client); + g_clear_pointer (&self->devices, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_plugin_modalias_parent_class)->dispose (object); +} + +static void +gs_plugin_modalias_ensure_devices (GsPluginModalias *self) +{ + g_autoptr(GList) list = NULL; + + /* already set */ + if (self->devices->len > 0) + return; + + /* get the devices, and assume ownership of each */ + list = g_udev_client_query_by_subsystem (self->client, NULL); + for (GList *l = list; l != NULL; l = l->next) { + GUdevDevice *device = G_UDEV_DEVICE (l->data); + if (g_udev_device_get_sysfs_attr (device, "modalias") == NULL) { + g_object_unref (device); + continue; + } + g_ptr_array_add (self->devices, device); + } + g_debug ("%u devices with modalias", self->devices->len); +} + +static gboolean +gs_plugin_modalias_matches (GsPluginModalias *self, + const gchar *modalias) +{ + gs_plugin_modalias_ensure_devices (self); + for (guint i = 0; i < self->devices->len; i++) { + GUdevDevice *device = g_ptr_array_index (self->devices, i); + const gchar *modalias_tmp; + + /* get the (optional) device modalias */ + modalias_tmp = g_udev_device_get_sysfs_attr (device, "modalias"); + if (modalias_tmp == NULL) + continue; + if (fnmatch (modalias, modalias_tmp, 0) == 0) { + g_debug ("matched %s against %s", modalias_tmp, modalias); + return TRUE; + } + } + return FALSE; +} + +static gboolean +refine_app (GsPluginModalias *self, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *provided; + guint i; + + /* not required */ + if (gs_app_get_icons (app) != NULL) + return TRUE; + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_DRIVER) + return TRUE; + + /* do any of the modaliases match any installed hardware */ + provided = gs_app_get_provided (app); + for (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_MODALIAS) + continue; + items = as_provided_get_items (prov); + for (guint j = 0; j < items->len; j++) { + if (gs_plugin_modalias_matches (self, (const gchar*) g_ptr_array_index (items, j))) { + g_autoptr(GIcon) ic = NULL; + ic = g_themed_icon_new ("emblem-system-symbolic"); + gs_app_add_icon (app, ic); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + break; + } + } + } + return TRUE; +} + +static void +gs_plugin_modalias_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginModalias *self = GS_PLUGIN_MODALIAS (plugin); + 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_modalias_refine_async); + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (self, app, 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_modalias_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_modalias_class_init (GsPluginModaliasClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_modalias_dispose; + + plugin_class->refine_async = gs_plugin_modalias_refine_async; + plugin_class->refine_finish = gs_plugin_modalias_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_MODALIAS; +} diff --git a/plugins/modalias/gs-plugin-modalias.h b/plugins/modalias/gs-plugin-modalias.h new file mode 100644 index 0000000..386a667 --- /dev/null +++ b/plugins/modalias/gs-plugin-modalias.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_MODALIAS (gs_plugin_modalias_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginModalias, gs_plugin_modalias, GS, PLUGIN_MODALIAS, GsPlugin) + +G_END_DECLS diff --git a/plugins/modalias/gs-self-test.c b/plugins/modalias/gs-self-test.c new file mode 100644 index 0000000..e2c59da --- /dev/null +++ b/plugins/modalias/gs-self-test.c @@ -0,0 +1,113 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_modalias_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get search result based on addon keyword */ + keywords[0] = "colorhug2"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is one entry, the parent app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "com.hughski.ColorHug2.driver"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_DRIVER); + g_assert (gs_app_has_category (app, "Addon")); + g_assert (gs_app_has_category (app, "Driver")); +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *xml = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar * const allowlist[] = { + "appstream", + "dummy", + "modalias", + NULL + }; + + gs_test_init (&argc, &argv); + g_setenv ("GS_SELF_TEST_DUMMY_ENABLE", "1", TRUE); + + xml = g_strdup_printf ("<?xml version=\"1.0\"?>\n" + "<components version=\"0.9\">\n" + " <component type=\"driver\">\n" + " <id>com.hughski.ColorHug2.driver</id>\n" + " <name>ColorHug2</name>\n" + " <summary>ColorHug2 Colorimeter Driver</summary>\n" + " <pkgname>colorhug-client</pkgname>\n" + " <provides>\n" + " <modalias>pci:*</modalias>\n" + " </provides>\n" + " </component>\n" + " <info>\n" + " <scope>system</scope>\n" + " </info>\n" + "</components>\n"); + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-modalias-test-XXXXXX", NULL); + g_assert (tmp_root != NULL); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_DUMMY); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/modalias", + plugin_loader, + (GTestDataFunc) gs_plugins_modalias_func); + + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/modalias/meson.build b/plugins/modalias/meson.build new file mode 100644 index 0000000..fc0c806 --- /dev/null +++ b/plugins/modalias/meson.build @@ -0,0 +1,36 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginModalias"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] +cargs += ['-DLOCALPLUGINDIR_DUMMY="' + meson.current_build_dir() + '/../dummy"'] + +shared_module( + 'gs_plugin_modalias', +sources : 'gs-plugin-modalias.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, gudev ], +) + +if get_option('tests') + e = executable( + 'gs-self-test-modalias', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + c_args : cargs, + ) + test('gs-self-test-modalias', e, suite: ['plugins', 'modalias'], env: test_env) +endif diff --git a/plugins/packagekit/gs-markdown.c b/plugins/packagekit/gs-markdown.c new file mode 100644 index 0000000..b7be06b --- /dev/null +++ b/plugins/packagekit/gs-markdown.c @@ -0,0 +1,856 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2008 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib.h> + +#include "gs-markdown.h" + +/******************************************************************************* + * + * This is a simple Markdown parser. + * It can output to Pango, HTML or plain text. The following limitations are + * already known, and properly deliberate: + * + * - No code section support + * - No ordered list support + * - No blockquote section support + * - No image support + * - No links or email support + * - No backslash escapes support + * - No HTML escaping support + * - Auto-escapes certain word patterns, like http:// + * + * It does support the rest of the standard pretty well, although it's not + * been run against any conformance tests. The parsing is single pass, with + * a simple enumerated interpretor mode and a single line back-memory. + * + ******************************************************************************/ + +typedef enum { + GS_MARKDOWN_MODE_BLANK, + GS_MARKDOWN_MODE_RULE, + GS_MARKDOWN_MODE_BULLETT, + GS_MARKDOWN_MODE_PARA, + GS_MARKDOWN_MODE_H1, + GS_MARKDOWN_MODE_H2, + GS_MARKDOWN_MODE_UNKNOWN +} GsMarkdownMode; + +typedef struct { + const gchar *em_start; + const gchar *em_end; + const gchar *strong_start; + const gchar *strong_end; + const gchar *code_start; + const gchar *code_end; + const gchar *h1_start; + const gchar *h1_end; + const gchar *h2_start; + const gchar *h2_end; + const gchar *bullet_start; + const gchar *bullet_end; + const gchar *rule; +} GsMarkdownTags; + +struct _GsMarkdown { + GObject parent_instance; + + GsMarkdownMode mode; + GsMarkdownTags tags; + GsMarkdownOutputKind output; + gint max_lines; + gint line_count; + gboolean smart_quoting; + gboolean escape; + gboolean autocode; + gboolean autolinkify; + GString *pending; + GString *processed; +}; + +G_DEFINE_TYPE (GsMarkdown, gs_markdown, G_TYPE_OBJECT) + +/* + * gs_markdown_to_text_line_is_rule: + * + * Horizontal rules are created by placing three or more hyphens, asterisks, + * or underscores on a line by themselves. + * You may use spaces between the hyphens or asterisks. + **/ +static gboolean +gs_markdown_to_text_line_is_rule (const gchar *line) +{ + guint i; + guint len; + guint count = 0; + g_autofree gchar *copy = NULL; + + len = (guint) strlen (line); + if (len == 0) + return FALSE; + + /* replace non-rule chars with ~ */ + copy = g_strdup (line); + g_strcanon (copy, "-*_ ", '~'); + for (i = 0; i < len; i++) { + if (copy[i] == '~') + return FALSE; + if (copy[i] != ' ') + count++; + } + + /* if we matched, return true */ + if (count >= 3) + return TRUE; + return FALSE; +} + +static gboolean +gs_markdown_to_text_line_is_bullet (const gchar *line) +{ + return (g_str_has_prefix (line, "- ") || + g_str_has_prefix (line, "* ") || + g_str_has_prefix (line, "+ ") || + g_str_has_prefix (line, " - ") || + g_str_has_prefix (line, " * ") || + g_str_has_prefix (line, " + ")); +} + +static gboolean +gs_markdown_to_text_line_is_header1 (const gchar *line) +{ + return g_str_has_prefix (line, "# "); +} + +static gboolean +gs_markdown_to_text_line_is_header2 (const gchar *line) +{ + return g_str_has_prefix (line, "## "); +} + +static gboolean +gs_markdown_to_text_line_is_header1_type2 (const gchar *line) +{ + return g_str_has_prefix (line, "==="); +} + +static gboolean +gs_markdown_to_text_line_is_header2_type2 (const gchar *line) +{ + return g_str_has_prefix (line, "---"); +} + +#if 0 +static gboolean +gs_markdown_to_text_line_is_code (const gchar *line) +{ + return (g_str_has_prefix (line, " ") || + g_str_has_prefix (line, "\t")); +} + +static gboolean +gs_markdown_to_text_line_is_blockquote (const gchar *line) +{ + return (g_str_has_prefix (line, "> ")); +} +#endif + +static gboolean +gs_markdown_to_text_line_is_blank (const gchar *line) +{ + guint i; + guint len; + + /* a line with no characters is blank by definition */ + len = (guint) strlen (line); + if (len == 0) + return TRUE; + + /* find if there are only space chars */ + for (i = 0; i < len; i++) { + if (line[i] != ' ' && line[i] != '\t') + return FALSE; + } + + /* if we matched, return true */ + return TRUE; +} + +static gchar * +gs_markdown_replace (const gchar *haystack, + const gchar *needle, + const gchar *replace) +{ + g_auto(GStrv) split = NULL; + split = g_strsplit (haystack, needle, -1); + return g_strjoinv (replace, split); +} + +static gchar * +gs_markdown_strstr_spaces (const gchar *haystack, const gchar *needle) +{ + gchar *found; + const gchar *haystack_new = haystack; + +retry: + /* don't find if surrounded by spaces */ + found = strstr (haystack_new, needle); + if (found == NULL) + return NULL; + + /* start of the string, always valid */ + if (found == haystack) + return found; + + /* end of the string, always valid */ + if (*(found-1) == ' ' && *(found+1) == ' ') { + haystack_new = found+1; + goto retry; + } + return found; +} + +static gchar * +gs_markdown_to_text_line_formatter (const gchar *line, + const gchar *formatter, + const gchar *left, + const gchar *right) +{ + guint len; + gchar *str1; + gchar *str2; + gchar *start = NULL; + gchar *middle = NULL; + gchar *end = NULL; + g_autofree gchar *copy = NULL; + + /* needed to know for shifts */ + len = (guint) strlen (formatter); + if (len == 0) + return NULL; + + /* find sections */ + copy = g_strdup (line); + str1 = gs_markdown_strstr_spaces (copy, formatter); + if (str1 != NULL) { + *str1 = '\0'; + str2 = gs_markdown_strstr_spaces (str1+len, formatter); + if (str2 != NULL) { + *str2 = '\0'; + middle = str1 + len; + start = copy; + end = str2 + len; + } + } + + /* if we found, replace and keep looking for the same string */ + if (start != NULL && middle != NULL && end != NULL) { + g_autofree gchar *temp = NULL; + temp = g_strdup_printf ("%s%s%s%s%s", start, left, middle, right, end); + /* recursive */ + return gs_markdown_to_text_line_formatter (temp, formatter, left, right); + } + + /* not found, keep return as-is */ + return g_strdup (line); +} + +static gchar * +gs_markdown_to_text_line_format_sections (GsMarkdown *self, const gchar *line) +{ + gchar *data = g_strdup (line); + gchar *temp; + + /* bold1 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "**", + self->tags.strong_start, + self->tags.strong_end); + g_free (temp); + + /* bold2 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "__", + self->tags.strong_start, + self->tags.strong_end); + g_free (temp); + + /* italic1 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "*", + self->tags.em_start, + self->tags.em_end); + g_free (temp); + + /* italic2 */ + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "_", + self->tags.em_start, + self->tags.em_end); + g_free (temp); + + /* em-dash */ + temp = data; + data = gs_markdown_replace (temp, " -- ", " — "); + g_free (temp); + + /* smart quoting */ + if (self->smart_quoting) { + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "\"", "“", "”"); + g_free (temp); + + temp = data; + data = gs_markdown_to_text_line_formatter (temp, "'", "‘", "’"); + g_free (temp); + } + + return data; +} + +static gchar * +gs_markdown_to_text_line_format (GsMarkdown *self, const gchar *line) +{ + GString *string; + gboolean mode = FALSE; + gchar *text; + guint i; + g_auto(GStrv) codes = NULL; + + /* optimise the trivial case where we don't have any code tags */ + text = strstr (line, "`"); + if (text == NULL) + return gs_markdown_to_text_line_format_sections (self, line); + + /* we want to parse the code sections without formatting */ + codes = g_strsplit (line, "`", -1); + string = g_string_new (""); + for (i = 0; codes[i] != NULL; i++) { + if (!mode) { + text = gs_markdown_to_text_line_format_sections (self, codes[i]); + g_string_append (string, text); + g_free (text); + mode = TRUE; + } else { + /* just append without formatting */ + g_string_append (string, self->tags.code_start); + g_string_append (string, codes[i]); + g_string_append (string, self->tags.code_end); + mode = FALSE; + } + } + return g_string_free (string, FALSE); +} + +static gboolean +gs_markdown_add_pending (GsMarkdown *self, const gchar *line) +{ + g_autofree gchar *copy = NULL; + + /* would put us over the limit */ + if (self->max_lines > 0 && self->line_count >= self->max_lines) + return FALSE; + + copy = g_strdup (line); + + /* strip leading and trailing spaces */ + g_strstrip (copy); + + /* append */ + g_string_append_printf (self->pending, "%s ", copy); + return TRUE; +} + +static gboolean +gs_markdown_add_pending_header (GsMarkdown *self, const gchar *line) +{ + g_autofree gchar *copy = NULL; + + /* strip trailing # */ + copy = g_strdup (line); + g_strdelimit (copy, "#", ' '); + return gs_markdown_add_pending (self, copy); +} + +static guint +gs_markdown_count_chars_in_word (const gchar *text, gchar find) +{ + guint i; + guint len; + guint count = 0; + + /* get length */ + len = (guint) strlen (text); + if (len == 0) + return 0; + + /* find matching chars */ + for (i = 0; i < len; i++) { + if (text[i] == find) + count++; + } + return count; +} + +static gboolean +gs_markdown_word_is_code (const gchar *text) +{ + /* already code */ + if (g_str_has_prefix (text, "`")) + return FALSE; + if (g_str_has_suffix (text, "`")) + return FALSE; + + /* paths */ + if (g_str_has_prefix (text, "/")) + return TRUE; + + /* bugzillas */ + if (g_str_has_prefix (text, "#")) + return TRUE; + + /* patch files */ + if (g_strrstr (text, ".patch") != NULL) + return TRUE; + if (g_strrstr (text, ".diff") != NULL) + return TRUE; + + /* function names */ + if (g_strrstr (text, "()") != NULL) + return TRUE; + + /* email addresses */ + if (g_strrstr (text, "@") != NULL) + return TRUE; + + /* compiler defines */ + if (text[0] != '_' && + gs_markdown_count_chars_in_word (text, '_') > 1) + return TRUE; + + /* nothing special */ + return FALSE; +} + +static gchar * +gs_markdown_word_auto_format_code (const gchar *text) +{ + guint i; + gchar *temp; + gboolean ret = FALSE; + g_auto(GStrv) words = NULL; + + /* split sentence up with space */ + words = g_strsplit (text, " ", -1); + + /* search each word */ + for (i = 0; words[i] != NULL; i++) { + if (gs_markdown_word_is_code (words[i])) { + temp = g_strdup_printf ("`%s`", words[i]); + g_free (words[i]); + words[i] = temp; + ret = TRUE; + } + } + + /* no replacements, so just return a copy */ + if (!ret) + return g_strdup (text); + + /* join the array back into a string */ + return g_strjoinv (" ", words); +} + +static gboolean +gs_markdown_word_is_url (const gchar *text) +{ + if (g_str_has_prefix (text, "http://")) + return TRUE; + if (g_str_has_prefix (text, "https://")) + return TRUE; + if (g_str_has_prefix (text, "ftp://")) + return TRUE; + return FALSE; +} + +static gchar * +gs_markdown_word_auto_format_urls (const gchar *text) +{ + guint i; + gchar *temp; + gboolean ret = FALSE; + g_auto(GStrv) words = NULL; + + /* split sentence up with space */ + words = g_strsplit (text, " ", -1); + + /* search each word */ + for (i = 0; words[i] != NULL; i++) { + if (gs_markdown_word_is_url (words[i])) { + temp = g_strdup_printf ("<a href=\"%s\">%s</a>", + words[i], words[i]); + g_free (words[i]); + words[i] = temp; + ret = TRUE; + } + } + + /* no replacements, so just return a copy */ + if (!ret) + return g_strdup (text); + + /* join the array back into a string */ + return g_strjoinv (" ", words); +} + +static void +gs_markdown_flush_pending (GsMarkdown *self) +{ + g_autofree gchar *copy = NULL; + g_autofree gchar *temp = NULL; + + /* no data yet */ + if (self->mode == GS_MARKDOWN_MODE_UNKNOWN) + return; + + /* remove trailing spaces */ + while (g_str_has_suffix (self->pending->str, " ")) + g_string_set_size (self->pending, self->pending->len - 1); + + /* pango requires escaping */ + copy = g_strdup (self->pending->str); + if (!self->escape && self->output == GS_MARKDOWN_OUTPUT_PANGO) { + g_strdelimit (copy, "<", '('); + g_strdelimit (copy, ">", ')'); + g_strdelimit (copy, "&", '+'); + } + + /* check words for code */ + if (self->autocode && + (self->mode == GS_MARKDOWN_MODE_PARA || + self->mode == GS_MARKDOWN_MODE_BULLETT)) { + temp = gs_markdown_word_auto_format_code (copy); + g_free (copy); + copy = temp; + } + + /* escape */ + if (self->escape) { + temp = g_markup_escape_text (copy, -1); + g_free (copy); + copy = temp; + } + + /* check words for URLS */ + if (self->autolinkify && + self->output == GS_MARKDOWN_OUTPUT_PANGO && + (self->mode == GS_MARKDOWN_MODE_PARA || + self->mode == GS_MARKDOWN_MODE_BULLETT)) { + temp = gs_markdown_word_auto_format_urls (copy); + g_free (copy); + copy = temp; + } + + /* do formatting */ + temp = gs_markdown_to_text_line_format (self, copy); + if (self->mode == GS_MARKDOWN_MODE_BULLETT) { + g_string_append_printf (self->processed, "%s%s%s\n", + self->tags.bullet_start, + temp, + self->tags.bullet_end); + self->line_count++; + } else if (self->mode == GS_MARKDOWN_MODE_H1) { + g_string_append_printf (self->processed, "%s%s%s\n", + self->tags.h1_start, + temp, + self->tags.h1_end); + } else if (self->mode == GS_MARKDOWN_MODE_H2) { + g_string_append_printf (self->processed, "%s%s%s\n", + self->tags.h2_start, + temp, + self->tags.h2_end); + } else if (self->mode == GS_MARKDOWN_MODE_PARA || + self->mode == GS_MARKDOWN_MODE_RULE) { + g_string_append_printf (self->processed, "%s\n", temp); + self->line_count++; + } + + /* clear */ + g_string_truncate (self->pending, 0); +} + +static gboolean +gs_markdown_to_text_line_process (GsMarkdown *self, const gchar *line) +{ + gboolean ret; + + /* blank */ + ret = gs_markdown_to_text_line_is_blank (line); + if (ret) { + gs_markdown_flush_pending (self); + /* a new line after a list is the end of list, not a gap */ + if (self->mode != GS_MARKDOWN_MODE_BULLETT) + ret = gs_markdown_add_pending (self, "\n"); + self->mode = GS_MARKDOWN_MODE_BLANK; + goto out; + } + + /* header1_type2 */ + ret = gs_markdown_to_text_line_is_header1_type2 (line); + if (ret) { + if (self->mode == GS_MARKDOWN_MODE_PARA) + self->mode = GS_MARKDOWN_MODE_H1; + goto out; + } + + /* header2_type2 */ + ret = gs_markdown_to_text_line_is_header2_type2 (line); + if (ret) { + if (self->mode == GS_MARKDOWN_MODE_PARA) + self->mode = GS_MARKDOWN_MODE_H2; + goto out; + } + + /* rule */ + ret = gs_markdown_to_text_line_is_rule (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_RULE; + ret = gs_markdown_add_pending (self, self->tags.rule); + goto out; + } + + /* bullet */ + ret = gs_markdown_to_text_line_is_bullet (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_BULLETT; + ret = gs_markdown_add_pending (self, &line[2]); + goto out; + } + + /* header1 */ + ret = gs_markdown_to_text_line_is_header1 (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_H1; + ret = gs_markdown_add_pending_header (self, &line[2]); + goto out; + } + + /* header2 */ + ret = gs_markdown_to_text_line_is_header2 (line); + if (ret) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_H2; + ret = gs_markdown_add_pending_header (self, &line[3]); + goto out; + } + + /* paragraph */ + if (self->mode == GS_MARKDOWN_MODE_BLANK || + self->mode == GS_MARKDOWN_MODE_UNKNOWN) { + gs_markdown_flush_pending (self); + self->mode = GS_MARKDOWN_MODE_PARA; + } + + /* add to pending */ + ret = gs_markdown_add_pending (self, line); +out: + /* if we failed to add, we don't know the mode */ + if (!ret) + self->mode = GS_MARKDOWN_MODE_UNKNOWN; + return ret; +} + +static void +gs_markdown_set_output_kind (GsMarkdown *self, GsMarkdownOutputKind output) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + + self->output = output; + switch (output) { + case GS_MARKDOWN_OUTPUT_PANGO: + /* PangoMarkup */ + self->tags.em_start = "<i>"; + self->tags.em_end = "</i>"; + self->tags.strong_start = "<b>"; + self->tags.strong_end = "</b>"; + self->tags.code_start = "<tt>"; + self->tags.code_end = "</tt>"; + self->tags.h1_start = "<big>"; + self->tags.h1_end = "</big>"; + self->tags.h2_start = "<b>"; + self->tags.h2_end = "</b>"; + self->tags.bullet_start = "• "; + self->tags.bullet_end = ""; + self->tags.rule = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n"; + self->escape = TRUE; + self->autolinkify = TRUE; + break; + case GS_MARKDOWN_OUTPUT_HTML: + /* XHTML */ + self->tags.em_start = "<em>"; + self->tags.em_end = "<em>"; + self->tags.strong_start = "<strong>"; + self->tags.strong_end = "</strong>"; + self->tags.code_start = "<code>"; + self->tags.code_end = "</code>"; + self->tags.h1_start = "<h1>"; + self->tags.h1_end = "</h1>"; + self->tags.h2_start = "<h2>"; + self->tags.h2_end = "</h2>"; + self->tags.bullet_start = "<li>"; + self->tags.bullet_end = "</li>"; + self->tags.rule = "<hr>"; + self->escape = TRUE; + self->autolinkify = TRUE; + break; + case GS_MARKDOWN_OUTPUT_TEXT: + /* plain text */ + self->tags.em_start = ""; + self->tags.em_end = ""; + self->tags.strong_start = ""; + self->tags.strong_end = ""; + self->tags.code_start = ""; + self->tags.code_end = ""; + self->tags.h1_start = "["; + self->tags.h1_end = "]"; + self->tags.h2_start = "-"; + self->tags.h2_end = "-"; + self->tags.bullet_start = "* "; + self->tags.bullet_end = ""; + self->tags.rule = " ----- \n"; + self->escape = FALSE; + self->autolinkify = FALSE; + break; + default: + g_warning ("unknown output enum"); + break; + } +} + +void +gs_markdown_set_max_lines (GsMarkdown *self, gint max_lines) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->max_lines = max_lines; +} + +void +gs_markdown_set_smart_quoting (GsMarkdown *self, gboolean smart_quoting) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->smart_quoting = smart_quoting; +} + +void +gs_markdown_set_escape (GsMarkdown *self, gboolean escape) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->escape = escape; +} + +void +gs_markdown_set_autocode (GsMarkdown *self, gboolean autocode) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->autocode = autocode; +} + +void +gs_markdown_set_autolinkify (GsMarkdown *self, gboolean autolinkify) +{ + g_return_if_fail (GS_IS_MARKDOWN (self)); + self->autolinkify = autolinkify; +} + +gchar * +gs_markdown_parse (GsMarkdown *self, const gchar *markdown) +{ + gboolean ret; + gchar *temp; + guint i; + guint len; + g_auto(GStrv) lines = NULL; + + g_return_val_if_fail (GS_IS_MARKDOWN (self), NULL); + + /* process */ + self->mode = GS_MARKDOWN_MODE_UNKNOWN; + self->line_count = 0; + g_string_truncate (self->pending, 0); + g_string_truncate (self->processed, 0); + lines = g_strsplit (markdown, "\n", -1); + len = g_strv_length (lines); + + /* process each line */ + for (i = 0; i < len; i++) { + ret = gs_markdown_to_text_line_process (self, lines[i]); + if (!ret) + break; + } + gs_markdown_flush_pending (self); + + /* remove trailing \n */ + while (g_str_has_suffix (self->processed->str, "\n")) + g_string_set_size (self->processed, self->processed->len - 1); + + /* get a copy */ + temp = g_strdup (self->processed->str); + g_string_truncate (self->pending, 0); + g_string_truncate (self->processed, 0); + return temp; +} + +static void +gs_markdown_finalize (GObject *object) +{ + GsMarkdown *self; + + g_return_if_fail (GS_IS_MARKDOWN (object)); + + self = GS_MARKDOWN (object); + + g_string_free (self->pending, TRUE); + g_string_free (self->processed, TRUE); + + G_OBJECT_CLASS (gs_markdown_parent_class)->finalize (object); +} + +static void +gs_markdown_class_init (GsMarkdownClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_markdown_finalize; +} + +static void +gs_markdown_init (GsMarkdown *self) +{ + self->mode = GS_MARKDOWN_MODE_UNKNOWN; + self->pending = g_string_new (""); + self->processed = g_string_new (""); + self->max_lines = -1; + self->smart_quoting = FALSE; + self->escape = FALSE; + self->autocode = FALSE; +} + +GsMarkdown * +gs_markdown_new (GsMarkdownOutputKind output) +{ + GsMarkdown *self; + self = g_object_new (GS_TYPE_MARKDOWN, NULL); + gs_markdown_set_output_kind (self, output); + return GS_MARKDOWN (self); +} diff --git a/plugins/packagekit/gs-markdown.h b/plugins/packagekit/gs-markdown.h new file mode 100644 index 0000000..51e6233 --- /dev/null +++ b/plugins/packagekit/gs-markdown.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) 2008-2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_MARKDOWN (gs_markdown_get_type ()) + +G_DECLARE_FINAL_TYPE (GsMarkdown, gs_markdown, GS, MARKDOWN, GObject) + +typedef enum { + GS_MARKDOWN_OUTPUT_TEXT, + GS_MARKDOWN_OUTPUT_PANGO, + GS_MARKDOWN_OUTPUT_HTML, + GS_MARKDOWN_OUTPUT_LAST +} GsMarkdownOutputKind; + +GsMarkdown *gs_markdown_new (GsMarkdownOutputKind output); +void gs_markdown_set_max_lines (GsMarkdown *self, + gint max_lines); +void gs_markdown_set_smart_quoting (GsMarkdown *self, + gboolean smart_quoting); +void gs_markdown_set_escape (GsMarkdown *self, + gboolean escape); +void gs_markdown_set_autocode (GsMarkdown *self, + gboolean autocode); +void gs_markdown_set_autolinkify (GsMarkdown *self, + gboolean autolinkify); +gchar *gs_markdown_parse (GsMarkdown *self, + const gchar *text); + +G_END_DECLS diff --git a/plugins/packagekit/gs-packagekit-helper.c b/plugins/packagekit/gs-packagekit-helper.c new file mode 100644 index 0000000..7ae42c1 --- /dev/null +++ b/plugins/packagekit/gs-packagekit-helper.c @@ -0,0 +1,141 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-packagekit-helper.h" +#include "packagekit-common.h" + +struct _GsPackagekitHelper { + GObject parent_instance; + GHashTable *apps; + GsApp *progress_app; + GsAppList *progress_list; + GsPlugin *plugin; +}; + +G_DEFINE_TYPE (GsPackagekitHelper, gs_packagekit_helper, G_TYPE_OBJECT) + +void +gs_packagekit_helper_cb (PkProgress *progress, PkProgressType type, gpointer user_data) +{ + GsPackagekitHelper *self = (GsPackagekitHelper *) user_data; + GsPlugin *plugin = gs_packagekit_helper_get_plugin (self); + const gchar *package_id = pk_progress_get_package_id (progress); + GsApp *app = NULL; + + /* optional */ + if (self->progress_app != NULL) + app = self->progress_app; + else if (package_id != NULL) + app = gs_packagekit_helper_get_app_by_id (self, package_id); + + if (type == PK_PROGRESS_TYPE_STATUS) { + PkStatusEnum status = pk_progress_get_status (progress); + GsPluginStatus plugin_status = packagekit_status_enum_to_plugin_status (status); + if (plugin_status != GS_PLUGIN_STATUS_UNKNOWN) + gs_plugin_status_update (plugin, app, plugin_status); + } else if (type == PK_PROGRESS_TYPE_PERCENTAGE) { + gint percentage = pk_progress_get_percentage (progress); + if (app != NULL && percentage >= 0 && percentage <= 100) + gs_app_set_progress (app, (guint) percentage); + if (self->progress_list != NULL && percentage >= 0 && percentage <= 100) + gs_app_list_override_progress (self->progress_list, (guint) percentage); + } + + /* Only go from TRUE to FALSE - it doesn't make sense for a package + * install to become uncancellable later on */ + if (app != NULL && gs_app_get_allow_cancel (app)) + gs_app_set_allow_cancel (app, pk_progress_get_allow_cancel (progress)); +} + +void +gs_packagekit_helper_add_app (GsPackagekitHelper *self, GsApp *app) +{ + GPtrArray *source_ids = gs_app_get_source_ids (app); + + g_return_if_fail (GS_IS_PACKAGEKIT_HELPER (self)); + g_return_if_fail (GS_IS_APP (app)); + + for (guint i = 0; i < source_ids->len; i++) { + const gchar *source_id = g_ptr_array_index (source_ids, i); + g_hash_table_insert (self->apps, + g_strdup (source_id), + g_object_ref (app)); + } +} + +void +gs_packagekit_helper_set_progress_app (GsPackagekitHelper *self, GsApp *progress_app) +{ + g_set_object (&self->progress_app, progress_app); +} + +void +gs_packagekit_helper_set_progress_list (GsPackagekitHelper *self, GsAppList *progress_list) +{ + g_set_object (&self->progress_list, progress_list); +} + +GsPlugin * +gs_packagekit_helper_get_plugin (GsPackagekitHelper *self) +{ + g_return_val_if_fail (GS_IS_PACKAGEKIT_HELPER (self), NULL); + return self->plugin; +} + +GsApp * +gs_packagekit_helper_get_app_by_id (GsPackagekitHelper *self, const gchar *package_id) +{ + g_return_val_if_fail (GS_IS_PACKAGEKIT_HELPER (self), NULL); + g_return_val_if_fail (package_id != NULL, NULL); + return g_hash_table_lookup (self->apps, package_id); +} + +static void +gs_packagekit_helper_finalize (GObject *object) +{ + GsPackagekitHelper *self; + + g_return_if_fail (GS_IS_PACKAGEKIT_HELPER (object)); + + self = GS_PACKAGEKIT_HELPER (object); + + g_object_unref (self->plugin); + g_clear_object (&self->progress_app); + g_clear_object (&self->progress_list); + g_hash_table_unref (self->apps); + + G_OBJECT_CLASS (gs_packagekit_helper_parent_class)->finalize (object); +} + +static void +gs_packagekit_helper_class_init (GsPackagekitHelperClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_packagekit_helper_finalize; +} + +static void +gs_packagekit_helper_init (GsPackagekitHelper *self) +{ + self->apps = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_object_unref); +} + +GsPackagekitHelper * +gs_packagekit_helper_new (GsPlugin *plugin) +{ + GsPackagekitHelper *self; + self = g_object_new (GS_TYPE_PACKAGEKIT_HELPER, NULL); + self->plugin = g_object_ref (plugin); + return GS_PACKAGEKIT_HELPER (self); +} diff --git a/plugins/packagekit/gs-packagekit-helper.h b/plugins/packagekit/gs-packagekit-helper.h new file mode 100644 index 0000000..594d0c0 --- /dev/null +++ b/plugins/packagekit/gs-packagekit-helper.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) 2016-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gnome-software.h> +#include <packagekit-glib2/packagekit.h> + +G_BEGIN_DECLS + +#define GS_TYPE_PACKAGEKIT_HELPER (gs_packagekit_helper_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPackagekitHelper, gs_packagekit_helper, GS, PACKAGEKIT_HELPER, GObject) + +GsPackagekitHelper *gs_packagekit_helper_new (GsPlugin *plugin); +GsPlugin *gs_packagekit_helper_get_plugin (GsPackagekitHelper *self); +void gs_packagekit_helper_add_app (GsPackagekitHelper *self, + GsApp *app); +void gs_packagekit_helper_set_progress_app (GsPackagekitHelper *self, + GsApp *progress_app); +void gs_packagekit_helper_set_progress_list (GsPackagekitHelper *self, + GsAppList *progress_list); +GsApp *gs_packagekit_helper_get_app_by_id (GsPackagekitHelper *self, + const gchar *package_id); +void gs_packagekit_helper_cb (PkProgress *progress, + PkProgressType type, + gpointer user_data); + + +G_END_DECLS diff --git a/plugins/packagekit/gs-packagekit-task.c b/plugins/packagekit/gs-packagekit-task.c new file mode 100644 index 0000000..7727ce3 --- /dev/null +++ b/plugins/packagekit/gs-packagekit-task.c @@ -0,0 +1,280 @@ +/* -*- 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 "config.h" + +#include <glib/gi18n-lib.h> + +#include "gs-packagekit-task.h" + +/** + * SECTION:gs-packagekit-task + * @short_description: PkTask subclass which implements vfuncs for user interaction during a task + * + * #GsPackagekitTask is a subclass of #PkTask which represents a single + * operation on PackageKit. + * + * By subclassing #PkTask, it can implement vfuncs which allow decisions + * to be made about the task while it’s running. For example, to decide + * what to do if an untrusted package needs to be installed. + * + * Since: 42 + */ + +typedef struct { + GWeakRef plugin_weakref; /* GsPlugin * */ + GsPluginAction action; + GsPackagekitHelper *helper; + +} GsPackagekitTaskPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsPackagekitTask, gs_packagekit_task, PK_TYPE_TASK) + +static gboolean +gs_packagekit_task_user_accepted (PkTask *task, + const gchar *title, + const gchar *msg, + const gchar *details, + const gchar *accept_label) +{ + GsPackagekitTask *gs_task = GS_PACKAGEKIT_TASK (task); + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (gs_task); + g_autoptr(GsPlugin) plugin = NULL; + gboolean accepts = FALSE; + + plugin = g_weak_ref_get (&priv->plugin_weakref); + if (plugin) + accepts = gs_plugin_ask_untrusted (plugin, title, msg, details, accept_label); + + return accepts; +} + +typedef struct { + GWeakRef task_weakref; + guint request; + gchar *title; + gchar *msg; + gchar *details; + gchar *accept_label; +} QuestionData; + +static QuestionData * +question_data_new (GsPackagekitTask *task, + guint request, + const gchar *title, + const gchar *msg, + const gchar *details, + const gchar *accept_label) +{ + QuestionData *qd; + + qd = g_slice_new0 (QuestionData); + g_weak_ref_init (&qd->task_weakref, task); + qd->request = request; + qd->title = g_strdup (title); + qd->msg = g_strdup (msg); + qd->details = g_strdup (details); + qd->accept_label = g_strdup (accept_label); + + return qd; +} + +static void +question_data_free (gpointer ptr) +{ + QuestionData *qd = ptr; + g_weak_ref_clear (&qd->task_weakref); + g_free (qd->title); + g_free (qd->msg); + g_free (qd->details); + g_free (qd->accept_label); + g_slice_free (QuestionData, qd); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (QuestionData, question_data_free) + +static gboolean +gs_packagekit_task_question_idle_cb (gpointer user_data) +{ + QuestionData *qd = user_data; + g_autoptr(PkTask) task = NULL; + + task = g_weak_ref_get (&qd->task_weakref); + if (task) { + if (gs_packagekit_task_user_accepted (task, qd->title, qd->msg, qd->details, qd->accept_label)) + pk_task_user_accepted (task, qd->request); + else + pk_task_user_declined (task, qd->request); + } + + return G_SOURCE_REMOVE; +} + +static void +gs_packagekit_task_schedule_question (GsPackagekitTask *task, + guint request, + const gchar *title, + const gchar *msg, + const gchar *details, + const gchar *accept_label) +{ + g_autoptr(QuestionData) qd = NULL; + + qd = question_data_new (task, request, title, msg, details, accept_label); + g_idle_add_full (G_PRIORITY_HIGH_IDLE, gs_packagekit_task_question_idle_cb, g_steal_pointer (&qd), question_data_free); +} + +/* This may be called in a PackageKit worker thread. */ +static void +gs_packagekit_task_untrusted_question (PkTask *task, + guint request, + PkResults *results) +{ + GsPackagekitTask *gs_task = GS_PACKAGEKIT_TASK (task); + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (gs_task); + g_autoptr(PkError) error = NULL; + const gchar *title; + const gchar *msg; + const gchar *details; + const gchar *accept_label; + + switch (priv->action) { + case GS_PLUGIN_ACTION_INSTALL: + title = _("Install Unsigned Software?"); + msg = _("Software that is to be installed is not signed. It will not be possible to verify the origin of updates to this software, or whether updates have been tampered with."); + accept_label = _("_Install"); + break; + case GS_PLUGIN_ACTION_DOWNLOAD: + title = _("Download Unsigned Software?"); + msg = _("Unsigned updates are available. Without a signature, it is not possible to verify the origin of the update, or whether it has been tampered with."); + accept_label = _("_Download"); + break; + case GS_PLUGIN_ACTION_UPDATE: + title = _("Update Unsigned Software?"); + msg = _("Unsigned updates are available. Without a signature, it is not possible to verify the origin of the update, or whether it has been tampered with. Software updates will be disabled until unsigned updates are either removed or updated."); + accept_label = _("_Update"); + break; + default: + pk_task_user_declined (task, request); + return; + } + + error = pk_results_get_error_code (results); + if (error) + details = pk_error_get_details (error); + else + details = NULL; + + gs_packagekit_task_schedule_question (gs_task, request, title, msg, details, accept_label); +} + +static void +gs_packagekit_task_finalize (GObject *object) +{ + GsPackagekitTask *task = GS_PACKAGEKIT_TASK (object); + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (task); + + g_weak_ref_clear (&priv->plugin_weakref); + g_clear_object (&priv->helper); + + G_OBJECT_CLASS (gs_packagekit_task_parent_class)->finalize (object); +} + +static void +gs_packagekit_task_class_init (GsPackagekitTaskClass *klass) +{ + GObjectClass *object_class; + PkTaskClass *task_class; + + task_class = PK_TASK_CLASS (klass); + task_class->untrusted_question = gs_packagekit_task_untrusted_question; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_packagekit_task_finalize; +} + +static void +gs_packagekit_task_init (GsPackagekitTask *task) +{ + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (task); + + g_weak_ref_init (&priv->plugin_weakref, NULL); +} + +PkTask * +gs_packagekit_task_new (GsPlugin *plugin) +{ + GsPackagekitTask *task; + GsPackagekitTaskPrivate *priv; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL); + + task = g_object_new (GS_TYPE_PACKAGEKIT_TASK, NULL); + priv = gs_packagekit_task_get_instance_private (task); + + g_weak_ref_set (&priv->plugin_weakref, plugin); + + return PK_TASK (task); +} + +void +gs_packagekit_task_setup (GsPackagekitTask *task, + GsPluginAction action, + gboolean interactive) +{ + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (task); + + g_return_if_fail (GS_IS_PACKAGEKIT_TASK (task)); + + priv->action = action; + + /* The :interactive and :background properties have slightly different + * purposes: + * - :interactive controls whether the task can create interactive + * authentication (polkit) prompts + * - :background controls the scheduling of the task relative to other + * PackageKit tasks from this client and other clients + * However, we always want to set them both based on the same + * conditions. */ + pk_client_set_interactive (PK_CLIENT (task), interactive); + pk_client_set_background (PK_CLIENT (task), !interactive); +} + +GsPluginAction +gs_packagekit_task_get_action (GsPackagekitTask *task) +{ + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (task); + + g_return_val_if_fail (GS_IS_PACKAGEKIT_TASK (task), GS_PLUGIN_ACTION_UNKNOWN); + + return priv->action; +} + +void +gs_packagekit_task_take_helper (GsPackagekitTask *task, + GsPackagekitHelper *helper) +{ + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (task); + + g_return_if_fail (GS_IS_PACKAGEKIT_TASK (task)); + + if (priv->helper != helper) { + g_clear_object (&priv->helper); + priv->helper = helper; + } +} + +GsPackagekitHelper * +gs_packagekit_task_get_helper (GsPackagekitTask *task) +{ + GsPackagekitTaskPrivate *priv = gs_packagekit_task_get_instance_private (task); + + g_return_val_if_fail (GS_IS_PACKAGEKIT_TASK (task), NULL); + + return priv->helper; +} diff --git a/plugins/packagekit/gs-packagekit-task.h b/plugins/packagekit/gs-packagekit-task.h new file mode 100644 index 0000000..ff4bcc6 --- /dev/null +++ b/plugins/packagekit/gs-packagekit-task.h @@ -0,0 +1,38 @@ +/* -*- 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-object.h> +#include <gnome-software.h> +#include <packagekit-glib2/packagekit.h> + +#include "gs-packagekit-helper.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PACKAGEKIT_TASK (gs_packagekit_task_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsPackagekitTask, gs_packagekit_task, GS, PACKAGEKIT_TASK, PkTask) + +struct _GsPackagekitTaskClass +{ + PkTaskClass parent_class; +}; + +PkTask *gs_packagekit_task_new (GsPlugin *plugin); +void gs_packagekit_task_setup (GsPackagekitTask *task, + GsPluginAction action, + gboolean interactive); +GsPluginAction gs_packagekit_task_get_action (GsPackagekitTask *task); +void gs_packagekit_task_take_helper (GsPackagekitTask *task, + GsPackagekitHelper *helper); +GsPackagekitHelper * + gs_packagekit_task_get_helper (GsPackagekitTask *task); + +G_END_DECLS diff --git a/plugins/packagekit/gs-plugin-packagekit.c b/plugins/packagekit/gs-plugin-packagekit.c new file mode 100644 index 0000000..5b601c9 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit.c @@ -0,0 +1,4080 @@ +/* -*- 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-2018 Kalev Lember <klember@redhat.com> + * Copyright (C) 2017 Canonical Ltd + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n-lib.h> +#include <gnome-software.h> +#include <gsettings-desktop-schemas/gdesktop-enums.h> +#include <packagekit-glib2/packagekit.h> +#include <string.h> + +#include "packagekit-common.h" +#include "gs-markdown.h" +#include "gs-packagekit-helper.h" +#include "gs-packagekit-task.h" +#include "gs-plugin-private.h" + +#include "gs-plugin-packagekit.h" + +/* + * SECTION: + * Uses the system PackageKit instance to return installed packages, + * sources and the ability to add and remove packages. Supports package history + * and converting URIs to apps. + * + * Supports setting the session proxy on the system PackageKit instance. + * + * Also supports doing a PackageKit UpdatePackages(ONLY_DOWNLOAD) method on + * refresh and also converts any package files to applications the best we can. + * + * Also supports converting repo filenames to package-ids. + * + * Also supports marking previously downloaded packages as zero size, and allows + * scheduling the offline update. + * + * Requires: | [source-id], [repos::repo-filename] + * Refines: | [source-id], [source], [update-details], [management-plugin] + */ + +#define GS_PLUGIN_PACKAGEKIT_HISTORY_TIMEOUT 5000 /* ms */ + +/* Timeout to trigger auto-prepare update after the prepared update had been invalidated */ +#define PREPARE_UPDATE_TIMEOUT_SECS 30 + +struct _GsPluginPackagekit { + GsPlugin parent; + + PkControl *control_refine; + + PkControl *control_proxy; + GSettings *settings_proxy; + GSettings *settings_http; + GSettings *settings_https; + GSettings *settings_ftp; + GSettings *settings_socks; + + GFileMonitor *monitor; + GFileMonitor *monitor_trigger; + GPermission *permission; + gboolean is_triggered; + GHashTable *prepared_updates; /* (element-type utf8); set of package IDs for updates which are already prepared */ + GMutex prepared_updates_mutex; + guint prepare_update_timeout_id; + + GCancellable *proxy_settings_cancellable; /* (nullable) (owned) */ +}; + +G_DEFINE_TYPE (GsPluginPackagekit, gs_plugin_packagekit, GS_TYPE_PLUGIN) + +static void gs_plugin_packagekit_updates_changed_cb (PkControl *control, GsPlugin *plugin); +static void gs_plugin_packagekit_repo_list_changed_cb (PkControl *control, GsPlugin *plugin); +static void gs_plugin_packagekit_refine_history_async (GsPluginPackagekit *self, + GsAppList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +static gboolean gs_plugin_packagekit_refine_history_finish (GsPluginPackagekit *self, + GAsyncResult *result, + GError **error); +static void gs_plugin_packagekit_proxy_changed_cb (GSettings *settings, + const gchar *key, + gpointer user_data); +static void reload_proxy_settings_async (GsPluginPackagekit *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +static gboolean reload_proxy_settings_finish (GsPluginPackagekit *self, + GAsyncResult *result, + GError **error); + +static void +gs_plugin_packagekit_init (GsPluginPackagekit *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + /* refine */ + self->control_refine = pk_control_new (); + g_signal_connect (self->control_refine, "updates-changed", + G_CALLBACK (gs_plugin_packagekit_updates_changed_cb), plugin); + g_signal_connect (self->control_refine, "repo-list-changed", + G_CALLBACK (gs_plugin_packagekit_repo_list_changed_cb), plugin); + + /* proxy */ + self->control_proxy = pk_control_new (); + self->settings_proxy = g_settings_new ("org.gnome.system.proxy"); + g_signal_connect (self->settings_proxy, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), self); + + self->settings_http = g_settings_new ("org.gnome.system.proxy.http"); + self->settings_https = g_settings_new ("org.gnome.system.proxy.https"); + self->settings_ftp = g_settings_new ("org.gnome.system.proxy.ftp"); + self->settings_socks = g_settings_new ("org.gnome.system.proxy.socks"); + g_signal_connect (self->settings_http, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), self); + g_signal_connect (self->settings_https, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), self); + g_signal_connect (self->settings_ftp, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), self); + g_signal_connect (self->settings_socks, "changed", + G_CALLBACK (gs_plugin_packagekit_proxy_changed_cb), self); + + /* offline updates */ + g_mutex_init (&self->prepared_updates_mutex); + self->prepared_updates = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + + /* need pkgname and ID */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + + /* we can return better results than dpkg directly */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "dpkg"); + + /* need repos::repo-filename */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "repos"); + + /* generic updates happen after PackageKit offline updates */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "generic-updates"); +} + +static void +gs_plugin_packagekit_dispose (GObject *object) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (object); + + if (self->prepare_update_timeout_id) { + g_source_remove (self->prepare_update_timeout_id); + self->prepare_update_timeout_id = 0; + } + + g_cancellable_cancel (self->proxy_settings_cancellable); + g_clear_object (&self->proxy_settings_cancellable); + + /* refine */ + g_clear_object (&self->control_refine); + + /* proxy */ + g_clear_object (&self->control_proxy); + g_clear_object (&self->settings_proxy); + g_clear_object (&self->settings_http); + g_clear_object (&self->settings_https); + g_clear_object (&self->settings_ftp); + g_clear_object (&self->settings_socks); + + /* offline updates */ + g_clear_pointer (&self->prepared_updates, g_hash_table_unref); + g_clear_object (&self->monitor); + g_clear_object (&self->monitor_trigger); + + G_OBJECT_CLASS (gs_plugin_packagekit_parent_class)->dispose (object); +} + +static void +gs_plugin_packagekit_finalize (GObject *object) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (object); + + g_mutex_clear (&self->prepared_updates_mutex); + + G_OBJECT_CLASS (gs_plugin_packagekit_parent_class)->finalize (object); +} + +typedef gboolean (*GsAppFilterFunc) (GsApp *app); + +static gboolean +package_is_installed (const gchar *package_id) +{ + g_auto(GStrv) split = NULL; + const gchar *data; + + split = pk_package_id_split (package_id); + if (split == NULL) { + return FALSE; + } + + data = split[PK_PACKAGE_ID_DATA]; + if (g_str_has_prefix (data, "installed") || + g_str_has_prefix (data, "manual:") || + g_str_has_prefix (data, "auto:")) { + return TRUE; + } + + return FALSE; +} + +/* The elements in the returned #GPtrArray reference memory from within the + * @apps list, so the array is only valid as long as @apps is not modified or + * freed. The array is not NULL-terminated. + * + * If @apps is %NULL, that’s considered equivalent to an empty list. */ +static GPtrArray * +app_list_get_package_ids (GsAppList *apps, + GsAppFilterFunc app_filter, + gboolean ignore_installed) +{ + g_autoptr(GPtrArray) list_package_ids = g_ptr_array_new_with_free_func (NULL); + + for (guint i = 0; apps != NULL && i < gs_app_list_length (apps); i++) { + GsApp *app = gs_app_list_index (apps, i); + GPtrArray *app_source_ids; + + if (app_filter != NULL && !app_filter (app)) + continue; + + app_source_ids = gs_app_get_source_ids (app); + for (guint j = 0; j < app_source_ids->len; j++) { + const gchar *package_id = g_ptr_array_index (app_source_ids, j); + + if (ignore_installed && package_is_installed (package_id)) + continue; + + g_ptr_array_add (list_package_ids, (gchar *) package_id); + } + } + + return g_steal_pointer (&list_package_ids); +} + +static gboolean +gs_plugin_add_sources_related (GsPlugin *plugin, + GHashTable *hash, + GCancellable *cancellable, + GError **error) +{ + guint i; + GsApp *app; + GsApp *app_tmp; + PkBitfield filter; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_related = NULL; + const gchar *id; + gboolean ret = TRUE; + g_autoptr(GsAppList) installed = gs_app_list_new (); + g_autoptr(PkResults) results = NULL; + + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_INSTALLED, + PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + PK_FILTER_ENUM_NOT_COLLECTIONS, + -1); + + task_related = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_related), GS_PLUGIN_ACTION_GET_SOURCES, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_client_get_packages (PK_CLIENT (task_related), + filter, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to get sources related: "); + return FALSE; + } + ret = gs_plugin_packagekit_add_results (plugin, + installed, + results, + error); + if (!ret) + return FALSE; + for (i = 0; i < gs_app_list_length (installed); i++) { + g_auto(GStrv) split = NULL; + app = gs_app_list_index (installed, i); + split = pk_package_id_split (gs_app_get_source_id_default (app)); + if (split == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "invalid package-id: %s", + gs_app_get_source_id_default (app)); + return FALSE; + } + if (g_str_has_prefix (split[PK_PACKAGE_ID_DATA], "installed:")) { + id = split[PK_PACKAGE_ID_DATA] + 10; + app_tmp = g_hash_table_lookup (hash, id); + if (app_tmp != NULL) { + g_debug ("found package %s from %s", + gs_app_get_source_default (app), id); + gs_app_add_related (app_tmp, app); + } + } + } + return TRUE; +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + PkBitfield filter; + PkRepoDetail *rd; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_sources = NULL; + const gchar *id; + guint i; + g_autoptr(GHashTable) hash = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + + /* ask PK for the repo details */ + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NOT_SOURCE, + PK_FILTER_ENUM_NOT_DEVELOPMENT, + -1); + + task_sources = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_sources), GS_PLUGIN_ACTION_GET_SOURCES, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_client_get_repo_list (PK_CLIENT (task_sources), + filter, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + array = pk_results_get_repo_detail_array (results); + for (i = 0; i < array->len; i++) { + g_autoptr(GsApp) app = NULL; + rd = g_ptr_array_index (array, i); + id = pk_repo_detail_get_id (rd); + app = gs_app_new (id); + gs_app_set_management_plugin (app, plugin); + gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + gs_app_set_state (app, pk_repo_detail_get_enabled (rd) ? + GS_APP_STATE_INSTALLED : GS_APP_STATE_AVAILABLE); + gs_app_set_name (app, + GS_APP_QUALITY_NORMAL, + pk_repo_detail_get_description (rd)); + gs_app_set_summary (app, + GS_APP_QUALITY_NORMAL, + pk_repo_detail_get_description (rd)); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_metadata (app, "GnomeSoftware::SortKey", "300"); + gs_app_set_origin_ui (app, _("Packages")); + gs_app_list_add (list, app); + g_hash_table_insert (hash, + g_strdup (id), + (gpointer) app); + } + + /* get every application on the system and add it as a related package + * if it matches */ + return gs_plugin_add_sources_related (plugin, hash, cancellable, error); +} + +static gboolean +gs_plugin_app_origin_repo_enable (GsPluginPackagekit *self, + PkTask *task_enable_repo, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPlugin *plugin = GS_PLUGIN (self); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(GsApp) repo_app = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(PkError) error_code = NULL; + const gchar *repo_id; + + repo_id = gs_app_get_origin (app); + if (repo_id == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "origin not set"); + return FALSE; + } + + /* do sync call */ + gs_plugin_status_update (plugin, app, GS_PLUGIN_STATUS_WAITING); + results = pk_client_repo_enable (PK_CLIENT (task_enable_repo), + repo_id, + TRUE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + /* pk_client_repo_enable() returns an error if the repo is already enabled. */ + if (results != NULL && + (error_code = pk_results_get_error_code (results)) != NULL && + pk_error_get_code (error_code) == PK_ERROR_ENUM_REPO_ALREADY_SET) { + g_clear_error (error); + } else if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + + /* now that the repo is enabled, the app (not the repo!) moves from + * UNAVAILABLE state to AVAILABLE */ + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + + /* Construct a simple fake GsApp for the repository, used only by the signal handler */ + repo_app = gs_app_new (repo_id); + gs_app_set_state (repo_app, GS_APP_STATE_INSTALLED); + gs_plugin_repository_changed (plugin, repo_app); + + return TRUE; +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + g_autoptr(GsAppList) addons = NULL; + GPtrArray *source_ids; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_install = NULL; + const gchar *package_id; + guint i; + g_autofree gchar *local_filename = NULL; + g_auto(GStrv) package_ids = NULL; + g_autoptr(GPtrArray) array_package_ids = NULL; + g_autoptr(PkResults) results = NULL; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* enable repo, handled by dedicated function */ + g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE); + + /* queue for install if installation needs the network */ + if (!gs_plugin_get_network_available (plugin)) { + gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL); + return TRUE; + } + + /* Set up a #PkTask to handle the D-Bus calls to packagekitd. */ + task_install = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_install), GS_PLUGIN_ACTION_INSTALL, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + if (gs_app_get_state (app) == GS_APP_STATE_UNAVAILABLE) { + /* get everything up front we need */ + source_ids = gs_app_get_source_ids (app); + if (source_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "installing not available"); + return FALSE; + } + package_ids = g_new0 (gchar *, 2); + package_ids[0] = g_strdup (g_ptr_array_index (source_ids, 0)); + + /* enable the repo where the unavailable app is coming from */ + if (!gs_plugin_app_origin_repo_enable (self, task_install, app, cancellable, error)) + return FALSE; + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + + /* FIXME: this is a hack, to allow PK time to re-initialize + * everything in order to match an actual result. The root cause + * is probably some kind of hard-to-debug race in the daemon. */ + g_usleep (G_USEC_PER_SEC * 3); + + /* actually install the package */ + gs_packagekit_helper_add_app (helper, app); + + results = pk_task_install_packages_sync (task_install, + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + /* if we remove the app again later, we should be able to + * cancel the installation if we'd never installed it */ + gs_app_set_allow_cancel (app, TRUE); + + /* no longer valid */ + gs_app_clear_source_ids (app); + return TRUE; + } + + /* get the list of available package ids to install */ + switch (gs_app_get_state (app)) { + case GS_APP_STATE_AVAILABLE: + case GS_APP_STATE_UPDATABLE: + case GS_APP_STATE_QUEUED_FOR_INSTALL: + source_ids = gs_app_get_source_ids (app); + if (source_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "installing not available"); + return FALSE; + } + + addons = gs_app_dup_addons (app); + array_package_ids = app_list_get_package_ids (addons, + gs_app_get_to_be_installed, + TRUE); + + for (i = 0; i < source_ids->len; i++) { + package_id = g_ptr_array_index (source_ids, i); + if (package_is_installed (package_id)) + continue; + g_ptr_array_add (array_package_ids, (gpointer) package_id); + } + + if (array_package_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no packages to install"); + return FALSE; + } + + /* NULL-terminate the array */ + g_ptr_array_add (array_package_ids, NULL); + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + + 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)) + gs_app_set_state (addon, GS_APP_STATE_INSTALLING); + } + gs_packagekit_helper_add_app (helper, app); + + results = pk_task_install_packages_sync (task_install, + (gchar **) array_package_ids->pdata, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + for (i = 0; addons != NULL && i < gs_app_list_length (addons); i++) { + GsApp *addon = gs_app_list_index (addons, i); + if (gs_app_get_state (addon) == GS_APP_STATE_INSTALLING) + gs_app_set_state_recover (addon); + } + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + for (i = 0; addons != NULL && i < gs_app_list_length (addons); i++) { + GsApp *addon = gs_app_list_index (addons, i); + if (gs_app_get_state (addon) == GS_APP_STATE_INSTALLING) { + gs_app_set_state (addon, GS_APP_STATE_INSTALLED); + gs_app_clear_source_ids (addon); + } + } + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + break; + case GS_APP_STATE_AVAILABLE_LOCAL: + if (gs_app_get_local_file (app) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "local package, but no filename"); + return FALSE; + } + local_filename = g_file_get_path (gs_app_get_local_file (app)); + package_ids = g_strsplit (local_filename, "\t", -1); + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + gs_packagekit_helper_add_app (helper, app); + + results = pk_task_install_files_sync (task_install, + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + /* get the new icon from the package */ + gs_app_set_local_file (app, NULL); + gs_app_remove_all_icons (app); + break; + default: + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "do not know how to install app in state %s", + gs_app_state_to_string (gs_app_get_state (app))); + return FALSE; + } + + /* no longer valid */ + gs_app_clear_source_ids (app); + + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + const gchar *package_id; + GPtrArray *source_ids; + g_autoptr(GsAppList) addons = NULL; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_remove = NULL; + guint i; + guint cnt = 0; + g_autoptr(PkResults) results = NULL; + g_auto(GStrv) package_ids = NULL; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* disable repo, handled by dedicated function */ + g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE); + + /* get the list of available package ids to install */ + source_ids = gs_app_get_source_ids (app); + if (source_ids->len == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "removing not available"); + return FALSE; + } + package_ids = g_new0 (gchar *, source_ids->len + 1); + for (i = 0; i < source_ids->len; i++) { + package_id = g_ptr_array_index (source_ids, i); + if (!package_is_installed (package_id)) + continue; + package_ids[cnt++] = g_strdup (package_id); + } + if (cnt == 0) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no packages to remove"); + return FALSE; + } + + /* do the action */ + gs_app_set_state (app, GS_APP_STATE_REMOVING); + gs_packagekit_helper_add_app (helper, app); + + task_remove = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_remove), GS_PLUGIN_ACTION_REMOVE, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_task_remove_packages_sync (task_remove, + package_ids, + TRUE, GS_PACKAGEKIT_AUTOREMOVE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* Make sure addons' state is updated as well */ + 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_state (addon) == GS_APP_STATE_INSTALLED) { + gs_app_set_state (addon, GS_APP_STATE_UNKNOWN); + gs_app_clear_source_ids (addon); + } + } + + /* state is not known: we don't know if we can re-install this app */ + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + + /* no longer valid */ + gs_app_clear_source_ids (app); + + return TRUE; +} + +static GsApp * +gs_plugin_packagekit_build_update_app (GsPlugin *plugin, PkPackage *package) +{ + GsApp *app = gs_plugin_cache_lookup (plugin, pk_package_get_id (package)); + if (app != NULL) { + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + return app; + } + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_add_source (app, pk_package_get_name (package)); + gs_app_add_source_id (app, pk_package_get_id (package)); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, + pk_package_get_name (package)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, + pk_package_get_summary (package)); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_set_management_plugin (app, plugin); + gs_app_set_update_version (app, pk_package_get_version (package)); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + gs_plugin_cache_add (plugin, pk_package_get_id (package), app); + return app; +} + +static gboolean +gs_plugin_packagekit_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_updates = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GsApp) first_app = NULL; + gboolean all_downloaded = TRUE; + + /* do sync call */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + + task_updates = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_updates), GS_PLUGIN_ACTION_GET_UPDATES, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_client_get_updates (PK_CLIENT (task_updates), + pk_bitfield_value (PK_FILTER_ENUM_NONE), + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + + /* add results */ + array = pk_results_get_package_array (results); + for (guint i = 0; i < array->len; i++) { + PkPackage *package = g_ptr_array_index (array, i); + g_autoptr(GsApp) app = NULL; + guint64 size_download_bytes; + + app = gs_plugin_packagekit_build_update_app (plugin, package); + all_downloaded = (all_downloaded && + gs_app_get_size_download (app, &size_download_bytes) == GS_SIZE_TYPE_VALID && + size_download_bytes == 0); + if (all_downloaded && first_app == NULL) + first_app = g_object_ref (app); + gs_app_list_add (list, app); + } + /* Having all packages downloaded doesn't mean the update is also prepared, + because the 'prepared-update' file can be missing, thus verify it and + if not found, then set one application as needed download, to have + the update properly prepared. */ + if (all_downloaded && first_app != NULL) { + g_auto(GStrv) prepared_ids = NULL; + /* It's an overhead to get all the package IDs, but there's no easier + way to verify the prepared-update file exists. */ + prepared_ids = pk_offline_get_prepared_ids (NULL); + if (prepared_ids == NULL || prepared_ids[0] == NULL) + gs_app_set_size_download (first_app, GS_SIZE_TYPE_VALID, 1); + } + + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) local_error = NULL; + if (!gs_plugin_packagekit_add_updates (plugin, list, cancellable, &local_error)) + g_debug ("Failed to get updates: %s", local_error->message); + return TRUE; +} + +static void list_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_packagekit_list_apps_async (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PkBitfield filter; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_list_apps = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE); + g_autoptr(GTask) task = NULL; + const gchar *provides_tag = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_list_apps_async); + g_task_set_task_data (task, g_object_ref (helper), g_object_unref); + + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + gs_packagekit_helper_set_progress_app (helper, app_dl); + + task_list_apps = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_list_apps), GS_PLUGIN_ACTION_UNKNOWN, interactive); + + if (gs_app_query_get_provides_files (query) != NULL) { + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + -1); + pk_client_search_files_async (PK_CLIENT (task_list_apps), + filter, + (gchar **) gs_app_query_get_provides_files (query), + cancellable, + gs_packagekit_helper_cb, helper, + list_apps_cb, g_steal_pointer (&task)); + } else if (gs_app_query_get_provides (query, &provides_tag) != GS_APP_QUERY_PROVIDES_UNKNOWN) { + const gchar * const provides_tag_strv[2] = { provides_tag, NULL }; + + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + -1); + + pk_client_what_provides_async (PK_CLIENT (task_list_apps), + filter, + (gchar **) provides_tag_strv, + cancellable, + gs_packagekit_helper_cb, helper, + list_apps_cb, g_steal_pointer (&task)); + } else { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported query"); + } +} + +static void +list_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPlugin *plugin = g_task_get_source_object (task); + g_autoptr(PkResults) results = NULL; + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + + if (!gs_plugin_packagekit_results_valid (results, &local_error) || + !gs_plugin_packagekit_add_results (plugin, list, results, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + } else { + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); + } +} + +static GsAppList * +gs_plugin_packagekit_list_apps_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +static gboolean +plugin_packagekit_pick_rpm_desktop_file_cb (GsPlugin *plugin, + GsApp *app, + const gchar *filename, + GKeyFile *key_file) +{ + return strstr (filename, "/snapd/") == NULL && + strstr (filename, "/snap/") == NULL && + strstr (filename, "/flatpak/") == NULL && + g_key_file_has_group (key_file, "Desktop Entry") && + !g_key_file_has_key (key_file, "Desktop Entry", "X-Flatpak", NULL) && + !g_key_file_has_key (key_file, "Desktop Entry", "X-SnapInstanceName", NULL); +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + return gs_plugin_app_launch_filtered (plugin, app, plugin_packagekit_pick_rpm_desktop_file_cb, NULL, error); +} + +static void +gs_plugin_packagekit_updates_changed_cb (PkControl *control, GsPlugin *plugin) +{ + gs_plugin_updates_changed (plugin); +} + +static void +gs_plugin_packagekit_repo_list_changed_cb (PkControl *control, GsPlugin *plugin) +{ + gs_plugin_reload (plugin); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE && + gs_app_get_scope (app) == AS_COMPONENT_SCOPE_SYSTEM) { + gs_app_set_management_plugin (app, plugin); + gs_plugin_packagekit_set_packaging_format (plugin, app); + return; + } else if (gs_app_get_kind (app) == AS_COMPONENT_KIND_OPERATING_SYSTEM) { + gs_app_set_management_plugin (app, plugin); + } +} + +typedef struct +{ + GsAppList *list; /* (owned) (not nullable) */ + GsPackagekitHelper *progress_data; /* (owned) (not nullable) */ +} ResolvePackagesWithFilterData; + +static void +resolve_packages_with_filter_data_free (ResolvePackagesWithFilterData *data) +{ + g_clear_object (&data->list); + g_clear_object (&data->progress_data); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ResolvePackagesWithFilterData, resolve_packages_with_filter_data_free) + +static void resolve_packages_with_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_packagekit_resolve_packages_with_filter_async (GsPluginPackagekit *self, + PkClient *client_refine, + GsAppList *list, + PkBitfield filter, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (self); + GPtrArray *sources; + GsApp *app; + const gchar *pkgname; + guint i; + guint j; + g_autoptr(GPtrArray) package_ids = NULL; + g_autoptr(GTask) task = NULL; + g_autoptr(ResolvePackagesWithFilterData) data = NULL; + ResolvePackagesWithFilterData *data_unowned; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_resolve_packages_with_filter_async); + data_unowned = data = g_new0 (ResolvePackagesWithFilterData, 1); + data->list = g_object_ref (list); + data->progress_data = gs_packagekit_helper_new (plugin); + g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) resolve_packages_with_filter_data_free); + + package_ids = g_ptr_array_new_with_free_func (g_free); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + sources = gs_app_get_sources (app); + for (j = 0; j < sources->len; j++) { + pkgname = g_ptr_array_index (sources, j); + if (pkgname == NULL || pkgname[0] == '\0') { + g_warning ("invalid pkgname '%s' for %s", + pkgname, + gs_app_get_unique_id (app)); + continue; + } + g_ptr_array_add (package_ids, g_strdup (pkgname)); + } + } + + if (package_ids->len == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + g_ptr_array_add (package_ids, NULL); + + /* resolve them all at once */ + pk_client_resolve_async (client_refine, + filter, + (gchar **) package_ids->pdata, + cancellable, + gs_packagekit_helper_cb, data_unowned->progress_data, + resolve_packages_with_filter_cb, + g_steal_pointer (&task)); +} + +static void +resolve_packages_with_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginPackagekit *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + ResolvePackagesWithFilterData *data = g_task_get_task_data (task); + GsAppList *list = data->list; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) packages = NULL; + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + + if (!gs_plugin_packagekit_results_valid (results, &local_error)) { + g_prefix_error (&local_error, "failed to resolve package_ids: "); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* get results */ + packages = pk_results_get_package_array (results); + + /* if the user types more characters we'll get cancelled - don't go on + * to mark apps as unavailable because packages->len = 0 */ + if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) { + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (gs_app_get_local_file (app) != NULL) + continue; + gs_plugin_packagekit_resolve_packages_app (GS_PLUGIN (self), packages, app); + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_packagekit_resolve_packages_with_filter_finish (GsPluginPackagekit *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +/* + * gs_plugin_packagekit_fixup_update_description: + * + * Lets assume Fedora is sending us valid markdown, but fall back to + * plain text if this fails. + */ +static gchar * +gs_plugin_packagekit_fixup_update_description (const gchar *text) +{ + gchar *tmp; + g_autoptr(GsMarkdown) markdown = NULL; + + /* nothing to do */ + if (text == NULL) + return NULL; + + /* try to parse */ + markdown = gs_markdown_new (GS_MARKDOWN_OUTPUT_PANGO); + gs_markdown_set_smart_quoting (markdown, FALSE); + gs_markdown_set_autocode (markdown, FALSE); + gs_markdown_set_autolinkify (markdown, FALSE); + tmp = gs_markdown_parse (markdown, text); + if (tmp != NULL) + return tmp; + return g_strdup (text); +} + +static gboolean +gs_plugin_refine_app_needs_details (GsPluginRefineFlags flags, + GsApp *app) +{ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0 && + gs_app_get_license (app) == NULL) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) > 0 && + gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) > 0 && + gs_app_get_size_installed (app, NULL) != GS_SIZE_TYPE_VALID) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) > 0 && + gs_app_get_size_download (app, NULL) != GS_SIZE_TYPE_VALID) + return TRUE; + return FALSE; +} + +static gboolean +gs_plugin_refine_requires_version (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_version (app); + if (tmp != NULL) + return FALSE; + return (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) > 0; +} + +static gboolean +gs_plugin_refine_requires_update_details (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_update_details_markup (app); + if (tmp != NULL) + return FALSE; + return (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) > 0; +} + +static gboolean +gs_plugin_refine_requires_origin (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_origin (app); + if (tmp != NULL) + return FALSE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) > 0) + return TRUE; + return FALSE; +} + +static gboolean +gs_plugin_refine_requires_package_id (GsApp *app, GsPluginRefineFlags flags) +{ + const gchar *tmp; + tmp = gs_app_get_source_id_default (app); + if (tmp != NULL) + return FALSE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) > 0) + return TRUE; + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION) > 0) + return TRUE; + return FALSE; +} + +static gboolean +gs_plugin_packagekit_refine_valid_package_name (const gchar *source) +{ + if (g_strstr_len (source, -1, "/") != NULL) + return FALSE; + return TRUE; +} + +static gboolean +gs_plugin_systemd_update_cache (GsPluginPackagekit *self, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_auto(GStrv) package_ids = NULL; + g_autoptr(GHashTable) new_prepared_updates = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + /* get new list of package-ids. This loads a local file, so should be + * just about fast enough to be sync. */ + new_prepared_updates = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + package_ids = pk_offline_get_prepared_ids (&error_local); + if (package_ids == NULL) { + if (g_error_matches (error_local, + PK_OFFLINE_ERROR, + PK_OFFLINE_ERROR_NO_DATA)) { + return TRUE; + } + gs_plugin_packagekit_error_convert (&error_local); + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "Failed to get prepared IDs: %s", + error_local->message); + return FALSE; + } + + /* Build the new table, stealing all the elements from @package_ids. */ + for (guint i = 0; package_ids[i] != NULL; i++) { + g_hash_table_add (new_prepared_updates, g_steal_pointer (&package_ids[i])); + } + + g_clear_pointer (&package_ids, g_free); + + /* Update the shared state. */ + locker = g_mutex_locker_new (&self->prepared_updates_mutex); + g_clear_pointer (&self->prepared_updates, g_hash_table_unref); + self->prepared_updates = g_steal_pointer (&new_prepared_updates); + + return TRUE; +} + +typedef struct { + /* Track pending operations. */ + guint n_pending_operations; + gboolean completed; + GError *error; /* (nullable) (owned) */ + GPtrArray *progress_datas; /* (element-type GsPackagekitHelper) (owned) (not nullable) */ + PkClient *client_refine; /* (owned) */ + + /* Input data for operations. */ + GsAppList *full_list; /* (nullable) (owned) */ + GsAppList *resolve_list; /* (nullable) (owned) */ + GsApp *app_operating_system; /* (nullable) (owned) */ + GsAppList *update_details_list; /* (nullable) (owned) */ + GsAppList *details_list; /* (nullable) (owned) */ +} RefineData; + +static void +refine_data_free (RefineData *data) +{ + g_assert (data->n_pending_operations == 0); + g_assert (data->completed); + + g_clear_error (&data->error); + g_clear_pointer (&data->progress_datas, g_ptr_array_unref); + g_clear_object (&data->client_refine); + g_clear_object (&data->full_list); + g_clear_object (&data->resolve_list); + g_clear_object (&data->app_operating_system); + g_clear_object (&data->update_details_list); + g_clear_object (&data->details_list); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (RefineData, refine_data_free) + +/* Add @helper to the list of progress data closures to free when the + * #RefineData is freed. This means it can be reliably used, 0 or more times, + * by the async operation up until the operation is finished. */ +static GsPackagekitHelper * +refine_task_add_progress_data (GTask *refine_task, + GsPackagekitHelper *helper) +{ + RefineData *data = g_task_get_task_data (refine_task); + + g_ptr_array_add (data->progress_datas, g_object_ref (helper)); + + return helper; +} + +static GTask * +refine_task_add_operation (GTask *refine_task) +{ + RefineData *data = g_task_get_task_data (refine_task); + + g_assert (!data->completed); + data->n_pending_operations++; + + return g_object_ref (refine_task); +} + +static void +refine_task_complete_operation (GTask *refine_task) +{ + RefineData *data = g_task_get_task_data (refine_task); + + g_assert (data->n_pending_operations > 0); + data->n_pending_operations--; + + /* Have all operations completed? */ + if (data->n_pending_operations == 0) { + g_assert (!data->completed); + data->completed = TRUE; + + if (data->error != NULL) + g_task_return_error (refine_task, g_steal_pointer (&data->error)); + else + g_task_return_boolean (refine_task, TRUE); + } +} + +static void +refine_task_complete_operation_with_error (GTask *refine_task, + GError *error /* (transfer full) */) +{ + RefineData *data = g_task_get_task_data (refine_task); + g_autoptr(GError) owned_error = g_steal_pointer (&error); + + /* Multiple operations might fail. Just take the first error. */ + if (data->error == NULL) + data->error = g_steal_pointer (&owned_error); + + refine_task_complete_operation (refine_task); +} + +typedef struct { + GTask *refine_task; /* (owned) (not nullable) */ + GsApp *app; /* (owned) (not nullable) */ + gchar *filename; /* (owned) (not nullable) */ +} SearchFilesData; + +static void +search_files_data_free (SearchFilesData *data) +{ + g_free (data->filename); + g_clear_object (&data->app); + g_clear_object (&data->refine_task); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SearchFilesData, search_files_data_free) + +static SearchFilesData * +search_files_data_new_operation (GTask *refine_task, + GsApp *app, + const gchar *filename) +{ + g_autoptr(SearchFilesData) data = g_new0 (SearchFilesData, 1); + data->refine_task = refine_task_add_operation (refine_task); + data->app = g_object_ref (app); + data->filename = g_strdup (filename); + + return g_steal_pointer (&data); +} + +static void upgrade_system_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void resolve_all_packages_with_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void search_files_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void get_update_detail_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void get_details_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void get_updates_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void refine_all_history_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_packagekit_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + g_autoptr(GsAppList) resolve_list = gs_app_list_new (); + g_autoptr(GsAppList) update_details_list = gs_app_list_new (); + g_autoptr(GsAppList) details_list = gs_app_list_new (); + g_autoptr(GsAppList) history_list = gs_app_list_new (); + g_autoptr(GsAppList) repos_list = gs_app_list_new (); + g_autoptr(GTask) task = NULL; + g_autoptr(RefineData) data = NULL; + RefineData *data_unowned = NULL; + g_autoptr(GError) local_error = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_refine_async); + data_unowned = data = g_new0 (RefineData, 1); + data->full_list = g_object_ref (list); + data->n_pending_operations = 1; /* to prevent the task being completed before all operations have been started */ + data->progress_datas = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + data->client_refine = pk_client_new (); + pk_client_set_interactive (data->client_refine, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) refine_data_free); + + /* Process the @list and work out what information is needed for each + * app. */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + GPtrArray *sources; + const gchar *filename; + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + + if (!gs_app_has_management_plugin (app, NULL) && + !gs_app_has_management_plugin (app, GS_PLUGIN (self))) + continue; + + /* Repositories */ + filename = gs_app_get_metadata_item (app, "repos::repo-filename"); + + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY && + filename != NULL) { + gs_app_list_add (repos_list, app); + } + + /* Apps */ + sources = gs_app_get_sources (app); + + if (sources->len > 0 && + gs_plugin_packagekit_refine_valid_package_name (g_ptr_array_index (sources, 0)) && + (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN || + gs_plugin_refine_requires_package_id (app, flags) || + gs_plugin_refine_requires_origin (app, flags) || + gs_plugin_refine_requires_version (app, flags))) { + gs_app_list_add (resolve_list, app); + } + + if ((gs_app_get_state (app) == GS_APP_STATE_UPDATABLE || + gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) && + gs_app_get_source_id_default (app) != NULL && + gs_plugin_refine_requires_update_details (app, flags)) { + gs_app_list_add (update_details_list, app); + } + + if (gs_app_get_source_id_default (app) != NULL && + gs_plugin_refine_app_needs_details (flags, app)) { + gs_app_list_add (details_list, app); + } + + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY) != 0 && + sources->len > 0 && + gs_app_get_install_date (app) == 0) { + gs_app_list_add (history_list, app); + } + } + + /* re-read /var/lib/PackageKit/prepared-update so we know what packages + * to mark as already downloaded and prepared for offline updates */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) && + !gs_plugin_systemd_update_cache (self, &local_error)) { + refine_task_complete_operation_with_error (task, g_steal_pointer (&local_error)); + return; + } + + /* when we need the cannot-be-upgraded applications, we implement this + * by doing a UpgradeSystem(SIMULATE) which adds the removed packages + * to the related-apps list with a state of %GS_APP_STATE_UNAVAILABLE */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED) { + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + guint cache_age_save; + + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_OPERATING_SYSTEM) + continue; + + gs_packagekit_helper_add_app (helper, app); + + /* Expose the @app to the callback functions so that + * upgrade packages can be added as related. This only + * supports one OS. */ + g_assert (data_unowned->app_operating_system == NULL); + data_unowned->app_operating_system = g_object_ref (app); + + /* ask PK to simulate upgrading the system */ + cache_age_save = pk_client_get_cache_age (data_unowned->client_refine); + pk_client_set_cache_age (data_unowned->client_refine, 60 * 60 * 24 * 7); /* once per week */ + pk_client_set_interactive (data_unowned->client_refine, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + pk_client_upgrade_system_async (data_unowned->client_refine, + pk_bitfield_from_enums (PK_TRANSACTION_FLAG_ENUM_SIMULATE, -1), + gs_app_get_version (app), + PK_UPGRADE_KIND_ENUM_COMPLETE, + cancellable, + gs_packagekit_helper_cb, refine_task_add_progress_data (task, helper), + upgrade_system_cb, + refine_task_add_operation (task)); + pk_client_set_cache_age (data_unowned->client_refine, cache_age_save); + + /* Only support one operating system. */ + break; + } + } + + /* can we resolve in one go? */ + if (gs_app_list_length (resolve_list) > 0) { + PkBitfield filter; + + /* Expose the @resolve_list to the callback functions in case a + * second attempt is needed. */ + g_assert (data_unowned->resolve_list == NULL); + data_unowned->resolve_list = g_object_ref (resolve_list); + + /* first, try to resolve packages with ARCH filter */ + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + -1); + + gs_plugin_packagekit_resolve_packages_with_filter_async (self, + data_unowned->client_refine, + resolve_list, + filter, + cancellable, + resolve_all_packages_with_filter_cb, + refine_task_add_operation (task)); + } + + /* set the package-id for an installed desktop file */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION) != 0) { + for (guint i = 0; i < gs_app_list_length (list); i++) { + g_autofree gchar *fn = NULL; + GsApp *app = gs_app_list_index (list, i); + const gchar *tmp; + const gchar *to_array[] = { NULL, NULL }; + g_autoptr(GsPackagekitHelper) helper = NULL; + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (gs_app_get_source_id_default (app) != NULL) + continue; + if (!gs_app_has_management_plugin (app, NULL) && + !gs_app_has_management_plugin (app, GS_PLUGIN (self))) + continue; + tmp = gs_app_get_id (app); + if (tmp == NULL) + continue; + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_DESKTOP_APP: + fn = g_strdup_printf ("/usr/share/applications/%s", tmp); + break; + case AS_COMPONENT_KIND_ADDON: + fn = g_strdup_printf ("/usr/share/metainfo/%s.metainfo.xml", tmp); + if (!g_file_test (fn, G_FILE_TEST_EXISTS)) { + g_free (fn); + fn = g_strdup_printf ("/usr/share/appdata/%s.metainfo.xml", tmp); + } + break; + default: + break; + } + if (fn == NULL) + continue; + if (!g_file_test (fn, G_FILE_TEST_EXISTS)) { + g_debug ("ignoring %s as does not exist", fn); + continue; + } + + helper = gs_packagekit_helper_new (plugin); + to_array[0] = fn; + gs_packagekit_helper_add_app (helper, app); + pk_client_search_files_async (data_unowned->client_refine, + pk_bitfield_from_enums (PK_FILTER_ENUM_INSTALLED, -1), + (gchar **) to_array, + cancellable, + gs_packagekit_helper_cb, refine_task_add_progress_data (task, helper), + search_files_cb, + search_files_data_new_operation (task, app, fn)); + } + } + + /* Refine repo package names */ + for (guint i = 0; i < gs_app_list_length (repos_list); i++) { + GsApp *app = gs_app_list_index (repos_list, i); + const gchar *filename; + const gchar *to_array[] = { NULL, NULL }; + g_autoptr(GsPackagekitHelper) helper = NULL; + + filename = gs_app_get_metadata_item (app, "repos::repo-filename"); + + /* set the source package name for an installed .repo file */ + helper = gs_packagekit_helper_new (plugin); + to_array[0] = filename; + gs_packagekit_helper_add_app (helper, app); + + pk_client_search_files_async (data_unowned->client_refine, + pk_bitfield_from_enums (PK_FILTER_ENUM_INSTALLED, -1), + (gchar **) to_array, + cancellable, + gs_packagekit_helper_cb, refine_task_add_progress_data (task, helper), + search_files_cb, + search_files_data_new_operation (task, app, filename)); + } + + /* any update details missing? */ + if (gs_app_list_length (update_details_list) > 0) { + GsApp *app; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autofree const gchar **package_ids = NULL; + + /* Expose the @update_details_list to the callback functions so + * its apps can be updated. */ + g_assert (data_unowned->update_details_list == NULL); + data_unowned->update_details_list = g_object_ref (update_details_list); + + package_ids = g_new0 (const gchar *, gs_app_list_length (update_details_list) + 1); + for (guint i = 0; i < gs_app_list_length (update_details_list); i++) { + app = gs_app_list_index (update_details_list, i); + package_ids[i] = gs_app_get_source_id_default (app); + g_assert (package_ids[i] != NULL); /* checked when update_details_list is built */ + } + + /* get any update details */ + pk_client_get_update_detail_async (data_unowned->client_refine, + (gchar **) package_ids, + cancellable, + gs_packagekit_helper_cb, refine_task_add_progress_data (task, helper), + get_update_detail_cb, + refine_task_add_operation (task)); + } + + /* any package details missing? */ + if (gs_app_list_length (details_list) > 0) { + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(GPtrArray) package_ids = NULL; + + /* Expose the @details_list to the callback functions so + * its apps can be updated. */ + g_assert (data_unowned->details_list == NULL); + data_unowned->details_list = g_object_ref (details_list); + + package_ids = app_list_get_package_ids (details_list, NULL, FALSE); + + if (package_ids->len > 0) { + /* NULL-terminate the array */ + g_ptr_array_add (package_ids, NULL); + + /* get any details */ + pk_client_get_details_async (data_unowned->client_refine, + (gchar **) package_ids->pdata, + cancellable, + gs_packagekit_helper_cb, refine_task_add_progress_data (task, helper), + get_details_cb, + refine_task_add_operation (task)); + } + } + + /* get the update severity */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY) != 0) { + PkBitfield filter; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + + /* get the list of updates */ + filter = pk_bitfield_value (PK_FILTER_ENUM_NONE); + pk_client_get_updates_async (data_unowned->client_refine, + filter, + cancellable, + gs_packagekit_helper_cb, refine_task_add_progress_data (task, helper), + get_updates_cb, + refine_task_add_operation (task)); + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + continue; + + /* the scope is always system-wide */ + if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN) + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + } + + /* add any missing history data */ + if (gs_app_list_length (history_list) > 0) { + gs_plugin_packagekit_refine_history_async (self, + history_list, + cancellable, + refine_all_history_cb, + refine_task_add_operation (task)); + } + + /* Mark the operation to set up all the other operations as completed. + * The @refine_task will now be completed once all the async operations + * have completed, and the task callback invoked. */ + refine_task_complete_operation (task); +} + +static void +upgrade_system_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(GTask) refine_task = g_steal_pointer (&user_data); + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (g_task_get_source_object (refine_task)); + RefineData *data = g_task_get_task_data (refine_task); + g_autoptr(PkResults) results = NULL; + g_autoptr(GsAppList) results_list = NULL; + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + if (!gs_plugin_packagekit_results_valid (results, &local_error)) { + g_prefix_error (&local_error, "failed to refine distro upgrade: "); + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + results_list = gs_app_list_new (); + if (!gs_plugin_packagekit_add_results (GS_PLUGIN (self), results_list, results, &local_error)) { + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + /* add each of these as related applications */ + for (guint j = 0; j < gs_app_list_length (results_list); j++) { + GsApp *app2 = gs_app_list_index (results_list, j); + if (gs_app_get_state (app2) != GS_APP_STATE_UNAVAILABLE) + continue; + gs_app_add_related (data->app_operating_system, app2); + } + + refine_task_complete_operation (refine_task); +} + +static gboolean +gs_plugin_packagekit_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void resolve_all_packages_with_filter_cb2 (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +resolve_all_packages_with_filter_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (source_object); + g_autoptr(GTask) refine_task = g_steal_pointer (&user_data); + RefineData *data = g_task_get_task_data (refine_task); + GCancellable *cancellable = g_task_get_cancellable (refine_task); + GsAppList *resolve_list = data->resolve_list; + g_autoptr(GsAppList) resolve2_list = NULL; + PkBitfield filter; + g_autoptr(GError) local_error = NULL; + + if (!gs_plugin_packagekit_resolve_packages_with_filter_finish (self, + result, + &local_error)) { + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + /* if any packages remaining in UNKNOWN state, try to resolve them again, + * but this time without ARCH filter */ + resolve2_list = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (resolve_list); i++) { + GsApp *app = gs_app_list_index (resolve_list, i); + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_list_add (resolve2_list, app); + } + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_NOT_ARCH, + PK_FILTER_ENUM_NOT_SOURCE, + -1); + + gs_plugin_packagekit_resolve_packages_with_filter_async (self, + data->client_refine, + resolve2_list, + filter, + cancellable, + resolve_all_packages_with_filter_cb2, + g_steal_pointer (&refine_task)); +} + +static void +resolve_all_packages_with_filter_cb2 (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (source_object); + g_autoptr(GTask) refine_task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + if (!gs_plugin_packagekit_resolve_packages_with_filter_finish (self, + result, + &local_error)) { + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + refine_task_complete_operation (refine_task); +} + +static void +search_files_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(SearchFilesData) search_files_data = g_steal_pointer (&user_data); + GTask *refine_task = search_files_data->refine_task; + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (g_task_get_source_object (refine_task)); + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) packages = NULL; + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + + if (!gs_plugin_packagekit_results_valid (results, &local_error)) { + g_prefix_error (&local_error, "failed to search file %s: ", search_files_data->filename); + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + /* get results */ + packages = pk_results_get_package_array (results); + if (packages->len == 1) { + PkPackage *package; + package = g_ptr_array_index (packages, 0); + gs_plugin_packagekit_set_metadata_from_package (GS_PLUGIN (self), search_files_data->app, package); + } else { + g_debug ("Failed to find one package for %s, %s, [%u]", + gs_app_get_id (search_files_data->app), search_files_data->filename, packages->len); + } + + refine_task_complete_operation (refine_task); +} + +static void +get_update_detail_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(GTask) refine_task = g_steal_pointer (&user_data); + RefineData *data = g_task_get_task_data (refine_task); + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + if (!gs_plugin_packagekit_results_valid (results, &local_error)) { + g_prefix_error (&local_error, "failed to get update details: "); + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + /* set the update details for the update */ + array = pk_results_get_update_detail_array (results); + for (guint j = 0; j < gs_app_list_length (data->update_details_list); j++) { + GsApp *app = gs_app_list_index (data->update_details_list, j); + const gchar *package_id = gs_app_get_source_id_default (app); + + for (guint i = 0; i < array->len; i++) { + const gchar *tmp; + g_autofree gchar *desc = NULL; + PkUpdateDetail *update_detail; + + /* right package? */ + update_detail = g_ptr_array_index (array, i); + if (g_strcmp0 (package_id, pk_update_detail_get_package_id (update_detail)) != 0) + continue; + tmp = pk_update_detail_get_update_text (update_detail); + desc = gs_plugin_packagekit_fixup_update_description (tmp); + if (desc != NULL) + gs_app_set_update_details_markup (app, desc); + break; + } + } + + refine_task_complete_operation (refine_task); +} + +static void +get_details_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(GTask) refine_task = g_steal_pointer (&user_data); + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (g_task_get_source_object (refine_task)); + RefineData *data = g_task_get_task_data (refine_task); + g_autoptr(GPtrArray) array = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GHashTable) details_collection = NULL; + g_autoptr(GHashTable) prepared_updates = NULL; + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + + if (!gs_plugin_packagekit_results_valid (results, &local_error)) { + g_autoptr(GPtrArray) package_ids = app_list_get_package_ids (data->details_list, NULL, FALSE); + g_autofree gchar *package_ids_str = NULL; + /* NULL-terminate the array */ + g_ptr_array_add (package_ids, NULL); + package_ids_str = g_strjoinv (",", (gchar **) package_ids->pdata); + g_prefix_error (&local_error, "failed to get details for %s: ", + package_ids_str); + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + /* get the results and copy them into a hash table for fast lookups: + * there are typically 400 to 700 elements in @array, and 100 to 200 + * elements in @list, each with 1 or 2 source IDs to look up (but + * sometimes 200) */ + array = pk_results_get_details_array (results); + details_collection = gs_plugin_packagekit_details_array_to_hash (array); + + /* set the update details for the update */ + g_mutex_lock (&self->prepared_updates_mutex); + prepared_updates = g_hash_table_ref (self->prepared_updates); + g_mutex_unlock (&self->prepared_updates_mutex); + + for (guint i = 0; i < gs_app_list_length (data->details_list); i++) { + GsApp *app = gs_app_list_index (data->details_list, i); + gs_plugin_packagekit_refine_details_app (GS_PLUGIN (self), details_collection, prepared_updates, app); + } + + refine_task_complete_operation (refine_task); +} + +static void +get_updates_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(GTask) refine_task = g_steal_pointer (&user_data); + RefineData *data = g_task_get_task_data (refine_task); + g_autoptr(PkPackageSack) sack = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + + if (!gs_plugin_packagekit_results_valid (results, &local_error)) { + g_prefix_error (&local_error, "failed to get updates for urgency: "); + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + /* set the update severity for the app */ + sack = pk_results_get_package_sack (results); + for (guint i = 0; i < gs_app_list_length (data->full_list); i++) { + g_autoptr(PkPackage) pkg = NULL; + const gchar *package_id; + GsApp *app = gs_app_list_index (data->full_list, i); + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + package_id = gs_app_get_source_id_default (app); + if (package_id == NULL) + continue; + pkg = pk_package_sack_find_by_id (sack, package_id); + if (pkg == NULL) + continue; + #ifdef HAVE_PK_PACKAGE_GET_UPDATE_SEVERITY + switch (pk_package_get_update_severity (pkg)) { + case PK_INFO_ENUM_LOW: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_LOW); + break; + case PK_INFO_ENUM_NORMAL: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_MEDIUM); + break; + case PK_INFO_ENUM_IMPORTANT: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_HIGH); + break; + case PK_INFO_ENUM_CRITICAL: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_CRITICAL); + break; + default: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN); + break; + } + #else + switch (pk_package_get_info (pkg)) { + case PK_INFO_ENUM_AVAILABLE: + case PK_INFO_ENUM_NORMAL: + case PK_INFO_ENUM_LOW: + case PK_INFO_ENUM_ENHANCEMENT: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_LOW); + break; + case PK_INFO_ENUM_BUGFIX: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_MEDIUM); + break; + case PK_INFO_ENUM_SECURITY: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_CRITICAL); + break; + case PK_INFO_ENUM_IMPORTANT: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_HIGH); + break; + default: + gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN); + g_warning ("unhandled info state %s", + pk_info_enum_to_string (pk_package_get_info (pkg))); + break; + } + #endif + } + + refine_task_complete_operation (refine_task); +} + +static void +refine_all_history_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (source_object); + g_autoptr(GTask) refine_task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + if (!gs_plugin_packagekit_refine_history_finish (self, result, &local_error)) { + refine_task_complete_operation_with_error (refine_task, g_steal_pointer (&local_error)); + return; + } + + refine_task_complete_operation (refine_task); +} + +static void +gs_plugin_packagekit_refine_add_history (GsApp *app, GVariant *dict) +{ + const gchar *version; + gboolean ret; + guint64 timestamp; + PkInfoEnum info_enum; + g_autoptr(GsApp) history = NULL; + + /* create new history item with same ID as parent */ + history = gs_app_new (gs_app_get_id (app)); + gs_app_set_kind (history, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_name (history, GS_APP_QUALITY_NORMAL, gs_app_get_name (app)); + + /* get the installed state */ + ret = g_variant_lookup (dict, "info", "u", &info_enum); + g_assert (ret); + switch (info_enum) { + case PK_INFO_ENUM_INSTALLING: + gs_app_set_state (history, GS_APP_STATE_INSTALLED); + break; + case PK_INFO_ENUM_REMOVING: + gs_app_set_state (history, GS_APP_STATE_AVAILABLE); + break; + case PK_INFO_ENUM_UPDATING: + gs_app_set_state (history, GS_APP_STATE_UPDATABLE); + break; + default: + g_debug ("ignoring history kind: %s", + pk_info_enum_to_string (info_enum)); + return; + } + + /* set the history time and date */ + ret = g_variant_lookup (dict, "timestamp", "t", ×tamp); + g_assert (ret); + gs_app_set_install_date (history, timestamp); + + /* set the history version number */ + ret = g_variant_lookup (dict, "version", "&s", &version); + g_assert (ret); + gs_app_set_version (history, version); + + /* add the package to the main application */ + gs_app_add_history (app, history); + + /* use the last event as approximation of the package timestamp */ + gs_app_set_install_date (app, timestamp); +} + +/* Run in the main thread. */ +static void +gs_plugin_packagekit_permission_cb (GPermission *permission, + GParamSpec *pspec, + gpointer data) +{ + GsPlugin *plugin = GS_PLUGIN (data); + gboolean ret = g_permission_get_allowed (permission) || + g_permission_get_can_acquire (permission); + gs_plugin_set_allow_updates (plugin, ret); +} + +static gboolean +gs_plugin_packagekit_download (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +static void +gs_plugin_packagekit_auto_prepare_update_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPlugin *plugin = source_object; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GError) local_error = NULL; + + g_return_if_fail (GS_IS_PLUGIN_PACKAGEKIT (plugin)); + + list = gs_app_list_new (); + if (!gs_plugin_packagekit_add_updates (plugin, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (gs_app_list_length (list) > 0 && + !gs_plugin_packagekit_download (plugin, list, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Ignore errors here */ + gs_plugin_systemd_update_cache (GS_PLUGIN_PACKAGEKIT (source_object), NULL); + + g_task_return_boolean (task, TRUE); +} + +static void +gs_plugin_packagekit_auto_prepare_update_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) local_error = NULL; + + if (g_task_propagate_boolean (G_TASK (result), &local_error)) { + g_debug ("Successfully auto-prepared update"); + gs_plugin_updates_changed (GS_PLUGIN (source_object)); + } else { + g_debug ("Failed to auto-prepare update: %s", local_error->message); + } +} + +static gboolean +gs_plugin_packagekit_run_prepare_update_cb (gpointer user_data) +{ + GsPluginPackagekit *self = user_data; + g_autoptr(GTask) task = NULL; + + self->prepare_update_timeout_id = 0; + + g_debug ("Going to auto-prepare update"); + task = g_task_new (self, self->proxy_settings_cancellable, gs_plugin_packagekit_auto_prepare_update_cb, NULL); + g_task_set_source_tag (task, gs_plugin_packagekit_run_prepare_update_cb); + g_task_run_in_thread (task, gs_plugin_packagekit_auto_prepare_update_thread); + return G_SOURCE_REMOVE; +} + +/* Run in the main thread. */ +static void +gs_plugin_packagekit_prepared_update_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (user_data); + + /* Interested only in these events. */ + if (event_type != G_FILE_MONITOR_EVENT_CHANGED && + event_type != G_FILE_MONITOR_EVENT_DELETED && + event_type != G_FILE_MONITOR_EVENT_CREATED) + return; + + /* This is going to break, if PackageKit renames the file, but it's unlikely to happen; + there is no API to get the file name from, sadly. */ + if (g_file_peek_path (file) == NULL || + !g_str_has_suffix (g_file_peek_path (file), "prepared-update")) + return; + + if (event_type == G_FILE_MONITOR_EVENT_DELETED) { + g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software"); + if (g_settings_get_boolean (settings, "download-updates")) { + /* The prepared-update file had been removed, but the user has set + to have the updates downloaded, thus prepared, thus prepare + the update again. */ + if (self->prepare_update_timeout_id) + g_source_remove (self->prepare_update_timeout_id); + g_debug ("Scheduled to auto-prepare update in %d s", PREPARE_UPDATE_TIMEOUT_SECS); + self->prepare_update_timeout_id = g_timeout_add_seconds (PREPARE_UPDATE_TIMEOUT_SECS, + gs_plugin_packagekit_run_prepare_update_cb, self); + } else { + if (self->prepare_update_timeout_id) { + g_source_remove (self->prepare_update_timeout_id); + self->prepare_update_timeout_id = 0; + g_debug ("Cancelled auto-prepare update"); + } + } + } else if (self->prepare_update_timeout_id) { + g_source_remove (self->prepare_update_timeout_id); + self->prepare_update_timeout_id = 0; + g_debug ("Cancelled auto-prepare update"); + } + + /* update UI */ + gs_plugin_systemd_update_cache (self, NULL); + gs_plugin_updates_changed (GS_PLUGIN (self)); +} + +static void +gs_plugin_packagekit_refresh_is_triggered (GsPluginPackagekit *self, + GCancellable *cancellable) +{ + g_autoptr(GFile) file_trigger = NULL; + file_trigger = g_file_new_for_path ("/system-update"); + self->is_triggered = g_file_query_exists (file_trigger, NULL); + g_debug ("offline trigger is now %s", + self->is_triggered ? "enabled" : "disabled"); +} + +/* Run in the main thread. */ +static void +gs_plugin_systemd_trigger_changed_cb (GFileMonitor *monitor, + GFile *file, GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (user_data); + + gs_plugin_packagekit_refresh_is_triggered (self, NULL); +} + +static void setup_proxy_settings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void get_offline_update_permission_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_packagekit_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + g_autoptr(GTask) task = NULL; + + g_debug ("PackageKit version: %d.%d.%d", + PK_MAJOR_VERSION, + PK_MINOR_VERSION, + PK_MICRO_VERSION); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_setup_async); + + reload_proxy_settings_async (self, cancellable, setup_proxy_settings_cb, g_steal_pointer (&task)); +} + +static void +setup_proxy_settings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginPackagekit *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autoptr(GFile) file_trigger = NULL; + g_autoptr(GError) local_error = NULL; + + if (!reload_proxy_settings_finish (self, result, &local_error)) + g_warning ("Failed to load proxy settings: %s", local_error->message); + g_clear_error (&local_error); + + /* watch the prepared file */ + self->monitor = pk_offline_get_prepared_monitor (cancellable, &local_error); + if (self->monitor == NULL) { + g_debug ("Failed to get prepared update file monitor: %s", local_error->message); + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_signal_connect (self->monitor, "changed", + G_CALLBACK (gs_plugin_packagekit_prepared_update_changed_cb), + self); + + /* watch the trigger file */ + file_trigger = g_file_new_for_path ("/system-update"); + self->monitor_trigger = g_file_monitor_file (file_trigger, + G_FILE_MONITOR_NONE, + NULL, + &local_error); + if (self->monitor_trigger == NULL) { + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_signal_connect (self->monitor_trigger, "changed", + G_CALLBACK (gs_plugin_systemd_trigger_changed_cb), + self); + + /* check if we have permission to trigger offline updates */ + gs_utils_get_permission_async ("org.freedesktop.packagekit.trigger-offline-update", + cancellable, get_offline_update_permission_cb, g_steal_pointer (&task)); +} + +static void +get_offline_update_permission_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginPackagekit *self = g_task_get_source_object (task); + g_autoptr(GError) local_error = NULL; + + self->permission = gs_utils_get_permission_finish (result, &local_error); + if (self->permission != NULL) { + g_signal_connect (self->permission, "notify", + G_CALLBACK (gs_plugin_packagekit_permission_cb), + self); + } + + /* get the list of currently downloaded packages */ + if (!gs_plugin_systemd_update_cache (self, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_packagekit_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_packagekit_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_shutdown_async); + + /* Cancel any ongoing proxy settings loading operation. */ + g_cancellable_cancel (self->proxy_settings_cancellable); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_packagekit_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void refine_history_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_packagekit_refine_history_async (GsPluginPackagekit *self, + GsAppList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsApp *app; + g_autofree const gchar **package_names = NULL; + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_refine_history_async); + g_task_set_task_data (task, g_object_ref (list), (GDestroyNotify) g_object_unref); + + /* get an array of package names */ + package_names = g_new0 (const gchar *, gs_app_list_length (list) + 1); + for (guint i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + package_names[i] = gs_app_get_source_default (app); + } + + g_debug ("getting history for %u packages", gs_app_list_length (list)); + g_dbus_connection_call (gs_plugin_get_system_bus_connection (GS_PLUGIN (self)), + "org.freedesktop.PackageKit", + "/org/freedesktop/PackageKit", + "org.freedesktop.PackageKit", + "GetPackageHistory", + g_variant_new ("(^asu)", package_names, 0), + NULL, + G_DBUS_CALL_FLAGS_NONE, + GS_PLUGIN_PACKAGEKIT_HISTORY_TIMEOUT, + cancellable, + refine_history_cb, + g_steal_pointer (&task)); +} + +static void +refine_history_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection = G_DBUS_CONNECTION (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginPackagekit *self = g_task_get_source_object (task); + GsPlugin *plugin = GS_PLUGIN (self); + GsAppList *list = g_task_get_task_data (task); + gboolean ret; + guint i = 0; + GVariantIter iter; + GVariant *value; + g_autoptr(GVariant) result_variant = NULL; + g_autoptr(GVariant) tuple = NULL; + g_autoptr(GError) error_local = NULL; + + result_variant = g_dbus_connection_call_finish (connection, result, &error_local); + + if (result_variant == NULL) { + g_dbus_error_strip_remote_error (error_local); + if (g_error_matches (error_local, + G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD)) { + g_debug ("No history available as PackageKit is too old: %s", + error_local->message); + + /* just set this to something non-zero so we don't keep + * trying to call GetPackageHistory */ + for (i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gs_app_set_install_date (app, GS_APP_INSTALL_DATE_UNKNOWN); + } + } else if (g_error_matches (error_local, + G_IO_ERROR, + G_IO_ERROR_CANCELLED)) { + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED, + "Failed to get history: %s", + error_local->message); + return; + } else if (g_error_matches (error_local, + G_IO_ERROR, + G_IO_ERROR_TIMED_OUT)) { + g_debug ("No history as PackageKit took too long: %s", + error_local->message); + for (i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gs_app_set_install_date (app, GS_APP_INSTALL_DATE_UNKNOWN); + } + } + + g_task_return_new_error (task, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Failed to get history: %s", + error_local->message); + return; + } + + /* get any results */ + tuple = g_variant_get_child_value (result_variant, 0); + for (i = 0; i < gs_app_list_length (list); i++) { + g_autoptr(GVariant) entries = NULL; + GsApp *app = gs_app_list_index (list, i); + ret = g_variant_lookup (tuple, + gs_app_get_source_default (app), + "@aa{sv}", + &entries); + if (!ret) { + /* make up a fake entry as we know this package was at + * least installed at some point in time */ + if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED) { + g_autoptr(GsApp) app_dummy = NULL; + app_dummy = gs_app_new (gs_app_get_id (app)); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_metadata (app_dummy, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_set_install_date (app_dummy, GS_APP_INSTALL_DATE_UNKNOWN); + gs_app_set_kind (app_dummy, AS_COMPONENT_KIND_GENERIC); + gs_app_set_state (app_dummy, GS_APP_STATE_INSTALLED); + gs_app_set_version (app_dummy, gs_app_get_version (app)); + gs_app_add_history (app, app_dummy); + } + gs_app_set_install_date (app, GS_APP_INSTALL_DATE_UNKNOWN); + continue; + } + + /* add history for application */ + g_variant_iter_init (&iter, entries); + while ((value = g_variant_iter_next_value (&iter))) { + gs_plugin_packagekit_refine_add_history (app, value); + g_variant_unref (value); + } + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_packagekit_refine_history_finish (GsPluginPackagekit *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +gs_plugin_packagekit_refresh_guess_app_id (GsPluginPackagekit *self, + GsApp *app, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + GsPlugin *plugin = GS_PLUGIN (self); + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_auto(GStrv) files = NULL; + g_autoptr(PkTask) task_local = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GString) basename_best = g_string_new (NULL); + + /* get file list so we can work out ID */ + files = g_strsplit (filename, "\t", -1); + gs_packagekit_helper_add_app (helper, app); + + task_local = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_local), GS_PLUGIN_ACTION_FILE_TO_APP, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_client_get_files_local (PK_CLIENT (task_local), + files, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + array = pk_results_get_files_array (results); + if (array->len == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no files for %s", filename); + return FALSE; + } + + /* find the smallest length desktop file, on the logic that + * ${app}.desktop is going to be better than ${app}-${action}.desktop */ + for (guint i = 0; i < array->len; i++) { + PkFiles *item = g_ptr_array_index (array, i); + gchar **fns = pk_files_get_files (item); + for (guint j = 0; fns[j] != NULL; j++) { + if (g_str_has_prefix (fns[j], "/etc/yum.repos.d/") && + g_str_has_suffix (fns[j], ".repo")) { + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); + } + if (g_str_has_prefix (fns[j], "/usr/share/applications/") && + g_str_has_suffix (fns[j], ".desktop")) { + g_autofree gchar *basename = g_path_get_basename (fns[j]); + if (basename_best->len == 0 || + strlen (basename) < basename_best->len) + g_string_assign (basename_best, basename); + } + } + } + if (basename_best->len > 0) { + gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP); + gs_app_set_id (app, basename_best->str); + } + + return TRUE; +} + +static void +add_quirks_from_package_name (GsApp *app, const gchar *package_name) +{ + /* these packages don't have a .repo file in their file lists, but + * instead install one through rpm scripts / cron job */ + const gchar *packages_with_repos[] = { + "google-chrome-stable", + "google-earth-pro-stable", + "google-talkplugin", + NULL }; + + if (g_strv_contains (packages_with_repos, package_name)) + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); +} + +static gboolean +gs_plugin_packagekit_local_check_installed (GsPluginPackagekit *self, + PkTask *task_local, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + PkBitfield filter; + const gchar *names[] = { gs_app_get_source_default (app), NULL }; + g_autoptr(GPtrArray) packages = NULL; + g_autoptr(PkResults) results = NULL; + + filter = pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, + PK_FILTER_ENUM_ARCH, + PK_FILTER_ENUM_INSTALLED, + -1); + results = pk_client_resolve (PK_CLIENT (task_local), filter, (gchar **) names, + cancellable, NULL, NULL, error); + if (results == NULL) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + packages = pk_results_get_package_array (results); + if (packages->len > 0) { + gboolean is_higher_version = FALSE; + const gchar *app_version = gs_app_get_version (app); + for (guint i = 0; i < packages->len; i++){ + PkPackage *pkg = g_ptr_array_index (packages, i); + gs_app_add_source_id (app, pk_package_get_id (pkg)); + if (!is_higher_version && + as_vercmp_simple (pk_package_get_version (pkg), app_version) < 0) + is_higher_version = TRUE; + } + if (!is_higher_version) { + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + } + } + return TRUE; +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + const gchar *package_id; + PkDetails *item; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_local = NULL; + g_autoptr(PkResults) results = NULL; + g_autofree gchar *content_type = NULL; + g_autofree gchar *filename = NULL; + g_autofree gchar *license_spdx = NULL; + g_auto(GStrv) files = NULL; + g_auto(GStrv) split = NULL; + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GsApp) app = NULL; + const gchar *mimetypes[] = { + "application/x-app-package", + "application/x-deb", + "application/vnd.debian.binary-package", + "application/x-redhat-package-manager", + "application/x-rpm", + NULL }; + + /* does this match any of the mimetypes we support */ + content_type = gs_utils_get_content_type (file, cancellable, error); + if (content_type == NULL) + return FALSE; + if (!g_strv_contains (mimetypes, content_type)) + return TRUE; + + /* get details */ + filename = g_file_get_path (file); + files = g_strsplit (filename, "\t", -1); + + task_local = gs_packagekit_task_new (plugin); + pk_client_set_cache_age (PK_CLIENT (task_local), G_MAXUINT); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_local), GS_PLUGIN_ACTION_FILE_TO_APP, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_client_get_details_local (PK_CLIENT (task_local), + files, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) + return FALSE; + + /* get results */ + array = pk_results_get_details_array (results); + if (array->len == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no details for %s", filename); + return FALSE; + } + if (array->len > 1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "too many details [%u] for %s", + array->len, filename); + return FALSE; + } + + /* create application */ + item = g_ptr_array_index (array, 0); + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + package_id = pk_details_get_package_id (item); + split = pk_package_id_split (package_id); + if (split == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "invalid package-id: %s", package_id); + return FALSE; + } + gs_app_set_management_plugin (app, plugin); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, split[PK_PACKAGE_ID_NAME]); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, + pk_details_get_summary (item)); + gs_app_set_version (app, split[PK_PACKAGE_ID_VERSION]); + gs_app_add_source (app, split[PK_PACKAGE_ID_NAME]); + gs_app_add_source_id (app, package_id); + gs_app_set_description (app, GS_APP_QUALITY_LOWEST, + pk_details_get_description (item)); + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, pk_details_get_url (item)); + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, pk_details_get_size (item)); + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0); + license_spdx = as_license_to_spdx_id (pk_details_get_license (item)); + gs_app_set_license (app, GS_APP_QUALITY_LOWEST, license_spdx); + add_quirks_from_package_name (app, split[PK_PACKAGE_ID_NAME]); + + /* is already installed? */ + if (!gs_plugin_packagekit_local_check_installed (self, + task_local, + app, + cancellable, + error)) + return FALSE; + + /* look for a desktop file so we can use a valid application id */ + if (!gs_plugin_packagekit_refresh_guess_app_id (self, + app, + filename, + cancellable, + error)) + return FALSE; + + gs_app_list_add (list, app); + return TRUE; +} + +static gboolean +gs_plugin_packagekit_convert_error (GError **error, + PkErrorEnum error_enum, + const gchar *details) +{ + switch (error_enum) { + case PK_ERROR_ENUM_PACKAGE_DOWNLOAD_FAILED: + case PK_ERROR_ENUM_NO_CACHE: + case PK_ERROR_ENUM_NO_NETWORK: + case PK_ERROR_ENUM_NO_MORE_MIRRORS_TO_TRY: + case PK_ERROR_ENUM_CANNOT_FETCH_SOURCES: + case PK_ERROR_ENUM_UNFINISHED_TRANSACTION: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_NETWORK, + details); + break; + case PK_ERROR_ENUM_BAD_GPG_SIGNATURE: + case PK_ERROR_ENUM_CANNOT_UPDATE_REPO_UNSIGNED: + case PK_ERROR_ENUM_GPG_FAILURE: + case PK_ERROR_ENUM_MISSING_GPG_SIGNATURE: + case PK_ERROR_ENUM_PACKAGE_CORRUPT: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_SECURITY, + details); + break; + case PK_ERROR_ENUM_TRANSACTION_CANCELLED: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_CANCELLED, + details); + break; + case PK_ERROR_ENUM_NO_PACKAGES_TO_UPDATE: + case PK_ERROR_ENUM_UPDATE_NOT_FOUND: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + details); + break; + case PK_ERROR_ENUM_NO_SPACE_ON_DEVICE: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NO_SPACE, + details); + break; + default: + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + details); + break; + } + return FALSE; +} + +gboolean +gs_plugin_add_updates_historical (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + guint64 mtime; + guint i; + g_autoptr(GPtrArray) package_array = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(PkResults) results = NULL; + PkExitEnum exit_code; + + /* get the results */ + results = pk_offline_get_results (&error_local); + if (results == NULL) { + /* was any offline update attempted */ + if (g_error_matches (error_local, + PK_OFFLINE_ERROR, + PK_OFFLINE_ERROR_NO_DATA)) { + return TRUE; + } + + gs_plugin_packagekit_error_convert (&error_local); + + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "Failed to get offline update results: %s", + error_local->message); + return FALSE; + } + + /* get the mtime of the results */ + mtime = pk_offline_get_results_mtime (error); + if (mtime == 0) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* only return results if successful */ + exit_code = pk_results_get_exit_code (results); + if (exit_code != PK_EXIT_ENUM_SUCCESS) { + g_autoptr(PkError) error_code = NULL; + + error_code = pk_results_get_error_code (results); + if (error_code == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Offline update failed without error_code set"); + return FALSE; + } + + return gs_plugin_packagekit_convert_error (error, + pk_error_get_code (error_code), + pk_error_get_details (error_code)); + } + + /* distro upgrade? */ + if (pk_results_get_role (results) == PK_ROLE_ENUM_UPGRADE_SYSTEM) { + g_autoptr(GsApp) app = NULL; + + app = gs_app_new (NULL); + gs_app_set_from_unique_id (app, "*/*/*/system/*", AS_COMPONENT_KIND_GENERIC); + gs_app_set_management_plugin (app, plugin); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + gs_app_set_kind (app, AS_COMPONENT_KIND_OPERATING_SYSTEM); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_install_date (app, mtime); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + + return TRUE; + } + + /* get list of package-ids */ + package_array = pk_results_get_package_array (results); + for (i = 0; i < package_array->len; i++) { + PkPackage *pkg = g_ptr_array_index (package_array, i); + const gchar *package_id; + g_autoptr(GsApp) app = NULL; + g_auto(GStrv) split = NULL; + + app = gs_app_new (NULL); + package_id = pk_package_get_id (pkg); + split = g_strsplit (package_id, ";", 4); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_add_source (app, split[0]); + gs_app_set_update_version (app, split[1]); + gs_app_set_management_plugin (app, plugin); + gs_app_add_source_id (app, package_id); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_install_date (app, mtime); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + g_autofree gchar *scheme = NULL; + g_autofree gchar *path = NULL; + const gchar *id = NULL; + const gchar * const *id_like = NULL; + g_auto(GStrv) package_ids = NULL; + g_autoptr(PkResults) results = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + g_autoptr(GPtrArray) packages = NULL; + g_autoptr(GPtrArray) details = NULL; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkClient) client_url_to_app = NULL; + + path = gs_utils_get_url_path (url); + + /* only do this for apt:// on debian or debian-like distros */ + os_release = gs_os_release_new (error); + if (os_release == NULL) { + g_prefix_error (error, "failed to determine OS information:"); + return FALSE; + } else { + id = gs_os_release_get_id (os_release); + id_like = gs_os_release_get_id_like (os_release); + scheme = gs_utils_get_url_scheme (url); + if (!(g_strcmp0 (scheme, "apt") == 0 && + (g_strcmp0 (id, "debian") == 0 || + g_strv_contains (id_like, "debian")))) { + return TRUE; + } + } + + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_add_source (app, path); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + + package_ids = g_new0 (gchar *, 2); + package_ids[0] = g_strdup (path); + + client_url_to_app = pk_client_new (); + pk_client_set_interactive (client_url_to_app, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_client_resolve (client_url_to_app, + pk_bitfield_from_enums (PK_FILTER_ENUM_NEWEST, PK_FILTER_ENUM_ARCH, -1), + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + g_prefix_error (error, "failed to resolve package_ids: "); + return FALSE; + } + + /* get results */ + packages = pk_results_get_package_array (results); + details = pk_results_get_details_array (results); + + if (packages->len >= 1) { + g_autoptr(GHashTable) details_collection = NULL; + g_autoptr(GHashTable) prepared_updates = NULL; + + if (gs_app_get_local_file (app) != NULL) + return TRUE; + + details_collection = gs_plugin_packagekit_details_array_to_hash (details); + + g_mutex_lock (&self->prepared_updates_mutex); + prepared_updates = g_hash_table_ref (self->prepared_updates); + g_mutex_unlock (&self->prepared_updates_mutex); + + gs_plugin_packagekit_resolve_packages_app (GS_PLUGIN (self), packages, app); + gs_plugin_packagekit_refine_details_app (plugin, details_collection, prepared_updates, app); + + gs_app_list_add (list, app); + } else { + g_warning ("no results returned"); + } + + return TRUE; +} + +static gchar * +get_proxy_http (GsPluginPackagekit *self) +{ + gboolean ret; + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + g_autofree gchar *password = NULL; + g_autofree gchar *username = NULL; + + proxy_mode = g_settings_get_enum (self->settings_proxy, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (self->settings_http, "host"); + if (host == NULL || host[0] == '\0') + return NULL; + + port = g_settings_get_int (self->settings_http, "port"); + + ret = g_settings_get_boolean (self->settings_http, + "use-authentication"); + if (ret) { + username = g_settings_get_string (self->settings_http, + "authentication-user"); + password = g_settings_get_string (self->settings_http, + "authentication-password"); + } + + /* make PackageKit proxy string */ + string = g_string_new (""); + if (username != NULL || password != NULL) { + if (username != NULL) + g_string_append_printf (string, "%s", username); + if (password != NULL) + g_string_append_printf (string, ":%s", password); + g_string_append (string, "@"); + } + g_string_append (string, host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_proxy_https (GsPluginPackagekit *self) +{ + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + + proxy_mode = g_settings_get_enum (self->settings_proxy, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (self->settings_https, "host"); + if (host == NULL || host[0] == '\0') + return NULL; + port = g_settings_get_int (self->settings_https, "port"); + if (port == 0) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_proxy_ftp (GsPluginPackagekit *self) +{ + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + + proxy_mode = g_settings_get_enum (self->settings_proxy, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (self->settings_ftp, "host"); + if (host == NULL || host[0] == '\0') + return NULL; + port = g_settings_get_int (self->settings_ftp, "port"); + if (port == 0) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_proxy_socks (GsPluginPackagekit *self) +{ + GString *string = NULL; + gint port; + GDesktopProxyMode proxy_mode; + g_autofree gchar *host = NULL; + + proxy_mode = g_settings_get_enum (self->settings_proxy, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + host = g_settings_get_string (self->settings_socks, "host"); + if (host == NULL || host[0] == '\0') + return NULL; + port = g_settings_get_int (self->settings_socks, "port"); + if (port == 0) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (host); + if (port > 0) + g_string_append_printf (string, ":%i", port); + return g_string_free (string, FALSE); +} + +static gchar * +get_no_proxy (GsPluginPackagekit *self) +{ + GString *string = NULL; + GDesktopProxyMode proxy_mode; + g_autofree gchar **hosts = NULL; + guint i; + + proxy_mode = g_settings_get_enum (self->settings_proxy, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_MANUAL) + return NULL; + + hosts = g_settings_get_strv (self->settings_proxy, "ignore-hosts"); + if (hosts == NULL) + return NULL; + + /* make PackageKit proxy string */ + string = g_string_new (""); + for (i = 0; hosts[i] != NULL; i++) { + if (i == 0) + g_string_assign (string, hosts[i]); + else + g_string_append_printf (string, ",%s", hosts[i]); + g_free (hosts[i]); + } + + return g_string_free (string, FALSE); +} + +static gchar * +get_pac (GsPluginPackagekit *self) +{ + GDesktopProxyMode proxy_mode; + gchar *url = NULL; + + proxy_mode = g_settings_get_enum (self->settings_proxy, "mode"); + if (proxy_mode != G_DESKTOP_PROXY_MODE_AUTO) + return NULL; + + url = g_settings_get_string (self->settings_proxy, "autoconfig-url"); + if (url == NULL) + return NULL; + + return url; +} + +static void get_permission_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void set_proxy_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +reload_proxy_settings_async (GsPluginPackagekit *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, reload_proxy_settings_async); + + /* only if we can achieve the action *without* an auth dialog */ + gs_utils_get_permission_async ("org.freedesktop.packagekit." + "system-network-proxy-configure", + cancellable, get_permission_cb, + g_steal_pointer (&task)); +} + +static void +get_permission_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginPackagekit *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + g_autofree gchar *proxy_http = NULL; + g_autofree gchar *proxy_https = NULL; + g_autofree gchar *proxy_ftp = NULL; + g_autofree gchar *proxy_socks = NULL; + g_autofree gchar *no_proxy = NULL; + g_autofree gchar *pac = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPermission) permission = NULL; + g_autoptr(GError) local_error = NULL; + + permission = gs_utils_get_permission_finish (result, &local_error); + if (permission == NULL) { + g_debug ("not setting proxy as no permission: %s", local_error->message); + g_task_return_boolean (task, TRUE); + return; + } + if (!g_permission_get_allowed (permission)) { + g_debug ("not setting proxy as no auth requested"); + g_task_return_boolean (task, TRUE); + return; + } + + proxy_http = get_proxy_http (self); + proxy_https = get_proxy_https (self); + proxy_ftp = get_proxy_ftp (self); + proxy_socks = get_proxy_socks (self); + no_proxy = get_no_proxy (self); + pac = get_pac (self); + + g_debug ("Setting proxies (http: %s, https: %s, ftp: %s, socks: %s, " + "no_proxy: %s, pac: %s)", + proxy_http, proxy_https, proxy_ftp, proxy_socks, + no_proxy, pac); + + pk_control_set_proxy2_async (self->control_proxy, + proxy_http, + proxy_https, + proxy_ftp, + proxy_socks, + no_proxy, + pac, + cancellable, + set_proxy_cb, + g_steal_pointer (&task)); +} + +static void +set_proxy_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkControl *control = PK_CONTROL (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + g_autoptr(GError) local_error = NULL; + + if (!pk_control_set_proxy_finish (control, result, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +reload_proxy_settings_finish (GsPluginPackagekit *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void proxy_changed_reload_proxy_settings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_packagekit_proxy_changed_cb (GSettings *settings, + const gchar *key, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (user_data); + + if (!gs_plugin_get_enabled (GS_PLUGIN (self))) + return; + + g_cancellable_cancel (self->proxy_settings_cancellable); + g_clear_object (&self->proxy_settings_cancellable); + self->proxy_settings_cancellable = g_cancellable_new (); + + reload_proxy_settings_async (self, self->proxy_settings_cancellable, + proxy_changed_reload_proxy_settings_cb, self); +} + +static void +proxy_changed_reload_proxy_settings_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (user_data); + g_autoptr(GError) local_error = NULL; + + if (!reload_proxy_settings_finish (self, result, &local_error) && + !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to set proxies: %s", local_error->message); +} + +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_upgrade = NULL; + g_autoptr(PkResults) results = NULL; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* check is distro-upgrade */ + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_OPERATING_SYSTEM) + return TRUE; + + /* ask PK to download enough packages to upgrade the system */ + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + gs_packagekit_helper_set_progress_app (helper, app); + + task_upgrade = gs_packagekit_task_new (plugin); + pk_task_set_only_download (task_upgrade, TRUE); + pk_client_set_cache_age (PK_CLIENT (task_upgrade), 60 * 60 * 24); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_upgrade), GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_task_upgrade_system_sync (task_upgrade, + gs_app_get_version (app), + PK_UPGRADE_KIND_ENUM_COMPLETE, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + return TRUE; +} + +static void gs_plugin_packagekit_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +static void +gs_plugin_packagekit_enable_repository_refresh_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (g_task_get_source_object (task)); + GsPluginManageRepositoryData *data = g_task_get_task_data (task); + + gs_plugin_repository_changed (GS_PLUGIN (self), data->repository); + + /* Ignore refresh errors */ + g_task_return_boolean (task, TRUE); +} + +static void +gs_plugin_packagekit_enable_repository_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(PkResults) results = NULL; + g_autoptr(PkError) error_code = NULL; + g_autoptr(GError) local_error = NULL; + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (g_task_get_source_object (task)); + GsPluginManageRepositoryData *data = g_task_get_task_data (task); + GsPluginRefreshMetadataFlags metadata_flags; + GCancellable *cancellable = g_task_get_cancellable (task); + + results = pk_client_generic_finish (PK_CLIENT (source_object), result, &local_error); + + /* pk_client_repo_enable() returns an error if the repo is already enabled. */ + if (results != NULL && + (error_code = pk_results_get_error_code (results)) != NULL && + pk_error_get_code (error_code) == PK_ERROR_ENUM_REPO_ALREADY_SET) { + g_clear_error (&local_error); + } else if (local_error != NULL || !gs_plugin_packagekit_results_valid (results, &local_error)) { + gs_app_set_state_recover (data->repository); + gs_utils_error_add_origin_id (&local_error, data->repository); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* state is known */ + gs_app_set_state (data->repository, GS_APP_STATE_INSTALLED); + + metadata_flags = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE) != 0 ? + GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE : + GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE; + + gs_plugin_packagekit_refresh_metadata_async (GS_PLUGIN (self), + 1, /* cache age */ + metadata_flags, + cancellable, + gs_plugin_packagekit_enable_repository_refresh_ready_cb, + g_steal_pointer (&task)); +} + +static void +gs_plugin_packagekit_enable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GsPackagekitHelper) helper = NULL; + g_autoptr(PkTask) task_enable_repo = NULL; + g_autoptr(GTask) task = NULL; + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_enable_repository_async); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* is repo */ + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + /* do the call */ + gs_plugin_status_update (plugin, repository, GS_PLUGIN_STATUS_WAITING); + gs_app_set_state (repository, GS_APP_STATE_INSTALLING); + + helper = gs_packagekit_helper_new (plugin); + gs_packagekit_helper_add_app (helper, repository); + + task_enable_repo = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_enable_repo), GS_PLUGIN_ACTION_ENABLE_REPO, + (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE) != 0); + gs_packagekit_task_take_helper (GS_PACKAGEKIT_TASK (task_enable_repo), helper); + + pk_client_repo_enable_async (PK_CLIENT (task_enable_repo), + gs_app_get_id (repository), + TRUE, + cancellable, + gs_packagekit_helper_cb, g_steal_pointer (&helper), + gs_plugin_packagekit_enable_repository_ready_cb, + g_steal_pointer (&task)); +} + +static gboolean +gs_plugin_packagekit_enable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_packagekit_disable_repository_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + g_autoptr(PkResults) results = NULL; + g_autoptr(PkError) error_code = NULL; + g_autoptr(GError) local_error = NULL; + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (g_task_get_source_object (task)); + GsPluginManageRepositoryData *data = g_task_get_task_data (task); + + results = pk_client_generic_finish (PK_CLIENT (source_object), result, &local_error); + + /* pk_client_repo_enable() returns an error if the repo is already disabled. */ + if (results != NULL && + (error_code = pk_results_get_error_code (results)) != NULL && + pk_error_get_code (error_code) == PK_ERROR_ENUM_REPO_ALREADY_SET) { + g_clear_error (&local_error); + } else if (local_error != NULL || !gs_plugin_packagekit_results_valid (results, &local_error)) { + gs_app_set_state_recover (data->repository); + gs_utils_error_add_origin_id (&local_error, data->repository); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* state is known */ + gs_app_set_state (data->repository, GS_APP_STATE_AVAILABLE); + + gs_plugin_repository_changed (GS_PLUGIN (self), data->repository); + + g_task_return_boolean (task, TRUE); +} + +static void +gs_plugin_packagekit_disable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GsPackagekitHelper) helper = NULL; + g_autoptr(PkTask) task_disable_repo = NULL; + g_autoptr(GTask) task = NULL; + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_disable_repository_async); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + /* is repo */ + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + /* do the call */ + gs_plugin_status_update (plugin, repository, GS_PLUGIN_STATUS_WAITING); + gs_app_set_state (repository, GS_APP_STATE_REMOVING); + + helper = gs_packagekit_helper_new (plugin); + gs_packagekit_helper_add_app (helper, repository); + + task_disable_repo = gs_packagekit_task_new (plugin); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_disable_repo), GS_PLUGIN_ACTION_DISABLE_REPO, + (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE) != 0); + gs_packagekit_task_take_helper (GS_PACKAGEKIT_TASK (task_disable_repo), helper); + + pk_client_repo_enable_async (PK_CLIENT (task_disable_repo), + gs_app_get_id (repository), + FALSE, + cancellable, + gs_packagekit_helper_cb, g_steal_pointer (&helper), + gs_plugin_packagekit_disable_repository_ready_cb, + g_steal_pointer (&task)); +} + +static gboolean +gs_plugin_packagekit_disable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static gboolean +_download_only (GsPluginPackagekit *self, + GsAppList *list, + GsAppList *progress_list, + GCancellable *cancellable, + GError **error) +{ + GsPlugin *plugin = GS_PLUGIN (self); + g_auto(GStrv) package_ids = NULL; + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(PkTask) task_refresh = NULL; + g_autoptr(PkPackageSack) sack = NULL; + g_autoptr(PkResults) results2 = NULL; + g_autoptr(PkResults) results = NULL; + + /* get the list of packages to update */ + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + + /* never refresh the metadata here as this can surprise the frontend if + * we end up downloading a different set of packages than what was + * shown to the user */ + task_refresh = gs_packagekit_task_new (plugin); + pk_task_set_only_download (task_refresh, TRUE); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_refresh), GS_PLUGIN_ACTION_DOWNLOAD, gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)); + + results = pk_client_get_updates (PK_CLIENT (task_refresh), + pk_bitfield_value (PK_FILTER_ENUM_NONE), + cancellable, + gs_packagekit_helper_cb, helper, + error); + + if (!gs_plugin_packagekit_results_valid (results, error)) { + return FALSE; + } + + /* download all the packages */ + sack = pk_results_get_package_sack (results); + if (pk_package_sack_get_size (sack) == 0) + return TRUE; + package_ids = pk_package_sack_get_ids (sack); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gs_packagekit_helper_add_app (helper, app); + } + gs_packagekit_helper_set_progress_list (helper, progress_list); + + /* never refresh the metadata here as this can surprise the frontend if + * we end up downloading a different set of packages than what was + * shown to the user */ + results2 = pk_task_update_packages_sync (task_refresh, + package_ids, + cancellable, + gs_packagekit_helper_cb, helper, + error); + + gs_app_list_override_progress (progress_list, GS_APP_PROGRESS_UNKNOWN); + if (results2 == NULL) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + /* To indicate the app is already downloaded */ + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0); + } + return TRUE; +} + +static gboolean +gs_plugin_packagekit_download (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + g_autoptr(GsAppList) list_tmp = gs_app_list_new (); + g_autoptr(GError) error_local = NULL; + gboolean retval; + gpointer schedule_entry_handle = NULL; + + /* add any packages */ + 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); + + /* add this app */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)) { + if (gs_app_has_management_plugin (app, plugin)) + gs_app_list_add (list_tmp, app); + continue; + } + + /* add each related app */ + for (guint j = 0; j < gs_app_list_length (related); j++) { + GsApp *app_tmp = gs_app_list_index (related, j); + if (gs_app_has_management_plugin (app_tmp, plugin)) + gs_app_list_add (list_tmp, app_tmp); + } + } + + if (gs_app_list_length (list_tmp) == 0) + return TRUE; + + if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) { + if (!gs_metered_block_app_list_on_download_scheduler (list_tmp, &schedule_entry_handle, cancellable, &error_local)) { + g_warning ("Failed to block on download scheduler: %s", + error_local->message); + g_clear_error (&error_local); + } + } + + retval = _download_only (self, list_tmp, list, cancellable, error); + + if (!gs_metered_remove_from_download_scheduler (schedule_entry_handle, NULL, &error_local)) + g_warning ("Failed to remove schedule entry: %s", error_local->message); + + if (retval) + gs_plugin_updates_changed (plugin); + + return retval; +} + +gboolean +gs_plugin_download (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + return gs_plugin_packagekit_download (plugin, list, cancellable, error); +} + +static void refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_packagekit_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GsPackagekitHelper) helper = gs_packagekit_helper_new (plugin); + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + gboolean interactive = (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE); + g_autoptr(GTask) task = NULL; + g_autoptr(PkTask) task_refresh = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_packagekit_refresh_metadata_async); + g_task_set_task_data (task, g_object_ref (helper), g_object_unref); + + gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_WAITING); + gs_packagekit_helper_set_progress_app (helper, app_dl); + + task_refresh = gs_packagekit_task_new (plugin); + pk_task_set_only_download (task_refresh, TRUE); + gs_packagekit_task_setup (GS_PACKAGEKIT_TASK (task_refresh), GS_PLUGIN_ACTION_UNKNOWN, interactive); + pk_client_set_cache_age (PK_CLIENT (task_refresh), cache_age_secs); + + /* refresh the metadata */ + pk_client_refresh_cache_async (PK_CLIENT (task_refresh), + FALSE /* force */, + cancellable, + gs_packagekit_helper_cb, helper, + refresh_metadata_cb, g_steal_pointer (&task)); +} + +static void +refresh_metadata_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + PkClient *client = PK_CLIENT (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPlugin *plugin = g_task_get_source_object (task); + g_autoptr(PkResults) results = NULL; + g_autoptr(GError) local_error = NULL; + + results = pk_client_generic_finish (client, result, &local_error); + + if (!gs_plugin_packagekit_results_valid (results, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + } else { + gs_plugin_updates_changed (plugin); + g_task_return_boolean (task, TRUE); + } +} + +static gboolean +gs_plugin_packagekit_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +#ifdef HAVE_PK_OFFLINE_WITH_FLAGS + +static PkOfflineFlags +gs_systemd_get_offline_flags (GsPlugin *plugin) +{ + if (gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) + return PK_OFFLINE_FLAGS_INTERACTIVE; + return PK_OFFLINE_FLAGS_NONE; +} + +static gboolean +gs_systemd_call_trigger (GsPlugin *plugin, + PkOfflineAction action, + GCancellable *cancellable, + GError **error) +{ + return pk_offline_trigger_with_flags (action, + gs_systemd_get_offline_flags (plugin), + cancellable, error); +} + +static gboolean +gs_systemd_call_cancel (GsPlugin *plugin, + GCancellable *cancellable, + GError **error) +{ + return pk_offline_cancel_with_flags (gs_systemd_get_offline_flags (plugin), cancellable, error); +} + +static gboolean +gs_systemd_call_trigger_upgrade (GsPlugin *plugin, + PkOfflineAction action, + GCancellable *cancellable, + GError **error) +{ + return pk_offline_trigger_upgrade_with_flags (action, + gs_systemd_get_offline_flags (plugin), + cancellable, error); +} + +#else /* HAVE_PK_OFFLINE_WITH_FLAGS */ + +static GDBusCallFlags +gs_systemd_get_gdbus_call_flags (GsPlugin *plugin) +{ + if (gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) + return G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION; + return G_DBUS_CALL_FLAGS_NONE; +} + +static gboolean +gs_systemd_call_trigger (GsPlugin *plugin, + PkOfflineAction action, + GCancellable *cancellable, + GError **error) +{ + const gchar *tmp; + g_autoptr(GVariant) res = NULL; + + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + tmp = pk_offline_action_to_string (action); + res = g_dbus_connection_call_sync (gs_plugin_get_system_bus_connection (plugin), + "org.freedesktop.PackageKit", + "/org/freedesktop/PackageKit", + "org.freedesktop.PackageKit.Offline", + "Trigger", + g_variant_new ("(s)", tmp), + NULL, + gs_systemd_get_gdbus_call_flags (plugin), + -1, + cancellable, + error); + if (res == NULL) + return FALSE; + return TRUE; +} + +static gboolean +gs_systemd_call_cancel (GsPlugin *plugin, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GVariant) res = NULL; + + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + res = g_dbus_connection_call_sync (gs_plugin_get_system_bus_connection (plugin), + "org.freedesktop.PackageKit", + "/org/freedesktop/PackageKit", + "org.freedesktop.PackageKit.Offline", + "Cancel", + NULL, + NULL, + gs_systemd_get_gdbus_call_flags (plugin), + -1, + cancellable, + error); + if (res == NULL) + return FALSE; + return TRUE; +} + +static gboolean +gs_systemd_call_trigger_upgrade (GsPlugin *plugin, + PkOfflineAction action, + GCancellable *cancellable, + GError **error) +{ + const gchar *tmp; + g_autoptr(GVariant) res = NULL; + + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + tmp = pk_offline_action_to_string (action); + res = g_dbus_connection_call_sync (gs_plugin_get_system_bus_connection (plugin), + "org.freedesktop.PackageKit", + "/org/freedesktop/PackageKit", + "org.freedesktop.PackageKit.Offline", + "TriggerUpgrade", + g_variant_new ("(s)", tmp), + NULL, + gs_systemd_get_gdbus_call_flags (plugin), + -1, + cancellable, + error); + if (res == NULL) + return FALSE; + return TRUE; +} + +#endif /* HAVE_PK_OFFLINE_WITH_FLAGS */ + +static gboolean +_systemd_trigger_app (GsPluginPackagekit *self, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* if we can process this online do not require a trigger */ + if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE) + return TRUE; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, GS_PLUGIN (self))) + return TRUE; + + /* already in correct state */ + if (self->is_triggered) + return TRUE; + + /* trigger offline update */ + if (!gs_systemd_call_trigger (GS_PLUGIN (self), PK_OFFLINE_ACTION_REBOOT, cancellable, error)) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* don't rely on the file monitor */ + gs_plugin_packagekit_refresh_is_triggered (self, cancellable); + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_update (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + + /* any are us? */ + 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); + + /* try to trigger this app */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)) { + if (!_systemd_trigger_app (self, app, cancellable, error)) + return FALSE; + continue; + } + + /* try to trigger each related app */ + for (guint j = 0; j < gs_app_list_length (related); j++) { + GsApp *app_tmp = gs_app_list_index (related, j); + if (!_systemd_trigger_app (self, app_tmp, cancellable, error)) + return FALSE; + } + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_update_cancel (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginPackagekit *self = GS_PLUGIN_PACKAGEKIT (plugin); + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* already in correct state */ + if (!self->is_triggered) + return TRUE; + + /* cancel offline update */ + if (!gs_systemd_call_cancel (plugin, cancellable, error)) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* don't rely on the file monitor */ + gs_plugin_packagekit_refresh_is_triggered (self, cancellable); + + /* success! */ + return TRUE; +} + +gboolean +gs_plugin_app_upgrade_trigger (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + if (!gs_systemd_call_trigger_upgrade (plugin, PK_OFFLINE_ACTION_REBOOT, cancellable, error)) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + return TRUE; +} + +static void +gs_plugin_packagekit_class_init (GsPluginPackagekitClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_packagekit_dispose; + object_class->finalize = gs_plugin_packagekit_finalize; + + plugin_class->setup_async = gs_plugin_packagekit_setup_async; + plugin_class->setup_finish = gs_plugin_packagekit_setup_finish; + plugin_class->shutdown_async = gs_plugin_packagekit_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_packagekit_shutdown_finish; + plugin_class->refine_async = gs_plugin_packagekit_refine_async; + plugin_class->refine_finish = gs_plugin_packagekit_refine_finish; + plugin_class->refresh_metadata_async = gs_plugin_packagekit_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_packagekit_refresh_metadata_finish; + plugin_class->list_apps_async = gs_plugin_packagekit_list_apps_async; + plugin_class->list_apps_finish = gs_plugin_packagekit_list_apps_finish; + plugin_class->enable_repository_async = gs_plugin_packagekit_enable_repository_async; + plugin_class->enable_repository_finish = gs_plugin_packagekit_enable_repository_finish; + plugin_class->disable_repository_async = gs_plugin_packagekit_disable_repository_async; + plugin_class->disable_repository_finish = gs_plugin_packagekit_disable_repository_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_PACKAGEKIT; +} diff --git a/plugins/packagekit/gs-plugin-packagekit.h b/plugins/packagekit/gs-plugin-packagekit.h new file mode 100644 index 0000000..8b698d0 --- /dev/null +++ b/plugins/packagekit/gs-plugin-packagekit.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_PACKAGEKIT (gs_plugin_packagekit_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginPackagekit, gs_plugin_packagekit, GS, PLUGIN_PACKAGEKIT, GsPlugin) + +G_END_DECLS diff --git a/plugins/packagekit/gs-self-test.c b/plugins/packagekit/gs-self-test.c new file mode 100644 index 0000000..51ae947 --- /dev/null +++ b/plugins/packagekit/gs-self-test.c @@ -0,0 +1,275 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-markdown.h" +#include "gs-test.h" + +static void +gs_markdown_func (void) +{ + gchar *text; + const gchar *markdown; + const gchar *markdown_expected; + g_autoptr(GsMarkdown) md = NULL; + + /* get GsMarkdown object */ + md = gs_markdown_new (GS_MARKDOWN_OUTPUT_PANGO); + g_assert (md); + + markdown = "OEMs\n" + "====\n" + " - Bullett\n"; + markdown_expected = + "<big>OEMs</big>\n" + "• Bullett"; + /* markdown (type2 header) */ + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (autocode) */ + markdown = "this is http://www.hughsie.com/with_spaces_in_url inline link\n"; + markdown_expected = "this is <tt>http://www.hughsie.com/with_spaces_in_url</tt> inline link"; + gs_markdown_set_autocode (md, TRUE); + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown some invalid header */ + markdown = "*** This software is currently in alpha state ***\n"; + markdown_expected = "<b><i> This software is currently in alpha state </b></i>"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (complex1) */ + markdown = " - This is a *very*\n" + " short paragraph\n" + " that is not usual.\n" + " - Another"; + markdown_expected = + "• This is a <i>very</i> short paragraph that is not usual.\n" + "• Another"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (complex1) */ + markdown = "* This is a *very*\n" + " short paragraph\n" + " that is not usual.\n" + "* This is the second\n" + " bullett point.\n" + "* And the third.\n" + " \n" + "* * *\n" + " \n" + "Paragraph one\n" + "isn't __very__ long at all.\n" + "\n" + "Paragraph two\n" + "isn't much better."; + markdown_expected = + "• This is a <i>very</i> short paragraph that is not usual.\n" + "• This is the second bullett point.\n" + "• And the third.\n" + "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n" + "Paragraph one isn't <b>very</b> long at all.\n" + "Paragraph two isn't much better."; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + markdown = "This is a spec file description or\n" + "an **update** description in bohdi.\n" + "\n" + "* * *\n" + "# Big title #\n" + "\n" + "The *following* things 'were' fixed:\n" + "- Fix `dave`\n" + "* Fubar update because of \"security\"\n"; + markdown_expected = + "This is a spec file description or an <b>update</b> description in bohdi.\n" + "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n" + "<big>Big title</big>\n" + "The <i>following</i> things 'were' fixed:\n" + "• Fix <tt>dave</tt>\n" + "• Fubar update because of \"security\""; + /* markdown (complex2) */ + text = gs_markdown_parse (md, markdown); + if (g_strcmp0 (text, markdown_expected) == 0) + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (list with spaces) */ + markdown = "* list seporated with spaces -\n" + " first item\n" + "\n" + "* second item\n" + "\n" + "* third item\n"; + markdown_expected = + "• list seporated with spaces - first item\n" + "• second item\n" + "• third item"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + gs_markdown_set_max_lines (md, 1); + + /* markdown (one line limit) */ + markdown = "* list seporated with spaces -\n" + " first item\n" + "* second item\n"; + markdown_expected = + "• list seporated with spaces - first item"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + gs_markdown_set_max_lines (md, 1); + + /* markdown (escaping) */ + markdown = "* list & <spaces>"; + markdown_expected = + "• list & <spaces>"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (URLs) */ + markdown = "this is the http://www.hughsie.com/ coolest site"; + markdown_expected = + "this is the " + "<a href=\"http://www.hughsie.com/\">http://www.hughsie.com/</a>" + " coolest site"; + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); + + /* markdown (free text) */ + gs_markdown_set_escape (md, FALSE); + text = gs_markdown_parse (md, "This isn't a present"); + g_assert_cmpstr (text, ==, "This isn't a present"); + g_free (text); + + /* markdown (autotext underscore) */ + text = gs_markdown_parse (md, "This isn't CONFIG_UEVENT_HELPER_PATH present"); + g_assert_cmpstr (text, ==, "This isn't <tt>CONFIG_UEVENT_HELPER_PATH</tt> present"); + g_free (text); + + /* markdown (end of bullett) */ + markdown = "*Thu Mar 12 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.79-1\n" + "- Update to upstream \n" + " * Netlink socket handoff patch from Adam Jackson.\n" + " * AVC caching of compute_create results by Eric Paris.\n" + "\n" + "*Tue Mar 10 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.78-5\n" + "- Add patch from ajax to accellerate X SELinux \n" + "- Update eparis patch\n"; + markdown_expected = + "<i>Thu Mar 12 12:00:00 2009</i> Dan Walsh <tt><dwalsh@redhat.com></tt> - 2.0.79-1\n" + "• Update to upstream\n" + "• Netlink socket handoff patch from Adam Jackson.\n" + "• AVC caching of compute_create results by Eric Paris.\n" + "<i>Tue Mar 10 12:00:00 2009</i> Dan Walsh <tt><dwalsh@redhat.com></tt> - 2.0.78-5\n" + "• Add patch from ajax to accellerate X SELinux\n" + "• Update eparis patch"; + gs_markdown_set_escape (md, TRUE); + gs_markdown_set_max_lines (md, 1024); + text = gs_markdown_parse (md, markdown); + g_assert_cmpstr (text, ==, markdown_expected); + g_free (text); +} + +static void +gs_plugins_packagekit_local_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *fn = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* no packagekit, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "packagekit")) { + g_test_skip ("not enabled"); + return; + } + + /* load local file */ + fn = gs_test_get_filename (TESTDATADIR, "chiron-1.1-1.fc24.x86_64.rpm"); + g_assert (fn != NULL); + file = g_file_new_for_path (fn); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + NULL); + app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED)) { + g_test_skip ("rpm files not supported"); + return; + } + g_assert_no_error (error); + g_assert (app != NULL); + g_assert_cmpstr (gs_app_get_source_default (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/"); + g_assert_cmpstr (gs_app_get_name (app), ==, "chiron"); + g_assert_cmpstr (gs_app_get_version (app), ==, "1.1-1.fc24"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis"); + g_assert_cmpstr (gs_app_get_description (app), ==, + "This is the first paragraph in the example " + "package spec file.\n\nThis is the second paragraph."); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar * const allowlist[] = { + "packagekit", + NULL + }; + + /* The tests access the system proxy schemas, so pre-load those before + * %G_TEST_OPTION_ISOLATE_DIRS resets the XDG system dirs. */ + g_settings_schema_source_get_default (); + + gs_test_init (&argc, &argv); + + /* generic tests go here */ + g_test_add_func ("/gnome-software/markdown", gs_markdown_func); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + + /* plugin tests go here */ + if (!g_file_test ("/run/ostree-booted", G_FILE_TEST_EXISTS)) { + g_test_add_data_func ("/gnome-software/plugins/packagekit/local", + plugin_loader, + (GTestDataFunc) gs_plugins_packagekit_local_func); + } + + return g_test_run (); +} diff --git a/plugins/packagekit/meson.build b/plugins/packagekit/meson.build new file mode 100644 index 0000000..8dc2d5d --- /dev/null +++ b/plugins/packagekit/meson.build @@ -0,0 +1,50 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginPackageKit"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] +deps = [ + plugin_libs, + packagekit, +] + +if get_option('mogwai') + deps += [mogwai_schedule_client] +endif + +shared_module( + 'gs_plugin_packagekit', + sources : [ + 'gs-plugin-packagekit.c', + 'gs-packagekit-helper.c', + 'gs-packagekit-task.c', + 'packagekit-common.c', + 'gs-markdown.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : deps, +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-packagekit', + compiled_schemas, + sources : [ + 'gs-markdown.c', + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + c_args : cargs, + ) + test('gs-self-test-packagekit', e, suite: ['plugins', 'packagekit'], env: test_env) +endif diff --git a/plugins/packagekit/packagekit-common.c b/plugins/packagekit/packagekit-common.c new file mode 100644 index 0000000..4b6b165 --- /dev/null +++ b/plugins/packagekit/packagekit-common.c @@ -0,0 +1,585 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <packagekit-glib2/packagekit.h> + +#include <gnome-software.h> + +#include "packagekit-common.h" + +GsPluginStatus +packagekit_status_enum_to_plugin_status (PkStatusEnum status) +{ + GsPluginStatus plugin_status = GS_PLUGIN_STATUS_UNKNOWN; + + switch (status) { + case PK_STATUS_ENUM_SETUP: + case PK_STATUS_ENUM_CANCEL: + case PK_STATUS_ENUM_FINISHED: + case PK_STATUS_ENUM_UNKNOWN: + break; + case PK_STATUS_ENUM_WAIT: + case PK_STATUS_ENUM_WAITING_FOR_LOCK: + case PK_STATUS_ENUM_WAITING_FOR_AUTH: + plugin_status = GS_PLUGIN_STATUS_WAITING; + break; + case PK_STATUS_ENUM_LOADING_CACHE: + case PK_STATUS_ENUM_TEST_COMMIT: + case PK_STATUS_ENUM_RUNNING: + case PK_STATUS_ENUM_SIG_CHECK: + case PK_STATUS_ENUM_REFRESH_CACHE: + plugin_status = GS_PLUGIN_STATUS_SETUP; + break; + case PK_STATUS_ENUM_DOWNLOAD: + case PK_STATUS_ENUM_DOWNLOAD_REPOSITORY: + case PK_STATUS_ENUM_DOWNLOAD_PACKAGELIST: + case PK_STATUS_ENUM_DOWNLOAD_FILELIST: + case PK_STATUS_ENUM_DOWNLOAD_CHANGELOG: + case PK_STATUS_ENUM_DOWNLOAD_GROUP: + case PK_STATUS_ENUM_DOWNLOAD_UPDATEINFO: + plugin_status = GS_PLUGIN_STATUS_DOWNLOADING; + break; + case PK_STATUS_ENUM_INSTALL: + case PK_STATUS_ENUM_UPDATE: + plugin_status = GS_PLUGIN_STATUS_INSTALLING; + break; + case PK_STATUS_ENUM_CLEANUP: + case PK_STATUS_ENUM_REMOVE: + plugin_status = GS_PLUGIN_STATUS_REMOVING; + break; + case PK_STATUS_ENUM_REQUEST: + case PK_STATUS_ENUM_QUERY: + case PK_STATUS_ENUM_INFO: + case PK_STATUS_ENUM_DEP_RESOLVE: + plugin_status = GS_PLUGIN_STATUS_QUERYING; + break; + default: + g_warning ("no mapping for %s", + pk_status_enum_to_string (status)); + break; + } + return plugin_status; +} + +gboolean +gs_plugin_packagekit_error_convert (GError **error) +{ + GError *error_tmp; + + if (error == NULL) + return FALSE; + + if (*error != NULL) + g_dbus_error_strip_remote_error (*error); + + /* these are allowed for low-level errors */ + if (gs_utils_error_convert_gio (error)) + return TRUE; + + /* not set */ + error_tmp = *error; + if (error_tmp == NULL) + return FALSE; + + /* already correct */ + if (error_tmp->domain == GS_PLUGIN_ERROR) + return TRUE; + + /* get a local version */ + if (error_tmp->domain != PK_CLIENT_ERROR) + return FALSE; + + /* daemon errors */ + if (error_tmp->code <= 0xff) { + switch (error_tmp->code) { + case PK_CLIENT_ERROR_CANNOT_START_DAEMON: + case PK_CLIENT_ERROR_INVALID_FILE: + case PK_CLIENT_ERROR_NOT_SUPPORTED: + error_tmp->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + #if PK_CHECK_VERSION(1, 2, 4) + case PK_CLIENT_ERROR_DECLINED_INTERACTION: + error_tmp->code = GS_PLUGIN_ERROR_CANCELLED; + break; + #else + case PK_CLIENT_ERROR_FAILED: + /* The text is not localized on the PackageKit side and it uses a generic error code + * FIXME: This can be dropped when we depend on a + * PackageKit version which includes https://github.com/PackageKit/PackageKit/pull/497 */ + if (g_strcmp0 (error_tmp->message, "user declined interaction") == 0) + error_tmp->code = GS_PLUGIN_ERROR_CANCELLED; + else + error_tmp->code = GS_PLUGIN_ERROR_FAILED; + break; + #endif + /* this is working around a bug in libpackagekit-glib */ + case PK_ERROR_ENUM_TRANSACTION_CANCELLED: + error_tmp->code = GS_PLUGIN_ERROR_CANCELLED; + break; + default: + error_tmp->code = GS_PLUGIN_ERROR_FAILED; + break; + } + + /* backend errors */ + } else { + switch (error_tmp->code - 0xff) { + case PK_ERROR_ENUM_INVALID_PACKAGE_FILE: + case PK_ERROR_ENUM_NOT_SUPPORTED: + case PK_ERROR_ENUM_PACKAGE_INSTALL_BLOCKED: + error_tmp->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case PK_ERROR_ENUM_NO_CACHE: + case PK_ERROR_ENUM_NO_NETWORK: + error_tmp->code = GS_PLUGIN_ERROR_NO_NETWORK; + break; + case PK_ERROR_ENUM_PACKAGE_DOWNLOAD_FAILED: + case PK_ERROR_ENUM_NO_MORE_MIRRORS_TO_TRY: + case PK_ERROR_ENUM_CANNOT_FETCH_SOURCES: + error_tmp->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + break; + case PK_ERROR_ENUM_BAD_GPG_SIGNATURE: + case PK_ERROR_ENUM_CANNOT_INSTALL_REPO_UNSIGNED: + case PK_ERROR_ENUM_CANNOT_UPDATE_REPO_UNSIGNED: + case PK_ERROR_ENUM_GPG_FAILURE: + case PK_ERROR_ENUM_MISSING_GPG_SIGNATURE: + case PK_ERROR_ENUM_NO_LICENSE_AGREEMENT: + case PK_ERROR_ENUM_NOT_AUTHORIZED: + case PK_ERROR_ENUM_RESTRICTED_DOWNLOAD: + error_tmp->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case PK_ERROR_ENUM_NO_SPACE_ON_DEVICE: + error_tmp->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + case PK_ERROR_ENUM_CANCELLED_PRIORITY: + case PK_ERROR_ENUM_TRANSACTION_CANCELLED: + error_tmp->code = GS_PLUGIN_ERROR_CANCELLED; + break; + default: + error_tmp->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } + error_tmp->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +gboolean +gs_plugin_packagekit_results_valid (PkResults *results, GError **error) +{ + g_autoptr(PkError) error_code = NULL; + + /* method failed? */ + if (results == NULL) { + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* check error code */ + error_code = pk_results_get_error_code (results); + if (error_code != NULL) { + g_set_error_literal (error, + PK_CLIENT_ERROR, + pk_error_get_code (error_code), + pk_error_get_details (error_code)); + gs_plugin_packagekit_error_convert (error); + return FALSE; + } + + /* all good */ + return TRUE; +} + +gboolean +gs_plugin_packagekit_add_results (GsPlugin *plugin, + GsAppList *list, + PkResults *results, + GError **error) +{ + const gchar *package_id; + guint i; + PkPackage *package; + g_autoptr(GHashTable) installed = NULL; + g_autoptr(PkError) error_code = NULL; + g_autoptr(GPtrArray) array_filtered = NULL; + g_autoptr(GPtrArray) array = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE); + + /* check error code */ + error_code = pk_results_get_error_code (results); + if (error_code != NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "failed to get-packages: %s, %s", + pk_error_enum_to_string (pk_error_get_code (error_code)), + pk_error_get_details (error_code)); + return FALSE; + } + + /* add all installed packages to a hash */ + installed = g_hash_table_new (g_str_hash, g_str_equal); + array = pk_results_get_package_array (results); + for (i = 0; i < array->len; i++) { + package = g_ptr_array_index (array, i); + if (pk_package_get_info (package) != PK_INFO_ENUM_INSTALLED) + continue; + g_hash_table_insert (installed, + (const gpointer) pk_package_get_name (package), + (const gpointer) pk_package_get_id (package)); + } + + /* if the search returns more than one package with the same name, + * ignore everything with that name except the installed package */ + array_filtered = g_ptr_array_new (); + for (i = 0; i < array->len; i++) { + package = g_ptr_array_index (array, i); + package_id = g_hash_table_lookup (installed, pk_package_get_name (package)); + if (pk_package_get_info (package) == PK_INFO_ENUM_INSTALLED || package_id == NULL) { + g_ptr_array_add (array_filtered, package); + } else { + g_debug ("ignoring available %s as installed %s also reported", + pk_package_get_id (package), package_id); + } + } + + /* process packages */ + for (i = 0; i < array_filtered->len; i++) { + g_autoptr(GsApp) app = NULL; + GsAppState state = GS_APP_STATE_UNKNOWN; + package = g_ptr_array_index (array_filtered, i); + + app = gs_plugin_cache_lookup (plugin, pk_package_get_id (package)); + if (app == NULL) { + app = gs_app_new (NULL); + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_management_plugin (app, plugin); + gs_app_add_source (app, pk_package_get_name (package)); + gs_app_add_source_id (app, pk_package_get_id (package)); + gs_plugin_cache_add (plugin, pk_package_get_id (package), app); + } + gs_app_set_name (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_name (package)); + gs_app_set_summary (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_summary (package)); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_set_version (app, pk_package_get_version (package)); + switch (pk_package_get_info (package)) { + case PK_INFO_ENUM_INSTALLED: + state = GS_APP_STATE_INSTALLED; + break; + case PK_INFO_ENUM_AVAILABLE: + state = GS_APP_STATE_AVAILABLE; + break; + case PK_INFO_ENUM_INSTALLING: + case PK_INFO_ENUM_UPDATING: + case PK_INFO_ENUM_DOWNGRADING: + case PK_INFO_ENUM_OBSOLETING: + case PK_INFO_ENUM_UNTRUSTED: + break; + case PK_INFO_ENUM_UNAVAILABLE: + case PK_INFO_ENUM_REMOVING: + state = GS_APP_STATE_UNAVAILABLE; + break; + default: + g_warning ("unknown info state of %s", + pk_info_enum_to_string (pk_package_get_info (package))); + } + if (state != GS_APP_STATE_UNKNOWN && gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, state); + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN) + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_list_add (list, app); + } + return TRUE; +} + +void +gs_plugin_packagekit_resolve_packages_app (GsPlugin *plugin, + GPtrArray *packages, + GsApp *app) +{ + GPtrArray *sources; + PkPackage *package; + const gchar *pkgname; + guint i, j; + guint number_available = 0; + guint number_installed = 0; + + /* find any packages that match the package name */ + number_installed = 0; + number_available = 0; + sources = gs_app_get_sources (app); + for (j = 0; j < sources->len; j++) { + pkgname = g_ptr_array_index (sources, j); + for (i = 0; i < packages->len; i++) { + package = g_ptr_array_index (packages, i); + if (g_strcmp0 (pk_package_get_name (package), pkgname) == 0) { + gs_plugin_packagekit_set_metadata_from_package (plugin, app, package); + switch (pk_package_get_info (package)) { + case PK_INFO_ENUM_INSTALLED: + number_installed++; + break; + case PK_INFO_ENUM_AVAILABLE: + number_available++; + break; + case PK_INFO_ENUM_UNAVAILABLE: + number_available++; + break; + default: + /* should we expect anything else? */ + break; + } + } + } + } + + /* if *all* the source packages for the app are installed then the + * application is considered completely installed */ + if (number_installed == sources->len && number_available == 0) { + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + } else if (number_installed + number_available == sources->len) { + /* if all the source packages are installed and all the rest + * of the packages are available then the app is available */ + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + } else if (number_installed + number_available > sources->len) { + /* we have more packages returned than source packages */ + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + } else if (number_installed + number_available < sources->len) { + g_autofree gchar *tmp = NULL; + /* we have less packages returned than source packages */ + tmp = gs_app_to_string (app); + g_debug ("Failed to find all packages for:\n%s", tmp); + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + } +} + +void +gs_plugin_packagekit_set_metadata_from_package (GsPlugin *plugin, + GsApp *app, + PkPackage *package) +{ + const gchar *data; + + gs_plugin_packagekit_set_packaging_format (plugin, app); + gs_app_set_management_plugin (app, plugin); + gs_app_add_source (app, pk_package_get_name (package)); + gs_app_add_source_id (app, pk_package_get_id (package)); + + /* set origin */ + if (gs_app_get_origin (app) == NULL) { + data = pk_package_get_data (package); + if (g_str_has_prefix (data, "installed:")) + data += 10; + gs_app_set_origin (app, data); + } + + /* set unavailable state */ + if (pk_package_get_info (package) == PK_INFO_ENUM_UNAVAILABLE) { + gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE); + if (gs_app_get_size_installed (app, NULL) == GS_SIZE_TYPE_UNKNOWN) + gs_app_set_size_installed (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + if (gs_app_get_size_download (app, NULL) == GS_SIZE_TYPE_UNKNOWN) + gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + } + if (gs_app_get_version (app) == NULL) + gs_app_set_version (app, pk_package_get_version (package)); + gs_app_set_name (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_name (package)); + gs_app_set_summary (app, + GS_APP_QUALITY_LOWEST, + pk_package_get_summary (package)); +} + +/* Hash functions which compare PkPackageIds on NAME, VERSION and ARCH, but not DATA. + * This is because some backends do not append the origin. + * + * Borrowing some implementation details from pk-package-id.c, a package + * ID is a semicolon-separated list of NAME;[VERSION];[ARCH];[DATA], + * so a comparison which ignores DATA is just a strncmp() up to and + * including the final semicolon. + * + * Doing it this way means zero allocations, which allows the hash and + * equality functions to be fast. This is important when dealing with + * large refine() package lists. + * + * The hash and equality functions assume that the IDs they are passed are + * valid. */ +static guint +package_id_hash (gconstpointer key) +{ + const gchar *package_id = key; + gchar *no_data; + gsize i, last_semicolon = 0; + + /* find the last semicolon, which starts the DATA section */ + for (i = 0; package_id[i] != '\0'; i++) { + if (package_id[i] == ';') + last_semicolon = i; + } + + /* exit early if the DATA section was empty */ + if (last_semicolon + 1 == i) + return g_str_hash (package_id); + + /* extract up to (and including) the last semicolon into a local string */ + no_data = g_alloca (last_semicolon + 2); + memcpy (no_data, package_id, last_semicolon + 1); + no_data[last_semicolon + 1] = '\0'; + + return g_str_hash (no_data); +} + +static gboolean +package_id_equal (gconstpointer a, + gconstpointer b) +{ + const gchar *package_id_a = a; + const gchar *package_id_b = b; + gsize i, n_semicolons = 0; + + /* compare up to and including the last semicolon */ + for (i = 0; package_id_a[i] != '\0' && package_id_b[i] != '\0'; i++) { + if (package_id_a[i] != package_id_b[i]) + return FALSE; + if (package_id_a[i] == ';') + n_semicolons++; + if (n_semicolons == 4) + return TRUE; + } + + return package_id_a[i] == package_id_b[i]; +} + +GHashTable * +gs_plugin_packagekit_details_array_to_hash (GPtrArray *array) +{ + g_autoptr(GHashTable) details_collection = NULL; + + details_collection = g_hash_table_new_full (package_id_hash, package_id_equal, + NULL, NULL); + + for (gsize i = 0; i < array->len; i++) { + PkDetails *details = g_ptr_array_index (array, i); + g_hash_table_insert (details_collection, + (void *) pk_details_get_package_id (details), + details); + } + + return g_steal_pointer (&details_collection); +} + +void +gs_plugin_packagekit_refine_details_app (GsPlugin *plugin, + GHashTable *details_collection, + GHashTable *prepared_updates, + GsApp *app) +{ + GPtrArray *source_ids; + PkDetails *details; + const gchar *package_id; + guint j; + guint64 download_size = 0, install_size = 0; + + /* @source_ids can have as many as 200 elements (google-noto); typically + * it has 1 or 2 + * + * @details_collection is typically a large list of apps in the + * repository, on the order of 400 or 700 apps */ + source_ids = gs_app_get_source_ids (app); + for (j = 0; j < source_ids->len; j++) { + #ifdef HAVE_PK_DETAILS_GET_DOWNLOAD_SIZE + guint64 download_sz; + #endif + package_id = g_ptr_array_index (source_ids, j); + details = g_hash_table_lookup (details_collection, package_id); + if (details == NULL) + continue; + + if (gs_app_get_license (app) == NULL) { + g_autofree gchar *license_spdx = NULL; + license_spdx = as_license_to_spdx_id (pk_details_get_license (details)); + if (license_spdx != NULL) { + gs_app_set_license (app, + GS_APP_QUALITY_LOWEST, + license_spdx); + } + } + if (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL) { + gs_app_set_url (app, + AS_URL_KIND_HOMEPAGE, + pk_details_get_url (details)); + } + if (gs_app_get_description (app) == NULL) { + gs_app_set_description (app, + GS_APP_QUALITY_LOWEST, + pk_details_get_description (details)); + } + install_size += pk_details_get_size (details); + #ifdef HAVE_PK_DETAILS_GET_DOWNLOAD_SIZE + download_sz = pk_details_get_download_size (details); + + /* If the package is already prepared as part of an offline + * update, no additional downloads need to be done. */ + if (download_sz != G_MAXUINT64 && + !g_hash_table_contains (prepared_updates, package_id)) + download_size += download_sz; + #endif + } + + #ifndef HAVE_PK_DETAILS_GET_DOWNLOAD_SIZE + download_size = install_size; + #endif + + /* the size is the size of all sources */ + if (gs_app_get_state (app) == GS_APP_STATE_UPDATABLE) { + if (install_size > 0 && gs_app_get_size_installed (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, install_size); + if (download_size > 0 && gs_app_get_size_download (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, download_size); + } else if (gs_app_is_installed (app)) { + if (gs_app_get_size_download (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + if (install_size > 0 && gs_app_get_size_installed (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, install_size); + } else { + if (install_size > 0 && gs_app_get_size_installed (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, install_size); + if (download_size > 0 && gs_app_get_size_download (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, download_size); + } +} + +void +gs_plugin_packagekit_set_packaging_format (GsPlugin *plugin, GsApp *app) +{ + if (gs_plugin_check_distro_id (plugin, "fedora") || + gs_plugin_check_distro_id (plugin, "rhel")) { + gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "RPM"); + } else if (gs_plugin_check_distro_id (plugin, "debian") || + gs_plugin_check_distro_id (plugin, "ubuntu")) { + gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "deb"); + } else { + return; + } + + gs_app_set_metadata (app, "GnomeSoftware::PackagingBaseCssColor", "error_color"); +} diff --git a/plugins/packagekit/packagekit-common.h b/plugins/packagekit/packagekit-common.h new file mode 100644 index 0000000..6d673bc --- /dev/null +++ b/plugins/packagekit/packagekit-common.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) 2013 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib.h> +#include <gnome-software.h> + +#include <packagekit-glib2/packagekit.h> + +G_BEGIN_DECLS + +GsPluginStatus packagekit_status_enum_to_plugin_status (PkStatusEnum status); + +gboolean gs_plugin_packagekit_add_results (GsPlugin *plugin, + GsAppList *list, + PkResults *results, + GError **error); +gboolean gs_plugin_packagekit_error_convert (GError **error); +gboolean gs_plugin_packagekit_results_valid (PkResults *results, + GError **error); +void gs_plugin_packagekit_resolve_packages_app (GsPlugin *plugin, + GPtrArray *packages, + GsApp *app); +void gs_plugin_packagekit_set_metadata_from_package (GsPlugin *plugin, + GsApp *app, + PkPackage *package); +GHashTable * gs_plugin_packagekit_details_array_to_hash (GPtrArray *array); +void gs_plugin_packagekit_refine_details_app (GsPlugin *plugin, + GHashTable *details_collection, + GHashTable *prepared_updates, + GsApp *app); +void gs_plugin_packagekit_set_packaging_format (GsPlugin *plugin, + GsApp *app); + +G_END_DECLS diff --git a/plugins/packagekit/tests/build-rpm.sh b/plugins/packagekit/tests/build-rpm.sh new file mode 100755 index 0000000..90a4163 --- /dev/null +++ b/plugins/packagekit/tests/build-rpm.sh @@ -0,0 +1,2 @@ +rpmbuild -ba chiron.spec +cp ~/rpmbuild/RPMS/*/chiron*.rpm . diff --git a/plugins/packagekit/tests/chiron-1.1-1.fc24.x86_64.rpm b/plugins/packagekit/tests/chiron-1.1-1.fc24.x86_64.rpm Binary files differnew file mode 100644 index 0000000..1453f48 --- /dev/null +++ b/plugins/packagekit/tests/chiron-1.1-1.fc24.x86_64.rpm diff --git a/plugins/packagekit/tests/chiron.spec b/plugins/packagekit/tests/chiron.spec new file mode 100644 index 0000000..6cbba7e --- /dev/null +++ b/plugins/packagekit/tests/chiron.spec @@ -0,0 +1,22 @@ +Summary: Single line synopsis +Name: chiron +Version: 1.1 +Release: 1%{?dist} +URL: http://127.0.0.1/ +License: GPLv2+ + +%description +This is the first paragraph in the example package spec file. + +This is the second paragraph. + +%install +mkdir -p $RPM_BUILD_ROOT/%{_bindir} +touch $RPM_BUILD_ROOT/%{_bindir}/chiron + +%files +%{_bindir}/chiron + +%changelog +* Tue Apr 26 2016 Richard Hughes <richard@hughsie.com> - 1.1-1 +- Initial version diff --git a/plugins/repos/gs-plugin-repos.c b/plugins/repos/gs-plugin-repos.c new file mode 100644 index 0000000..8e57e8b --- /dev/null +++ b/plugins/repos/gs-plugin-repos.c @@ -0,0 +1,422 @@ +/* -*- 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-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +#include "gs-plugin-repos.h" + +/* + * SECTION: + * Plugin to set URLs and origin hostnames on repos and apps using data from + * `/etc/yum.repos.d` + * + * This plugin is only useful on distributions which use `/etc/yum.repos.d`. + * + * It enumerates `/etc/yum.repos.d` in a worker thread and updates its internal + * hash tables and state from that worker thread (while holding a lock). + * + * Other tasks on the plugin access the data synchronously, not using a worker + * thread. Data accesses should be fast. + */ + +struct _GsPluginRepos { + GsPlugin parent; + + /* These hash tables are replaced by a worker thread. They are immutable + * once set, and will only be replaced with a new hash table instance. + * This means they are safe to access from the refine function in the + * main thread with a strong reference and no lock. + * + * @mutex must be held when getting a strong reference to them, or + * replacing them. */ + GHashTable *fns; /* origin : filename */ + GHashTable *urls; /* origin : url */ + + GFileMonitor *monitor; + gchar *reposdir; + + GMutex mutex; + + /* Used to cancel a pending update operation which is loading the repos + * data in a worker thread. */ + GCancellable *update_cancellable; /* (nullable) (owned) */ +}; + +G_DEFINE_TYPE (GsPluginRepos, gs_plugin_repos, GS_TYPE_PLUGIN) + +static void +gs_plugin_repos_init (GsPluginRepos *self) +{ + GsPlugin *plugin = GS_PLUGIN (self); + + g_mutex_init (&self->mutex); + + /* for debugging and the self tests */ + self->reposdir = g_strdup (g_getenv ("GS_SELF_TEST_REPOS_DIR")); + if (self->reposdir == NULL) + self->reposdir = g_strdup ("/etc/yum.repos.d"); + + /* plugin only makes sense if this exists at startup */ + if (!g_file_test (self->reposdir, G_FILE_TEST_EXISTS)) { + gs_plugin_set_enabled (plugin, FALSE); + return; + } +} + +static void +gs_plugin_repos_dispose (GObject *object) +{ + GsPluginRepos *self = GS_PLUGIN_REPOS (object); + + g_cancellable_cancel (self->update_cancellable); + g_clear_object (&self->update_cancellable); + g_clear_pointer (&self->reposdir, g_free); + g_clear_pointer (&self->fns, g_hash_table_unref); + g_clear_pointer (&self->urls, g_hash_table_unref); + g_clear_object (&self->monitor); + + G_OBJECT_CLASS (gs_plugin_repos_parent_class)->dispose (object); +} + +static void +gs_plugin_repos_finalize (GObject *object) +{ + GsPluginRepos *self = GS_PLUGIN_REPOS (object); + + g_mutex_clear (&self->mutex); + + G_OBJECT_CLASS (gs_plugin_repos_parent_class)->finalize (object); +} + +/* Run in a worker thread; will take the mutex */ +static gboolean +gs_plugin_repos_load (GsPluginRepos *self, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GDir) dir = NULL; + const gchar *fn; + g_autoptr(GHashTable) new_filenames = NULL; + g_autoptr(GHashTable) new_urls = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + new_filenames = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + new_urls = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + /* search all files */ + dir = g_dir_open (self->reposdir, 0, error); + if (dir == NULL) { + gs_utils_error_convert_gio (error); + return FALSE; + } + while ((fn = g_dir_read_name (dir)) != NULL) { + g_autofree gchar *filename = NULL; + g_auto(GStrv) groups = NULL; + g_autoptr(GKeyFile) kf = g_key_file_new (); + guint i; + + /* not a repo */ + if (!g_str_has_suffix (fn, ".repo")) + continue; + + /* load file */ + filename = g_build_filename (self->reposdir, fn, NULL); + if (!g_key_file_load_from_file (kf, filename, + G_KEY_FILE_NONE, + error)) { + gs_utils_error_convert_gio (error); + return FALSE; + } + + /* we can have multiple repos in one file */ + groups = g_key_file_get_groups (kf, NULL); + for (i = 0; groups[i] != NULL; i++) { + g_autofree gchar *baseurl = NULL, *metalink = NULL; + + g_hash_table_insert (new_filenames, + g_strdup (groups[i]), + g_strdup (filename)); + + baseurl = g_key_file_get_string (kf, groups[i], "baseurl", NULL); + if (baseurl != NULL) { + g_hash_table_insert (new_urls, + g_strdup (groups[i]), + g_steal_pointer (&baseurl)); + continue; + } + + metalink = g_key_file_get_string (kf, groups[i], "metalink", NULL); + if (metalink != NULL) { + g_hash_table_insert (new_urls, + g_strdup (groups[i]), + g_steal_pointer (&metalink)); + continue; + } + } + } + + /* success; replace the hash table pointers in the object while the lock + * is held */ + locker = g_mutex_locker_new (&self->mutex); + + g_clear_pointer (&self->fns, g_hash_table_unref); + self->fns = g_steal_pointer (&new_filenames); + g_clear_pointer (&self->urls, g_hash_table_unref); + self->urls = g_steal_pointer (&new_urls); + + g_assert (self->fns != NULL && self->urls != NULL); + + return TRUE; +} + +/* Run in a worker thread. */ +static void +update_repos_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginRepos *self = GS_PLUGIN_REPOS (source_object); + g_autoptr(GError) local_error = NULL; + + if (!gs_plugin_repos_load (self, cancellable, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +/* Run in the main thread. */ +static void +gs_plugin_repos_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + GsPluginRepos *self = GS_PLUGIN_REPOS (user_data); + g_autoptr(GTask) task = NULL; + + /* Cancel any pending updates and schedule a new update of the repo data + * in a worker thread. */ + g_cancellable_cancel (self->update_cancellable); + g_clear_object (&self->update_cancellable); + self->update_cancellable = g_cancellable_new (); + + task = g_task_new (self, self->update_cancellable, NULL, NULL); + g_task_set_source_tag (task, gs_plugin_repos_changed_cb); + g_task_run_in_thread (task, update_repos_thread_cb); +} + +static void +gs_plugin_repos_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRepos *self = GS_PLUGIN_REPOS (plugin); + g_autoptr(GFile) file = g_file_new_for_path (self->reposdir); + 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_repos_setup_async); + + /* watch for changes in the main thread */ + self->monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, cancellable, &local_error); + if (self->monitor == NULL) { + gs_utils_error_convert_gio (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_signal_connect (self->monitor, "changed", + G_CALLBACK (gs_plugin_repos_changed_cb), self); + + /* Set up the repos at startup. */ + g_task_run_in_thread (task, update_repos_thread_cb); +} + +static gboolean +gs_plugin_repos_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_repos_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRepos *self = GS_PLUGIN_REPOS (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_repos_shutdown_async); + + /* Cancel any ongoing update operations. */ + g_cancellable_cancel (self->update_cancellable); + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_repos_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +refine_app (GsApp *app, + GsPluginRefineFlags flags, + GHashTable *filenames, + GHashTable *urls) +{ + const gchar *tmp; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) == 0) + return; + if (gs_app_get_origin_hostname (app) != NULL) + return; + + /* make sure we don't end up refining flatpak repos */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE) + return; + + /* find hostname */ + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_REPOSITORY: + if (gs_app_get_id (app) == NULL) + return; + tmp = g_hash_table_lookup (urls, gs_app_get_id (app)); + if (tmp != NULL) + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, tmp); + break; + default: + if (gs_app_get_origin (app) == NULL) + return; + tmp = g_hash_table_lookup (urls, gs_app_get_origin (app)); + if (tmp != NULL) + gs_app_set_origin_hostname (app, tmp); + else { + GHashTableIter iter; + gpointer key, value; + const gchar *origin; + + origin = gs_app_get_origin (app); + + /* Some repos, such as rpmfusion, can have set the name with a distribution + number in the appstream file, thus check those specifically */ + g_hash_table_iter_init (&iter, urls); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if (g_str_has_prefix (origin, key)) { + const gchar *rest = origin + strlen (key); + while (*rest == '-' || (*rest >= '0' && *rest <= '9')) + rest++; + if (!*rest) { + gs_app_set_origin_hostname (app, value); + break; + } + } + } + } + break; + } + + /* find filename */ + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_REPOSITORY: + if (gs_app_get_id (app) == NULL) + return; + tmp = g_hash_table_lookup (filenames, gs_app_get_id (app)); + if (tmp != NULL) + gs_app_set_metadata (app, "repos::repo-filename", tmp); + break; + default: + break; + } +} + +static void +gs_plugin_repos_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRepos *self = GS_PLUGIN_REPOS (plugin); + g_autoptr(GHashTable) filenames = NULL; /* (element-type utf8 filename) mapping origin to filename */ + g_autoptr(GHashTable) urls = NULL; /* (element-type utf8 utf8) mapping origin to URL */ + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GTask) task = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_repos_refine_async); + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) == 0) { + g_task_return_boolean (task, TRUE); + return; + } + + /* Grab a reference to the object’s state so it can be accessed without + * holding the lock throughout, to keep the critical section small. */ + locker = g_mutex_locker_new (&self->mutex); + filenames = g_hash_table_ref (self->fns); + urls = g_hash_table_ref (self->urls); + g_clear_pointer (&locker, g_mutex_locker_free); + + /* Update each of the apps */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + refine_app (app, flags, filenames, urls); + } + + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_repos_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_repos_class_init (GsPluginReposClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_repos_dispose; + object_class->finalize = gs_plugin_repos_finalize; + + plugin_class->setup_async = gs_plugin_repos_setup_async; + plugin_class->setup_finish = gs_plugin_repos_setup_finish; + plugin_class->shutdown_async = gs_plugin_repos_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_repos_shutdown_finish; + plugin_class->refine_async = gs_plugin_repos_refine_async; + plugin_class->refine_finish = gs_plugin_repos_refine_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_REPOS; +} diff --git a/plugins/repos/gs-plugin-repos.h b/plugins/repos/gs-plugin-repos.h new file mode 100644 index 0000000..36ceeed --- /dev/null +++ b/plugins/repos/gs-plugin-repos.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_REPOS (gs_plugin_repos_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginRepos, gs_plugin_repos, GS, PLUGIN_REPOS, GsPlugin) + +G_END_DECLS diff --git a/plugins/repos/gs-self-test.c b/plugins/repos/gs-self-test.c new file mode 100644 index 0000000..5a0d108 --- /dev/null +++ b/plugins/repos/gs-self-test.c @@ -0,0 +1,71 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static void +gs_plugins_repos_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* get the extra bits */ + app = gs_app_new ("testrepos.desktop"); + gs_app_set_origin (app, "utopia"); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + plugin_job = gs_plugin_job_refine_new_for_app (app, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "people.freedesktop.org"); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autofree gchar *reposdir = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar * const allowlist[] = { + "repos", + NULL + }; + + gs_test_init (&argc, &argv); + + /* dummy data */ + reposdir = gs_test_get_filename (TESTDATADIR, "yum.repos.d"); + g_assert (reposdir != NULL); + g_setenv ("GS_SELF_TEST_REPOS_DIR", reposdir, TRUE); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/repos", + plugin_loader, + (GTestDataFunc) gs_plugins_repos_func); + + return g_test_run (); +} diff --git a/plugins/repos/meson.build b/plugins/repos/meson.build new file mode 100644 index 0000000..81172d2 --- /dev/null +++ b/plugins/repos/meson.build @@ -0,0 +1,35 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginRepos"'] +cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + +shared_module( + 'gs_plugin_repos', + sources : 'gs-plugin-repos.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-repos', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + ], + c_args : cargs, + ) + test('gs-self-test-repos', e, suite: ['plugins', 'repos'], env: test_env) +endif diff --git a/plugins/repos/tests/yum.repos.d/utopia.repo b/plugins/repos/tests/yum.repos.d/utopia.repo new file mode 100644 index 0000000..e912ec4 --- /dev/null +++ b/plugins/repos/tests/yum.repos.d/utopia.repo @@ -0,0 +1,5 @@ +[utopia] +name=utopia for Fedora $releasever +baseurl=http://people.freedesktop.org/~hughsient/fedora/$releasever/x86_64/ +enabled=1 +gpgcheck=0 diff --git a/plugins/rpm-ostree/gs-plugin-rpm-ostree.c b/plugins/rpm-ostree/gs-plugin-rpm-ostree.c new file mode 100644 index 0000000..b983c24 --- /dev/null +++ b/plugins/rpm-ostree/gs-plugin-rpm-ostree.c @@ -0,0 +1,2878 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +#include <fcntl.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> +#include <glib/gstdio.h> +#include <glib/gi18n-lib.h> +#include <libdnf/libdnf.h> +#include <ostree.h> +#include <rpm/rpmdb.h> +#include <rpm/rpmlib.h> +#include <rpm/rpmts.h> +#include <rpmostree.h> + +#include "gs-plugin-private.h" +#include "gs-plugin-rpm-ostree.h" +#include "gs-rpmostree-generated.h" + +/* + * SECTION: + * Exposes rpm-ostree system updates and overlays. + * + * The plugin has a worker thread which all operations are delegated to, as + * while the rpm-ostreed API is asynchronous over D-Bus, the plugin also needs + * to use lower level libostree and libdnf APIs which are entirely synchronous. + * Message passing to the worker thread is by gs_worker_thread_queue(). + */ + +/* This shows up in the `rpm-ostree status` as the software that + * initiated the update. + */ +#define GS_RPMOSTREE_CLIENT_ID PACKAGE_NAME + +/* How long to wait between two consecutive requests, before considering + * the connection to the rpm-ostree daemon inactive and disconnect from it. + */ +#define INACTIVE_TIMEOUT_SECONDS 60 + +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(Header, headerFree, NULL) +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(rpmts, rpmtsFree, NULL); +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(rpmdbMatchIterator, rpmdbFreeIterator, NULL); + +struct _GsPluginRpmOstree { + GsPlugin parent; + + GsWorkerThread *worker; /* (owned) */ + + GMutex mutex; + GsRPMOSTreeOS *os_proxy; + GsRPMOSTreeSysroot *sysroot_proxy; + OstreeRepo *ot_repo; + OstreeSysroot *ot_sysroot; + DnfContext *dnf_context; + gboolean update_triggered; + guint inactive_timeout_id; +}; + +G_DEFINE_TYPE (GsPluginRpmOstree, gs_plugin_rpm_ostree, GS_TYPE_PLUGIN) + +#define assert_in_worker(self) \ + g_assert (gs_worker_thread_is_in_worker_context (self->worker)) + +static void +gs_plugin_rpm_ostree_dispose (GObject *object) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (object); + + g_clear_handle_id (&self->inactive_timeout_id, g_source_remove); + g_clear_object (&self->os_proxy); + g_clear_object (&self->sysroot_proxy); + g_clear_object (&self->ot_sysroot); + g_clear_object (&self->ot_repo); + g_clear_object (&self->dnf_context); + g_clear_object (&self->worker); + + G_OBJECT_CLASS (gs_plugin_rpm_ostree_parent_class)->dispose (object); +} + +static void +gs_plugin_rpm_ostree_finalize (GObject *object) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (object); + + g_mutex_clear (&self->mutex); + + G_OBJECT_CLASS (gs_plugin_rpm_ostree_parent_class)->finalize (object); +} + +static void +gs_plugin_rpm_ostree_init (GsPluginRpmOstree *self) +{ + /* only works on OSTree */ + if (!g_file_test ("/run/ostree-booted", G_FILE_TEST_EXISTS)) { + gs_plugin_set_enabled (GS_PLUGIN (self), FALSE); + return; + } + + g_mutex_init (&self->mutex); + + /* open transaction */ + rpmReadConfigFiles (NULL, NULL); + + /* rpm-ostree is already a daemon with a DBus API; hence it makes + * more sense to use a custom plugin instead of using PackageKit. + */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_CONFLICTS, "packagekit"); + + /* need pkgname */ + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +static void +gs_rpmostree_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* parse remote RPM_OSTREED_ERROR */ + if (g_dbus_error_is_remote_error (error)) { + g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); + + g_dbus_error_strip_remote_error (error); + + if (g_strcmp0 (remote_error, "org.projectatomic.rpmostreed.Error.NotAuthorized") == 0) { + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + } else if (g_str_has_prefix (remote_error, "org.projectatomic.rpmostreed.Error")) { + error->code = GS_PLUGIN_ERROR_FAILED; + } else { + g_warning ("can't reliably fixup remote error %s", remote_error); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; + return; + } + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gdbus (perror)) + return; +} + +static void +gs_rpmostree_unregister_client_done_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + + if (!gs_rpmostree_sysroot_call_unregister_client_finish (GS_RPMOSTREE_SYSROOT (source_object), result, &error)) + g_debug ("Failed to unregister client: %s", error->message); + else + g_debug ("Unregistered client from the rpm-ostreed"); +} + +static gboolean +gs_rpmostree_inactive_timeout_cb (gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (user_data); + g_autoptr(GMutexLocker) locker = NULL; + + if (g_source_is_destroyed (g_main_current_source ())) + return G_SOURCE_REMOVE; + + locker = g_mutex_locker_new (&self->mutex); + + /* In case it gets destroyed before the lock is acquired */ + if (!g_source_is_destroyed (g_main_current_source ()) && + self->inactive_timeout_id == g_source_get_id (g_main_current_source ())) { + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + + if (self->sysroot_proxy) + sysroot_proxy = g_steal_pointer (&self->sysroot_proxy); + + g_clear_object (&self->os_proxy); + g_clear_object (&self->sysroot_proxy); + g_clear_object (&self->ot_sysroot); + g_clear_object (&self->ot_repo); + g_clear_object (&self->dnf_context); + self->inactive_timeout_id = 0; + + g_clear_pointer (&locker, g_mutex_locker_free); + + if (sysroot_proxy) { + g_autoptr(GVariantBuilder) options_builder = NULL; + options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (options_builder, "{sv}", "id", + g_variant_new_string (GS_RPMOSTREE_CLIENT_ID)); + gs_rpmostree_sysroot_call_unregister_client (sysroot_proxy, + g_variant_builder_end (options_builder), + NULL, + gs_rpmostree_unregister_client_done_cb, + NULL); + } + } + + return G_SOURCE_REMOVE; +} + +/* Hold the plugin mutex when called */ +static gboolean +gs_rpmostree_ref_proxies_locked (GsPluginRpmOstree *self, + GsRPMOSTreeOS **out_os_proxy, + GsRPMOSTreeSysroot **out_sysroot_proxy, + GCancellable *cancellable, + GError **error) +{ + if (self->inactive_timeout_id) { + g_source_remove (self->inactive_timeout_id); + self->inactive_timeout_id = 0; + } + + /* Create a proxy for sysroot */ + if (self->sysroot_proxy == NULL) { + g_autoptr(GVariantBuilder) options_builder = NULL; + + self->sysroot_proxy = gs_rpmostree_sysroot_proxy_new_sync (gs_plugin_get_system_bus_connection (GS_PLUGIN (self)), + G_DBUS_PROXY_FLAGS_NONE, + "org.projectatomic.rpmostree1", + "/org/projectatomic/rpmostree1/Sysroot", + cancellable, + error); + if (self->sysroot_proxy == NULL) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (options_builder, "{sv}", "id", + g_variant_new_string (GS_RPMOSTREE_CLIENT_ID)); + /* Register as a client so that the rpm-ostree daemon doesn't exit */ + if (!gs_rpmostree_sysroot_call_register_client_sync (self->sysroot_proxy, + g_variant_builder_end (options_builder), + cancellable, + error)) { + g_clear_object (&self->sysroot_proxy); + gs_rpmostree_error_convert (error); + return FALSE; + } + + g_debug ("Registered client on the rpm-ostreed"); + } + + /* Create a proxy for currently booted OS */ + if (self->os_proxy == NULL) { + g_autofree gchar *os_object_path = NULL; + + os_object_path = gs_rpmostree_sysroot_dup_booted (self->sysroot_proxy); + if (os_object_path == NULL && + !gs_rpmostree_sysroot_call_get_os_sync (self->sysroot_proxy, + "", + &os_object_path, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + g_clear_object (&self->sysroot_proxy); + return FALSE; + } + + self->os_proxy = gs_rpmostree_os_proxy_new_sync (gs_plugin_get_system_bus_connection (GS_PLUGIN (self)), + G_DBUS_PROXY_FLAGS_NONE, + "org.projectatomic.rpmostree1", + os_object_path, + cancellable, + error); + if (self->os_proxy == NULL) { + gs_rpmostree_error_convert (error); + g_clear_object (&self->sysroot_proxy); + return FALSE; + } + } + + /* Load ostree sysroot and repo */ + if (self->ot_sysroot == NULL) { + g_autofree gchar *sysroot_path = NULL; + g_autoptr(GFile) sysroot_file = NULL; + + sysroot_path = gs_rpmostree_sysroot_dup_path (self->sysroot_proxy); + sysroot_file = g_file_new_for_path (sysroot_path); + + self->ot_sysroot = ostree_sysroot_new (sysroot_file); + if (!ostree_sysroot_load (self->ot_sysroot, cancellable, error)) { + gs_rpmostree_error_convert (error); + g_clear_object (&self->sysroot_proxy); + g_clear_object (&self->os_proxy); + g_clear_object (&self->ot_sysroot); + return FALSE; + } + + if (!ostree_sysroot_get_repo (self->ot_sysroot, &self->ot_repo, cancellable, error)) { + gs_rpmostree_error_convert (error); + g_clear_object (&self->sysroot_proxy); + g_clear_object (&self->os_proxy); + g_clear_object (&self->ot_sysroot); + return FALSE; + } + } + + self->inactive_timeout_id = g_timeout_add_seconds (INACTIVE_TIMEOUT_SECONDS, + gs_rpmostree_inactive_timeout_cb, self); + + if (out_os_proxy) + *out_os_proxy = g_object_ref (self->os_proxy); + + if (out_sysroot_proxy) + *out_sysroot_proxy = g_object_ref (self->sysroot_proxy); + + return TRUE; +} + +static gboolean +gs_rpmostree_ref_proxies (GsPluginRpmOstree *self, + GsRPMOSTreeOS **out_os_proxy, + GsRPMOSTreeSysroot **out_sysroot_proxy, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GMutexLocker) locker = NULL; + + locker = g_mutex_locker_new (&self->mutex); + + return gs_rpmostree_ref_proxies_locked (self, out_os_proxy, out_sysroot_proxy, cancellable, error); +} + +static gint +get_priority_for_interactivity (gboolean interactive) +{ + return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW; +} + +static void setup_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_rpm_ostree_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GTask) task = NULL; + + g_debug ("rpm-ostree version: %s", RPM_OSTREE_VERSION_S); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rpm_ostree_setup_async); + + /* Start up a worker thread to process all the plugin’s function calls. */ + self->worker = gs_worker_thread_new ("gs-plugin-rpm-ostree"); + + /* Queue a job to set up the D-Bus proxies. While these could be set + * up from the main thread asynchronously, setting them up in the worker + * thread means their signal emissions will correctly be in the worker + * thread, and locking is simpler. */ + gs_worker_thread_queue (self->worker, G_PRIORITY_DEFAULT, + setup_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +setup_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (source_object); + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (!gs_rpmostree_ref_proxies (self, NULL, NULL, cancellable, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_rpm_ostree_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_rpm_ostree_shutdown_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rpm_ostree_shutdown_async); + + /* Stop checking for inactivity. */ + g_clear_handle_id (&self->inactive_timeout_id, g_source_remove); + + /* Stop the worker thread. */ + gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task)); +} + +static void +shutdown_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginRpmOstree *self = g_task_get_source_object (task); + g_autoptr(GsWorkerThread) worker = NULL; + g_autoptr(GError) local_error = NULL; + + worker = g_steal_pointer (&self->worker); + + if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_rpm_ostree_shutdown_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +app_set_rpm_ostree_packaging_format (GsApp *app) +{ + gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "RPM"); + gs_app_set_metadata (app, "GnomeSoftware::PackagingBaseCssColor", "error_color"); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE && + gs_app_get_scope (app) == AS_COMPONENT_SCOPE_SYSTEM) { + gs_app_set_management_plugin (app, plugin); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + } + + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_OPERATING_SYSTEM) { + gs_app_set_management_plugin (app, plugin); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + } +} + +typedef struct { + GsPlugin *plugin; + GError *error; + GMainContext *context; + GsApp *app; + gboolean complete; + gboolean owner_changed; +} TransactionProgress; + +static TransactionProgress * +transaction_progress_new (void) +{ + TransactionProgress *self; + + self = g_slice_new0 (TransactionProgress); + self->context = g_main_context_ref_thread_default (); + + return self; +} + +static void +transaction_progress_free (TransactionProgress *self) +{ + g_clear_object (&self->plugin); + g_clear_error (&self->error); + g_main_context_unref (self->context); + g_clear_object (&self->app); + g_slice_free (TransactionProgress, self); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(TransactionProgress, transaction_progress_free); + +static void +transaction_progress_end (TransactionProgress *self) +{ + self->complete = TRUE; + g_main_context_wakeup (self->context); +} + +static void +on_transaction_progress (GDBusProxy *proxy, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + TransactionProgress *tp = user_data; + + if (g_strcmp0 (signal_name, "PercentProgress") == 0) { + const gchar *message = NULL; + guint32 percentage; + + g_variant_get_child (parameters, 0, "&s", &message); + g_variant_get_child (parameters, 1, "u", &percentage); + g_debug ("PercentProgress: %u, %s\n", percentage, message); + + if (tp->app != NULL) + gs_app_set_progress (tp->app, (guint) percentage); + + if (tp->app != NULL && tp->plugin != NULL) { + GsPluginStatus plugin_status; + + switch (gs_app_get_state (tp->app)) { + case GS_APP_STATE_INSTALLING: + plugin_status = GS_PLUGIN_STATUS_INSTALLING; + break; + case GS_APP_STATE_REMOVING: + plugin_status = GS_PLUGIN_STATUS_REMOVING; + break; + default: + plugin_status = GS_PLUGIN_STATUS_DOWNLOADING; + break; + } + gs_plugin_status_update (tp->plugin, tp->app, plugin_status); + } + } else if (g_strcmp0 (signal_name, "Finished") == 0) { + if (tp->error == NULL) { + g_autofree gchar *error_message = NULL; + gboolean success = FALSE; + + g_variant_get (parameters, "(bs)", &success, &error_message); + + if (!success) { + tp->error = g_dbus_error_new_for_dbus_error ("org.projectatomic.rpmostreed.Error.Failed", + error_message); + } + } + + transaction_progress_end (tp); + } +} + +static void +on_owner_notify (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + TransactionProgress *tp = user_data; + + tp->owner_changed = TRUE; + + /* Wake up the context so it can notice the server has disappeared. */ + g_main_context_wakeup (tp->context); +} + +static void +cancelled_handler (GCancellable *cancellable, + gpointer user_data) +{ + GsRPMOSTreeTransaction *transaction = user_data; + gs_rpmostree_transaction_call_cancel_sync (transaction, NULL, NULL); +} + +static gboolean +gs_rpmostree_transaction_get_response_sync (GsRPMOSTreeSysroot *sysroot_proxy, + const gchar *transaction_address, + TransactionProgress *tp, + GCancellable *cancellable, + GError **error) +{ + GsRPMOSTreeTransaction *transaction = NULL; + g_autoptr(GDBusConnection) peer_connection = NULL; + gint cancel_handler = 0; + gulong signal_handler = 0; + gulong notify_handler = 0; + gboolean success = FALSE; + gboolean just_started = FALSE; + gboolean saw_name_owner = FALSE; + g_autofree gchar *name_owner = NULL; + + peer_connection = g_dbus_connection_new_for_address_sync (transaction_address, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, + NULL, + cancellable, + error); + + if (peer_connection == NULL) + goto out; + + transaction = gs_rpmostree_transaction_proxy_new_sync (peer_connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "/", + cancellable, + error); + if (transaction == NULL) + goto out; + + if (cancellable) { + /* setup cancel handler */ + cancel_handler = g_cancellable_connect (cancellable, + G_CALLBACK (cancelled_handler), + transaction, NULL); + } + + signal_handler = g_signal_connect (transaction, "g-signal", + G_CALLBACK (on_transaction_progress), + tp); + + notify_handler = g_signal_connect (transaction, "notify::g-name-owner", + G_CALLBACK (on_owner_notify), + tp); + + /* Tell the server we're ready to receive signals. */ + if (!gs_rpmostree_transaction_call_start_sync (transaction, + &just_started, + cancellable, + error)) + goto out; + + /* Process all the signals until we receive the Finished signal or the + * daemon disappears (which can happen if it crashes). + * + * The property can be NULL right after connecting to it, before the D-Bus + * transfers the property value to the client. */ + while (!tp->complete && + !g_cancellable_is_cancelled (cancellable) && + ((name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (transaction))) != NULL || + (!saw_name_owner && !tp->owner_changed))) { + saw_name_owner = saw_name_owner || name_owner != NULL; + g_clear_pointer (&name_owner, g_free); + g_main_context_iteration (tp->context, TRUE); + } + + if (!g_cancellable_set_error_if_cancelled (cancellable, error)) { + if (tp->error) { + g_propagate_error (error, g_steal_pointer (&tp->error)); + } else if (!tp->complete && name_owner == NULL) { + g_set_error_literal (error, G_DBUS_ERROR, G_DBUS_ERROR_NO_REPLY, + "Daemon disappeared"); + } else { + success = TRUE; + } + } + +out: + if (cancel_handler) + g_cancellable_disconnect (cancellable, cancel_handler); + if (notify_handler != 0) + g_signal_handler_disconnect (transaction, notify_handler); + if (signal_handler) + g_signal_handler_disconnect (transaction, signal_handler); + if (transaction != NULL) + g_object_unref (transaction); + + return success; +} + +/* FIXME: Refactor this once rpmostree returns a specific error code + * for ‘transaction in progress’, to avoid the slight race here where + * gnome-software could return from this function just as another client + * starts a new transaction. + * https://github.com/coreos/rpm-ostree/issues/3070 */ +static gboolean +gs_rpmostree_wait_for_ongoing_transaction_end (GsRPMOSTreeSysroot *sysroot_proxy, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *current_path = NULL; + g_autoptr(GMainContext) main_context = NULL; + gulong notify_handler, cancelled_handler = 0; + + current_path = gs_rpmostree_sysroot_dup_active_transaction_path (sysroot_proxy); + if (current_path == NULL || *current_path == '\0') + return TRUE; + + main_context = g_main_context_ref_thread_default (); + + notify_handler = g_signal_connect_swapped (sysroot_proxy, "notify::active-transaction-path", + G_CALLBACK (g_main_context_wakeup), main_context); + if (cancellable) { + /* Not using g_cancellable_connect() here for simplicity and because checking the state below anyway. */ + cancelled_handler = g_signal_connect_swapped (cancellable, "cancelled", + G_CALLBACK (g_main_context_wakeup), main_context); + } + + while (!g_cancellable_set_error_if_cancelled (cancellable, error)) { + g_clear_pointer (¤t_path, g_free); + current_path = gs_rpmostree_sysroot_dup_active_transaction_path (sysroot_proxy); + if (current_path == NULL || *current_path == '\0') { + g_clear_signal_handler (¬ify_handler, sysroot_proxy); + g_clear_signal_handler (&cancelled_handler, cancellable); + return TRUE; + } + g_main_context_iteration (main_context, TRUE); + } + + g_clear_signal_handler (¬ify_handler, sysroot_proxy); + g_clear_signal_handler (&cancelled_handler, cancellable); + + gs_rpmostree_error_convert (error); + + return FALSE; +} + +static GsApp * +app_from_modified_pkg_variant (GsPlugin *plugin, GVariant *variant) +{ + g_autoptr(GsApp) app = NULL; + const char *name; + const char *old_evr, *old_arch; + const char *new_evr, *new_arch; + g_autofree char *old_nevra = NULL; + g_autofree char *new_nevra = NULL; + + g_variant_get (variant, "(us(ss)(ss))", NULL /* type*/, &name, &old_evr, &old_arch, &new_evr, &new_arch); + old_nevra = g_strdup_printf ("%s-%s-%s", name, old_evr, old_arch); + new_nevra = g_strdup_printf ("%s-%s-%s", name, new_evr, new_arch); + + app = gs_plugin_cache_lookup (plugin, old_nevra); + if (app != NULL) + return g_steal_pointer (&app); + + /* create new app */ + app = gs_app_new (NULL); + gs_app_set_management_plugin (app, plugin); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + + /* update or downgrade */ + gs_app_add_source (app, name); + gs_app_set_version (app, old_evr); + gs_app_set_update_version (app, new_evr); + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + + g_debug ("!%s\n", old_nevra); + g_debug ("=%s\n", new_nevra); + + gs_plugin_cache_add (plugin, old_nevra, app); + return g_steal_pointer (&app); +} + +static GsApp * +app_from_single_pkg_variant (GsPlugin *plugin, GVariant *variant, gboolean addition) +{ + g_autoptr(GsApp) app = NULL; + const char *name; + const char *evr; + const char *arch; + g_autofree char *nevra = NULL; + + g_variant_get (variant, "(usss)", NULL /* type*/, &name, &evr, &arch); + nevra = g_strdup_printf ("%s-%s-%s", name, evr, arch); + + app = gs_plugin_cache_lookup (plugin, nevra); + if (app != NULL) + return g_steal_pointer (&app); + + /* create new app */ + app = gs_app_new (NULL); + gs_app_set_management_plugin (app, plugin); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + + if (addition) { + /* addition */ + gs_app_add_source (app, name); + gs_app_set_version (app, evr); + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + + g_debug ("+%s\n", nevra); + } else { + /* removal */ + gs_app_add_source (app, name); + gs_app_set_version (app, evr); + gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE); + + g_debug ("-%s\n", nevra); + } + + gs_plugin_cache_add (plugin, nevra, app); + return g_steal_pointer (&app); +} + +static GVariant * +make_rpmostree_options_variant (gboolean reboot, + gboolean allow_downgrade, + gboolean cache_only, + gboolean download_only, + gboolean skip_purge, + gboolean no_pull_base, + gboolean dry_run, + gboolean no_overrides) +{ + GVariantDict dict; + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "reboot", "b", reboot); + g_variant_dict_insert (&dict, "allow-downgrade", "b", allow_downgrade); + g_variant_dict_insert (&dict, "cache-only", "b", cache_only); + g_variant_dict_insert (&dict, "download-only", "b", download_only); + g_variant_dict_insert (&dict, "skip-purge", "b", skip_purge); + g_variant_dict_insert (&dict, "no-pull-base", "b", no_pull_base); + g_variant_dict_insert (&dict, "dry-run", "b", dry_run); + g_variant_dict_insert (&dict, "no-overrides", "b", no_overrides); + return g_variant_ref_sink (g_variant_dict_end (&dict)); +} + +static GVariant * +make_refresh_md_options_variant (gboolean force) +{ + GVariantDict dict; + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "force", "b", force); + return g_variant_ref_sink (g_variant_dict_end (&dict)); +} + +static gboolean +make_rpmostree_modifiers_variant (const char *install_package, + const char *uninstall_package, + const char *install_local_package, + GVariant **out_modifiers, + GUnixFDList **out_fd_list, + GError **error) +{ + GVariantDict dict; + g_autoptr(GUnixFDList) fd_list = g_unix_fd_list_new (); + + g_variant_dict_init (&dict, NULL); + + if (install_package != NULL) { + g_autoptr(GPtrArray) repo_pkgs = g_ptr_array_new (); + + g_ptr_array_add (repo_pkgs, (gpointer) install_package); + + g_variant_dict_insert_value (&dict, "install-packages", + g_variant_new_strv ((const char *const*)repo_pkgs->pdata, + repo_pkgs->len)); + + } + + if (uninstall_package != NULL) { + g_autoptr(GPtrArray) repo_pkgs = g_ptr_array_new (); + + g_ptr_array_add (repo_pkgs, (gpointer) uninstall_package); + + g_variant_dict_insert_value (&dict, "uninstall-packages", + g_variant_new_strv ((const char *const*)repo_pkgs->pdata, + repo_pkgs->len)); + + } + + if (install_local_package != NULL) { + g_auto(GVariantBuilder) builder; + int fd; + int idx; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("ah")); + + fd = openat (AT_FDCWD, install_local_package, O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (fd == -1) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Failed to open %s", install_local_package); + return FALSE; + } + + idx = g_unix_fd_list_append (fd_list, fd, error); + if (idx < 0) { + close (fd); + return FALSE; + } + + g_variant_builder_add (&builder, "h", idx); + g_variant_dict_insert_value (&dict, "install-local-packages", + g_variant_new ("ah", &builder)); + close (fd); + } + + *out_fd_list = g_steal_pointer (&fd_list); + *out_modifiers = g_variant_ref_sink (g_variant_dict_end (&dict)); + return TRUE; +} + +static gboolean +rpmostree_update_deployment (GsRPMOSTreeOS *os_proxy, + const char *install_package, + const char *uninstall_package, + const char *install_local_package, + GVariant *options, + char **out_transaction_address, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) modifiers = NULL; + + if (!make_rpmostree_modifiers_variant (install_package, + uninstall_package, + install_local_package, + &modifiers, &fd_list, error)) + return FALSE; + + return gs_rpmostree_os_call_update_deployment_sync (os_proxy, + modifiers, + options, + fd_list, + out_transaction_address, + NULL, + cancellable, + error); +} + +#define RPMOSTREE_CORE_CACHEDIR "/var/cache/rpm-ostree/" +#define RPMOSTREE_DIR_CACHE_REPOMD "repomd" +#define RPMOSTREE_DIR_CACHE_SOLV "solv" + +static DnfContext * +gs_rpmostree_create_bare_dnf_context (GCancellable *cancellable, + GError **error) +{ + g_autoptr(DnfContext) context = dnf_context_new (); + + dnf_context_set_repo_dir (context, "/etc/yum.repos.d"); + dnf_context_set_cache_dir (context, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_REPOMD); + dnf_context_set_solv_dir (context, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_SOLV); + dnf_context_set_cache_age (context, G_MAXUINT); + dnf_context_set_enable_filelists (context, FALSE); + + if (!dnf_context_setup (context, cancellable, error)) { + gs_rpmostree_error_convert (error); + return NULL; + } + + return g_steal_pointer (&context); +} + +static gboolean +gs_rpmostree_ref_dnf_context_locked (GsPluginRpmOstree *self, + GsRPMOSTreeOS **out_os_proxy, + GsRPMOSTreeSysroot **out_sysroot_proxy, + DnfContext **out_dnf_context, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(DnfContext) context = NULL; + g_autoptr(DnfState) state = NULL; + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + + if (!gs_rpmostree_ref_proxies_locked (self, &os_proxy, &sysroot_proxy, cancellable, error)) + return FALSE; + + if (self->dnf_context != NULL) { + if (out_os_proxy) + *out_os_proxy = g_steal_pointer (&os_proxy); + if (out_sysroot_proxy) + *out_sysroot_proxy = g_steal_pointer (&sysroot_proxy); + if (out_dnf_context) + *out_dnf_context = g_object_ref (self->dnf_context); + return TRUE; + } + + context = gs_rpmostree_create_bare_dnf_context (cancellable, error); + if (!context) + return FALSE; + + state = dnf_state_new (); + + if (!dnf_context_setup_sack_with_flags (context, state, DNF_CONTEXT_SETUP_SACK_FLAG_SKIP_RPMDB, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + g_set_object (&self->dnf_context, context); + + if (out_os_proxy) + *out_os_proxy = g_steal_pointer (&os_proxy); + if (out_sysroot_proxy) + *out_sysroot_proxy = g_steal_pointer (&sysroot_proxy); + if (out_dnf_context) + *out_dnf_context = g_object_ref (self->dnf_context); + + return TRUE; +} + +static void refresh_metadata_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_rpm_ostree_refresh_metadata_async (GsPlugin *plugin, + guint64 cache_age_secs, + GsPluginRefreshMetadataFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE); + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rpm_ostree_refresh_metadata_async); + g_task_set_task_data (task, gs_plugin_refresh_metadata_data_new (cache_age_secs, flags), (GDestroyNotify) gs_plugin_refresh_metadata_data_free); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refresh_metadata_thread_cb, g_steal_pointer (&task)); +} + +static gboolean +gs_plugin_rpm_ostree_refresh_metadata_in_worker (GsPluginRpmOstree *self, + GsPluginRefreshMetadataData *data, + GsRPMOSTreeOS *os_proxy, + GsRPMOSTreeSysroot *sysroot_proxy, + GCancellable *cancellable, + GError **error) +{ + GsPlugin *plugin = GS_PLUGIN (self); + g_autoptr(GError) local_error = NULL; + gboolean done; + + assert_in_worker (self); + + { + g_autofree gchar *transaction_address = NULL; + g_autoptr(GsApp) progress_app = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = NULL; + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + progress_app = gs_app_new (gs_plugin_get_name (plugin)); + tp = transaction_progress_new (); + tp->app = g_object_ref (progress_app); + tp->plugin = g_object_ref (plugin); + + options = make_refresh_md_options_variant (FALSE /* force */); + done = FALSE; + while (!done) { + done = TRUE; + if (!gs_rpmostree_os_call_refresh_md_sync (os_proxy, + options, + &transaction_address, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + done = FALSE; + continue; + } + + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + if (data->cache_age_secs == G_MAXUINT64) + return TRUE; + + { + g_autofree gchar *transaction_address = NULL; + g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin)); + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + tp->app = g_object_ref (progress_app); + tp->plugin = g_object_ref (plugin); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + FALSE, /* cache-only */ + TRUE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + done = FALSE; + while (!done) { + done = TRUE; + if (!gs_rpmostree_os_call_upgrade_sync (os_proxy, + options, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + done = FALSE; + continue; + } + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + { + g_autofree gchar *transaction_address = NULL; + g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin)); + g_autoptr(GVariant) options = NULL; + GVariantDict dict; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + tp->app = g_object_ref (progress_app); + tp->plugin = g_object_ref (plugin); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "mode", "s", "check"); + options = g_variant_ref_sink (g_variant_dict_end (&dict)); + + done = FALSE; + while (!done) { + done = TRUE; + if (!gs_rpmostree_os_call_automatic_update_trigger_sync (os_proxy, + options, + NULL, + &transaction_address, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + done = FALSE; + continue; + } + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + /* update UI */ + gs_plugin_updates_changed (plugin); + + return TRUE; +} + +static void +refresh_metadata_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + GsPluginRefreshMetadataData *data = task_data; + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (gs_plugin_rpm_ostree_refresh_metadata_in_worker (self, data, os_proxy, sysroot_proxy, cancellable, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_rpm_ostree_refresh_metadata_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GVariant) cached_update = NULL; + g_autoptr(GVariant) rpm_diff = NULL; + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + const gchar *checksum = NULL; + const gchar *version = NULL; + g_auto(GVariantDict) cached_update_dict; + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, &local_error)) { + g_debug ("Failed to ref proxies to get updates: %s", local_error->message); + return TRUE; + } + + /* ensure D-Bus properties are updated before reading them */ + if (!gs_rpmostree_sysroot_call_reload_sync (sysroot_proxy, cancellable, &local_error)) { + g_debug ("Failed to call reload to get updates: %s", local_error->message); + return TRUE; + } + + cached_update = gs_rpmostree_os_dup_cached_update (os_proxy); + g_variant_dict_init (&cached_update_dict, cached_update); + + if (!g_variant_dict_lookup (&cached_update_dict, "checksum", "&s", &checksum)) + return TRUE; + if (!g_variant_dict_lookup (&cached_update_dict, "version", "&s", &version)) + return TRUE; + + g_debug ("got CachedUpdate version '%s', checksum '%s'", version, checksum); + + rpm_diff = g_variant_dict_lookup_value (&cached_update_dict, "rpm-diff", G_VARIANT_TYPE ("a{sv}")); + if (rpm_diff != NULL) { + GVariantIter iter; + GVariant *child; + g_autoptr(GVariant) upgraded = NULL; + g_autoptr(GVariant) downgraded = NULL; + g_autoptr(GVariant) removed = NULL; + g_autoptr(GVariant) added = NULL; + g_auto(GVariantDict) rpm_diff_dict; + g_variant_dict_init (&rpm_diff_dict, rpm_diff); + + upgraded = g_variant_dict_lookup_value (&rpm_diff_dict, "upgraded", G_VARIANT_TYPE ("a(us(ss)(ss))")); + if (upgraded == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'upgraded' in rpm-diff dict"); + return FALSE; + } + downgraded = g_variant_dict_lookup_value (&rpm_diff_dict, "downgraded", G_VARIANT_TYPE ("a(us(ss)(ss))")); + if (downgraded == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'downgraded' in rpm-diff dict"); + return FALSE; + } + removed = g_variant_dict_lookup_value (&rpm_diff_dict, "removed", G_VARIANT_TYPE ("a(usss)")); + if (removed == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'removed' in rpm-diff dict"); + return FALSE; + } + added = g_variant_dict_lookup_value (&rpm_diff_dict, "added", G_VARIANT_TYPE ("a(usss)")); + if (added == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_INVALID_FORMAT, + "no 'added' in rpm-diff dict"); + return FALSE; + } + + /* iterate over all upgraded packages and add them */ + g_variant_iter_init (&iter, upgraded); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_modified_pkg_variant (plugin, child); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + + /* iterate over all downgraded packages and add them */ + g_variant_iter_init (&iter, downgraded); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_modified_pkg_variant (plugin, child); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + + /* iterate over all removed packages and add them */ + g_variant_iter_init (&iter, removed); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_single_pkg_variant (plugin, child, FALSE); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + + /* iterate over all added packages and add them */ + g_variant_iter_init (&iter, added); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + g_autoptr(GsApp) app = app_from_single_pkg_variant (plugin, child, TRUE); + if (app != NULL) + gs_app_list_add (list, app); + g_variant_unref (child); + } + } + + return TRUE; +} + +static gboolean +trigger_rpmostree_update (GsPluginRpmOstree *self, + GsApp *app, + GsRPMOSTreeOS *os_proxy, + GsRPMOSTreeSysroot *sysroot_proxy, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + g_autoptr(GError) local_error = NULL; + gboolean done; + + /* if we can process this online do not require a trigger */ + if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE) + return TRUE; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, GS_PLUGIN (self))) + return TRUE; + + /* already in correct state */ + if (self->update_triggered) + return TRUE; + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + /* trigger the update */ + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + TRUE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + done = FALSE; + while (!done) { + done = TRUE; + if (!gs_rpmostree_os_call_upgrade_sync (os_proxy, + options, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + done = FALSE; + continue; + } + if (local_error) + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + self->update_triggered = TRUE; + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + GsAppList *related = gs_app_get_related (app); + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, error)) + return FALSE; + + /* we don't currently don't put all updates in the OsUpdate proxy app */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY)) + return trigger_rpmostree_update (self, app, os_proxy, sysroot_proxy, cancellable, error); + + /* try to trigger each related app */ + for (guint i = 0; i < gs_app_list_length (related); i++) { + GsApp *app_tmp = gs_app_list_index (related, i); + if (!trigger_rpmostree_update (self, app_tmp, os_proxy, sysroot_proxy, cancellable, error)) + return FALSE; + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_app_upgrade_trigger (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + const char *packages[] = { NULL }; + g_autofree gchar *new_refspec = NULL; + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + gboolean done; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* check is distro-upgrade */ + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_OPERATING_SYSTEM) + return TRUE; + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, error)) + return FALSE; + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + /* construct new refspec based on the distro version we're upgrading to */ + new_refspec = g_strdup_printf ("ostree://fedora/%s/x86_64/silverblue", + gs_app_get_version (app)); + + /* trigger the upgrade */ + options = make_rpmostree_options_variant (FALSE, /* reboot */ + TRUE, /* allow-downgrade */ + TRUE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + done = FALSE; + while (!done) { + done = TRUE; + if (!gs_rpmostree_os_call_rebase_sync (os_proxy, + options, + new_refspec, + packages, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + done = FALSE; + continue; + } + if (local_error) + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + + if (g_strrstr ((*error)->message, "Old and new refs are equal")) { + /* don't error out if the correct tree is already deployed */ + g_debug ("ignoring rpm-ostree error: %s", (*error)->message); + g_clear_error (error); + } else { + return FALSE; + } + } + + /* success */ + return TRUE; +} + +static gboolean +gs_rpmostree_repo_enable (GsPlugin *plugin, + GsApp *app, + gboolean enable, + GsRPMOSTreeOS *os_proxy, + GsRPMOSTreeSysroot *sysroot_proxy, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariantBuilder) options_builder = NULL; + g_autoptr(TransactionProgress) tp = NULL; + g_autoptr(GError) local_error = NULL; + gboolean done; + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + if (enable) + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + else + gs_app_set_state (app, GS_APP_STATE_REMOVING); + + done = FALSE; + while (!done) { + done = TRUE; + g_clear_pointer (&options_builder, g_variant_builder_unref); + options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{ss}")); + g_variant_builder_add (options_builder, "{ss}", "enabled", enable ? "1" : "0"); + if (!gs_rpmostree_os_call_modify_yum_repo_sync (os_proxy, + gs_app_get_id (app), + g_variant_builder_end (options_builder), + &transaction_address, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) { + gs_app_set_state_recover (app); + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + done = FALSE; + continue; + } + if (local_error) + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + } + + tp = transaction_progress_new (); + tp->app = g_object_ref (app); + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + gs_utils_error_add_origin_id (error, app); + return FALSE; + } + + + /* state is known */ + if (enable) + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + else + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + + gs_plugin_repository_changed (plugin, app); + + return TRUE; +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + const gchar *install_package = NULL; + g_autofree gchar *local_filename = NULL; + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + gboolean done; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* enable repo, handled by dedicated function */ + g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE); + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, error)) + return FALSE; + + switch (gs_app_get_state (app)) { + case GS_APP_STATE_AVAILABLE: + case GS_APP_STATE_QUEUED_FOR_INSTALL: + if (gs_app_get_source_default (app) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no source set"); + return FALSE; + } + + install_package = gs_app_get_source_default (app); + break; + case GS_APP_STATE_AVAILABLE_LOCAL: + if (gs_app_get_local_file (app) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "local package, but no filename"); + return FALSE; + } + + local_filename = g_file_get_path (gs_app_get_local_file (app)); + break; + default: + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "do not know how to install app in state %s", + gs_app_state_to_string (gs_app_get_state (app))); + return FALSE; + } + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + tp->app = g_object_ref (app); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + FALSE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + TRUE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + done = FALSE; + while (!done) { + done = TRUE; + if (!rpmostree_update_deployment (os_proxy, + install_package, + NULL /* remove package */, + local_filename, + options, + &transaction_address, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + done = FALSE; + continue; + } + if (local_error) + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + /* state is known */ + gs_app_set_state (app, GS_APP_STATE_PENDING_INSTALL); + + /* get the new icon from the package */ + gs_app_set_local_file (app, NULL); + gs_app_remove_all_icons (app); + + /* no longer valid */ + gs_app_clear_source_ids (app); + + return TRUE; +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + gboolean done; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, error)) + return FALSE; + + /* disable repo, handled by dedicated function */ + g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE); + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + gs_app_set_state (app, GS_APP_STATE_REMOVING); + tp->app = g_object_ref (app); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + FALSE, /* allow-downgrade */ + TRUE, /* cache-only */ + FALSE, /* download-only */ + FALSE, /* skip-purge */ + TRUE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + done = FALSE; + while (!done) { + done = TRUE; + if (!rpmostree_update_deployment (os_proxy, + NULL /* install package */, + gs_app_get_source_default (app), + NULL /* install local package */, + options, + &transaction_address, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + done = FALSE; + continue; + } + if (local_error) + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + + if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT)) { + gs_app_set_state (app, GS_APP_STATE_PENDING_REMOVE); + } else { + /* state is not known: we don't know if we can re-install this app */ + gs_app_set_state (app, GS_APP_STATE_UNKNOWN); + } + + return TRUE; +} + +static DnfPackage * +find_package_by_name (DnfSack *sack, + const char *pkgname) +{ + g_autoptr(GPtrArray) pkgs = NULL; + hy_autoquery HyQuery query = hy_query_create (sack); + + hy_query_filter (query, HY_PKG_NAME, HY_EQ, pkgname); + hy_query_filter_latest_per_arch (query, TRUE); + + pkgs = hy_query_run (query); + if (pkgs->len == 0) + return NULL; + + return g_object_ref (pkgs->pdata[pkgs->len-1]); +} + +static GPtrArray * +find_packages_by_provides (DnfSack *sack, + gchar **search) +{ + g_autoptr(GPtrArray) pkgs = NULL; + hy_autoquery HyQuery query = hy_query_create (sack); + + hy_query_filter_provides_in (query, search); + hy_query_filter_latest_per_arch (query, TRUE); + + pkgs = hy_query_run (query); + + return g_steal_pointer (&pkgs); +} + +static gboolean +gs_rpm_ostree_has_launchable (GsApp *app) +{ + const gchar *desktop_id; + GDesktopAppInfo *desktop_appinfo; + + if (gs_app_has_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE) || + gs_app_has_quirk (app, GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE)) + return FALSE; + + desktop_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID); + if (!desktop_id) + desktop_id = gs_app_get_id (app); + if (!desktop_id) + return FALSE; + + desktop_appinfo = gs_utils_get_desktop_app_info (desktop_id); + if (!desktop_appinfo) + return FALSE; + + return TRUE; +} + +static gboolean +resolve_installed_packages_app (GsPlugin *plugin, + GHashTable *packages, + GHashTable *layered_packages, + GHashTable *layered_local_packages, + GsApp *app) +{ + RpmOstreePackage *pkg; + + if (!gs_app_get_source_default (app)) + return FALSE; + + pkg = g_hash_table_lookup (packages, gs_app_get_source_default (app)); + + if (pkg) { + gs_app_set_version (app, rpm_ostree_package_get_evr (pkg)); + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) { + /* Kind of hack, pending installs do not have available the desktop file */ + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_DESKTOP_APP || gs_rpm_ostree_has_launchable (app)) + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + else + gs_app_set_state (app, GS_APP_STATE_PENDING_INSTALL); + } + if ((rpm_ostree_package_get_name (pkg) && + g_hash_table_contains (layered_packages, rpm_ostree_package_get_name (pkg))) || + (rpm_ostree_package_get_nevra (pkg) && + g_hash_table_contains (layered_local_packages, rpm_ostree_package_get_nevra (pkg)))) { + /* layered packages can always be removed */ + gs_app_remove_quirk (app, GS_APP_QUIRK_COMPULSORY); + } else { + /* can't remove packages that are part of the base system */ + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); + } + if (gs_app_get_origin (app) == NULL) + gs_app_set_origin (app, "rpm-ostree"); + if (gs_app_get_name (app) == NULL) + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, rpm_ostree_package_get_name (pkg)); + return TRUE /* found */; + } + + return FALSE /* not found */; +} + +static gboolean +resolve_available_packages_app (GsPlugin *plugin, + DnfSack *sack, + GsApp *app) +{ + g_autoptr(DnfPackage) pkg = NULL; + + pkg = find_package_by_name (sack, gs_app_get_source_default (app)); + if (pkg != NULL) { + gs_app_set_version (app, dnf_package_get_evr (pkg)); + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + + /* anything not part of the base system can be removed */ + gs_app_remove_quirk (app, GS_APP_QUIRK_COMPULSORY); + + /* set origin */ + if (gs_app_get_origin (app) == NULL) { + const gchar *reponame = dnf_package_get_reponame (pkg); + gs_app_set_origin (app, reponame); + } + + /* set more metadata for packages that don't have appstream data */ + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, dnf_package_get_name (pkg)); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, dnf_package_get_summary (pkg)); + + /* set hide-from-search quirk for available apps we don't want to show; results for non-installed desktop apps + * are intentionally hidden (as recommended by Matthias Clasen) by a special quirk because app layering + * should be intended for power users and not a common practice on Fedora Silverblue */ + if (!gs_app_is_installed (app)) { + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_DESKTOP_APP: + case AS_COMPONENT_KIND_WEB_APP: + case AS_COMPONENT_KIND_CONSOLE_APP: + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_FROM_SEARCH); + break; + default: + break; + } + } + + return TRUE /* found */; + } + + return FALSE /* not found */; +} + +static gboolean +resolve_appstream_source_file_to_package_name (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + Header h; + const gchar *fn; + gint rc; + g_auto(rpmdbMatchIterator) mi = NULL; + g_auto(rpmts) ts = NULL; + + /* open db readonly */ + ts = rpmtsCreate(); + rpmtsSetRootDir (ts, NULL); + rc = rpmtsOpenDB (ts, O_RDONLY); + if (rc != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Failed to open rpmdb: %i", rc); + return FALSE; + } + + /* look for a specific file */ + fn = gs_app_get_metadata_item (app, "appstream::source-file"); + if (fn == NULL) + return TRUE; + + mi = rpmtsInitIterator (ts, RPMDBI_INSTFILENAMES, fn, 0); + if (mi == NULL) { + g_debug ("rpm: no search results for %s", fn); + return TRUE; + } + + /* process any results */ + g_debug ("rpm: querying for %s with %s", gs_app_get_id (app), fn); + while ((h = rpmdbNextIterator (mi)) != NULL) { + const gchar *name; + + /* add default source */ + name = headerGetString (h, RPMTAG_NAME); + if (gs_app_get_source_default (app) == NULL) { + g_debug ("rpm: setting source to %s", name); + gs_app_add_source (app, name); + gs_app_set_management_plugin (app, plugin); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + } + } + + return TRUE; +} + +static gboolean +gs_rpm_ostree_refine_apps (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GHashTable) packages = NULL; + g_autoptr(GHashTable) layered_packages = NULL; + g_autoptr(GHashTable) layered_local_packages = NULL; + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GPtrArray) pkglist = NULL; + g_autoptr(GVariant) default_deployment = NULL; + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(DnfContext) dnf_context = NULL; + g_autoptr(OstreeRepo) ot_repo = NULL; + g_auto(GStrv) layered_packages_strv = NULL; + g_auto(GStrv) layered_local_packages_strv = NULL; + g_autofree gchar *checksum = NULL; + + locker = g_mutex_locker_new (&self->mutex); + + if (!gs_rpmostree_ref_dnf_context_locked (self, &os_proxy, &sysroot_proxy, &dnf_context, cancellable, error)) + return FALSE; + + ot_repo = g_object_ref (self->ot_repo); + + if (!dnf_context) + return FALSE; + + g_clear_pointer (&locker, g_mutex_locker_free); + + /* ensure D-Bus properties are updated before reading them */ + if (!gs_rpmostree_sysroot_call_reload_sync (sysroot_proxy, cancellable, error)) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + default_deployment = gs_rpmostree_os_dup_default_deployment (os_proxy); + g_assert (g_variant_lookup (default_deployment, + "packages", "^as", + &layered_packages_strv)); + g_assert (g_variant_lookup (default_deployment, + "requested-local-packages", "^as", + &layered_local_packages_strv)); + g_assert (g_variant_lookup (default_deployment, + "checksum", "s", + &checksum)); + + pkglist = rpm_ostree_db_query_all (ot_repo, checksum, cancellable, error); + if (pkglist == NULL) { + gs_rpmostree_error_convert (error); + return FALSE; + } + + packages = g_hash_table_new (g_str_hash, g_str_equal); + layered_packages = g_hash_table_new (g_str_hash, g_str_equal); + layered_local_packages = g_hash_table_new (g_str_hash, g_str_equal); + + for (guint ii = 0; ii < pkglist->len; ii++) { + RpmOstreePackage *pkg = g_ptr_array_index (pkglist, ii); + if (rpm_ostree_package_get_name (pkg)) + g_hash_table_insert (packages, (gpointer) rpm_ostree_package_get_name (pkg), pkg); + } + + for (guint ii = 0; layered_packages_strv && layered_packages_strv[ii]; ii++) { + g_hash_table_add (layered_packages, layered_packages_strv[ii]); + } + + for (guint ii = 0; layered_local_packages_strv && layered_local_packages_strv[ii]; ii++) { + g_hash_table_add (layered_local_packages, layered_local_packages_strv[ii]); + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gboolean found; + + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) + continue; + /* set management plugin for apps where appstream just added the source package name in refine() */ + if (gs_app_has_management_plugin (app, NULL) && + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE && + gs_app_get_scope (app) == AS_COMPONENT_SCOPE_SYSTEM && + gs_app_get_source_default (app) != NULL) { + gs_app_set_management_plugin (app, plugin); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + } + /* resolve the source package name based on installed appdata/desktop file name */ + if (gs_app_has_management_plugin (app, NULL) && + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN && + gs_app_get_scope (app) == AS_COMPONENT_SCOPE_SYSTEM && + gs_app_get_source_default (app) == NULL) { + if (!resolve_appstream_source_file_to_package_name (plugin, app, flags, cancellable, error)) + return FALSE; + } + if (!gs_app_has_management_plugin (app, plugin)) + continue; + if (gs_app_get_source_default (app) == NULL) + continue; + + /* first try to resolve from installed packages */ + found = resolve_installed_packages_app (plugin, packages, layered_packages, layered_local_packages, app); + + /* if we didn't find anything, try resolving from available packages */ + if (!found && dnf_context != NULL) + found = resolve_available_packages_app (plugin, dnf_context_get_sack (dnf_context), app); + + /* if we still didn't find anything then it's likely a package + * that is still in appstream data, but removed from the repos */ + if (!found) + g_debug ("failed to resolve %s", gs_app_get_unique_id (app)); + } + + return TRUE; +} + +static void refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_rpm_ostree_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE); + + task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rpm_ostree_refine_async); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + refine_thread_cb, g_steal_pointer (&task)); +} + +static void +refine_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPlugin *plugin = GS_PLUGIN (source_object); + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + GsPluginRefineData *data = task_data; + GsAppList *list = data->list; + GsPluginRefineFlags flags = data->flags; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (!gs_rpm_ostree_refine_apps (plugin, list, flags, cancellable, &local_error)) + g_task_return_error (task, g_steal_pointer (&local_error)); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_rpm_ostree_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +gboolean +gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + const char *packages[] = { NULL }; + g_autofree gchar *new_refspec = NULL; + g_autofree gchar *transaction_address = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(TransactionProgress) tp = transaction_progress_new (); + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + gboolean done; + + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + /* check is distro-upgrade */ + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_OPERATING_SYSTEM) + return TRUE; + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, error)) + return FALSE; + + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) + return FALSE; + + /* construct new refspec based on the distro version we're upgrading to */ + new_refspec = g_strdup_printf ("ostree://fedora/%s/x86_64/silverblue", + gs_app_get_version (app)); + + options = make_rpmostree_options_variant (FALSE, /* reboot */ + TRUE, /* allow-downgrade */ + FALSE, /* cache-only */ + TRUE, /* download-only */ + FALSE, /* skip-purge */ + FALSE, /* no-pull-base */ + FALSE, /* dry-run */ + FALSE); /* no-overrides */ + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + tp->app = g_object_ref (app); + + done = FALSE; + while (!done) { + done = TRUE; + if (!gs_rpmostree_os_call_rebase_sync (os_proxy, + options, + new_refspec, + packages, + NULL /* fd list */, + &transaction_address, + NULL /* fd list out */, + cancellable, + &local_error)) { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) { + g_clear_error (&local_error); + if (!gs_rpmostree_wait_for_ongoing_transaction_end (sysroot_proxy, cancellable, error)) { + gs_app_set_state_recover (app); + return FALSE; + } + done = FALSE; + continue; + } + if (local_error) + g_propagate_error (error, g_steal_pointer (&local_error)); + gs_rpmostree_error_convert (error); + gs_app_set_state_recover (app); + return FALSE; + } + } + + if (!gs_rpmostree_transaction_get_response_sync (sysroot_proxy, + transaction_address, + tp, + cancellable, + error)) { + gs_rpmostree_error_convert (error); + + if (g_strrstr ((*error)->message, "Old and new refs are equal")) { + /* don't error out if the correct tree is already deployed */ + g_debug ("ignoring rpm-ostree error: %s", (*error)->message); + g_clear_error (error); + } else { + gs_app_set_state_recover (app); + return FALSE; + } + } + + /* state is known */ + gs_app_set_state (app, GS_APP_STATE_UPDATABLE); + return TRUE; +} + + +static gboolean +plugin_rpmostree_pick_rpm_desktop_file_cb (GsPlugin *plugin, + GsApp *app, + const gchar *filename, + GKeyFile *key_file) +{ + return strstr (filename, "/snapd/") == NULL && + strstr (filename, "/snap/") == NULL && + strstr (filename, "/flatpak/") == NULL && + g_key_file_has_group (key_file, "Desktop Entry") && + !g_key_file_has_key (key_file, "Desktop Entry", "X-Flatpak", NULL) && + !g_key_file_has_key (key_file, "Desktop Entry", "X-SnapInstanceName", NULL); +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + /* only process this app if was created by this plugin */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + return gs_plugin_app_launch_filtered (plugin, app, plugin_rpmostree_pick_rpm_desktop_file_cb, NULL, error); +} + +static void +add_quirks_from_package_name (GsApp *app, const gchar *package_name) +{ + /* these packages don't have a .repo file in their file lists, but + * instead install one through rpm scripts / cron job */ + const gchar *packages_with_repos[] = { + "google-chrome-stable", + "google-earth-pro-stable", + "google-talkplugin", + NULL }; + + if (g_strv_contains (packages_with_repos, package_name)) + gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE); +} + +gboolean +gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + gboolean ret = FALSE; + FD_t rpmfd = NULL; + guint64 epoch; + guint64 size; + const gchar *name; + const gchar *version; + const gchar *release; + const gchar *license; + g_auto(Header) h = NULL; + g_auto(rpmts) ts = NULL; + g_autofree gchar *evr = NULL; + g_autofree gchar *filename = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) tmp_list = NULL; + + filename = g_file_get_path (file); + if (!g_str_has_suffix (filename, ".rpm")) { + ret = TRUE; + goto out; + } + + ts = rpmtsCreate (); + rpmtsSetVSFlags (ts, _RPMVSF_NOSIGNATURES); + + /* librpm needs Fopenfd */ + rpmfd = Fopen (filename, "r.fdio"); + if (rpmfd == NULL) { + g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, + "Opening %s failed", filename); + goto out; + } + if (Ferror (rpmfd)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Opening %s failed: %s", + filename, + Fstrerror (rpmfd)); + goto out; + } + + if (rpmReadPackageFile (ts, rpmfd, filename, &h) != RPMRC_OK) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Verification of %s failed", + filename); + goto out; + } + + app = gs_app_new (NULL); + gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (plugin)); + gs_app_set_management_plugin (app, plugin); + if (h) { + const gchar *str; + + str = headerGetString (h, RPMTAG_NAME); + if (str && *str) + gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, str); + + str = headerGetString (h, RPMTAG_SUMMARY); + if (str && *str) + gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, str); + + str = headerGetString (h, RPMTAG_DESCRIPTION); + if (str && *str) + gs_app_set_description (app, GS_APP_QUALITY_HIGHEST, str); + } + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + + /* add default source */ + name = headerGetString (h, RPMTAG_NAME); + g_debug ("rpm: setting source to %s", name); + gs_app_add_source (app, name); + + /* add version */ + epoch = headerGetNumber (h, RPMTAG_EPOCH); + version = headerGetString (h, RPMTAG_VERSION); + release = headerGetString (h, RPMTAG_RELEASE); + if (epoch > 0) { + evr = g_strdup_printf ("%" G_GUINT64_FORMAT ":%s-%s", + epoch, version, release); + } else { + evr = g_strdup_printf ("%s-%s", + version, release); + } + g_debug ("rpm: setting version to %s", evr); + gs_app_set_version (app, evr); + + /* set size */ + size = headerGetNumber (h, RPMTAG_SIZE); + gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, size); + + /* set license */ + license = headerGetString (h, RPMTAG_LICENSE); + if (license != NULL) { + g_autofree gchar *license_spdx = NULL; + license_spdx = as_license_to_spdx_id (license); + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, license_spdx); + g_debug ("rpm: setting license to %s", license_spdx); + } + + add_quirks_from_package_name (app, name); + + tmp_list = gs_app_list_new (); + gs_app_list_add (tmp_list, app); + + if (gs_rpm_ostree_refine_apps (plugin, tmp_list, 0, cancellable, error)) { + if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) + gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL); + + gs_app_list_add (list, app); + ret = TRUE; + } + +out: + if (rpmfd != NULL) + (void) Fclose (rpmfd); + return ret; +} + +static gchar ** +what_provides_decompose (GsAppQueryProvidesType provides_type, + const gchar *provides_tag) +{ + g_autoptr(GPtrArray) array = g_ptr_array_new (); + + /* The provides_tag possibly already contains the prefix, thus use it as is */ + if (provides_type != GS_APP_QUERY_PROVIDES_UNKNOWN && + g_str_has_suffix (provides_tag, ")") && + strchr (provides_tag, '(') != NULL) + provides_type = GS_APP_QUERY_PROVIDES_PACKAGE_NAME; + + /* Wrap the @provides_tag with the appropriate Fedora prefix */ + switch (provides_type) { + case GS_APP_QUERY_PROVIDES_PACKAGE_NAME: + g_ptr_array_add (array, g_strdup (provides_tag)); + break; + case GS_APP_QUERY_PROVIDES_GSTREAMER: + g_ptr_array_add (array, g_strdup_printf ("gstreamer0.10(%s)", provides_tag)); + g_ptr_array_add (array, g_strdup_printf ("gstreamer1(%s)", provides_tag)); + break; + case GS_APP_QUERY_PROVIDES_FONT: + g_ptr_array_add (array, g_strdup_printf ("font(%s)", provides_tag)); + break; + case GS_APP_QUERY_PROVIDES_MIME_HANDLER: + g_ptr_array_add (array, g_strdup_printf ("mimehandler(%s)", provides_tag)); + break; + case GS_APP_QUERY_PROVIDES_PS_DRIVER: + g_ptr_array_add (array, g_strdup_printf ("postscriptdriver(%s)", provides_tag)); + break; + case GS_APP_QUERY_PROVIDES_PLASMA: + g_ptr_array_add (array, g_strdup_printf ("plasma4(%s)", provides_tag)); + g_ptr_array_add (array, g_strdup_printf ("plasma5(%s)", provides_tag)); + break; + case GS_APP_QUERY_PROVIDES_UNKNOWN: + default: + g_assert_not_reached (); + } + + g_ptr_array_add (array, NULL); + + return (gchar **) g_ptr_array_free (g_steal_pointer (&array), FALSE); +} + +static void list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_rpm_ostree_list_apps_async (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE); + + task = gs_plugin_list_apps_data_new_task (plugin, query, flags, + cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rpm_ostree_list_apps_async); + + /* Queue a job to get the apps. */ + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + list_apps_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +list_apps_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (source_object); + g_autoptr(GsAppList) list = gs_app_list_new (); + GsPluginListAppsData *data = task_data; + const gchar *provides_tag = NULL; + GsAppQueryProvidesType provides_type = GS_APP_QUERY_PROVIDES_UNKNOWN; + g_autoptr(GError) local_error = NULL; + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GPtrArray) pkglist = NULL; + g_autoptr(DnfContext) dnf_context = NULL; + g_auto(GStrv) provides = NULL; + + assert_in_worker (self); + + if (data->query != NULL) { + provides_type = gs_app_query_get_provides (data->query, &provides_tag); + } + + /* Currently only support a subset of query properties, and only one set at once. */ + if (provides_tag == NULL || + gs_app_query_get_n_properties_set (data->query) != 1) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported query"); + return; + } + + /* Prepare a dnf context */ + locker = g_mutex_locker_new (&self->mutex); + + if (!gs_rpmostree_ref_dnf_context_locked (self, NULL, NULL, &dnf_context, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + g_clear_pointer (&locker, g_mutex_locker_free); + + provides = what_provides_decompose (provides_type, provides_tag); + pkglist = find_packages_by_provides (dnf_context_get_sack (dnf_context), provides); + for (guint i = 0; i < pkglist->len; i++) { + DnfPackage *pkg = g_ptr_array_index (pkglist, i); + g_autoptr(GsApp) app = NULL; + + app = gs_plugin_cache_lookup (GS_PLUGIN (self), dnf_package_get_nevra (pkg)); + if (app != NULL) { + gs_app_list_add (list, app); + continue; + } + + /* create new app */ + app = gs_app_new (NULL); + gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (GS_PLUGIN (self))); + gs_app_set_management_plugin (app, GS_PLUGIN (self)); + gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT); + app_set_rpm_ostree_packaging_format (app); + gs_app_set_kind (app, AS_COMPONENT_KIND_GENERIC); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM); + gs_app_add_source (app, dnf_package_get_name (pkg)); + + gs_plugin_cache_add (GS_PLUGIN (self), dnf_package_get_nevra (pkg), app); + gs_app_list_add (list, app); + } + + g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref); +} + +static GsAppList * +gs_plugin_rpm_ostree_list_apps_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +gboolean +gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(DnfContext) dnf_context = NULL; + GPtrArray *repos; + + dnf_context = gs_rpmostree_create_bare_dnf_context (cancellable, error); + if (!dnf_context) + return FALSE; + + repos = dnf_context_get_repos (dnf_context); + if (repos == NULL) + return TRUE; + + for (guint i = 0; i < repos->len; i++) { + DnfRepo *repo = g_ptr_array_index (repos, i); + g_autofree gchar *description = NULL; + g_autoptr(GsApp) app = NULL; + gboolean enabled; + + /* hide these from the user */ + if (dnf_repo_is_devel (repo) || dnf_repo_is_source (repo)) + continue; + + app = gs_app_new (dnf_repo_get_id (repo)); + gs_app_set_management_plugin (app, plugin); + gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + + enabled = (dnf_repo_get_enabled (repo) & DNF_REPO_ENABLED_PACKAGES) > 0; + gs_app_set_state (app, enabled ? GS_APP_STATE_INSTALLED : GS_APP_STATE_AVAILABLE); + + description = dnf_repo_get_description (repo); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, description); + gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, description); + + gs_app_set_metadata (app, "GnomeSoftware::SortKey", "200"); + gs_app_set_origin_ui (app, _("Operating System (OSTree)")); + + gs_app_list_add (list, app); + } + + return TRUE; +} + +static void enable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_rpm_ostree_enable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rpm_ostree_enable_repository_async); + + /* only process this app if it was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + enable_repository_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +enable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (source_object); + GsPluginManageRepositoryData *data = task_data; + GsPluginRefreshMetadataData refresh_data = { 0 }; + gboolean interactive = (data->flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (!gs_rpmostree_repo_enable (GS_PLUGIN (self), data->repository, TRUE, os_proxy, sysroot_proxy, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + refresh_data.flags = interactive ? GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE : GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE; + refresh_data.cache_age_secs = 1; + + if (!gs_plugin_rpm_ostree_refresh_metadata_in_worker (self, &refresh_data, os_proxy, sysroot_proxy, cancellable, &local_error)) + g_debug ("Failed to refresh after repository enable: %s", local_error->message); + + /* This can fail silently, it's only to update necessary caches, to provide + * up-to-date information after the successful repository enable/install. + */ + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_rpm_ostree_enable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void disable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void +gs_plugin_rpm_ostree_disable_repository_async (GsPlugin *plugin, + GsApp *repository, + GsPluginManageRepositoryFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (plugin); + g_autoptr(GTask) task = NULL; + gboolean interactive = (flags & GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + + task = gs_plugin_manage_repository_data_new_task (plugin, repository, flags, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_rpm_ostree_disable_repository_async); + + /* only process this app if it was created by this plugin */ + if (!gs_app_has_management_plugin (repository, plugin)) { + g_task_return_boolean (task, TRUE); + return; + } + + g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY); + + gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive), + disable_repository_thread_cb, g_steal_pointer (&task)); +} + +/* Run in @worker. */ +static void +disable_repository_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GsPluginRpmOstree *self = GS_PLUGIN_RPM_OSTREE (source_object); + GsPluginManageRepositoryData *data = task_data; + g_autoptr(GsRPMOSTreeOS) os_proxy = NULL; + g_autoptr(GsRPMOSTreeSysroot) sysroot_proxy = NULL; + g_autoptr(GError) local_error = NULL; + + assert_in_worker (self); + + if (!gs_rpmostree_ref_proxies (self, &os_proxy, &sysroot_proxy, cancellable, &local_error)) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (gs_rpmostree_repo_enable (GS_PLUGIN (self), data->repository, FALSE, os_proxy, sysroot_proxy, cancellable, &local_error)) + g_task_return_boolean (task, TRUE); + else + g_task_return_error (task, g_steal_pointer (&local_error)); +} + +static gboolean +gs_plugin_rpm_ostree_disable_repository_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +gs_plugin_rpm_ostree_class_init (GsPluginRpmOstreeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_rpm_ostree_dispose; + object_class->finalize = gs_plugin_rpm_ostree_finalize; + + plugin_class->setup_async = gs_plugin_rpm_ostree_setup_async; + plugin_class->setup_finish = gs_plugin_rpm_ostree_setup_finish; + plugin_class->shutdown_async = gs_plugin_rpm_ostree_shutdown_async; + plugin_class->shutdown_finish = gs_plugin_rpm_ostree_shutdown_finish; + plugin_class->refine_async = gs_plugin_rpm_ostree_refine_async; + plugin_class->refine_finish = gs_plugin_rpm_ostree_refine_finish; + plugin_class->refresh_metadata_async = gs_plugin_rpm_ostree_refresh_metadata_async; + plugin_class->refresh_metadata_finish = gs_plugin_rpm_ostree_refresh_metadata_finish; + plugin_class->enable_repository_async = gs_plugin_rpm_ostree_enable_repository_async; + plugin_class->enable_repository_finish = gs_plugin_rpm_ostree_enable_repository_finish; + plugin_class->disable_repository_async = gs_plugin_rpm_ostree_disable_repository_async; + plugin_class->disable_repository_finish = gs_plugin_rpm_ostree_disable_repository_finish; + plugin_class->list_apps_async = gs_plugin_rpm_ostree_list_apps_async; + plugin_class->list_apps_finish = gs_plugin_rpm_ostree_list_apps_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_RPM_OSTREE; +} diff --git a/plugins/rpm-ostree/gs-plugin-rpm-ostree.h b/plugins/rpm-ostree/gs-plugin-rpm-ostree.h new file mode 100644 index 0000000..ab322b7 --- /dev/null +++ b/plugins/rpm-ostree/gs-plugin-rpm-ostree.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_RPM_OSTREE (gs_plugin_rpm_ostree_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginRpmOstree, gs_plugin_rpm_ostree, GS, PLUGIN_RPM_OSTREE, GsPlugin) + +G_END_DECLS diff --git a/plugins/rpm-ostree/meson.build b/plugins/rpm-ostree/meson.build new file mode 100644 index 0000000..724556e --- /dev/null +++ b/plugins/rpm-ostree/meson.build @@ -0,0 +1,23 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginRpmOstree"'] + +rpmostree_generated = gnome.gdbus_codegen( + 'gs-rpmostree-generated', + 'org.projectatomic.rpmostree1.xml', + interface_prefix : 'org.projectatomic.rpmostree1', + namespace : 'GsRPMOSTree' +) + +shared_module( + 'gs_plugin_rpm-ostree', + rpmostree_generated, + sources : 'gs-plugin-rpm-ostree.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + install_rpath: join_paths(rpm_ostree.get_variable('libdir'), 'rpm-ostree'), + c_args : cargs, + dependencies : [ plugin_libs, libdnf, ostree, rpm, rpm_ostree ], +) diff --git a/plugins/rpm-ostree/org.projectatomic.rpmostree1.xml b/plugins/rpm-ostree/org.projectatomic.rpmostree1.xml new file mode 100644 index 0000000..6ae04a4 --- /dev/null +++ b/plugins/rpm-ostree/org.projectatomic.rpmostree1.xml @@ -0,0 +1,458 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + + <!-- Deployment dictionary keys: + 'id' (type 's') + 'osname' (type 's') + 'serial' (type 'i') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + 'packages' (type 'as') + 'booted' (type 'b') + --> + + <interface name="org.projectatomic.rpmostree1.Sysroot"> + <!-- The booted OSName --> + <property name="Booted" type="o" access="read"/> + + <!-- The system root path --> + <property name="Path" type="s" access="read"/> + + <!-- The values are (method-name, sender-name, object path) --> + <property name="ActiveTransaction" type="(sss)" access="read"/> + <!-- A DBus address - connect to it to access its methods --> + <property name="ActiveTransactionPath" type="s" access="read"/> + + <!-- (Currently) optional method to denote the client plans + to either invoke methods on the daemon, or monitor status. + If no clients are registered, the daemon may exit. + + 'id (type 's') - Package/component name (e.g. `cockpit`, `gnome-software`) + --> + <method name="RegisterClient"> + <arg type="a{sv}" name="options" direction="in"/> + </method> + + <!-- You must call this if your process is no longer interested in talking to + rpm-ostree, but will remain connected to the bus. + + No options are currently defined. + --> + <method name="UnregisterClient"> + <arg type="a{sv}" name="options" direction="in"/> + </method> + + <!-- Reload sysroot if changed. This can also be used as a way to sync with the daemon + to ensure e.g. D-Bus properties are updated before reading them. --> + <method name="Reload"> + </method> + + <!-- Like Reload, but also reload configuration files. --> + <method name="ReloadConfig"> + </method> + + <!-- none, check, stage --> + <property name="AutomaticUpdatePolicy" type="s" access="read"/> + + <method name="CreateOSName"> + <arg type="s" name="name"/> + <arg type="o" name="result" direction="out"/> + </method> + + <method name="GetOS"> + <arg name="name" type="s"/> + <arg name="object_path" type="o" direction="out"/> + </method> + + <!-- Array of all deployments in boot order --> + <property name="Deployments" type="aa{sv}" access="read"/> + </interface> + + <interface name="org.projectatomic.rpmostree1.OS"> + <property name="BootedDeployment" type="a{sv}" access="read"/> + <property name="DefaultDeployment" type="a{sv}" access="read"/> + <property name="RollbackDeployment" type="a{sv}" access="read"/> + + <!-- CachedUpdate dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + 'gpg-enabled' (type 'b') + 'ref-has-new-commit' (type 'b') + TRUE if 'checksum' refers to a new base commit we're not booted in. + 'rpm-diff' (type 'a{sv}') + 'upgraded' (type 'a(us(ss)(ss))') + 'downgraded' (type 'a(us(ss)(ss))') + 'removed' (type 'a(usss)') + 'added' (type 'a(usss)') + 'advisories' (type 'a(suuasa{sv})') + --> + <property name="CachedUpdate" type="a{sv}" access="read"/> + <property name="HasCachedUpdateRpmDiff" type="b" access="read"/> + + <!-- Available options: + "mode" (type 's') + One of auto, none, check. Defaults to auto, which follows configured + policy (available in AutomaticUpdatePolicy property). + "output-to-self" (type 'b') + Whether output should go to the daemon itself rather than the + transaction. Defaults to TRUE. + + If automatic updates are not enabled, @enabled will be FALSE and + @transaction_address will be the empty string. + --> + <method name="AutomaticUpdateTrigger"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="b" name="enabled" direction="out"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <property name="Name" type="s" access="read"/> + + <method name="GetDeploymentsRpmDiff"> + <arg type="s" name="deployid0"/> + <arg type="s" name="deployid1"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + </method> + + <!-- Revision may be a full checksum or version string. + + Available options: + "reboot" (type 'b') + --> + <method name="Deploy"> + <arg type="s" name="revision" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <!-- details dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + --> + <method name="GetCachedDeployRpmDiff"> + <arg type="s" name="revision"/> + <arg type="as" name="packages"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + <arg type="a{sv}" name="details" direction="out"/> + </method> + + <method name="DownloadDeployRpmDiff"> + <arg type="s" name="revision"/> + <arg type="as" name="packages"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "allow-downgrade" (type 'b') + "reboot" (type 'b') + --> + <method name="Upgrade"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <!-- details dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + --> + <method name="GetCachedUpdateRpmDiff"> + <arg type="s" name="deployid"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + <arg type="a{sv}" name="details" direction="out"/> + </method> + + <method name="DownloadUpdateRpmDiff"> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + --> + <method name="Rollback"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + --> + <method name="ClearRollbackTarget"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "skip-purge" (type 'b') + "reboot" (type 'b') + "revision" (type 's') + --> + <method name="Rebase"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="refspec"/> + <arg type="as" name="packages"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <!-- details dictionary keys: + 'osname' (type 's') + 'checksum' (type 's') + 'version' (type 's') + 'timestamp' (type 't') + 'origin' (type 's') + 'signatures' (type 'av') + --> + <method name="GetCachedRebaseRpmDiff"> + <arg type="s" name="refspec"/> + <arg type="as" name="packages"/> + <arg type="a(sua{sv})" name="result" direction="out"/> + <arg type="a{sv}" name="details" direction="out"/> + </method> + + <method name="DownloadRebaseRpmDiff"> + <arg type="s" name="refspec"/> + <arg type="as" name="packages"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + "dry-run" (type 'b') + --> + <method name="PkgChange"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="as" name="packages_added"/> + <arg type="as" name="packages_removed"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + <method name="SetInitramfsState"> + <arg type="b" name="regenerate" direction="in"/> + <arg type="as" name="args" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available options: + "reboot" (type 'b') + --> + <method name="KernelArgs"> + <arg type="s" name="existing_kernel_arg_string"/> + <arg type="as" name="kernel_args_added" direction="in"/> + <arg type="as" name="kernel_args_replaced" direction="in"/> + <arg type="as" name="kernel_args_removed" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <method name="GetDeploymentBootConfig"> + <arg type="s" name="deployid" /> + <arg type="b" name="is_pending" direction="in"/> + <arg type="a{sv}" name="bootconfig" direction="out"/> + </method> + + <method name="Cleanup"> + <arg type="as" name="elements" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <method name="RefreshMd"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Set options in yum .repo files --> + <method name="ModifyYumRepo"> + <arg type="s" name="repo_id" direction="in"/> + <arg type="a{ss}" name="settings" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + <!-- Available modifiers: + "set-refspec" (type 's') + "set-revision" (type 's') + "install-packages" (type 'as') + "uninstall-packages" (type 'as') + "install-local-packages" (type 'ah') + "override-remove-packages" (type 'as') + "override-reset-packages" (type 'as') + "override-replace-packages" (type 'as') + "override-replace-local-packages" (type 'ah') + "custom-origin" (type '(ss)') + + Available options: + "reboot" (type 'b') + Initiate a reboot after transaction. + "allow-downgrade" (type 'b') + Allow newly pulled bases to have older timestamps than the + current base. Defaults to TRUE if either "set-refspec" or + "set-revision" is specified. + "skip-purge" (type 'b') + Do not purge the old refspec. Only valid if "set-refspec" is + specified. + "no-pull-base" (type 'b') + Do not pull a base layer from the remote. Not valid if + either "set-refspec" or "set-revision" is specified. + "dry-run" (type 'b') + Stop short of deploying the new tree. If layering packages, + the pkg diff is printed but packages are not downloaded or + imported. + "no-layering" (type 'b') + Remove all package requests. Requests in "install-packages" + are still subsequently processed if specified. + "no-overrides" (type 'b') + Remove all active overrides. Not valid if any override + modifiers are specified. + "no-initramfs" (type 'b') + Disable any initramfs regeneration. + "cache-only" (type 'b') + Do not update rpmmd repo metadata cache or ostree refspec. + Not valid if "download-only" is specified. + "download-only" (type 'b') + Update rpmmd repo metadata cache and ostree refspec. Do not + perform any deployments. This is like "dry-run" except that + the latter does not download and import packages. Not valid + if "cache-only" or "dry-run" is specified. + "allow-inactive-requests" (type 'b') + When installing packages, allow package requests which would + not immediately be active. + "idempotent-layering" (type 'b') + Don't error out on requests in install-* or uninstall-* + modifiers that are already satisfied. + --> + <method name="UpdateDeployment"> + <arg type="a{sv}" name="modifiers" direction="in"/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + </method> + + </interface> + + <interface name="org.projectatomic.rpmostree1.OSExperimental"> + + <!-- Just a test method --> + <method name="Moo"> + <arg type="b" name="utf8" direction="in"/> + <arg type="s" name="result" direction="out"/> + </method> + + <method name="LiveFs"> + <arg type="a{sv}" name="options" direction="in"/> + <arg type="s" name="transaction_address" direction="out"/> + </method> + + </interface> + + <interface name="org.projectatomic.rpmostree1.Transaction"> + + <!-- A single-line human-readable string --> + <property name="Title" type="s" access="read"/> + + <!-- Yes, we can. --> + <method name="Cancel"/> + + <!-- For a client to call when ready to receive signals. + The return boolean indicates whether the transaction was + started by this method call (true) or was already started + by another client (false). --> + <method name="Start"> + <arg type="b" name="started" direction="out"/> + </method> + + <signal name="Finished"> + <arg name="success" type="b" direction="out"/> + <arg name="error_message" type="s" direction="out"/> + </signal> + + <!-- For miscellaneous messages; line-buffered. --> + <signal name="Message"> + <arg name="text" type="s" direction="out"/> + </signal> + + <!-- Tasks are notifications that work is being done. --> + <signal name="TaskBegin"> + <arg name="text" type="s" direction="out"/> + </signal> + + <signal name="TaskEnd"> + <arg name="text" type="s" direction="out"/> + </signal> + + <!-- Generic percentage progress. --> + <signal name="PercentProgress"> + <arg name="text" type="s" direction="out"/> + <arg name="percentage" type="u" direction="out"/> + </signal> + + <signal name="DownloadProgress"> + <!-- time data, format is: + start time, elapsed seconds + --> + <arg name="time" type="(tt)" direction="out"/> + + <!-- + outstanding data counts, format is: + (outstanding fetches, outstanding writes) + --> + <arg name="outstanding" type="(uu)" direction="out"/> + + <!-- + metadata counts, format is: + (scanned, fetched, outstanding) + --> + <arg name="metadata" type="(uuu)" direction="out"/> + + <!-- + delta data, format is: + (total parts, fetched parts, total super blocks, total size) + --> + <arg name="delta" type="(uuut)" direction="out"/> + + <!-- + content data, format is: + (fetched, requested) + --> + <arg name="content" type="(uu)" direction="out"/> + + <!-- + transfer data, format is: + (bytes transfered, bytes/s) + --> + <arg name="transfer" type="(tt)" direction="out"/> + </signal> + + <signal name="SignatureProgress"> + <!-- An ostree GVariant containing signature data + see ostree_gpg_verify_result_get_all. + --> + <arg name="signature" type="av" direction="out"/> + <!-- The signed commit --> + <arg name="commit" type="s" direction="out"/> + </signal> + + <!-- Indicates progress signals are done and subsequent + Message signals should be output on separate lines. --> + <signal name="ProgressEnd"/> + </interface> +</node> diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c new file mode 100644 index 0000000..460641c --- /dev/null +++ b/plugins/snap/gs-plugin-snap.c @@ -0,0 +1,1924 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2018 Canonical Ltd + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gio/gdesktopappinfo.h> +#include <glib/gi18n.h> +#include <json-glib/json-glib.h> +#include <snapd-glib/snapd-glib.h> +#include <gnome-software.h> + +#include "gs-plugin-snap.h" + +/* + * SECTION: + * Lists and allows installation/uninstallation of snaps from the snap store. + * + * Since snapd is a daemon accessible via HTTP calls on a Unix socket, this + * plugin basically translates every job into one or more HTTP request, and all + * the real work is done in the snapd daemon. FIXME: This means the plugin can + * therefore execute entirely in the main thread, making asynchronous calls, + * once all the vfuncs have been ported. + */ + +struct _GsPluginSnap { + GsPlugin parent; + + gchar *store_name; + gchar *store_hostname; + SnapdSystemConfinement system_confinement; + + GMutex store_snaps_lock; + GHashTable *store_snaps; +}; + +G_DEFINE_TYPE (GsPluginSnap, gs_plugin_snap, GS_TYPE_PLUGIN) + +typedef struct { + SnapdSnap *snap; + gboolean full_details; +} CacheEntry; + +static CacheEntry * +cache_entry_new (SnapdSnap *snap, gboolean full_details) +{ + CacheEntry *entry = g_slice_new (CacheEntry); + entry->snap = g_object_ref (snap); + entry->full_details = full_details; + return entry; +} + +static void +cache_entry_free (CacheEntry *entry) +{ + g_object_unref (entry->snap); + g_slice_free (CacheEntry, entry); +} + +static SnapdAuthData * +get_auth_data (GsPluginSnap *self) +{ + g_autofree gchar *path = NULL; + g_autoptr(JsonParser) parser = NULL; + JsonNode *root; + JsonObject *object; + const gchar *macaroon; + g_autoptr(GPtrArray) discharges = NULL; + g_autoptr(GError) error = NULL; + + path = g_build_filename (g_get_home_dir (), ".snap", "auth.json", NULL); + parser = json_parser_new (); + if (!json_parser_load_from_file (parser, path, &error)) { + if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + g_warning ("Failed to load snap auth data: %s", error->message); + return NULL; + } + + root = json_parser_get_root (parser); + if (root == NULL) + return NULL; + + if (json_node_get_node_type (root) != JSON_NODE_OBJECT) { + g_warning ("Ignoring invalid snap auth data in %s", path); + return NULL; + } + object = json_node_get_object (root); + if (!json_object_has_member (object, "macaroon")) { + g_warning ("Ignoring invalid snap auth data in %s", path); + return NULL; + } + macaroon = json_object_get_string_member (object, "macaroon"); + discharges = g_ptr_array_new (); + if (json_object_has_member (object, "discharges")) { + JsonArray *discharge_array; + + discharge_array = json_object_get_array_member (object, "discharges"); + for (guint i = 0; i < json_array_get_length (discharge_array); i++) + g_ptr_array_add (discharges, (gpointer) json_array_get_string_element (discharge_array, i)); + } + g_ptr_array_add (discharges, NULL); + + return snapd_auth_data_new (macaroon, (GStrv) discharges->pdata); +} + +static SnapdClient * +get_client (GsPluginSnap *self, + gboolean interactive, + GError **error) +{ + g_autoptr(SnapdClient) client = NULL; + const gchar *old_user_agent; + g_autofree gchar *user_agent = NULL; + g_autoptr(SnapdAuthData) auth_data = NULL; + + client = snapd_client_new (); + snapd_client_set_allow_interaction (client, interactive); + old_user_agent = snapd_client_get_user_agent (client); + user_agent = g_strdup_printf ("%s %s", gs_user_agent (), old_user_agent); + snapd_client_set_user_agent (client, user_agent); + + auth_data = get_auth_data (self); + snapd_client_set_auth_data (client, auth_data); + + return g_steal_pointer (&client); +} + +static void +gs_plugin_snap_init (GsPluginSnap *self) +{ + g_autoptr(SnapdClient) client = NULL; + g_autoptr (GError) error = NULL; + + g_mutex_init (&self->store_snaps_lock); + + client = get_client (self, FALSE, &error); + if (client == NULL) { + gs_plugin_set_enabled (GS_PLUGIN (self), FALSE); + return; + } + + self->store_snaps = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) cache_entry_free); + + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_BETTER_THAN, "packagekit"); + gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_BEFORE, "icons"); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (GS_PLUGIN (self), "org.gnome.Software.Plugin.Snap"); +} + +void +gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app) +{ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_SNAP) + gs_app_set_management_plugin (app, plugin); + + if (gs_app_get_id (app) != NULL && g_str_has_prefix (gs_app_get_id (app), "io.snapcraft.")) { + g_autofree gchar *name_and_id = NULL; + gchar *divider, *snap_name;/*, *id;*/ + + name_and_id = g_strdup (gs_app_get_id (app) + strlen ("io.snapcraft.")); + divider = strrchr (name_and_id, '-'); + if (divider != NULL) { + *divider = '\0'; + snap_name = name_and_id; + /*id = divider + 1;*/ /* NOTE: Should probably validate ID */ + + gs_app_set_management_plugin (app, plugin); + gs_app_set_metadata (app, "snap::name", snap_name); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_SNAP); + } + } +} + +static void +snapd_error_convert (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return; + + /* this are allowed for low-level errors */ + if (gs_utils_error_convert_gio (perror)) + return; + + /* custom to this plugin */ + if (error->domain == SNAPD_ERROR) { + switch (error->code) { + case SNAPD_ERROR_AUTH_DATA_REQUIRED: + error->code = GS_PLUGIN_ERROR_AUTH_REQUIRED; + g_free (error->message); + error->message = g_strdup ("Requires authentication with @snapd"); + break; + case SNAPD_ERROR_AUTH_DATA_INVALID: + case SNAPD_ERROR_TWO_FACTOR_INVALID: + error->code = GS_PLUGIN_ERROR_AUTH_INVALID; + break; + case SNAPD_ERROR_AUTH_CANCELLED: + error->code = GS_PLUGIN_ERROR_CANCELLED; + break; + case SNAPD_ERROR_CONNECTION_FAILED: + case SNAPD_ERROR_WRITE_FAILED: + case SNAPD_ERROR_READ_FAILED: + case SNAPD_ERROR_BAD_REQUEST: + case SNAPD_ERROR_BAD_RESPONSE: + case SNAPD_ERROR_PERMISSION_DENIED: + case SNAPD_ERROR_FAILED: + case SNAPD_ERROR_TERMS_NOT_ACCEPTED: + case SNAPD_ERROR_PAYMENT_NOT_SETUP: + case SNAPD_ERROR_PAYMENT_DECLINED: + case SNAPD_ERROR_ALREADY_INSTALLED: + case SNAPD_ERROR_NOT_INSTALLED: + case SNAPD_ERROR_NO_UPDATE_AVAILABLE: + case SNAPD_ERROR_PASSWORD_POLICY_ERROR: + case SNAPD_ERROR_NEEDS_DEVMODE: + case SNAPD_ERROR_NEEDS_CLASSIC: + case SNAPD_ERROR_NEEDS_CLASSIC_SYSTEM: + 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; +} + +static void get_system_information_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void get_store_snap_async (GsPluginSnap *self, + SnapdClient *client, + const gchar *name, + gboolean need_details, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +static SnapdSnap *get_store_snap_finish (GsPluginSnap *self, + GAsyncResult *result, + GError **error); +static void add_channels (GsPluginSnap *self, + SnapdSnap *snap, + GsAppList *list); + +static void +gs_plugin_snap_setup_async (GsPlugin *plugin, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(SnapdClient) client = NULL; + g_autoptr(GTask) task = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + g_autoptr(GError) local_error = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_snap_setup_async); + + client = get_client (self, interactive, &local_error); + if (client == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + snapd_client_get_system_information_async (client, cancellable, + get_system_information_cb, g_steal_pointer (&task)); +} + +static void +get_system_information_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SnapdClient *client = SNAPD_CLIENT (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginSnap *self = g_task_get_source_object (task); + g_autoptr(SnapdSystemInformation) system_information = NULL; + g_autoptr(GError) local_error = NULL; + + system_information = snapd_client_get_system_information_finish (client, result, &local_error); + if (system_information == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + self->store_name = g_strdup (snapd_system_information_get_store (system_information)); + if (self->store_name == NULL) { + self->store_name = g_strdup (/* TRANSLATORS: default snap store name */ + _("Snap Store")); + self->store_hostname = g_strdup ("snapcraft.io"); + } + self->system_confinement = snapd_system_information_get_confinement (system_information); + + g_debug ("Version '%s' on OS %s %s", + snapd_system_information_get_version (system_information), + snapd_system_information_get_os_id (system_information), + snapd_system_information_get_os_version (system_information)); + + /* success */ + g_task_return_boolean (task, TRUE); +} + +static gboolean +gs_plugin_snap_setup_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static SnapdSnap * +store_snap_cache_lookup (GsPluginSnap *self, + const gchar *name, + gboolean need_details) +{ + CacheEntry *entry; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->store_snaps_lock); + + entry = g_hash_table_lookup (self->store_snaps, name); + if (entry == NULL) + return NULL; + + if (need_details && !entry->full_details) + return NULL; + + return g_object_ref (entry->snap); +} + +static void +store_snap_cache_update (GsPluginSnap *self, + GPtrArray *snaps, + gboolean full_details) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->store_snaps_lock); + guint i; + + for (i = 0; i < snaps->len; i++) { + SnapdSnap *snap = snaps->pdata[i]; + g_debug ("Caching '%s' by '%s' version %s revision %s", + snapd_snap_get_title (snap), + snapd_snap_get_publisher_display_name (snap), + snapd_snap_get_version (snap), + snapd_snap_get_revision (snap)); + g_hash_table_insert (self->store_snaps, g_strdup (snapd_snap_get_name (snap)), cache_entry_new (snap, full_details)); + } +} + +static GPtrArray * +find_snaps (GsPluginSnap *self, + SnapdClient *client, + SnapdFindFlags flags, + const gchar *section, + const gchar *query, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) snaps = NULL; + + snaps = snapd_client_find_section_sync (client, flags, section, query, NULL, cancellable, error); + if (snaps == NULL) { + snapd_error_convert (error); + return NULL; + } + + store_snap_cache_update (self, snaps, flags & SNAPD_FIND_FLAGS_MATCH_NAME); + + return g_steal_pointer (&snaps); +} + +static gchar * +get_appstream_id (SnapdSnap *snap) +{ + GStrv common_ids; + + /* Get the AppStream ID from the snap, or generate a fallback one */ + common_ids = snapd_snap_get_common_ids (snap); + if (g_strv_length (common_ids) == 1) + return g_strdup (common_ids[0]); + else + return g_strdup_printf ("io.snapcraft.%s-%s", snapd_snap_get_name (snap), snapd_snap_get_id (snap)); +} + +static AsComponentKind +snap_guess_component_kind (SnapdSnap *snap) +{ + switch (snapd_snap_get_snap_type (snap)) { + case SNAPD_SNAP_TYPE_APP: + return AS_COMPONENT_KIND_DESKTOP_APP; + case SNAPD_SNAP_TYPE_KERNEL: + case SNAPD_SNAP_TYPE_GADGET: + case SNAPD_SNAP_TYPE_OS: + return AS_COMPONENT_KIND_RUNTIME; + default: + case SNAPD_SNAP_TYPE_UNKNOWN: + return AS_COMPONENT_KIND_UNKNOWN; + } +} + +static GsApp * +snap_to_app (GsPluginSnap *self, SnapdSnap *snap, const gchar *branch) +{ + g_autofree gchar *cache_id = NULL; + g_autoptr(GsApp) app = NULL; + + cache_id = g_strdup_printf ("%s:%s", snapd_snap_get_name (snap), branch != NULL ? branch : ""); + + app = gs_plugin_cache_lookup (GS_PLUGIN (self), cache_id); + if (app == NULL) { + g_autofree gchar *appstream_id = NULL; + + appstream_id = get_appstream_id (snap); + app = gs_app_new (appstream_id); + gs_app_set_kind (app, snap_guess_component_kind (snap)); + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_SNAP); + gs_app_set_branch (app, branch); + gs_app_set_metadata (app, "snap::name", snapd_snap_get_name (snap)); + gs_app_set_metadata (app, "GnomeSoftware::PackagingIcon", "snap-symbolic"); + gs_plugin_cache_add (GS_PLUGIN (self), cache_id, app); + } + + gs_app_set_management_plugin (app, GS_PLUGIN (self)); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + if (gs_app_get_kind (app) != AS_COMPONENT_KIND_DESKTOP_APP) + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + if (gs_plugin_check_distro_id (GS_PLUGIN (self), "ubuntu")) + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + if (branch != NULL && (g_str_has_suffix (branch, "/beta") || g_str_has_suffix (branch, "/edge"))) + gs_app_add_quirk (app, GS_APP_QUIRK_DEVELOPMENT_SOURCE); + + return g_steal_pointer (&app); +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(SnapdClient) client = NULL; + g_autofree gchar *scheme = NULL; + g_autofree gchar *path = NULL; + g_autoptr(GPtrArray) snaps = NULL; + g_autoptr(GsApp) app = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + /* not us */ + scheme = gs_utils_get_url_scheme (url); + if (g_strcmp0 (scheme, "snap") != 0 && + g_strcmp0 (scheme, "appstream") != 0) + return TRUE; + + /* Create client. */ + client = get_client (self, interactive, error); + if (client == NULL) + return FALSE; + + /* create app */ + path = gs_utils_get_url_path (url); + snaps = find_snaps (self, client, + SNAPD_FIND_FLAGS_SCOPE_WIDE | SNAPD_FIND_FLAGS_MATCH_NAME, + NULL, path, cancellable, NULL); + if (snaps == NULL || snaps->len < 1) { + g_clear_pointer (&snaps, g_ptr_array_unref); + /* This works for the appstream:// URL-s */ + snaps = find_snaps (self, client, + SNAPD_FIND_FLAGS_SCOPE_WIDE | SNAPD_FIND_FLAGS_MATCH_COMMON_ID, + NULL, path, cancellable, NULL); + } + if (snaps == NULL || snaps->len < 1) + return TRUE; + + app = snap_to_app (self, g_ptr_array_index (snaps, 0), NULL); + gs_app_list_add (list, app); + + return TRUE; +} + +static void +gs_plugin_snap_dispose (GObject *object) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (object); + + g_clear_pointer (&self->store_name, g_free); + g_clear_pointer (&self->store_hostname, g_free); + g_clear_pointer (&self->store_snaps, g_hash_table_unref); + + G_OBJECT_CLASS (gs_plugin_snap_parent_class)->dispose (object); +} + +static void +gs_plugin_snap_finalize (GObject *object) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (object); + + g_mutex_clear (&self->store_snaps_lock); + + G_OBJECT_CLASS (gs_plugin_snap_parent_class)->finalize (object); +} + +static gboolean +is_banner_image (const gchar *filename) +{ + /* Check if this screenshot was uploaded as "banner.png" or "banner.jpg". + * The server optionally adds a 7 character suffix onto it if it would collide with + * an existing name, e.g. "banner_MgEy4MI.png" + * See https://forum.snapcraft.io/t/improve-method-for-setting-featured-snap-banner-image-in-store/ + */ + return g_regex_match_simple ("^banner(?:_[a-zA-Z0-9]{7})?\\.(?:png|jpg)$", filename, 0, 0); +} + +static gboolean +is_banner_icon_image (const gchar *filename) +{ + /* Check if this screenshot was uploaded as "banner-icon.png" or "banner-icon.jpg". + * The server optionally adds a 7 character suffix onto it if it would collide with + * an existing name, e.g. "banner-icon_Ugn6pmj.png" + * See https://forum.snapcraft.io/t/improve-method-for-setting-featured-snap-banner-image-in-store/ + */ + return g_regex_match_simple ("^banner-icon(?:_[a-zA-Z0-9]{7})?\\.(?:png|jpg)$", filename, 0, 0); +} + +/* Build a string representation of the IDs of a category and its parents. + * For example, `develop/featured`. */ +static gchar * +category_build_full_path (GsCategory *category) +{ + g_autoptr(GString) id = g_string_new (""); + GsCategory *c; + + for (c = category; c != NULL; c = gs_category_get_parent (c)) { + if (c != category) + g_string_prepend (id, "/"); + g_string_prepend (id, gs_category_get_id (c)); + } + + return g_string_free (g_steal_pointer (&id), FALSE); +} + +typedef struct { + /* In-progress data. */ + guint n_pending_ops; + GError *saved_error; /* (owned) (nullable) */ + GsAppList *results_list; /* (owned) (nullable) */ +} ListAppsData; + +static void +list_apps_data_free (ListAppsData *data) +{ + /* Error should have been propagated by now, and all pending ops completed. */ + g_assert (data->saved_error == NULL); + g_assert (data->n_pending_ops == 0); + g_assert (data->results_list == NULL); + + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ListAppsData, list_apps_data_free) + +static void list_installed_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void list_alternate_apps_snap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void list_alternate_apps_nonsnap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void list_alternative_apps_nonsnap_get_store_snap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void list_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void finish_list_apps_op (GTask *task, + GError *error); + +static void +gs_plugin_snap_list_apps_async (GsPlugin *plugin, + GsAppQuery *query, + GsPluginListAppsFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(GTask) task = NULL; + g_autoptr(ListAppsData) owned_data = NULL; + ListAppsData *data; + g_autoptr(SnapdClient) client = NULL; + gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE); + GsAppQueryTristate is_curated = GS_APP_QUERY_TRISTATE_UNSET; + GsCategory *category = NULL; + GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET; + const gchar * const *keywords = NULL; + GsApp *alternate_of = NULL; + const gchar * const *sections = NULL; + const gchar * const curated_sections[] = { "featured", NULL }; + g_autoptr(GError) local_error = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + data = owned_data = g_new0 (ListAppsData, 1); + g_task_set_task_data (task, g_steal_pointer (&owned_data), (GDestroyNotify) list_apps_data_free); + g_task_set_source_tag (task, gs_plugin_snap_list_apps_async); + + client = get_client (self, interactive, &local_error); + if (client == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + if (query != NULL) { + is_curated = gs_app_query_get_is_curated (query); + category = gs_app_query_get_category (query); + is_installed = gs_app_query_get_is_installed (query); + keywords = gs_app_query_get_keywords (query); + alternate_of = gs_app_query_get_alternate_of (query); + } + + /* Currently only support a subset of query properties, and only one set at once. + * Also don’t currently support GS_APP_QUERY_TRISTATE_FALSE. */ + if ((is_curated == GS_APP_QUERY_TRISTATE_UNSET && + category == NULL && + is_installed == GS_APP_QUERY_TRISTATE_UNSET && + keywords == NULL && + alternate_of == NULL) || + is_curated == GS_APP_QUERY_TRISTATE_FALSE || + is_installed == GS_APP_QUERY_TRISTATE_FALSE || + gs_app_query_get_n_properties_set (query) != 1) { + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported query"); + return; + } + + data->results_list = gs_app_list_new (); + + /* Listing installed apps requires calling a different libsnapd method, + * so check that first. */ + if (is_installed != GS_APP_QUERY_TRISTATE_UNSET) { + data->n_pending_ops++; + snapd_client_get_snaps_async (client, SNAPD_GET_SNAPS_FLAGS_NONE, NULL, + cancellable, list_installed_apps_cb, g_steal_pointer (&task)); + return; + } + + /* Listing alternates also requires special handling. */ + if (alternate_of != NULL) { + /* If it is a snap, find the channels that snap provides, otherwise find snaps that match on common id */ + if (gs_app_has_management_plugin (alternate_of, plugin)) { + const gchar *snap_name; + + snap_name = gs_app_get_metadata_item (alternate_of, "snap::name"); + + data->n_pending_ops++; + get_store_snap_async (self, client, snap_name, TRUE, cancellable, list_alternate_apps_snap_cb, g_steal_pointer (&task)); + /* The id can be NULL for example for local package files */ + } else if (gs_app_get_id (alternate_of) != NULL) { + data->n_pending_ops++; + snapd_client_find_section_async (client, + SNAPD_FIND_FLAGS_SCOPE_WIDE | SNAPD_FIND_FLAGS_MATCH_COMMON_ID, + NULL, gs_app_get_id (alternate_of), + cancellable, + list_alternate_apps_nonsnap_cb, g_steal_pointer (&task)); + } else { + g_clear_object (&data->results_list); + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Unsupported app without id"); + } + + return; + } + + /* Querying with keywords also requires calling the method differently. + * snapd will tokenise and stem @query internally. */ + if (keywords != NULL) { + g_autofree gchar *query_str = NULL; + + query_str = g_strjoinv (" ", (gchar **) keywords); + data->n_pending_ops++; + snapd_client_find_section_async (client, SNAPD_FIND_FLAGS_SCOPE_WIDE, NULL, query_str, + cancellable, list_apps_cb, g_steal_pointer (&task)); + return; + } + + /* Work out which sections we’re querying for. */ + if (is_curated != GS_APP_QUERY_TRISTATE_UNSET) { + sections = curated_sections; + } else if (category != NULL) { + g_autofree gchar *category_path = NULL; + + /* + * Unused categories: + * + * health-and-fitness + * personalisation + * devices-and-iot + * security + * server-and-cloud + * entertainment + */ + const struct { + const gchar *category_path; + const gchar *sections[4]; + } category_to_sections_map[] = { + { "play/featured", { "games", NULL, }}, + { "create/featured", { "photo-and-video", "art-and-design", "music-and-video", NULL, }}, + { "socialize/featured", { "social", "news-and-weather", NULL, }}, + { "work/featured", { "productivity", "finance", "utilities", NULL, }}, + { "develop/featured", { "development", NULL, }}, + { "learn/featured", { "education", "science", "books-and-reference", NULL, }}, + }; + + category_path = category_build_full_path (category); + + for (gsize i = 0; i < G_N_ELEMENTS (category_to_sections_map); i++) { + if (g_str_equal (category_to_sections_map[i].category_path, category_path)) { + sections = category_to_sections_map[i].sections; + break; + } + } + } + + /* Start a query for each of the sections we’re interested in, keeping a + * counter of pending operations which is initialised to 1 until all + * the operations are started. */ + data->n_pending_ops = 1; + + for (gsize i = 0; sections != NULL && sections[i] != NULL; i++) { + data->n_pending_ops++; + snapd_client_find_section_async (client, SNAPD_FIND_FLAGS_SCOPE_WIDE, sections[i], NULL, + cancellable, list_apps_cb, g_object_ref (task)); + } + + finish_list_apps_op (task, NULL); +} + +static void +list_installed_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SnapdClient *client = SNAPD_CLIENT (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginSnap *self = g_task_get_source_object (task); + ListAppsData *data = g_task_get_task_data (task); + g_autoptr(GPtrArray) snaps = NULL; + g_autoptr(GError) local_error = NULL; + + snaps = snapd_client_get_snaps_finish (client, result, &local_error); + + if (snaps == NULL) { + snapd_error_convert (&local_error); + } + + for (guint i = 0; snaps != NULL && i < snaps->len; i++) { + SnapdSnap *snap = g_ptr_array_index (snaps, i); + g_autoptr(GsApp) app = NULL; + + app = snap_to_app (self, snap, NULL); + gs_app_list_add (data->results_list, app); + } + + finish_list_apps_op (task, g_steal_pointer (&local_error)); +} + +static void +list_alternate_apps_snap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + ListAppsData *data = g_task_get_task_data (task); + g_autoptr(SnapdSnap) snap = NULL; + g_autoptr(GError) local_error = NULL; + + snap = get_store_snap_finish (self, result, &local_error); + + if (snap != NULL) + add_channels (self, snap, data->results_list); + + finish_list_apps_op (task, g_steal_pointer (&local_error)); +} + +static void +list_alternate_apps_nonsnap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SnapdClient *client = SNAPD_CLIENT (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginSnap *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + ListAppsData *data = g_task_get_task_data (task); + g_autoptr(GPtrArray) snaps = NULL; + g_autoptr(GError) local_error = NULL; + + snaps = snapd_client_find_section_finish (client, result, NULL, &local_error); + + if (snaps == NULL) { + snapd_error_convert (&local_error); + finish_list_apps_op (task, g_steal_pointer (&local_error)); + return; + } + + store_snap_cache_update (self, snaps, FALSE); + + for (guint i = 0; snaps != NULL && i < snaps->len; i++) { + SnapdSnap *snap = g_ptr_array_index (snaps, i); + + data->n_pending_ops++; + get_store_snap_async (self, client, snapd_snap_get_name (snap), + TRUE, cancellable, list_alternative_apps_nonsnap_get_store_snap_cb, g_object_ref (task)); + } + + finish_list_apps_op (task, NULL); +} + +static void +list_alternative_apps_nonsnap_get_store_snap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + ListAppsData *data = g_task_get_task_data (task); + g_autoptr(SnapdSnap) store_snap = NULL; + g_autoptr(GError) local_error = NULL; + + store_snap = get_store_snap_finish (self, result, &local_error); + + if (store_snap != NULL) + add_channels (self, store_snap, data->results_list); + + finish_list_apps_op (task, g_steal_pointer (&local_error)); +} + +static void +list_apps_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SnapdClient *client = SNAPD_CLIENT (source_object); + g_autoptr(GTask) task = G_TASK (user_data); + GsPluginSnap *self = g_task_get_source_object (task); + ListAppsData *data = g_task_get_task_data (task); + g_autoptr(GPtrArray) snaps = NULL; + g_autoptr(GError) local_error = NULL; + + snaps = snapd_client_find_section_finish (client, result, NULL, &local_error); + + if (snaps != NULL) { + store_snap_cache_update (self, snaps, FALSE); + + for (guint i = 0; i < snaps->len; i++) { + SnapdSnap *snap = g_ptr_array_index (snaps, i); + g_autoptr(GsApp) app = NULL; + + app = snap_to_app (self, snap, NULL); + gs_app_list_add (data->results_list, app); + } + } else { + snapd_error_convert (&local_error); + } + + finish_list_apps_op (task, g_steal_pointer (&local_error)); +} + +/* @error is (transfer full) if non-%NULL */ +static void +finish_list_apps_op (GTask *task, + GError *error) +{ + ListAppsData *data = g_task_get_task_data (task); + g_autoptr(GsAppList) results_list = NULL; + g_autoptr(GError) error_owned = g_steal_pointer (&error); + + if (error_owned != NULL && data->saved_error == NULL) + data->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 (data->n_pending_ops > 0); + data->n_pending_ops--; + + if (data->n_pending_ops > 0) + return; + + /* Get the results of the parallel ops. */ + results_list = g_steal_pointer (&data->results_list); + + if (data->saved_error != NULL) + g_task_return_error (task, g_steal_pointer (&data->saved_error)); + else + g_task_return_pointer (task, g_steal_pointer (&results_list), g_object_unref); +} + +static GsAppList * +gs_plugin_snap_list_apps_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +static SnapdSnap * +get_store_snap (GsPluginSnap *self, + SnapdClient *client, + const gchar *name, + gboolean need_details, + GCancellable *cancellable, + GError **error) +{ + SnapdSnap *snap = NULL; + g_autoptr(GPtrArray) snaps = NULL; + + /* use cached version if available */ + snap = store_snap_cache_lookup (self, name, need_details); + if (snap != NULL) + return g_object_ref (snap); + + snaps = find_snaps (self, client, + SNAPD_FIND_FLAGS_SCOPE_WIDE | SNAPD_FIND_FLAGS_MATCH_NAME, + NULL, name, cancellable, error); + if (snaps == NULL || snaps->len < 1) + return NULL; + + return g_object_ref (g_ptr_array_index (snaps, 0)); +} + +static void get_store_snap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +static void +get_store_snap_async (GsPluginSnap *self, + SnapdClient *client, + const gchar *name, + gboolean need_details, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + SnapdSnap *snap = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, get_store_snap_async); + + /* use cached version if available */ + snap = store_snap_cache_lookup (self, name, need_details); + if (snap != NULL) { + g_task_return_pointer (task, g_object_ref (snap), (GDestroyNotify) g_object_unref); + return; + } + + snapd_client_find_section_async (client, + SNAPD_FIND_FLAGS_SCOPE_WIDE | SNAPD_FIND_FLAGS_MATCH_NAME, + NULL, name, + cancellable, + get_store_snap_cb, g_steal_pointer (&task)); +} + +static void +get_store_snap_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SnapdClient *client = SNAPD_CLIENT (source_object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginSnap *self = g_task_get_source_object (task); + g_autoptr(GPtrArray) snaps = NULL; + g_autoptr(GError) local_error = NULL; + + snaps = snapd_client_find_section_finish (client, result, NULL, &local_error); + + if (snaps == NULL || snaps->len < 1) { + snapd_error_convert (&local_error); + g_task_return_error (task, g_steal_pointer (&local_error)); + } else { + store_snap_cache_update (self, snaps, TRUE); + g_task_return_pointer (task, g_object_ref (g_ptr_array_index (snaps, 0)), (GDestroyNotify) g_object_unref); + } +} + +static SnapdSnap * +get_store_snap_finish (GsPluginSnap *self, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +static int +track_value (const gchar *track, GStrv tracks) +{ + int r = 0; + while (tracks[r] != NULL && strcmp (track, tracks[r]) != 0) + r++; + return r; +} + +static int +risk_value (const gchar *risk) +{ + if (strcmp (risk, "stable") == 0) + return 0; + else if (strcmp (risk, "candidate") == 0) + return 1; + else if (strcmp (risk, "beta") == 0) + return 2; + else if (strcmp (risk, "edge") == 0) + return 3; + else + return 4; +} + +static int +compare_channel (gconstpointer a, gconstpointer b, gpointer user_data) +{ + SnapdChannel *channel_a = *(SnapdChannel **)a, *channel_b = *(SnapdChannel **)b; + GStrv tracks = user_data; + int r; + + r = track_value (snapd_channel_get_track (channel_a), tracks) - track_value (snapd_channel_get_track (channel_b), tracks); + if (r != 0) + return r; + + r = g_strcmp0 (snapd_channel_get_risk (channel_a), snapd_channel_get_risk (channel_b)); + if (r != 0) { + int r2; + + r2 = risk_value (snapd_channel_get_risk (channel_a)) - risk_value (snapd_channel_get_risk (channel_b)); + if (r2 != 0) + return r2; + else + return r; + } + + return g_strcmp0 (snapd_channel_get_branch (channel_a), snapd_channel_get_branch (channel_b)); +} + +static gchar * +expand_channel_name (const gchar *name) +{ + g_auto(GStrv) tokens = NULL; + const gchar *risks[] = { "stable", "candidate", "beta", "edge", NULL }; + + if (name == NULL) + return NULL; + + tokens = g_strsplit (name, "/", -1); + for (int i = 0; risks[i] != NULL; i++) { + if (strcmp (tokens[0], risks[i]) == 0) + return g_strconcat ("latest/", name, NULL); + } + + return g_strdup (name); +} + +static void +add_channels (GsPluginSnap *self, SnapdSnap *snap, GsAppList *list) +{ + GStrv tracks; + GPtrArray *channels; + g_autoptr(GPtrArray) sorted_channels = NULL; + + tracks = snapd_snap_get_tracks (snap); + channels = snapd_snap_get_channels (snap); + sorted_channels = g_ptr_array_new (); + for (guint i = 0; i < channels->len; i++) { + SnapdChannel *channel = g_ptr_array_index (channels, i); + g_ptr_array_add (sorted_channels, channel); + } + g_ptr_array_sort_with_data (sorted_channels, compare_channel, tracks); + + for (guint i = 0; i < sorted_channels->len; i++) { + SnapdChannel *channel = g_ptr_array_index (sorted_channels, i); + g_autoptr(GsApp) app = NULL; + g_autofree gchar *expanded_name = NULL; + + expanded_name = expand_channel_name (snapd_channel_get_name (channel)); + app = snap_to_app (self, snap, expanded_name); + + gs_app_list_add (list, app); + } +} + +static gboolean +app_name_matches_snap_name (SnapdSnap *snap, SnapdApp *app) +{ + return g_strcmp0 (snapd_snap_get_name (snap), snapd_app_get_name (app)) == 0; +} + +static SnapdApp * +get_primary_app (SnapdSnap *snap) +{ + GPtrArray *apps; + guint i; + SnapdApp *primary_app = NULL; + + /* Pick the "main" app from the snap. In order of + * preference, we want to pick: + * + * 1. the main app, provided it has a desktop file + * 2. the first app with a desktop file + * 3. the main app + * 4. the first app + * + * The "main app" is one whose name matches the snap name. + */ + apps = snapd_snap_get_apps (snap); + for (i = 0; i < apps->len; i++) { + SnapdApp *app = apps->pdata[i]; + + if (primary_app == NULL || + (snapd_app_get_desktop_file (primary_app) == NULL && snapd_app_get_desktop_file (app) != NULL) || + (!app_name_matches_snap_name (snap, primary_app) && app_name_matches_snap_name (snap, app))) + primary_app = app; + } + + return primary_app; +} + +static void +refine_icons (GsApp *app, + SnapdSnap *snap) +{ + GPtrArray *media; + guint i; + + media = snapd_snap_get_media (snap); + for (i = 0; i < media->len; i++) { + SnapdMedia *m = media->pdata[i]; + g_autoptr(GIcon) icon = NULL; + + if (g_strcmp0 (snapd_media_get_media_type (m), "icon") != 0) + continue; + + icon = gs_remote_icon_new (snapd_media_get_url (m)); + gs_icon_set_width (icon, snapd_media_get_width (m)); + gs_icon_set_height (icon, snapd_media_get_height (m)); + gs_app_add_icon (app, icon); + } +} + +static void serialize_node (SnapdMarkdownNode *node, GString *text, guint indentation); + +static gboolean +is_block_node (SnapdMarkdownNode *node) +{ + switch (snapd_markdown_node_get_node_type (node)) { + case SNAPD_MARKDOWN_NODE_TYPE_PARAGRAPH: + case SNAPD_MARKDOWN_NODE_TYPE_UNORDERED_LIST: + case SNAPD_MARKDOWN_NODE_TYPE_CODE_BLOCK: + return TRUE; + default: + return FALSE; + } +} + +static void +serialize_nodes (GPtrArray *nodes, GString *text, guint indentation) +{ + for (guint i = 0; i < nodes->len; i++) { + SnapdMarkdownNode *node = g_ptr_array_index (nodes, i); + + if (i != 0) { + SnapdMarkdownNode *last_node = g_ptr_array_index (nodes, i - 1); + if (is_block_node (node) && is_block_node (last_node)) + g_string_append (text, "\n"); + } + + serialize_node (node, text, indentation); + } +} + +static void +serialize_node (SnapdMarkdownNode *node, GString *text, guint indentation) +{ + GPtrArray *children = snapd_markdown_node_get_children (node); + g_autofree gchar *escaped_text = NULL; + g_autoptr(GString) url = NULL; + + switch (snapd_markdown_node_get_node_type (node)) { + case SNAPD_MARKDOWN_NODE_TYPE_TEXT: + escaped_text = g_markup_escape_text (snapd_markdown_node_get_text (node), -1); + g_string_append (text, escaped_text); + return; + + case SNAPD_MARKDOWN_NODE_TYPE_PARAGRAPH: + serialize_nodes (children, text, indentation); + g_string_append (text, "\n"); + return; + + case SNAPD_MARKDOWN_NODE_TYPE_UNORDERED_LIST: + serialize_nodes (children, text, indentation); + return; + + case SNAPD_MARKDOWN_NODE_TYPE_LIST_ITEM: + for (guint i = 0; i < indentation; i++) { + g_string_append (text, " "); + } + g_string_append_printf (text, " • "); + serialize_nodes (children, text, indentation + 1); + return; + + case SNAPD_MARKDOWN_NODE_TYPE_CODE_BLOCK: + case SNAPD_MARKDOWN_NODE_TYPE_CODE_SPAN: + g_string_append (text, "<tt>"); + serialize_nodes (children, text, indentation); + g_string_append (text, "</tt>"); + return; + + case SNAPD_MARKDOWN_NODE_TYPE_EMPHASIS: + g_string_append (text, "<i>"); + serialize_nodes (children, text, indentation); + g_string_append (text, "</i>"); + return; + + case SNAPD_MARKDOWN_NODE_TYPE_STRONG_EMPHASIS: + g_string_append (text, "<b>"); + serialize_nodes (children, text, indentation); + g_string_append (text, "</b>"); + return; + + case SNAPD_MARKDOWN_NODE_TYPE_URL: + url = g_string_new (""); + serialize_nodes (children, url, indentation); + g_string_append_printf (text, "<a href=\"%s\">%s</a>", url->str, url->str); + return; + + default: + g_assert_not_reached(); + } +} + +static gchar * +gs_plugin_snap_get_markup_description (SnapdSnap *snap) +{ + g_autoptr(SnapdMarkdownParser) parser = snapd_markdown_parser_new (SNAPD_MARKDOWN_VERSION_0); + g_autoptr(GPtrArray) nodes = NULL; + g_autoptr(GString) text = g_string_new (""); + + nodes = snapd_markdown_parser_parse (parser, snapd_snap_get_description (snap)); + serialize_nodes (nodes, text, 0); + return g_string_free (g_steal_pointer (&text), FALSE); +} + +static void +refine_screenshots (GsApp *app, SnapdSnap *snap) +{ + GPtrArray *media; + guint i; + + media = snapd_snap_get_media (snap); + for (i = 0; i < media->len; i++) { + SnapdMedia *m = media->pdata[i]; + const gchar *url; + g_autofree gchar *filename = NULL; + g_autoptr(AsScreenshot) ss = NULL; + g_autoptr(AsImage) image = NULL; + + if (g_strcmp0 (snapd_media_get_media_type (m), "screenshot") != 0) + continue; + + /* skip screenshots used for banner when app is featured */ + url = snapd_media_get_url (m); + filename = g_path_get_basename (url); + if (is_banner_image (filename) || is_banner_icon_image (filename)) + continue; + + ss = as_screenshot_new (); + as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_EXTRA); + image = as_image_new (); + as_image_set_url (image, snapd_media_get_url (m)); + as_image_set_kind (image, AS_IMAGE_KIND_SOURCE); + as_image_set_width (image, snapd_media_get_width (m)); + as_image_set_height (image, snapd_media_get_height (m)); + as_screenshot_add_image (ss, image); + gs_app_add_screenshot (app, ss); + } +} + +static gboolean +gs_snap_file_size_include_cb (const gchar *filename, + GFileTest file_kind, + gpointer user_data) +{ + return file_kind != G_FILE_TEST_IS_SYMLINK && + g_strcmp0 (filename, "common") != 0 && + g_strcmp0 (filename, "current") != 0; +} + +static guint64 +gs_snap_get_app_directory_size (const gchar *snap_name, + gboolean is_cache_size, + GCancellable *cancellable) +{ + g_autofree gchar *filename = NULL; + + if (is_cache_size) + filename = g_build_filename (g_get_home_dir (), "snap", snap_name, "common", NULL); + else + filename = g_build_filename (g_get_home_dir (), "snap", snap_name, NULL); + + return gs_utils_get_file_size (filename, is_cache_size ? NULL : gs_snap_file_size_include_cb, NULL, cancellable); +} + +static SnapdSnap * +find_snap_in_array (GPtrArray *snaps, + const gchar *snap_name) +{ + for (guint i = 0; i < snaps->len; i++) { + SnapdSnap *snap = SNAPD_SNAP (snaps->pdata[i]); + if (g_strcmp0 (snapd_snap_get_name (snap), snap_name) == 0) + return snap; + } + + return NULL; +} + +static void get_snaps_cb (GObject *object, + GAsyncResult *result, + gpointer user_data); + +static void +gs_plugin_snap_refine_async (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(SnapdClient) client = NULL; + g_autoptr(GPtrArray) snap_names = g_ptr_array_new_with_free_func (NULL); + g_autoptr(GTask) task = NULL; + g_autoptr(GsAppList) snap_apps = NULL; + g_autoptr(GsPluginRefineData) data = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + g_autoptr(GError) local_error = NULL; + + task = g_task_new (plugin, cancellable, callback, user_data); + g_task_set_source_tag (task, gs_plugin_snap_refine_async); + + /* Filter out apps that aren't managed by us */ + snap_apps = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + if (!gs_app_has_management_plugin (app, plugin)) + continue; + + gs_app_list_add (snap_apps, app); + } + + data = gs_plugin_refine_data_new (snap_apps, flags); + g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) gs_plugin_refine_data_free); + + client = get_client (self, interactive, &local_error); + if (client == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + /* Get information from locally installed snaps */ + for (guint i = 0; i < gs_app_list_length (snap_apps); i++) { + GsApp *app = gs_app_list_index (snap_apps, i); + g_ptr_array_add (snap_names, (gpointer) gs_app_get_metadata_item (app, "snap::name")); + } + + g_ptr_array_add (snap_names, NULL); /* NULL terminator */ + + snapd_client_get_snaps_async (client, SNAPD_GET_SNAPS_FLAGS_NONE, (gchar **) snap_names->pdata, cancellable, get_snaps_cb, g_steal_pointer (&task)); +} + +static void get_icon_cb (GObject *object, + GAsyncResult *result, + gpointer user_data); + +static void +get_snaps_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + SnapdClient *client = SNAPD_CLIENT (object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GsPluginSnap *self = g_task_get_source_object (task); + GCancellable *cancellable = g_task_get_cancellable (task); + GsPluginRefineData *data = g_task_get_task_data (task); + GsAppList *list = data->list; + GsPluginRefineFlags flags = data->flags; + g_autoptr(GPtrArray) local_snaps = NULL; + g_autoptr(GError) local_error = NULL; + + local_snaps = snapd_client_get_snaps_finish (client, result, &local_error); + if (local_snaps == NULL) { + g_task_return_error (task, g_steal_pointer (&local_error)); + return; + } + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + const gchar *snap_name, *name, *website, *contact, *version; + g_autofree gchar *channel = NULL; + g_autofree gchar *store_channel = NULL; + g_autofree gchar *tracking_channel = NULL; + gboolean need_details = FALSE; + SnapdConfinement confinement = SNAPD_CONFINEMENT_UNKNOWN; + SnapdSnap *local_snap, *snap; + g_autoptr(SnapdSnap) store_snap = NULL; + const gchar *developer_name; + g_autofree gchar *description = NULL; + guint64 release_date = 0; + + snap_name = gs_app_get_metadata_item (app, "snap::name"); + channel = g_strdup (gs_app_get_branch (app)); + + /* get information from locally installed snaps and information we already have */ + local_snap = find_snap_in_array (local_snaps, snap_name); + store_snap = store_snap_cache_lookup (self, snap_name, FALSE); + if (store_snap != NULL) + store_channel = expand_channel_name (snapd_snap_get_channel (store_snap)); + + /* check if requested information requires us to go to the Snap Store */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) + need_details = TRUE; + if (channel != NULL && g_strcmp0 (store_channel, channel) != 0) + need_details = TRUE; + if (need_details) { + g_clear_object (&store_snap); + store_snap = get_store_snap (self, client, snap_name, need_details, + cancellable, NULL); + } + + /* we don't know anything about this snap */ + if (local_snap == NULL && store_snap == NULL) + continue; + + if (local_snap != NULL) + tracking_channel = expand_channel_name (snapd_snap_get_tracking_channel (local_snap)); + + /* Get default channel to install */ + if (channel == NULL) { + if (local_snap != NULL) + channel = g_strdup (tracking_channel); + else + channel = expand_channel_name (snapd_snap_get_channel (store_snap)); + + gs_app_set_branch (app, channel); + } + + if (local_snap != NULL && g_strcmp0 (tracking_channel, channel) == 0) { + /* Do not set to installed state if app is updatable */ + if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE_LIVE) { + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + } + } else + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE); + + /* use store information for basic metadata over local information */ + snap = store_snap != NULL ? store_snap : local_snap; + name = snapd_snap_get_title (snap); + if (name == NULL || g_strcmp0 (name, "") == 0) + name = snapd_snap_get_name (snap); + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name); + website = snapd_snap_get_website (snap); + if (g_strcmp0 (website, "") == 0) + website = NULL; + gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, website); + contact = snapd_snap_get_contact (snap); + if (g_strcmp0 (contact, "") == 0) + contact = NULL; + gs_app_set_url (app, AS_URL_KIND_CONTACT, contact); + gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_summary (snap)); + description = gs_plugin_snap_get_markup_description (snap); + gs_app_set_description (app, GS_APP_QUALITY_NORMAL, description); + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, snapd_snap_get_license (snap)); + developer_name = snapd_snap_get_publisher_display_name (snap); + if (developer_name == NULL) + developer_name = snapd_snap_get_publisher_username (snap); + gs_app_set_developer_name (app, developer_name); + if (snapd_snap_get_publisher_validation (snap) == SNAPD_PUBLISHER_VALIDATION_VERIFIED) + gs_app_add_quirk (app, GS_APP_QUIRK_DEVELOPER_VERIFIED); + + snap = local_snap != NULL ? local_snap : store_snap; + version = snapd_snap_get_version (snap); + confinement = snapd_snap_get_confinement (snap); + + if (channel != NULL && store_snap != NULL) { + GPtrArray *channels = snapd_snap_get_channels (store_snap); + + for (guint j = 0; j < channels->len; j++) { + SnapdChannel *c = channels->pdata[j]; + g_autofree gchar *expanded_name = NULL; + GDateTime *dt; + + expanded_name = expand_channel_name (snapd_channel_get_name (c)); + if (g_strcmp0 (expanded_name, channel) != 0) + continue; + + version = snapd_channel_get_version (c); + confinement = snapd_channel_get_confinement (c); + + dt = snapd_channel_get_released_at (c); + if (dt) + release_date = (guint64) g_date_time_to_unix (dt); + } + } + + gs_app_set_version (app, version); + gs_app_set_release_date (app, release_date); + + if (confinement != SNAPD_CONFINEMENT_UNKNOWN) { + GEnumClass *enum_class = g_type_class_ref (SNAPD_TYPE_CONFINEMENT); + gs_app_set_metadata (app, "snap::confinement", g_enum_get_value (enum_class, confinement)->value_nick); + g_type_class_unref (enum_class); + } + + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS && + self->system_confinement == SNAPD_SYSTEM_CONFINEMENT_STRICT && + confinement == SNAPD_CONFINEMENT_STRICT) + gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED); + + gs_app_set_kind (app, snap_guess_component_kind (snap)); + + /* add information specific to installed snaps */ + if (local_snap != NULL) { + SnapdApp *snap_app; + GDateTime *install_date; + gint64 installed_size_bytes; + + install_date = snapd_snap_get_install_date (local_snap); + installed_size_bytes = snapd_snap_get_installed_size (local_snap); + + gs_app_set_size_installed (app, (installed_size_bytes > 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, (guint64) installed_size_bytes); + gs_app_set_install_date (app, install_date != NULL ? g_date_time_to_unix (install_date) : GS_APP_INSTALL_DATE_UNKNOWN); + + snap_app = get_primary_app (local_snap); + if (snap_app != NULL) { + gs_app_set_metadata (app, "snap::launch-name", snapd_app_get_name (snap_app)); + gs_app_set_metadata (app, "snap::launch-desktop", snapd_app_get_desktop_file (snap_app)); + } else { + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } + } + + /* add information specific to store snaps */ + if (store_snap != NULL) { + gint64 download_size_bytes; + + gs_app_set_origin (app, self->store_name); + gs_app_set_origin_hostname (app, self->store_hostname); + + download_size_bytes = snapd_snap_get_download_size (store_snap); + gs_app_set_size_download (app, (download_size_bytes > 0) ? GS_SIZE_TYPE_VALID : GS_SIZE_TYPE_UNKNOWN, (guint64) download_size_bytes); + + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS && gs_app_get_screenshots (app)->len == 0) + refine_screenshots (app, store_snap); + } + + /* load icon if requested */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) + refine_icons (app, snap); + + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE_DATA) != 0 && + gs_app_is_installed (app) && + gs_app_get_kind (app) != AS_COMPONENT_KIND_RUNTIME) { + if (gs_app_get_size_cache_data (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_cache_data (app, GS_SIZE_TYPE_VALID, gs_snap_get_app_directory_size (snap_name, TRUE, cancellable)); + if (gs_app_get_size_user_data (app, NULL) != GS_SIZE_TYPE_VALID) + gs_app_set_size_user_data (app, GS_SIZE_TYPE_VALID, gs_snap_get_app_directory_size (snap_name, FALSE, cancellable)); + + if (g_cancellable_is_cancelled (cancellable)) { + gs_app_set_size_cache_data (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + gs_app_set_size_user_data (app, GS_SIZE_TYPE_UNKNOWABLE, 0); + } + } + } + + /* Icons require async calls to get */ + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON && gs_app_list_length (list) > 0) { + GsApp *app = gs_app_list_index (list, 0); + snapd_client_get_icon_async (client, gs_app_get_metadata_item (app, "snap::name"), cancellable, get_icon_cb, g_steal_pointer (&task)); + } else { + g_task_return_boolean (task, TRUE); + } +} + +static void +get_icon_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + SnapdClient *client = SNAPD_CLIENT (object); + g_autoptr(GTask) task = g_steal_pointer (&user_data); + GCancellable *cancellable = g_task_get_cancellable (task); + GsPluginRefineData *data = g_task_get_task_data (task); + GsApp *app; + g_autoptr(SnapdIcon) snap_icon = NULL; + g_autoptr(GError) local_error = NULL; + + app = gs_app_list_index (data->list, 0); + snap_icon = snapd_client_get_icon_finish (client, result, &local_error); + if (snap_icon != NULL) { + g_autoptr(GIcon) icon = g_bytes_icon_new (snapd_icon_get_data (snap_icon)); + gs_app_add_icon (app, icon); + } + + /* Get next icon in the list or done */ + gs_app_list_remove (data->list, app); + if (gs_app_list_length (data->list) > 0) { + app = gs_app_list_index (data->list, 0); + snapd_client_get_icon_async (client, gs_app_get_metadata_item (app, "snap::name"), cancellable, get_icon_cb, g_steal_pointer (&task)); + } else { + g_task_return_boolean (task, TRUE); + } +} + +static gboolean +gs_plugin_snap_refine_finish (GsPlugin *plugin, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +progress_cb (SnapdClient *client, SnapdChange *change, gpointer deprecated, gpointer user_data) +{ + GsApp *app = user_data; + GPtrArray *tasks; + guint i; + gint64 done = 0, total = 0; + + tasks = snapd_change_get_tasks (change); + for (i = 0; i < tasks->len; i++) { + SnapdTask *task = tasks->pdata[i]; + done += snapd_task_get_progress_done (task); + total += snapd_task_get_progress_total (task); + } + + gs_app_set_progress (app, (guint) (100 * done / total)); +} + +gboolean +gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(SnapdClient) client = NULL; + const gchar *name, *channel; + SnapdInstallFlags flags = SNAPD_INSTALL_FLAGS_NONE; + gboolean result; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + g_autoptr(GError) error_local = NULL; + + /* We can only install apps we know of */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + client = get_client (self, interactive, error); + if (client == NULL) + return FALSE; + + name = gs_app_get_metadata_item (app, "snap::name"); + channel = gs_app_get_branch (app); + + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + + if (g_strcmp0 (gs_app_get_metadata_item (app, "snap::confinement"), "classic") == 0) + flags |= SNAPD_INSTALL_FLAGS_CLASSIC; + result = snapd_client_install2_sync (client, flags, name, channel, NULL, progress_cb, app, cancellable, &error_local); + + /* if already installed then just try to switch channel */ + if (!result && g_error_matches (error_local, SNAPD_ERROR, SNAPD_ERROR_ALREADY_INSTALLED)) { + g_clear_error (&error_local); + result = snapd_client_refresh_sync (client, name, channel, progress_cb, app, cancellable, &error_local); + } + + if (!result) { + gs_app_set_state_recover (app); + g_propagate_error (error, g_steal_pointer (&error_local)); + snapd_error_convert (error); + return FALSE; + } + + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + + return TRUE; +} + +// Check if an app is graphical by checking if it uses a known GUI interface. +// This doesn't necessarily mean that every binary uses this interfaces, but is probably true. +// https://bugs.launchpad.net/bugs/1595023 +static gboolean +is_graphical (GsPluginSnap *self, + SnapdClient *client, + GsApp *app, + GCancellable *cancellable) +{ + g_autoptr(GPtrArray) plugs = NULL; + guint i; + g_autoptr(GError) error = NULL; + + if (!snapd_client_get_connections2_sync (client, + SNAPD_GET_CONNECTIONS_FLAGS_SELECT_ALL, NULL, NULL, + NULL, NULL, &plugs, NULL, + cancellable, &error)) { + g_warning ("Failed to get connections: %s", error->message); + return FALSE; + } + + for (i = 0; i < plugs->len; i++) { + SnapdPlug *plug = plugs->pdata[i]; + const gchar *interface; + + // Only looks at the plugs for this snap + if (g_strcmp0 (snapd_plug_get_snap (plug), gs_app_get_metadata_item (app, "snap::name")) != 0) + continue; + + interface = snapd_plug_get_interface (plug); + if (interface == NULL) + continue; + + if (g_strcmp0 (interface, "unity7") == 0 || g_strcmp0 (interface, "x11") == 0 || g_strcmp0 (interface, "mir") == 0) + return TRUE; + } + + return FALSE; +} + +gboolean +gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + const gchar *launch_name; + const gchar *launch_desktop; + g_autoptr(GAppInfo) info = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + /* We can only launch apps we know of */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + launch_name = gs_app_get_metadata_item (app, "snap::launch-name"); + launch_desktop = gs_app_get_metadata_item (app, "snap::launch-desktop"); + if (!launch_name) + return TRUE; + + if (launch_desktop) { + info = (GAppInfo *)g_desktop_app_info_new_from_filename (launch_desktop); + } else { + g_autofree gchar *commandline = NULL; + g_autoptr(SnapdClient) client = NULL; + GAppInfoCreateFlags flags = G_APP_INFO_CREATE_NONE; + + if (g_strcmp0 (launch_name, gs_app_get_metadata_item (app, "snap::name")) == 0) + commandline = g_strdup_printf ("snap run %s", launch_name); + else + commandline = g_strdup_printf ("snap run %s.%s", gs_app_get_metadata_item (app, "snap::name"), launch_name); + + client = get_client (self, interactive, error); + if (client == NULL) + return FALSE; + + if (!is_graphical (self, client, app, cancellable)) + flags |= G_APP_INFO_CREATE_NEEDS_TERMINAL; + info = g_app_info_create_from_commandline (commandline, NULL, flags, error); + } + + if (info == NULL) + return FALSE; + + return g_app_info_launch (info, NULL, NULL, error); +} + +gboolean +gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(SnapdClient) client = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + /* We can only remove apps we know of */ + if (!gs_app_has_management_plugin (app, plugin)) + return TRUE; + + client = get_client (self, interactive, error); + if (client == NULL) + return FALSE; + + gs_app_set_state (app, GS_APP_STATE_REMOVING); + if (!snapd_client_remove2_sync (client, SNAPD_REMOVE_FLAGS_NONE, gs_app_get_metadata_item (app, "snap::name"), progress_cb, app, cancellable, error)) { + gs_app_set_state_recover (app); + snapd_error_convert (error); + return FALSE; + } + gs_app_set_state (app, GS_APP_STATE_AVAILABLE); + return TRUE; +} + +gboolean +gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(GPtrArray) apps = NULL; + g_autoptr(SnapdClient) client = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + g_autoptr(GError) error_local = NULL; + + client = get_client (self, interactive, &error_local); + if (client == NULL) { + g_debug ("Failed to get client to get updates: %s", error_local->message); + return TRUE; + } + + /* Get the list of refreshable snaps */ + apps = snapd_client_find_refreshable_sync (client, cancellable, &error_local); + if (apps == NULL) { + g_warning ("Failed to find refreshable snaps: %s", error_local->message); + return TRUE; + } + + for (guint i = 0; i < apps->len; i++) { + SnapdSnap *snap = g_ptr_array_index (apps, i); + g_autoptr(GsApp) app = NULL; + + /* Convert SnapdSnap to a GsApp */ + app = snap_to_app (self, snap, NULL); + + /* If for some reason the app is already getting updated, then + * don't change its state */ + if (gs_app_get_state (app) != GS_APP_STATE_INSTALLING) + gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE); + + /* Add GsApp to updatable GsAppList */ + gs_app_list_add (list, app); + } + + return TRUE; +} + +gboolean +gs_plugin_update (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginSnap *self = GS_PLUGIN_SNAP (plugin); + g_autoptr(SnapdClient) client = NULL; + gboolean interactive = gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); + + client = get_client (self, interactive, error); + if (client == NULL) + return FALSE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + /* Get the name of the snap to refresh */ + GsApp *app = gs_app_list_index (list, i); + const gchar *name = gs_app_get_metadata_item (app, "snap::name"); + + /* Refresh the snap */ + gs_app_set_state (app, GS_APP_STATE_INSTALLING); + + if (!snapd_client_refresh_sync (client, name, NULL, progress_cb, app, cancellable, error)) { + gs_app_set_state_recover (app); + snapd_error_convert (error); + return FALSE; + } + + gs_app_set_state (app, GS_APP_STATE_INSTALLED); + } + + return TRUE; +} + +static void +gs_plugin_snap_class_init (GsPluginSnapClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass); + + object_class->dispose = gs_plugin_snap_dispose; + object_class->finalize = gs_plugin_snap_finalize; + + plugin_class->setup_async = gs_plugin_snap_setup_async; + plugin_class->setup_finish = gs_plugin_snap_setup_finish; + plugin_class->refine_async = gs_plugin_snap_refine_async; + plugin_class->refine_finish = gs_plugin_snap_refine_finish; + plugin_class->list_apps_async = gs_plugin_snap_list_apps_async; + plugin_class->list_apps_finish = gs_plugin_snap_list_apps_finish; +} + +GType +gs_plugin_query_type (void) +{ + return GS_TYPE_PLUGIN_SNAP; +} diff --git a/plugins/snap/gs-plugin-snap.h b/plugins/snap/gs-plugin-snap.h new file mode 100644 index 0000000..26702a9 --- /dev/null +++ b/plugins/snap/gs-plugin-snap.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_SNAP (gs_plugin_snap_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginSnap, gs_plugin_snap, GS, PLUGIN_SNAP, GsPlugin) + +G_END_DECLS diff --git a/plugins/snap/gs-self-test.c b/plugins/snap/gs-self-test.c new file mode 100644 index 0000000..e748866 --- /dev/null +++ b/plugins/snap/gs-self-test.c @@ -0,0 +1,395 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Canonical Ltd + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <snapd-glib/snapd-glib.h> + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static gboolean snap_installed = FALSE; + +SnapdAuthData * +snapd_login_sync (const gchar *username, const gchar *password, const gchar *otp, + GCancellable *cancellable, GError **error) +{ + return snapd_auth_data_new ("macaroon", NULL); +} + +SnapdClient * +snapd_client_new (void) +{ + /* use a dummy object - we intercept all snapd-glib calls */ + return g_object_new (G_TYPE_OBJECT, NULL); +} + +void +snapd_client_set_allow_interaction (SnapdClient *client, gboolean allow_interaction) +{ +} + +void +snapd_client_set_auth_data (SnapdClient *client, SnapdAuthData *auth_data) +{ +} + +gboolean +snapd_client_connect_sync (SnapdClient *client, GCancellable *cancellable, GError **error) +{ + /* skip connection */ + return TRUE; +} + +const gchar * +snapd_client_get_user_agent (SnapdClient *client) +{ + return "snapd-glib/0.0.1"; +} + +void +snapd_client_set_user_agent (SnapdClient *client, const gchar *user_agent) +{ +} + +SnapdSystemInformation * +snapd_client_get_system_information_sync (SnapdClient *client, GCancellable *cancellable, GError **error) +{ + g_autoptr(GHashTable) sandbox_features = g_hash_table_new (g_str_hash, g_str_equal); + return g_object_new (SNAPD_TYPE_SYSTEM_INFORMATION, + "version", "2.31", + "confinement", SNAPD_SYSTEM_CONFINEMENT_STRICT, + "sandbox-features", sandbox_features, + NULL); +} + +static SnapdSnap * +make_snap (const gchar *name, SnapdSnapStatus status) +{ + gchar *common_ids[] = { NULL }; + g_autoptr(GDateTime) install_date = NULL; + g_autoptr(GPtrArray) apps = NULL; + g_autoptr(GPtrArray) media = NULL; + SnapdMedia *m; + + install_date = g_date_time_new_utc (2017, 1, 2, 11, 23, 58); + + apps = g_ptr_array_new_with_free_func (g_object_unref); + + media = g_ptr_array_new_with_free_func (g_object_unref); + m = g_object_new (SNAPD_TYPE_MEDIA, + "type", "screenshot", + "url", "http://example.com/screenshot1.jpg", + "width", 640, + "height", 480, + NULL); + g_ptr_array_add (media, m); + m = g_object_new (SNAPD_TYPE_MEDIA, + "type", "screenshot", + "url", "http://example.com/screenshot2.jpg", + "width", 1024, + "height", 768, + NULL); + g_ptr_array_add (media, m); + + return g_object_new (SNAPD_TYPE_SNAP, + "apps", status == SNAPD_SNAP_STATUS_INSTALLED ? apps : NULL, + "common-ids", common_ids, + "description", "DESCRIPTION", + "download-size", status == SNAPD_SNAP_STATUS_AVAILABLE ? 500 : 0, + "icon", status == SNAPD_SNAP_STATUS_AVAILABLE ? NULL : "/icon", + "id", name, + "install-date", status == SNAPD_SNAP_STATUS_INSTALLED ? install_date : NULL, + "installed-size", status == SNAPD_SNAP_STATUS_INSTALLED ? 1000 : 0, + "media", status == SNAPD_SNAP_STATUS_AVAILABLE ? media : NULL, + "name", name, + "status", status, + "snap-type", SNAPD_SNAP_TYPE_APP, + "summary", "SUMMARY", + "version", "VERSION", + NULL); +} + +GPtrArray * +snapd_client_get_snaps_sync (SnapdClient *client, + SnapdGetSnapsFlags flags, gchar **names, + GCancellable *cancellable, GError **error) +{ + GPtrArray *snaps; + + snaps = g_ptr_array_new_with_free_func (g_object_unref); + if (snap_installed) + g_ptr_array_add (snaps, make_snap ("snap", SNAPD_SNAP_STATUS_INSTALLED)); + + return snaps; +} + +SnapdSnap * +snapd_client_get_snap_sync (SnapdClient *client, + const gchar *name, + GCancellable *cancellable, GError **error) +{ + if (snap_installed) { + return make_snap ("snap", SNAPD_SNAP_STATUS_INSTALLED); + } else { + g_set_error_literal (error, SNAPD_ERROR, SNAPD_ERROR_NOT_INSTALLED, "not installed"); + return NULL; + } +} + +SnapdIcon * +snapd_client_get_icon_sync (SnapdClient *client, + const gchar *name, + GCancellable *cancellable, GError **error) +{ + g_autoptr(GBytes) data = NULL; + /* apparently this is the smallest valid PNG file (1x1) */ + const gchar png_data[67] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, + 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, + 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, + 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82 }; + + data = g_bytes_new (png_data, 67); + return g_object_new (SNAPD_TYPE_ICON, + "mime-type", "image/png", + "data", data, + NULL); +} + +gboolean +snapd_client_get_connections_sync (SnapdClient *client, + GPtrArray **established, GPtrArray **undesired, + GPtrArray **plugs, GPtrArray **slots, + GCancellable *cancellable, GError **error) +{ + if (plugs) + *plugs = g_ptr_array_new_with_free_func (g_object_unref); + if (slots) + *slots = g_ptr_array_new_with_free_func (g_object_unref); + return TRUE; +} + +GPtrArray * +snapd_client_find_section_sync (SnapdClient *client, + SnapdFindFlags flags, + const gchar *section, const gchar *query, + gchar **suggested_currency, + GCancellable *cancellable, GError **error) +{ + GPtrArray *snaps; + + snaps = g_ptr_array_new_with_free_func (g_object_unref); + g_ptr_array_add (snaps, make_snap ("snap", SNAPD_SNAP_STATUS_AVAILABLE)); + + return snaps; +} + +gboolean +snapd_client_install2_sync (SnapdClient *client, + SnapdInstallFlags flags, + const gchar *name, const gchar *channel, const gchar *revision, + SnapdProgressCallback progress_callback, gpointer progress_callback_data, + GCancellable *cancellable, GError **error) +{ + g_autoptr(SnapdChange) change = NULL; + g_autoptr(GPtrArray) tasks = NULL; + SnapdTask *task; + + g_assert_cmpstr (name, ==, "snap"); + g_assert (channel == NULL); + + tasks = g_ptr_array_new_with_free_func (g_object_unref); + task = g_object_new (SNAPD_TYPE_TASK, + "progress-done", 0, + "progress-total", 1, + NULL); + g_ptr_array_add (tasks, task); + change = g_object_new (SNAPD_TYPE_CHANGE, + "tasks", tasks, + NULL); + progress_callback (client, change, NULL, progress_callback_data); + + snap_installed = TRUE; + return TRUE; +} + +gboolean +snapd_client_remove_sync (SnapdClient *client, + const gchar *name, + SnapdProgressCallback progress_callback, gpointer progress_callback_data, + GCancellable *cancellable, GError **error) +{ + g_autoptr(SnapdChange) change = NULL; + g_autoptr(GPtrArray) tasks = NULL; + SnapdTask *task; + + g_assert_cmpstr (name, ==, "snap"); + + tasks = g_ptr_array_new_with_free_func (g_object_unref); + task = g_object_new (SNAPD_TYPE_TASK, + "progress-done", 0, + "progress-total", 1, + NULL); + g_ptr_array_add (tasks, task); + change = g_object_new (SNAPD_TYPE_CHANGE, + "tasks", tasks, + NULL); + progress_callback (client, change, NULL, progress_callback_data); + + snap_installed = FALSE; + return TRUE; +} + +static void +gs_plugins_snap_test_func (GsPluginLoader *plugin_loader) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppList) apps = NULL; + gboolean ret; + GsApp *app; + GPtrArray *screenshots, *images; + AsScreenshot *screenshot; + AsImage *image; + g_autoptr(GIcon) icon = NULL; + g_autoptr(GInputStream) icon_stream = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GError) error = NULL; + GsSizeType size_installed_type, size_download_type; + guint64 size_installed_bytes, size_download_bytes; + const gchar *keywords[] = { NULL, }; + g_autoptr(GsAppQuery) query = NULL; + + /* no snap, abort */ + if (!gs_plugin_loader_get_enabled (plugin_loader, "snap")) { + g_test_skip ("not enabled"); + return; + } + + keywords[0] = "snap"; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS, + "dedupe-flags", GS_PLUGIN_JOB_DEDUPE_FLAGS_DEFAULT, + "sort-func", gs_utils_app_sort_match_value, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + apps = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert (apps != NULL); + g_assert_cmpint (gs_app_list_length (apps), ==, 1); + app = gs_app_list_index (apps, 0); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_AVAILABLE); + g_assert_cmpstr (gs_app_get_name (app), ==, "snap"); + g_assert_cmpstr (gs_app_get_version (app), ==, "VERSION"); + g_assert_cmpstr (gs_app_get_summary (app), ==, "SUMMARY"); + g_assert_cmpstr (gs_app_get_description (app), ==, "DESCRIPTION"); + screenshots = gs_app_get_screenshots (app); + g_assert_cmpint (screenshots->len, ==, 2); + screenshot = g_ptr_array_index (screenshots, 0); + images = as_screenshot_get_images (screenshot); + g_assert_cmpint (images->len, ==, 1); + image = g_ptr_array_index (images, 0); + g_assert_cmpstr (as_image_get_url (image), ==, "http://example.com/screenshot1.jpg"); + g_assert_cmpint (as_image_get_width (image), ==, 640); + g_assert_cmpint (as_image_get_height (image), ==, 480); + screenshot = g_ptr_array_index (screenshots, 1); + images = as_screenshot_get_images (screenshot); + g_assert_cmpint (images->len, ==, 1); + image = g_ptr_array_index (images, 0); + g_assert_cmpstr (as_image_get_url (image), ==, "http://example.com/screenshot2.jpg"); + g_assert_cmpint (as_image_get_width (image), ==, 1024); + g_assert_cmpint (as_image_get_height (image), ==, 768); + icon = gs_app_get_icon_for_size (app, 64, 1, NULL); + g_assert_null (icon); + + size_installed_type = gs_app_get_size_installed (app, &size_installed_bytes); + g_assert_cmpint (size_installed_type, ==, GS_SIZE_TYPE_VALID); + g_assert_cmpuint (size_installed_bytes, ==, 0); + + size_download_type = gs_app_get_size_download (app, &size_download_bytes); + g_assert_cmpint (size_download_type, ==, GS_SIZE_TYPE_VALID); + g_assert_cmpuint (size_download_bytes, ==, 500); + + g_assert_cmpint (gs_app_get_install_date (app), ==, 0); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED); + + size_installed_type = gs_app_get_size_installed (app, &size_installed_bytes); + g_assert_cmpint (size_installed_type, ==, GS_SIZE_TYPE_VALID); + g_assert_cmpuint (size_installed_bytes, ==, 1000); + + g_assert_cmpint (gs_app_get_install_date (app), ==, g_date_time_to_unix (g_date_time_new_utc (2017, 1, 2, 11, 23, 58))); + + icon = gs_app_get_icon_for_size (app, 128, 1, NULL); + g_assert_nonnull (icon); + g_assert_true (G_IS_LOADABLE_ICON (icon)); + icon_stream = g_loadable_icon_load (G_LOADABLE_ICON (icon), 128, NULL, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (icon_stream); + pixbuf = gdk_pixbuf_new_from_stream (icon_stream, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (pixbuf); + g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, 128); + g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, 128); + + g_object_unref (plugin_job); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "app", app, + NULL); + gs_test_flush_main_context (); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + g_assert_no_error (error); + g_assert (ret); +} + +int +main (int argc, char **argv) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar * const allowlist[] = { + "snap", + NULL + }; + + gs_test_init (&argc, &argv); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (NULL, NULL); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR_CORE); + ret = gs_plugin_loader_setup (plugin_loader, + allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/snap/test", + plugin_loader, + (GTestDataFunc) gs_plugins_snap_test_func); + return g_test_run (); +} diff --git a/plugins/snap/meson.build b/plugins/snap/meson.build new file mode 100644 index 0000000..eceee1a --- /dev/null +++ b/plugins/snap/meson.build @@ -0,0 +1,48 @@ +cargs = ['-DG_LOG_DOMAIN="GsPluginSnap"'] + +shared_module( + 'gs_plugin_snap', + sources : [ + 'gs-plugin-snap.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ plugin_libs, snap ], +) +metainfo = 'org.gnome.Software.Plugin.Snap.metainfo.xml' + +i18n.merge_file( + input: metainfo + '.in', + output: metainfo, + type: 'xml', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'metainfo') +) + +if get_option('tests') + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DLOCALPLUGINDIR_CORE="' + meson.current_build_dir() + '/../core"'] + e = executable( + 'gs-self-test-snap', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + snap + ], + c_args : cargs, + ) + test('gs-self-test-snap', e, suite: ['plugins', 'snap'], env: test_env) +endif diff --git a/plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in b/plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in new file mode 100644 index 0000000..2478778 --- /dev/null +++ b/plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> --> +<component type="addon"> + <id>org.gnome.Software.Plugin.Snap</id> + <extends>org.gnome.Software.desktop</extends> + <name>Snap Support</name> + <summary>A snap is a universal Linux package</summary> + <url type="homepage">https://snapcraft.io/</url> + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-2.0+</project_license> + <update_contact>richard_at_hughsie.com</update_contact> +</component> |