From 6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:57:27 +0200 Subject: Adding upstream version 43.5. Signed-off-by: Daniel Baumann --- plugins/core/gs-plugin-appstream.c | 1653 ++++++++++++++++++++++++++ plugins/core/gs-plugin-appstream.h | 22 + plugins/core/gs-plugin-generic-updates.c | 148 +++ plugins/core/gs-plugin-generic-updates.h | 22 + plugins/core/gs-plugin-hardcoded-blocklist.c | 121 ++ plugins/core/gs-plugin-hardcoded-blocklist.h | 22 + plugins/core/gs-plugin-icons.c | 249 ++++ plugins/core/gs-plugin-icons.h | 22 + plugins/core/gs-plugin-os-release.c | 180 +++ plugins/core/gs-plugin-os-release.h | 22 + plugins/core/gs-plugin-provenance-license.c | 199 ++++ plugins/core/gs-plugin-provenance-license.h | 22 + plugins/core/gs-plugin-provenance.c | 292 +++++ plugins/core/gs-plugin-provenance.h | 22 + plugins/core/gs-plugin-rewrite-resource.c | 252 ++++ plugins/core/gs-plugin-rewrite-resource.h | 22 + plugins/core/gs-self-test.c | 277 +++++ plugins/core/meson.build | 133 +++ plugins/core/tests/os-release | 1 + 19 files changed, 3681 insertions(+) create mode 100644 plugins/core/gs-plugin-appstream.c create mode 100644 plugins/core/gs-plugin-appstream.h create mode 100644 plugins/core/gs-plugin-generic-updates.c create mode 100644 plugins/core/gs-plugin-generic-updates.h create mode 100644 plugins/core/gs-plugin-hardcoded-blocklist.c create mode 100644 plugins/core/gs-plugin-hardcoded-blocklist.h create mode 100644 plugins/core/gs-plugin-icons.c create mode 100644 plugins/core/gs-plugin-icons.h create mode 100644 plugins/core/gs-plugin-os-release.c create mode 100644 plugins/core/gs-plugin-os-release.h create mode 100644 plugins/core/gs-plugin-provenance-license.c create mode 100644 plugins/core/gs-plugin-provenance-license.h create mode 100644 plugins/core/gs-plugin-provenance.c create mode 100644 plugins/core/gs-plugin-provenance.h create mode 100644 plugins/core/gs-plugin-rewrite-resource.c create mode 100644 plugins/core/gs-plugin-rewrite-resource.h create mode 100644 plugins/core/gs-self-test.c create mode 100644 plugins/core/meson.build create mode 120000 plugins/core/tests/os-release (limited to 'plugins/core') 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 + * Copyright (C) 2015-2019 Kalev Lember + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include +#include +#include +#include +#include + +#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 . In that way, + * "GNOME-Classic:GNOME" shares compulsory apps with GNOME. + * + * As a special case, if the 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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include +#include + +#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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include +#include + +#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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * Copyright (C) 2015 Kalev Lember + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include +#include + +#include + +#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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include + +#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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * Copyright (C) 2016 Matthias Klumpp + * Copyright (C) 2018 Kalev Lember + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include + +#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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * Copyright (C) 2018 Kalev Lember + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include + +#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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include + +#include +#include + +#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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include +#include + +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 + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include + +#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 = "\n" + "\n" + " \n" + " arachne.desktop\n" + " test\n" + " Test\n" + " system-file-manager\n" + " arachne\n" + " \n" + " \n" + " org.fedoraproject.fedora-25\n" + " Fedora\n" + " Fedora Workstation\n" + " fedora-release\n" + " \n" + " \n" + " user\n" + " \n" + "\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 -- cgit v1.2.3