summaryrefslogtreecommitdiffstats
path: root/lib/gs-appstream.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gs-appstream.c')
-rw-r--r--lib/gs-appstream.c2165
1 files changed, 2165 insertions, 0 deletions
diff --git a/lib/gs-appstream.c b/lib/gs-appstream.c
new file mode 100644
index 0000000..fa94a6e
--- /dev/null
+++ b/lib/gs-appstream.c
@@ -0,0 +1,2165 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2015-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2018-2019 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <gnome-software.h>
+#include <locale.h>
+
+#include "gs-appstream.h"
+
+#define GS_APPSTREAM_MAX_SCREENSHOTS 5
+
+GsApp *
+gs_appstream_create_app (GsPlugin *plugin, XbSilo *silo, XbNode *component, GError **error)
+{
+ GsApp *app;
+ g_autoptr(GsApp) app_new = NULL;
+
+ /* The 'plugin' can be NULL, when creating app for --show-metainfo */
+ g_return_val_if_fail (XB_IS_SILO (silo), NULL);
+ g_return_val_if_fail (XB_IS_NODE (component), NULL);
+
+ app_new = gs_app_new (NULL);
+
+ /* refine enough to get the unique ID */
+ if (!gs_appstream_refine_app (plugin, app_new, silo, component,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID,
+ error))
+ return NULL;
+
+ /* never add wildcard apps to the plugin cache, and only add to
+ * the cache if it’s available */
+ if (gs_app_has_quirk (app_new, GS_APP_QUIRK_IS_WILDCARD) || plugin == NULL)
+ return g_steal_pointer (&app_new);
+
+ if (plugin == NULL)
+ return g_steal_pointer (&app_new);
+
+ /* look for existing object */
+ app = gs_plugin_cache_lookup (plugin, gs_app_get_unique_id (app_new));
+ if (app != NULL)
+ return app;
+
+ /* use the temp object we just created */
+ gs_app_set_metadata (app_new, "GnomeSoftware::Creator",
+ gs_plugin_get_name (plugin));
+ gs_plugin_cache_add (plugin, NULL, app_new);
+ return g_steal_pointer (&app_new);
+}
+
+/* Helper function to do the equivalent of
+ * *node = xb_node_get_next (*node)
+ * but with correct reference counting, since xb_node_get_next() returns a new
+ * ref. */
+static void
+node_set_to_next (XbNode **node)
+{
+ g_autoptr(XbNode) next_node = NULL;
+
+ g_assert (node != NULL);
+ g_assert (*node != NULL);
+
+ next_node = xb_node_get_next (*node);
+ g_object_unref (*node);
+ *node = g_steal_pointer (&next_node);
+}
+
+/* Returns escaped text */
+static gchar *
+gs_appstream_format_description_text (XbNode *node)
+{
+ g_autoptr(GString) str = g_string_new (NULL);
+ const gchar *node_text;
+
+ if (node == NULL)
+ return NULL;
+
+ node_text = xb_node_get_text (node);
+ if (node_text != NULL && *node_text != '\0') {
+ g_autofree gchar *escaped = g_markup_escape_text (node_text, -1);
+ g_string_append (str, escaped);
+ }
+
+ for (g_autoptr(XbNode) n = xb_node_get_child (node); n != NULL; node_set_to_next (&n)) {
+ const gchar *start_elem = "", *end_elem = "";
+ g_autofree gchar *text = NULL;
+ if (g_strcmp0 (xb_node_get_element (n), "em") == 0) {
+ start_elem = "<i>";
+ end_elem = "</i>";
+ } else if (g_strcmp0 (xb_node_get_element (n), "code") == 0) {
+ start_elem = "<tt>";
+ end_elem = "</tt>";
+ }
+
+ /* These can be nested */
+ text = gs_appstream_format_description_text (n);
+ if (text != NULL) {
+ g_string_append_printf (str, "%s%s%s", start_elem, text, end_elem);
+ }
+
+ node_text = xb_node_get_tail (n);
+ if (node_text != NULL && *node_text != '\0') {
+ g_autofree gchar *escaped = g_markup_escape_text (node_text, -1);
+ g_string_append (str, escaped);
+ }
+ }
+
+ if (str->len == 0)
+ return NULL;
+
+ return g_string_free (g_steal_pointer (&str), FALSE);
+}
+
+static gchar *
+gs_appstream_format_description (XbNode *root, GError **error)
+{
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ for (g_autoptr(XbNode) n = xb_node_get_child (root); n != NULL; node_set_to_next (&n)) {
+ /* support <p>, <em>, <code>, <ul>, <ol> and <li>, ignore all else */
+ if (g_strcmp0 (xb_node_get_element (n), "p") == 0) {
+ g_autofree gchar *escaped = gs_appstream_format_description_text (n);
+ /* Treat a self-closing paragraph (`<p/>`) as
+ * nonexistent. This is consistent with Firefox. */
+ if (escaped != NULL)
+ g_string_append_printf (str, "%s\n\n", escaped);
+ } else if (g_strcmp0 (xb_node_get_element (n), "ul") == 0) {
+ g_autoptr(GPtrArray) children = xb_node_get_children (n);
+
+ for (guint i = 0; i < children->len; i++) {
+ XbNode *nc = g_ptr_array_index (children, i);
+ if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) {
+ g_autofree gchar *escaped = gs_appstream_format_description_text (nc);
+
+ /* Treat a self-closing `<li/>` as an empty
+ * list element (equivalent to `<li></li>`).
+ * This is consistent with Firefox. */
+ g_string_append_printf (str, " • %s\n",
+ (escaped != NULL) ? escaped : "");
+ }
+ }
+ g_string_append (str, "\n");
+ } else if (g_strcmp0 (xb_node_get_element (n), "ol") == 0) {
+ g_autoptr(GPtrArray) children = xb_node_get_children (n);
+ for (guint i = 0; i < children->len; i++) {
+ XbNode *nc = g_ptr_array_index (children, i);
+ if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) {
+ g_autofree gchar *escaped = gs_appstream_format_description_text (nc);
+
+ /* Treat self-closing elements as with `<ul>` above. */
+ g_string_append_printf (str, " %u. %s\n",
+ i + 1,
+ (escaped != NULL) ? escaped : "");
+ }
+ }
+ g_string_append (str, "\n");
+ }
+ }
+
+ /* remove extra newlines */
+ while (str->len > 0 && str->str[str->len - 1] == '\n')
+ g_string_truncate (str, str->len - 1);
+
+ /* success */
+ return g_string_free (g_steal_pointer (&str), FALSE);
+}
+
+static gchar *
+gs_appstream_build_icon_prefix (XbNode *component)
+{
+ const gchar *origin;
+ const gchar *tmp;
+ gint npath;
+ g_auto(GStrv) path = NULL;
+ g_autoptr(XbNode) components = NULL;
+
+ /* no parent, e.g. AppData */
+ components = xb_node_get_parent (component);
+ if (components == NULL)
+ return NULL;
+
+ /* set explicitly */
+ tmp = xb_node_query_text (components, "info/icon-prefix", NULL);
+ if (tmp != NULL)
+ return g_strdup (tmp);
+
+ /* fall back to origin */
+ origin = xb_node_get_attr (components, "origin");
+ if (origin == NULL)
+ return NULL;
+
+ /* no metadata */
+ tmp = xb_node_query_text (components, "info/filename", NULL);
+ if (tmp == NULL)
+ return NULL;
+
+ /* check format */
+ path = g_strsplit (tmp, "/", -1);
+ npath = g_strv_length (path);
+ if (npath < 3 ||
+ !(g_strcmp0 (path[npath-2], "xmls") == 0 ||
+ g_strcmp0 (path[npath-2], "yaml") == 0 ||
+ g_strcmp0 (path[npath-2], "xml") == 0))
+ return NULL;
+
+ /* fix the new path */
+ g_free (path[npath-1]);
+ g_free (path[npath-2]);
+ path[npath-1] = g_strdup (origin);
+ path[npath-2] = g_strdup ("icons");
+ return g_strjoinv ("/", path);
+}
+
+/* This function is designed to do no disk or network I/O. */
+static AsIcon *
+gs_appstream_new_icon (XbNode *component, XbNode *n, AsIconKind icon_kind, guint sz)
+{
+ AsIcon *icon = as_icon_new ();
+ g_autofree gchar *icon_path = NULL;
+ as_icon_set_kind (icon, icon_kind);
+ switch (icon_kind) {
+ case AS_ICON_KIND_LOCAL:
+ as_icon_set_filename (icon, xb_node_get_text (n));
+ break;
+ case AS_ICON_KIND_REMOTE:
+ as_icon_set_url (icon, xb_node_get_text (n));
+ break;
+ default:
+ as_icon_set_name (icon, xb_node_get_text (n));
+ }
+ if (sz == 0) {
+ guint64 width = xb_node_get_attr_as_uint (n, "width");
+ if (width > 0 && width < G_MAXUINT)
+ sz = width;
+ }
+
+ if (sz > 0) {
+ as_icon_set_width (icon, sz);
+ as_icon_set_height (icon, sz);
+ }
+
+ if (icon_kind != AS_ICON_KIND_LOCAL && icon_kind != AS_ICON_KIND_REMOTE) {
+ /* add partial filename for now, we will compose the full one later */
+ icon_path = gs_appstream_build_icon_prefix (component);
+ as_icon_set_filename (icon, icon_path);
+ }
+ return icon;
+}
+
+static void
+app_add_icon (GsApp *app,
+ AsIcon *as_icon)
+{
+ g_autoptr(GIcon) icon = gs_icon_new_for_appstream_icon (as_icon);
+ if (icon != NULL)
+ gs_app_add_icon (app, icon);
+}
+
+static void
+traverse_component_icons (GsApp *app,
+ XbNode *component,
+ GPtrArray *icons)
+{
+ if (!icons)
+ return;
+
+ /* This code deliberately does *not* check that the icon files or theme
+ * icons exist, as that would mean doing disk I/O for all the apps in
+ * the appstream file, regardless of whether the calling code is
+ * actually going to use the icons. Better to add all the possible icons
+ * and let the calling code check which ones exist, if it needs to. */
+ for (guint i = 0; i < icons->len; i++) {
+ XbNode *icon_node = g_ptr_array_index (icons, i);
+ g_autoptr(AsIcon) icon = NULL;
+ const gchar *icon_kind_str = xb_node_get_attr (icon_node, "type");
+ AsIconKind icon_kind = as_icon_kind_from_string (icon_kind_str);
+
+ if (icon_kind == AS_ICON_KIND_UNKNOWN) {
+ g_debug ("unknown icon kind ‘%s’", icon_kind_str);
+ continue;
+ }
+
+ icon = gs_appstream_new_icon (component, icon_node, icon_kind, 0);
+ app_add_icon (app, icon);
+ }
+}
+
+static void
+traverse_components_xpath_for_icons (GsApp *app,
+ XbSilo *silo,
+ const gchar *xpath,
+ gboolean try_with_launchable)
+{
+ g_autoptr(GPtrArray) components = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ components = xb_silo_query (silo, xpath, 0, &local_error);
+ if (components) {
+ for (guint i = 0; i < components->len; i++) {
+ g_autoptr(GPtrArray) icons = NULL; /* (element-type XbNode) */
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autofree gchar *xml = xb_node_export (component, 0, NULL);
+ icons = xb_node_query (component, "icon", 0, NULL);
+ traverse_component_icons (app, component, icons);
+
+ if (try_with_launchable && gs_app_get_icons (app) == NULL) {
+ const gchar *launchable_id = xb_node_query_text (component, "launchable[@type='desktop-id']", NULL);
+ if (launchable_id != NULL) {
+ g_autofree gchar *xpath2 = NULL;
+
+ /* Inherit the icon from the .desktop file */
+ xpath2 = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..",
+ launchable_id);
+ traverse_components_xpath_for_icons (app, silo, xpath2, FALSE);
+ }
+ }
+ }
+ }
+}
+
+static void
+gs_appstream_refine_icon (GsApp *app,
+ XbSilo *silo,
+ XbNode *component)
+{
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GPtrArray) icons = NULL; /* (element-type XbNode) */
+
+ icons = xb_node_query (component, "icon", 0, &local_error);
+ traverse_component_icons (app, component, icons);
+ g_clear_pointer (&icons, g_ptr_array_unref);
+
+ /* If no icon found, try to inherit the icon from the .desktop file */
+ if (gs_app_get_icons (app) == NULL) {
+ g_autofree gchar *xpath = NULL;
+ const gchar *launchable_id = xb_node_query_text (component, "launchable[@type='desktop-id']", NULL);
+ if (launchable_id != NULL) {
+ xpath = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..",
+ launchable_id);
+ traverse_components_xpath_for_icons (app, silo, xpath, FALSE);
+ g_clear_pointer (&xpath, g_free);
+ }
+
+ xpath = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..",
+ gs_app_get_id (app));
+ traverse_components_xpath_for_icons (app, silo, xpath, FALSE);
+ }
+}
+
+static gboolean
+gs_appstream_refine_add_addons (GsPlugin *plugin,
+ GsApp *app,
+ XbSilo *silo,
+ GError **error)
+{
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) addons = NULL;
+ g_autoptr(GsAppList) addons_list = NULL;
+
+ /* get all components */
+ xpath = g_strdup_printf ("components/component/extends[text()='%s']/..",
+ gs_app_get_id (app));
+ addons = xb_silo_query (silo, xpath, 0, &error_local);
+ if (addons == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+
+ addons_list = gs_app_list_new ();
+
+ for (guint i = 0; i < addons->len; i++) {
+ XbNode *addon = g_ptr_array_index (addons, i);
+ g_autoptr(GsApp) addon_app = NULL;
+
+ addon_app = gs_appstream_create_app (plugin, silo, addon, error);
+ if (addon_app == NULL)
+ return FALSE;
+
+ gs_app_list_add (addons_list, addon_app);
+ }
+
+ gs_app_add_addons (app, addons_list);
+
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_add_images (GsApp *app,
+ AsScreenshot *ss,
+ XbNode *screenshot,
+ gboolean *out_any_added,
+ GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) images = NULL;
+
+ /* get all components */
+ images = xb_node_query (screenshot, "image", 0, &error_local);
+ if (images == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < images->len; i++) {
+ XbNode *image = g_ptr_array_index (images, i);
+ g_autoptr(AsImage) im = as_image_new ();
+ as_image_set_height (im, xb_node_get_attr_as_uint (image, "height"));
+ as_image_set_width (im, xb_node_get_attr_as_uint (image, "width"));
+ as_image_set_kind (im, as_image_kind_from_string (xb_node_get_attr (image, "type")));
+ as_image_set_url (im, xb_node_get_text (image));
+ as_screenshot_add_image (ss, im);
+ }
+
+ *out_any_added = *out_any_added || images->len > 0;
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_add_videos (GsApp *app,
+ AsScreenshot *ss,
+ XbNode *screenshot,
+ gboolean *out_any_added,
+ GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) videos = NULL;
+
+ videos = xb_node_query (screenshot, "video", 0, &error_local);
+ if (videos == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < videos->len; i++) {
+ XbNode *video = g_ptr_array_index (videos, i);
+ g_autoptr(AsVideo) vid = as_video_new ();
+ as_video_set_height (vid, xb_node_get_attr_as_uint (video, "height"));
+ as_video_set_width (vid, xb_node_get_attr_as_uint (video, "width"));
+ as_video_set_codec_kind (vid, as_video_codec_kind_from_string (xb_node_get_attr (video, "codec")));
+ as_video_set_container_kind (vid, as_video_container_kind_from_string (xb_node_get_attr (video, "container")));
+ as_video_set_url (vid, xb_node_get_text (video));
+ as_screenshot_add_video (ss, vid);
+ }
+
+ *out_any_added = *out_any_added || videos->len > 0;
+
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_add_screenshots (GsApp *app, XbNode *component, GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) screenshots = NULL;
+
+ /* get all components */
+ screenshots = xb_node_query (component, "screenshots/screenshot", 0, &error_local);
+ if (screenshots == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < screenshots->len; i++) {
+ XbNode *screenshot = g_ptr_array_index (screenshots, i);
+ g_autoptr(AsScreenshot) ss = as_screenshot_new ();
+ gboolean any_added = FALSE;
+ if (!gs_appstream_refine_add_images (app, ss, screenshot, &any_added, error) ||
+ !gs_appstream_refine_add_videos (app, ss, screenshot, &any_added, error))
+ return FALSE;
+ if (any_added)
+ gs_app_add_screenshot (app, ss);
+ }
+
+ /* FIXME: move into no refine flags section? */
+ if (screenshots ->len > 0)
+ gs_app_add_kudo (app, GS_APP_KUDO_HAS_SCREENSHOTS);
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_add_provides (GsApp *app, XbNode *component, GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) provides = NULL;
+
+ /* get all components */
+ provides = xb_node_query (component, "provides/*", 0, &error_local);
+ if (provides == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < provides->len; i++) {
+ AsProvidedKind kind;
+ const gchar *element_name;
+ XbNode *provide = g_ptr_array_index (provides, i);
+ element_name = xb_node_get_element (provide);
+
+ /* try the simple case */
+ kind = as_provided_kind_from_string (element_name);
+ if (kind == AS_PROVIDED_KIND_UNKNOWN) {
+ /* try the complex cases */
+
+ if (g_strcmp0 (element_name, "library") == 0) {
+ kind = AS_PROVIDED_KIND_LIBRARY;
+ } else if (g_strcmp0 (element_name, "binary") == 0) {
+ kind = AS_PROVIDED_KIND_BINARY;
+ } else if (g_strcmp0 (element_name, "firmware") == 0) {
+ const gchar *fw_type = xb_node_get_attr (provide, "type");
+ if (g_strcmp0 (fw_type, "runtime") == 0)
+ kind = AS_PROVIDED_KIND_FIRMWARE_RUNTIME;
+ else if (g_strcmp0 (fw_type, "flashed") == 0)
+ kind = AS_PROVIDED_KIND_FIRMWARE_FLASHED;
+ } else if (g_strcmp0 (element_name, "python2") == 0) {
+ kind = AS_PROVIDED_KIND_PYTHON_2;
+ } else if (g_strcmp0 (element_name, "python3") == 0) {
+ kind = AS_PROVIDED_KIND_PYTHON;
+ } else if (g_strcmp0 (element_name, "dbus") == 0) {
+ const gchar *dbus_type = xb_node_get_attr (provide, "type");
+ if (g_strcmp0 (dbus_type, "system") == 0)
+ kind = AS_PROVIDED_KIND_DBUS_SYSTEM;
+ else if ((g_strcmp0 (dbus_type, "user") == 0) || (g_strcmp0 (dbus_type, "session") == 0))
+ kind = AS_PROVIDED_KIND_DBUS_USER;
+ }
+ }
+
+ if (kind == AS_PROVIDED_KIND_UNKNOWN ||
+ xb_node_get_text (provide) == NULL) {
+ /* give up */
+ g_warning ("ignoring unknown or empty provided item type: %s", element_name);
+ continue;
+ }
+
+ gs_app_add_provided_item (app,
+ kind,
+ xb_node_get_text (provide));
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static guint64
+component_get_release_timestamp (XbNode *component)
+{
+ guint64 timestamp;
+ const gchar *date_str;
+
+ /* Spec says to prefer `timestamp` over `date` if both are provided:
+ * https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-releases */
+ timestamp = xb_node_query_attr_as_uint (component, "releases/release", "timestamp", NULL);
+ date_str = xb_node_query_attr (component, "releases/release", "date", NULL);
+
+ if (timestamp != G_MAXUINT64) {
+ return timestamp;
+ } else if (date_str != NULL) {
+ g_autoptr(GDateTime) date = g_date_time_new_from_iso8601 (date_str, NULL);
+ if (date != NULL)
+ return g_date_time_to_unix (date);
+ }
+
+ /* Unknown. */
+ return G_MAXUINT64;
+}
+
+static gboolean
+gs_appstream_is_recent_release (XbNode *component)
+{
+ guint64 ts;
+ gint64 secs;
+
+ /* get newest release */
+ ts = component_get_release_timestamp (component);
+ if (ts == G_MAXUINT64)
+ return FALSE;
+
+ /* is last build less than one year ago? */
+ secs = (g_get_real_time () / G_USEC_PER_SEC) - ts;
+ return secs / (60 * 60 * 24) < 365;
+}
+
+static gboolean
+gs_appstream_copy_metadata (GsApp *app, XbNode *component, GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) values = NULL;
+
+ /* get all components */
+ values = xb_node_query (component, "custom/value", 0, &error_local);
+ if (values == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < values->len; i++) {
+ XbNode *value = g_ptr_array_index (values, i);
+ const gchar *key = xb_node_get_attr (value, "key");
+ if (key == NULL)
+ continue;
+ if (gs_app_get_metadata_item (app, key) != NULL)
+ continue;
+ gs_app_set_metadata (app, key, xb_node_get_text (value));
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_app_updates (GsApp *app,
+ XbSilo *silo,
+ XbNode *component,
+ GError **error)
+{
+ AsUrgencyKind urgency_best = AS_URGENCY_KIND_UNKNOWN;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GHashTable) installed = g_hash_table_new (g_str_hash, g_str_equal);
+ g_autoptr(GPtrArray) releases_inst = NULL;
+ g_autoptr(GPtrArray) releases = NULL;
+ g_autoptr(GPtrArray) updates_list = g_ptr_array_new ();
+
+ /* only for UPDATABLE apps */
+ if (!gs_app_is_updatable (app))
+ return TRUE;
+
+ /* find out which releases are already installed */
+ xpath = g_strdup_printf ("component/id[text()='%s']/../releases/*[@version]",
+ gs_app_get_id (app));
+ releases_inst = xb_silo_query (silo, xpath, 0, &error_local);
+ if (releases_inst == NULL) {
+ if (!g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ } else {
+ for (guint i = 0; i < releases_inst->len; i++) {
+ XbNode *release = g_ptr_array_index (releases_inst, i);
+ g_hash_table_insert (installed,
+ (gpointer) xb_node_get_attr (release, "version"),
+ (gpointer) release);
+ }
+ }
+ g_clear_error (&error_local);
+
+ /* get all components */
+ releases = xb_node_query (component, "releases/*", 0, &error_local);
+ if (releases == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < releases->len; i++) {
+ XbNode *release = g_ptr_array_index (releases, i);
+ const gchar *version = xb_node_get_attr (release, "version");
+ g_autoptr(XbNode) description = NULL;
+ AsUrgencyKind urgency_tmp;
+
+ /* ignore releases with no version */
+ if (version == NULL)
+ continue;
+
+ /* already installed */
+ if (g_hash_table_lookup (installed, version) != NULL)
+ continue;
+
+ /* limit this to three versions backwards if there has never
+ * been a detected installed version */
+ if (g_hash_table_size (installed) == 0 && i >= 3)
+ break;
+
+ /* use the 'worst' urgency, e.g. critical over enhancement */
+ urgency_tmp = as_urgency_kind_from_string (xb_node_get_attr (release, "urgency"));
+ if (urgency_tmp > urgency_best)
+ urgency_best = urgency_tmp;
+
+ /* add updates with a description */
+ description = xb_node_query_first (release, "description", NULL);
+ if (description == NULL)
+ continue;
+ g_ptr_array_add (updates_list, release);
+ }
+
+ /* only set if known */
+ if (urgency_best != AS_URGENCY_KIND_UNKNOWN)
+ gs_app_set_update_urgency (app, urgency_best);
+
+ /* no prefix on each release */
+ if (updates_list->len == 1) {
+ XbNode *release = g_ptr_array_index (updates_list, 0);
+ g_autoptr(XbNode) n = NULL;
+ g_autofree gchar *desc = NULL;
+ n = xb_node_query_first (release, "description", NULL);
+ desc = gs_appstream_format_description (n, NULL);
+ gs_app_set_update_details_markup (app, desc);
+
+ /* get the descriptions with a version prefix */
+ } else if (updates_list->len > 1) {
+ const gchar *version = gs_app_get_version (app);
+ g_autoptr(GString) update_desc = g_string_new ("");
+ for (guint i = 0; i < updates_list->len; i++) {
+ XbNode *release = g_ptr_array_index (updates_list, i);
+ const gchar *release_version = xb_node_get_attr (release, "version");
+ g_autofree gchar *desc = NULL;
+ g_autoptr(XbNode) n = NULL;
+
+ /* skip the currently installed version and all below it */
+ if (version != NULL && as_vercmp_simple (version, release_version) >= 0)
+ continue;
+
+ n = xb_node_query_first (release, "description", NULL);
+ desc = gs_appstream_format_description (n, NULL);
+ g_string_append_printf (update_desc,
+ "Version %s:\n%s\n\n",
+ xb_node_get_attr (release, "version"),
+ desc);
+ }
+
+ /* remove trailing newlines */
+ if (update_desc->len > 2)
+ g_string_truncate (update_desc, update_desc->len - 2);
+ if (update_desc->len > 0)
+ gs_app_set_update_details_markup (app, update_desc->str);
+ }
+
+ /* if there is no already set update version use the newest */
+ if (gs_app_get_update_version (app) == NULL &&
+ updates_list->len > 0) {
+ XbNode *release = g_ptr_array_index (updates_list, 0);
+ gs_app_set_update_version (app, xb_node_get_attr (release, "version"));
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_add_version_history (GsApp *app, XbNode *component, GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) version_history = NULL; /* (element-type AsRelease) */
+ g_autoptr(GPtrArray) releases = NULL; /* (element-type XbNode) */
+
+ /* get all components */
+ releases = xb_node_query (component, "releases/*", 0, &error_local);
+ if (releases == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+
+ version_history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ for (guint i = 0; i < releases->len; i++) {
+ XbNode *release_node = g_ptr_array_index (releases, i);
+ const gchar *version = xb_node_get_attr (release_node, "version");
+ g_autoptr(XbNode) description_node = NULL;
+ g_autofree gchar *description = NULL;
+ guint64 timestamp;
+ const gchar *date_str;
+ g_autoptr(AsRelease) release = NULL;
+ g_autofree char *timestamp_xpath = NULL;
+
+ /* ignore releases with no version */
+ if (version == NULL)
+ continue;
+
+ timestamp_xpath = g_strdup_printf ("releases/release[%u]", i+1);
+ timestamp = xb_node_query_attr_as_uint (component, timestamp_xpath, "timestamp", NULL);
+ date_str = xb_node_query_attr (component, timestamp_xpath, "date", NULL);
+
+ /* include updates with or without a description */
+ description_node = xb_node_query_first (release_node, "description", NULL);
+ if (description_node != NULL)
+ description = gs_appstream_format_description (description_node, NULL);
+
+ release = as_release_new ();
+ as_release_set_version (release, version);
+ if (timestamp != G_MAXUINT64)
+ as_release_set_timestamp (release, timestamp);
+ else if (date_str != NULL) /* timestamp takes precedence over date */
+ as_release_set_date (release, date_str);
+ if (description != NULL)
+ as_release_set_description (release, description, NULL);
+
+ g_ptr_array_add (version_history, g_steal_pointer (&release));
+ }
+
+ if (version_history->len > 0)
+ gs_app_set_version_history (app, version_history);
+
+ /* success */
+ return TRUE;
+}
+
+/**
+ * _gs_utils_locale_has_translations:
+ * @locale: A locale, e.g. `en_GB` or `uz_UZ.utf8@cyrillic`
+ *
+ * Looks up if the locale is likely to have translations.
+ *
+ * Returns: %TRUE if the locale should have translations
+ **/
+static gboolean
+_gs_utils_locale_has_translations (const gchar *locale)
+{
+ g_autofree gchar *locale_copy = g_strdup (locale);
+ gchar *separator;
+
+ /* Strip off the territory, codeset and modifier, if present. */
+ separator = strpbrk (locale_copy, "_.@");
+ if (separator != NULL)
+ *separator = '\0';
+
+ if (g_strcmp0 (locale_copy, "C") == 0)
+ return FALSE;
+ if (g_strcmp0 (locale_copy, "en") == 0)
+ return FALSE;
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_origin_valid (const gchar *origin)
+{
+ if (origin == NULL)
+ return FALSE;
+ if (g_strcmp0 (origin, "") == 0)
+ return FALSE;
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_is_valid_project_group (const gchar *project_group)
+{
+ if (project_group == NULL)
+ return FALSE;
+ return as_utils_is_desktop_environment (project_group);
+}
+
+static gboolean
+gs_appstream_refine_app_content_rating (GsApp *app,
+ XbNode *content_rating,
+ GError **error)
+{
+ g_autoptr(AsContentRating) cr = as_content_rating_new ();
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) content_attributes = NULL;
+ const gchar *content_rating_kind = NULL;
+
+ /* get kind */
+ content_rating_kind = xb_node_get_attr (content_rating, "type");
+ /* we only really expect/support OARS 1.0 and 1.1 */
+ if (content_rating_kind == NULL ||
+ (g_strcmp0 (content_rating_kind, "oars-1.0") != 0 &&
+ g_strcmp0 (content_rating_kind, "oars-1.1") != 0)) {
+ return TRUE;
+ }
+
+ as_content_rating_set_kind (cr, content_rating_kind);
+
+ /* get attributes; no attributes being found (i.e.
+ * `<content_rating type="*"/>`) is OK: it means that all attributes have
+ * value `none`, as per the
+ * [OARS semantics](https://github.com/hughsie/oars/blob/HEAD/specification/oars-1.1.md) */
+ content_attributes = xb_node_query (content_rating, "content_attribute", 0, &error_local);
+ if (content_attributes == NULL &&
+ g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_clear_error (&error_local);
+ } else if (content_attributes == NULL) {
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+
+ for (guint i = 0; content_attributes != NULL && i < content_attributes->len; i++) {
+ XbNode *content_attribute = g_ptr_array_index (content_attributes, i);
+ as_content_rating_add_attribute (cr,
+ xb_node_get_attr (content_attribute, "id"),
+ as_content_rating_value_from_string (xb_node_get_text (content_attribute)));
+ }
+
+ gs_app_set_content_rating (app, cr);
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_app_content_ratings (GsApp *app,
+ XbNode *component,
+ GError **error)
+{
+ g_autoptr(GPtrArray) content_ratings = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* find any content ratings */
+ content_ratings = xb_node_query (component, "content_rating", 0, &error_local);
+ if (content_ratings == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < content_ratings->len; i++) {
+ XbNode *content_rating = g_ptr_array_index (content_ratings, i);
+ if (!gs_appstream_refine_app_content_rating (app, content_rating, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_app_relation (GsApp *app,
+ XbNode *relation_node,
+ AsRelationKind kind,
+ GError **error)
+{
+ /* Iterate over the children, which might be any combination of zero or
+ * more <id/>, <modalias/>, <kernel/>, <memory/>, <firmware/>,
+ * <control/> or <display_length/> elements. For the moment, we only
+ * support some of these. */
+ for (g_autoptr(XbNode) child = xb_node_get_child (relation_node); child != NULL; node_set_to_next (&child)) {
+ const gchar *item_kind = xb_node_get_element (child);
+ g_autoptr(AsRelation) relation = as_relation_new ();
+
+ as_relation_set_kind (relation, kind);
+
+ if (g_str_equal (item_kind, "control")) {
+ /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-relations-control */
+ as_relation_set_item_kind (relation, AS_RELATION_ITEM_KIND_CONTROL);
+ as_relation_set_value_control_kind (relation, as_control_kind_from_string (xb_node_get_text (child)));
+ } else if (g_str_equal (item_kind, "display_length")) {
+ AsDisplayLengthKind display_length_kind;
+ const gchar *compare;
+
+ /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-relations-display_length */
+ as_relation_set_item_kind (relation, AS_RELATION_ITEM_KIND_DISPLAY_LENGTH);
+
+ compare = xb_node_get_attr (child, "compare");
+ as_relation_set_compare (relation, (compare != NULL) ? as_relation_compare_from_string (compare) : AS_RELATION_COMPARE_GE);
+
+ display_length_kind = as_display_length_kind_from_string (xb_node_get_text (child));
+ if (display_length_kind != AS_DISPLAY_LENGTH_KIND_UNKNOWN) {
+ /* Ignore the `side` attribute */
+ as_relation_set_value_display_length_kind (relation, display_length_kind);
+ } else {
+ const gchar *side = xb_node_get_attr (child, "side");
+ as_relation_set_display_side_kind (relation, (side != NULL) ? as_display_side_kind_from_string (side) : AS_DISPLAY_SIDE_KIND_SHORTEST);
+ as_relation_set_value_px (relation, xb_node_get_text_as_uint (child));
+ }
+ } else {
+ g_debug ("Relation type ‘%s’ not currently supported for %s; ignoring",
+ item_kind, gs_app_get_id (app));
+ continue;
+ }
+
+ gs_app_add_relation (app, relation);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_refine_app_relations (GsApp *app,
+ XbNode *component,
+ GError **error)
+{
+ const struct {
+ const gchar *element_name;
+ AsRelationKind relation_kind;
+ } relation_types[] = {
+#if AS_CHECK_VERSION(0, 15, 0)
+ { "supports", AS_RELATION_KIND_SUPPORTS },
+#endif
+ { "recommends", AS_RELATION_KIND_RECOMMENDS },
+ { "requires", AS_RELATION_KIND_REQUIRES },
+ };
+
+ for (gsize i = 0; i < G_N_ELEMENTS (relation_types); i++) {
+ g_autoptr(GPtrArray) relations = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* find any instances of this @element_name */
+ relations = xb_node_query (component, relation_types[i].element_name, 0, &error_local);
+ if (relations == NULL &&
+ !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+
+ for (guint j = 0; relations != NULL && j < relations->len; j++) {
+ XbNode *relation = g_ptr_array_index (relations, j);
+ if (!gs_appstream_refine_app_relation (app, relation, relation_types[i].relation_kind, error))
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_appstream_refine_app (GsPlugin *plugin,
+ GsApp *app,
+ XbSilo *silo,
+ XbNode *component,
+ GsPluginRefineFlags refine_flags,
+ GError **error)
+{
+ const gchar *tmp;
+ guint64 timestamp;
+ g_autoptr(GPtrArray) bundles = NULL;
+ g_autoptr(GPtrArray) launchables = NULL;
+ g_autoptr(XbNode) req = NULL;
+
+ /* The 'plugin' can be NULL, when creating app for --show-metainfo */
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (XB_IS_NODE (component), FALSE);
+
+ /* is compatible */
+ req = xb_node_query_first (component,
+ "requires/id[@type='id']"
+ "[text()='org.gnome.Software.desktop']", NULL);
+ if (req != NULL) {
+ gint rc = as_vercmp_simple (xb_node_get_attr (req, "version"),
+ PACKAGE_VERSION);
+ if (rc > 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "not for this gnome-software");
+ return FALSE;
+ }
+ }
+
+ /* set id kind */
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_UNKNOWN ||
+ gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC) {
+ AsComponentKind kind;
+ tmp = xb_node_get_attr (component, "type");
+ kind = as_component_kind_from_string (tmp);
+ if (kind != AS_COMPONENT_KIND_UNKNOWN)
+ gs_app_set_kind (app, kind);
+ }
+
+ /* types we can never launch */
+ switch (gs_app_get_kind (app)) {
+ case AS_COMPONENT_KIND_ADDON:
+ case AS_COMPONENT_KIND_CODEC:
+ case AS_COMPONENT_KIND_DRIVER:
+ case AS_COMPONENT_KIND_FIRMWARE:
+ case AS_COMPONENT_KIND_FONT:
+ case AS_COMPONENT_KIND_GENERIC:
+ case AS_COMPONENT_KIND_INPUT_METHOD:
+ case AS_COMPONENT_KIND_LOCALIZATION:
+ case AS_COMPONENT_KIND_OPERATING_SYSTEM:
+ case AS_COMPONENT_KIND_RUNTIME:
+ case AS_COMPONENT_KIND_REPOSITORY:
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ break;
+ default:
+ break;
+ }
+
+ /* check if the special metadata affects the not-launchable quirk */
+ tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::not-launchable");
+ if (tmp != NULL) {
+ if (g_strcmp0 (tmp, "true") == 0)
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ else if (g_strcmp0 (tmp, "false") == 0)
+ gs_app_remove_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ }
+
+ tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::hide-everywhere");
+ if (tmp != NULL) {
+ if (g_strcmp0 (tmp, "true") == 0)
+ gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
+ else if (g_strcmp0 (tmp, "false") == 0)
+ gs_app_remove_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
+ }
+
+ /* try to detect old-style AppStream 'override'
+ * files without the merge attribute */
+ if (xb_node_query_text (component, "name", NULL) == NULL &&
+ xb_node_query_text (component, "metadata_license", NULL) == NULL) {
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ }
+
+ /* set id */
+ tmp = xb_node_query_text (component, "id", NULL);
+ if (tmp != NULL && gs_app_get_id (app) == NULL)
+ gs_app_set_id (app, tmp);
+
+ /* set source */
+ tmp = xb_node_query_text (component, "info/filename", NULL);
+ if (tmp == NULL)
+ tmp = xb_node_query_text (component, "../info/filename", NULL);
+ if (tmp != NULL && gs_app_get_metadata_item (app, "appstream::source-file") == NULL) {
+ gs_app_set_metadata (app, "appstream::source-file", tmp);
+ }
+
+ /* set scope */
+ tmp = xb_node_query_text (component, "../info/scope", NULL);
+ if (tmp != NULL)
+ gs_app_set_scope (app, as_component_scope_from_string (tmp));
+
+ /* set content rating */
+ if (TRUE) {
+ if (!gs_appstream_refine_app_content_ratings (app, component, error))
+ return FALSE;
+ }
+
+ /* recommends/requires
+ * FIXME: Technically this could do with a more specific refine flag,
+ * but essentially the relations are used on the details page and so
+ * are the permissions. It would be good to eliminate refine flags at
+ * some point in the future. */
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) {
+ if (!gs_appstream_refine_app_relations (app, component, error))
+ return FALSE;
+ }
+
+ /* set name */
+ tmp = xb_node_query_text (component, "name", NULL);
+ if (tmp != NULL)
+ gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, tmp);
+
+ /* set summary */
+ tmp = xb_node_query_text (component, "summary", NULL);
+ if (tmp != NULL)
+ gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, tmp);
+
+ /* add urls */
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) {
+ g_autoptr(GPtrArray) urls = NULL;
+ urls = xb_node_query (component, "url", 0, NULL);
+ if (urls != NULL) {
+ for (guint i = 0; i < urls->len; i++) {
+ XbNode *url = g_ptr_array_index (urls, i);
+ const gchar *kind = xb_node_get_attr (url, "type");
+ if (kind == NULL)
+ continue;
+ gs_app_set_url (app,
+ as_url_kind_from_string (kind),
+ xb_node_get_text (url));
+ }
+ }
+ }
+
+ /* add launchables */
+ launchables = xb_node_query (component, "launchable", 0, NULL);
+ if (launchables != NULL) {
+ for (guint i = 0; i < launchables->len; i++) {
+ XbNode *launchable = g_ptr_array_index (launchables, i);
+ const gchar *kind = xb_node_get_attr (launchable, "type");
+ if (g_strcmp0 (kind, "desktop-id") == 0) {
+ gs_app_set_launchable (app,
+ AS_LAUNCHABLE_KIND_DESKTOP_ID,
+ xb_node_get_text (launchable));
+ break;
+ } else if (g_strcmp0 (kind, "url") == 0) {
+ gs_app_set_launchable (app,
+ AS_LAUNCHABLE_KIND_URL,
+ xb_node_get_text (launchable));
+ }
+ }
+ }
+
+ /* set license */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0 &&
+ gs_app_get_license (app) == NULL) {
+ tmp = xb_node_query_text (component, "project_license", NULL);
+ if (tmp != NULL)
+ gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, tmp);
+ }
+
+ /* set description */
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) {
+ g_autofree gchar *description = NULL;
+ g_autoptr(XbNode) n = xb_node_query_first (component, "description", NULL);
+ if (n != NULL)
+ description = gs_appstream_format_description (n, NULL);
+ if (description != NULL)
+ gs_app_set_description (app, GS_APP_QUALITY_HIGHEST, description);
+ }
+
+ /* set icon */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) > 0 &&
+ gs_app_get_icons (app) == NULL)
+ gs_appstream_refine_icon (app, silo, component);
+
+ /* set categories */
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES) {
+ g_autoptr(GPtrArray) categories = NULL;
+ categories = xb_node_query (component, "categories/category", 0, NULL);
+ if (categories != NULL) {
+ for (guint i = 0; i < categories->len; i++) {
+ XbNode *category = g_ptr_array_index (categories, i);
+ gs_app_add_category (app, xb_node_get_text (category));
+
+ /* Special case: We used to use the `Blacklisted`
+ * category to hide apps from their .desktop
+ * file or appdata. We now use a quirk for that.
+ * This special case can be removed when all
+ * appstream files no longer use the `Blacklisted`
+ * category (including external-appstream files
+ * put together by distributions). */
+ if (g_strcmp0 (xb_node_get_text (category), "Blacklisted") == 0)
+ gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
+ }
+ }
+ }
+
+ /* set project group */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP) > 0 &&
+ gs_app_get_project_group (app) == NULL) {
+ tmp = xb_node_query_text (component, "project_group", NULL);
+ if (tmp != NULL && gs_appstream_is_valid_project_group (tmp))
+ gs_app_set_project_group (app, tmp);
+ }
+
+ /* set developer name */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) > 0 &&
+ gs_app_get_developer_name (app) == NULL) {
+ tmp = xb_node_query_text (component, "developer_name", NULL);
+ if (tmp != NULL)
+ gs_app_set_developer_name (app, tmp);
+ }
+
+ /* set the release date */
+ timestamp = component_get_release_timestamp (component);
+ if (timestamp != G_MAXUINT64)
+ gs_app_set_release_date (app, timestamp);
+
+ /* set the version history */
+ if (!gs_appstream_refine_add_version_history (app, component, error))
+ return FALSE;
+
+ /* copy all the metadata */
+ if (!gs_appstream_copy_metadata (app, component, error))
+ return FALSE;
+
+ /* add bundles */
+ bundles = xb_node_query (component, "bundle", 0, NULL);
+ if (bundles != NULL && gs_app_get_sources(app)->len == 0) {
+ for (guint i = 0; i < bundles->len; i++) {
+ XbNode *bundle = g_ptr_array_index (bundles, i);
+ const gchar *kind = xb_node_get_attr (bundle, "type");
+ const gchar *bundle_id = xb_node_get_text (bundle);
+
+ if (bundle_id == NULL || kind == NULL)
+ continue;
+
+ gs_app_add_source (app, bundle_id);
+ gs_app_set_bundle_kind (app, as_bundle_kind_from_string (kind));
+
+ /* get the type/name/arch/branch */
+ if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) {
+ g_auto(GStrv) split = g_strsplit (bundle_id, "/", -1);
+ if (g_strv_length (split) != 4) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "invalid ID %s for a flatpak ref",
+ bundle_id);
+ return FALSE;
+ }
+
+ /* we only need the branch for the unique ID */
+ gs_app_set_branch (app, split[3]);
+ }
+ }
+ }
+
+ /* add legacy package names */
+ if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN) {
+ g_autoptr(GPtrArray) pkgnames = NULL;
+ pkgnames = xb_node_query (component, "pkgname", 0, NULL);
+ if (pkgnames != NULL && gs_app_get_sources(app)->len == 0) {
+ for (guint i = 0; i < pkgnames->len; i++) {
+ XbNode *pkgname = g_ptr_array_index (pkgnames, i);
+ tmp = xb_node_get_text (pkgname);
+ if (tmp != NULL && tmp[0] != '\0')
+ gs_app_add_source (app, tmp);
+ }
+ gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
+ }
+ }
+
+ /* set origin */
+ tmp = xb_node_query_attr (component, "..", "origin", NULL);
+ if (gs_appstream_origin_valid (tmp)) {
+ gs_app_set_origin_appstream (app, tmp);
+
+ if (gs_app_get_origin (app) == NULL && (
+ gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK ||
+ gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE)) {
+ gs_app_set_origin (app, tmp);
+ }
+ }
+
+ /* set addons */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) != 0 &&
+ plugin != NULL && silo != NULL) {
+ if (!gs_appstream_refine_add_addons (plugin, app, silo, error))
+ return FALSE;
+ }
+
+ /* set screenshots */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) > 0 &&
+ gs_app_get_screenshots(app)->len == 0) {
+ if (!gs_appstream_refine_add_screenshots (app, component, error))
+ return FALSE;
+ }
+
+ /* set provides */
+ if (!gs_appstream_refine_add_provides (app, component, error))
+ return FALSE;
+
+ /* add kudos */
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) {
+ g_autoptr(GPtrArray) kudos = NULL;
+ tmp = setlocale (LC_MESSAGES, NULL);
+ if (!_gs_utils_locale_has_translations (tmp)) {
+ gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE);
+ } else {
+
+ g_autoptr(GString) xpath = g_string_new (NULL);
+ g_auto(GStrv) variants = g_get_locale_variants (tmp);
+
+ /* @variants includes @tmp */
+ for (gsize i = 0; variants[i] != NULL; i++)
+ xb_string_append_union (xpath, "languages/lang[(text()='%s') and (@percentage>50)]", variants[i]);
+
+ if (xb_node_query_text (component, xpath->str, NULL) != NULL)
+ gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE);
+ }
+
+ /* Set this under the FLAGS_REQUIRE_KUDOS flag because it’s
+ * only useful in combination with KUDO_MY_LANGUAGE */
+ if (xb_node_query_text (component, "languages/lang", NULL) != NULL)
+ gs_app_set_has_translations (app, TRUE);
+
+ /* any keywords */
+ if (xb_node_query_text (component, "keywords/keyword", NULL) != NULL)
+ gs_app_add_kudo (app, GS_APP_KUDO_HAS_KEYWORDS);
+
+ /* HiDPI icon */
+ if (xb_node_query_text (component, "icon[@width='128']", NULL) != NULL)
+ gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON);
+
+ /* was this application released recently */
+ if (gs_appstream_is_recent_release (component))
+ gs_app_add_kudo (app, GS_APP_KUDO_RECENT_RELEASE);
+
+ /* add a kudo to featured and popular apps */
+ if (xb_node_query_text (component, "kudos/kudo[text()='GnomeSoftware::popular']", NULL) != NULL)
+ gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);
+ if (xb_node_query_text (component, "categories/category[text()='Featured']", NULL) != NULL)
+ gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);
+ }
+
+ /* we have an origin in the XML */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) > 0 &&
+ gs_app_get_origin_appstream (app) == NULL) {
+ g_autoptr(XbNode) parent = xb_node_get_parent (component);
+ if (parent != NULL) {
+ tmp = xb_node_get_attr (parent, "origin");
+ if (gs_appstream_origin_valid (tmp))
+ gs_app_set_origin_appstream (app, tmp);
+ }
+ }
+
+ /* is there any update information */
+ if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) != 0 &&
+ silo != NULL) {
+ if (!gs_appstream_refine_app_updates (app,
+ silo,
+ component,
+ error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+typedef struct {
+ AsSearchTokenMatch match_value;
+ XbQuery *query;
+} GsAppstreamSearchHelper;
+
+static void
+gs_appstream_search_helper_free (GsAppstreamSearchHelper *helper)
+{
+ g_object_unref (helper->query);
+ g_free (helper);
+}
+
+static guint16
+gs_appstream_silo_search_component2 (GPtrArray *array, XbNode *component, const gchar *search)
+{
+ guint16 match_value = 0;
+
+ /* do searches */
+ for (guint i = 0; i < array->len; i++) {
+ g_autoptr(GPtrArray) n = NULL;
+ GsAppstreamSearchHelper *helper = g_ptr_array_index (array, i);
+#if LIBXMLB_CHECK_VERSION(0, 3, 0)
+ g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT ();
+ xb_value_bindings_bind_str (xb_query_context_get_bindings (&context), 0, search, NULL);
+ n = xb_node_query_with_context (component, helper->query, &context, NULL);
+#else
+ xb_query_bind_str (helper->query, 0, search, NULL);
+ n = xb_node_query_full (component, helper->query, NULL);
+#endif
+ if (n != NULL)
+ match_value |= helper->match_value;
+ }
+ return match_value;
+}
+
+static guint16
+gs_appstream_silo_search_component (GPtrArray *array, XbNode *component, const gchar * const *search)
+{
+ guint16 matches_sum = 0;
+
+ /* do *all* search keywords match */
+ for (guint i = 0; search[i] != NULL; i++) {
+ guint tmp = gs_appstream_silo_search_component2 (array, component, search[i]);
+ if (tmp == 0)
+ return 0;
+ matches_sum |= tmp;
+ }
+ return matches_sum;
+}
+
+typedef struct {
+ AsSearchTokenMatch match_value;
+ const gchar *xpath;
+} Query;
+
+static gboolean
+gs_appstream_do_search (GsPlugin *plugin,
+ XbSilo *silo,
+ const gchar * const *values,
+ const Query queries[],
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func ((GDestroyNotify) gs_appstream_search_helper_free);
+ g_autoptr(GPtrArray) components = NULL;
+ g_autoptr(GTimer) timer = g_timer_new ();
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (values != NULL, FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+
+ /* add some weighted queries */
+ for (guint i = 0; queries[i].xpath != NULL; i++) {
+ g_autoptr(GError) error_query = NULL;
+ g_autoptr(XbQuery) query = xb_query_new (silo, queries[i].xpath, &error_query);
+ if (query != NULL) {
+ GsAppstreamSearchHelper *helper = g_new0 (GsAppstreamSearchHelper, 1);
+ helper->match_value = queries[i].match_value;
+ helper->query = g_steal_pointer (&query);
+ g_ptr_array_add (array, helper);
+ } else {
+ g_debug ("ignoring: %s", error_query->message);
+ }
+ }
+
+ /* get all components */
+ components = xb_silo_query (silo, "components/component", 0, &error_local);
+ if (components == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ guint16 match_value = gs_appstream_silo_search_component (array, component, values);
+ if (match_value != 0) {
+ g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error);
+ if (app == NULL)
+ return FALSE;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
+ g_debug ("not returning wildcard %s",
+ gs_app_get_unique_id (app));
+ continue;
+ }
+ g_debug ("add %s", gs_app_get_unique_id (app));
+
+ /* The match value is used for prioritising results.
+ * Drop the ID token from it as it’s the highest
+ * numeric value but isn’t visible to the user in the
+ * UI, which leads to confusing results ordering. */
+ gs_app_set_match_value (app, match_value & (~AS_SEARCH_TOKEN_MATCH_ID));
+ gs_app_list_add (list, app);
+
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_ADDON) {
+ g_autoptr(GPtrArray) extends = NULL;
+
+ /* add the parent app as a wildcard, to be refined later */
+ extends = xb_node_query (component, "extends", 0, NULL);
+ for (guint jj = 0; extends && jj < extends->len; jj++) {
+ XbNode *extend = g_ptr_array_index (extends, jj);
+ g_autoptr(GsApp) app2 = NULL;
+ const gchar *tmp;
+ app2 = gs_app_new (xb_node_get_text (extend));
+ gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD);
+ tmp = xb_node_query_attr (extend, "../..", "origin", NULL);
+ if (gs_appstream_origin_valid (tmp))
+ gs_app_set_origin_appstream (app2, tmp);
+ gs_app_list_add (list, app2);
+ }
+ }
+ }
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return FALSE;
+ }
+ g_debug ("search took %fms", g_timer_elapsed (timer, NULL) * 1000);
+ return TRUE;
+}
+
+/* This tokenises and stems @values internally for comparison against the
+ * already-stemmed tokens in the libxmlb silo */
+gboolean
+gs_appstream_search (GsPlugin *plugin,
+ XbSilo *silo,
+ const gchar * const *values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const Query queries[] = {
+ { AS_SEARCH_TOKEN_MATCH_MIMETYPE, "mimetypes/mimetype[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_PKGNAME, "pkgname[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_SUMMARY, "summary[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_NAME, "name[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_KEYWORD, "keywords/keyword[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_ID, "id[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_ID, "launchable[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_ORIGIN, "../components[@origin~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_NONE, NULL }
+ };
+
+ return gs_appstream_do_search (plugin, silo, values, queries, list, cancellable, error);
+}
+
+gboolean
+gs_appstream_search_developer_apps (GsPlugin *plugin,
+ XbSilo *silo,
+ const gchar * const *values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const Query queries[] = {
+ { AS_SEARCH_TOKEN_MATCH_PKGNAME, "developer_name[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_SUMMARY, "project_group[text()~=stem(?)]" },
+ { AS_SEARCH_TOKEN_MATCH_NONE, NULL }
+ };
+
+ return gs_appstream_do_search (plugin, silo, values, queries, list, cancellable, error);
+}
+
+gboolean
+gs_appstream_add_category_apps (GsPlugin *plugin,
+ XbSilo *silo,
+ GsCategory *category,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GPtrArray *desktop_groups;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+
+ desktop_groups = gs_category_get_desktop_groups (category);
+ if (desktop_groups->len == 0) {
+ g_warning ("no desktop_groups for %s", gs_category_get_id (category));
+ return TRUE;
+ }
+ for (guint j = 0; j < desktop_groups->len; j++) {
+ const gchar *desktop_group = g_ptr_array_index (desktop_groups, j);
+ g_autofree gchar *xpath = NULL;
+ g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1);
+ g_autoptr(GPtrArray) components = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* generate query */
+ if (g_strv_length (split) == 1) {
+ xpath = g_strdup_printf ("components/component/categories/"
+ "category[text()='%s']/../..",
+ split[0]);
+ } else if (g_strv_length (split) == 2) {
+ xpath = g_strdup_printf ("components/component/categories/"
+ "category[text()='%s']/../"
+ "category[text()='%s']/../..",
+ split[0], split[1]);
+ }
+ components = xb_silo_query (silo, xpath, 0, &error_local);
+ if (components == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ continue;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+
+ /* create app */
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autoptr(GsApp) app = NULL;
+ const gchar *id = xb_node_query_text (component, "id", NULL);
+ if (id == NULL)
+ continue;
+ app = gs_app_new (id);
+ gs_app_set_metadata (app, "GnomeSoftware::Creator",
+ gs_plugin_get_name (plugin));
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ }
+
+ }
+ return TRUE;
+}
+
+static guint
+gs_appstream_count_component_for_groups (XbSilo *silo,
+ const gchar *desktop_group)
+{
+ guint limit = 10;
+ g_autofree gchar *xpath = NULL;
+ g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1);
+ g_autoptr(GPtrArray) array = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ if (g_strv_length (split) == 1) { /* "all" group for a parent category */
+ xpath = g_strdup_printf ("components/component/categories/"
+ "category[text()='%s']/../..",
+ split[0]);
+ } else if (g_strv_length (split) == 2) {
+ xpath = g_strdup_printf ("components/component/categories/"
+ "category[text()='%s']/../"
+ "category[text()='%s']/../..",
+ split[0], split[1]);
+ } else {
+ return 0;
+ }
+
+ array = xb_silo_query (silo, xpath, limit, &error_local);
+ if (array == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return 0;
+ g_warning ("%s", error_local->message);
+ return 0;
+ }
+ return array->len;
+}
+
+/* we're not actually adding categories here, we're just setting the number of
+ * applications available in each category */
+gboolean
+gs_appstream_refine_category_sizes (XbSilo *silo,
+ GPtrArray *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (list != NULL, FALSE);
+
+ for (guint j = 0; j < list->len; j++) {
+ GsCategory *parent = GS_CATEGORY (g_ptr_array_index (list, j));
+ GPtrArray *children = gs_category_get_children (parent);
+
+ for (guint i = 0; i < children->len; i++) {
+ GsCategory *cat = g_ptr_array_index (children, i);
+ GPtrArray *groups = gs_category_get_desktop_groups (cat);
+ for (guint k = 0; k < groups->len; k++) {
+ const gchar *group = g_ptr_array_index (groups, k);
+ guint cnt = gs_appstream_count_component_for_groups (silo, group);
+ if (cnt > 0) {
+ gs_category_increment_size (parent, cnt);
+ if (children->len > 1) {
+ /* Parent category has multiple groups, so increment
+ * each group's size too */
+ gs_category_increment_size (cat, cnt);
+ }
+ }
+ }
+ }
+ continue;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_appstream_add_installed (GsPlugin *plugin,
+ XbSilo *silo,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GPtrArray) components = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+
+ /* get all installed appdata files (notice no 'components/' prefix...) */
+ components = xb_silo_query (silo, "component/description/..", 0, NULL);
+ if (components == NULL)
+ return TRUE;
+
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error);
+ if (app == NULL)
+ return FALSE;
+
+ /* Can get cached GsApp, which has the state already updated */
+ if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE &&
+ gs_app_get_state (app) != GS_APP_STATE_UPDATABLE_LIVE)
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM);
+ gs_app_list_add (list, app);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_appstream_add_popular (XbSilo *silo,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) array = NULL;
+
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+
+ /* find out how many packages are in each category */
+ array = xb_silo_query (silo,
+ "components/component/kudos/"
+ "kudo[text()='GnomeSoftware::popular']/../..",
+ 0, &error_local);
+ if (array == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < array->len; i++) {
+ g_autoptr(GsApp) app = NULL;
+ XbNode *component = g_ptr_array_index (array, i);
+ const gchar *component_id = xb_node_query_text (component, "id", NULL);
+ if (component_id == NULL)
+ continue;
+ app = gs_app_new (component_id);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ }
+ return TRUE;
+}
+
+gboolean
+gs_appstream_add_recent (GsPlugin *plugin,
+ XbSilo *silo,
+ GsAppList *list,
+ guint64 age,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint64 now = (guint64) g_get_real_time () / G_USEC_PER_SEC;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) array = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+
+ /* use predicate conditions to the max */
+ xpath = g_strdup_printf ("components/component/releases/"
+ "release[@timestamp>%" G_GUINT64_FORMAT "]/../..",
+ now - age);
+ array = xb_silo_query (silo, xpath, 0, &error_local);
+ if (array == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < array->len; i++) {
+ XbNode *component = g_ptr_array_index (array, i);
+ g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error);
+ guint64 timestamp;
+ if (app == NULL)
+ return FALSE;
+ /* set the release date */
+ timestamp = component_get_release_timestamp (component);
+ if (timestamp != G_MAXUINT64)
+ gs_app_set_release_date (app, timestamp);
+ gs_app_list_add (list, app);
+ }
+ return TRUE;
+}
+
+gboolean
+gs_appstream_add_alternates (XbSilo *silo,
+ GsApp *app,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GPtrArray *sources = gs_app_get_sources (app);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) ids = NULL;
+ g_autoptr(GString) xpath = g_string_new (NULL);
+
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+
+ /* probably a package we know nothing about */
+ if (gs_app_get_id (app) == NULL)
+ return TRUE;
+
+ /* actual ID */
+ xb_string_append_union (xpath, "components/component/id[text()='%s']",
+ gs_app_get_id (app));
+
+ /* new ID -> old ID */
+ xb_string_append_union (xpath, "components/component/id[text()='%s']/../provides/id",
+ gs_app_get_id (app));
+
+ /* old ID -> new ID */
+ xb_string_append_union (xpath, "components/component/provides/id[text()='%s']/../../id",
+ gs_app_get_id (app));
+
+ /* find apps that use the same pkgname */
+ for (guint j = 0; j < sources->len; j++) {
+ const gchar *source = g_ptr_array_index (sources, j);
+ g_autofree gchar *source_safe = xb_string_escape (source);
+ xb_string_append_union (xpath,
+ "components/component/pkgname[text()='%s']/../id",
+ source_safe);
+ }
+
+ /* do a big query, and return all the unique results */
+ ids = xb_silo_query (silo, xpath->str, 0, &error_local);
+ if (ids == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < ids->len; i++) {
+ XbNode *n = g_ptr_array_index (ids, i);
+ g_autoptr(GsApp) app2 = NULL;
+ const gchar *tmp;
+ app2 = gs_app_new (xb_node_get_text (n));
+ gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD);
+
+ tmp = xb_node_query_attr (n, "../..", "origin", NULL);
+ if (gs_appstream_origin_valid (tmp))
+ gs_app_set_origin_appstream (app2, tmp);
+ gs_app_list_add (list, app2);
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_appstream_add_featured_with_query (XbSilo *silo,
+ const gchar *query,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) array = NULL;
+
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+
+ /* find out how many packages are in each category */
+ array = xb_silo_query (silo, query, 0, &error_local);
+ if (array == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < array->len; i++) {
+ g_autoptr(GsApp) app = NULL;
+ XbNode *component = g_ptr_array_index (array, i);
+ const gchar *component_id = xb_node_query_text (component, "id", NULL);
+ if (component_id == NULL)
+ continue;
+ app = gs_app_new (component_id);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ if (!gs_appstream_copy_metadata (app, component, error))
+ return FALSE;
+ gs_app_list_add (list, app);
+ }
+ return TRUE;
+}
+
+gboolean
+gs_appstream_add_featured (XbSilo *silo,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *query = "components/component/custom/value[@key='GnomeSoftware::FeatureTile']/../..|"
+ "components/component/custom/value[@key='GnomeSoftware::FeatureTile-css']/../..";
+ return gs_appstream_add_featured_with_query (silo, query, list, cancellable, error);
+}
+
+gboolean
+gs_appstream_add_deployment_featured (XbSilo *silo,
+ const gchar * const *deployments,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GString) query = g_string_new (NULL);
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (deployments != NULL, FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+ for (guint ii = 0; deployments[ii] != NULL; ii++) {
+ g_autofree gchar *escaped = xb_string_escape (deployments[ii]);
+ if (escaped != NULL && *escaped != '\0') {
+ xb_string_append_union (query,
+ "components/component/custom/value[@key='GnomeSoftware::DeploymentFeatured'][text()='%s']/../..",
+ escaped);
+ }
+ }
+ if (!query->len)
+ return TRUE;
+ return gs_appstream_add_featured_with_query (silo, query->str, list, cancellable, error);
+}
+
+gboolean
+gs_appstream_url_to_app (GsPlugin *plugin,
+ XbSilo *silo,
+ GsAppList *list,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *path = NULL;
+ g_autofree gchar *scheme = NULL;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GPtrArray) components = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (XB_IS_SILO (silo), FALSE);
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+ g_return_val_if_fail (url != NULL, FALSE);
+
+ /* not us */
+ scheme = gs_utils_get_url_scheme (url);
+ if (g_strcmp0 (scheme, "appstream") != 0)
+ return TRUE;
+
+ path = gs_utils_get_url_path (url);
+ xpath = g_strdup_printf ("components/component/id[text()='%s']/..", path);
+ components = xb_silo_query (silo, xpath, 0, NULL);
+ if (components == NULL)
+ return TRUE;
+
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autoptr(GsApp) app = NULL;
+ app = gs_appstream_create_app (plugin, silo, component, error);
+ if (app == NULL)
+ return FALSE;
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM);
+ gs_app_list_add (list, app);
+ }
+
+ return TRUE;
+}
+
+void
+gs_appstream_component_add_keyword (XbBuilderNode *component, const gchar *str)
+{
+ g_autoptr(XbBuilderNode) keyword = NULL;
+ g_autoptr(XbBuilderNode) keywords = NULL;
+
+ g_return_if_fail (XB_IS_BUILDER_NODE (component));
+ g_return_if_fail (str != NULL);
+
+ /* create <keywords> if it does not already exist */
+ keywords = xb_builder_node_get_child (component, "keywords", NULL);
+ if (keywords == NULL)
+ keywords = xb_builder_node_insert (component, "keywords", NULL);
+
+ /* create <keyword>str</keyword> if it does not already exist */
+ keyword = xb_builder_node_get_child (keywords, "keyword", str);
+ if (keyword == NULL) {
+ keyword = xb_builder_node_insert (keywords, "keyword", NULL);
+ xb_builder_node_set_text (keyword, str, -1);
+ }
+}
+
+void
+gs_appstream_component_add_provide (XbBuilderNode *component, const gchar *str)
+{
+ g_autoptr(XbBuilderNode) provide = NULL;
+ g_autoptr(XbBuilderNode) provides = NULL;
+
+ g_return_if_fail (XB_IS_BUILDER_NODE (component));
+ g_return_if_fail (str != NULL);
+
+ /* create <provides> if it does not already exist */
+ provides = xb_builder_node_get_child (component, "provides", NULL);
+ if (provides == NULL)
+ provides = xb_builder_node_insert (component, "provides", NULL);
+
+ /* create <id>str</id> if it does not already exist */
+ provide = xb_builder_node_get_child (provides, "id", str);
+ if (provide == NULL) {
+ provide = xb_builder_node_insert (provides, "id", NULL);
+ xb_builder_node_set_text (provide, str, -1);
+ }
+}
+
+void
+gs_appstream_component_add_category (XbBuilderNode *component, const gchar *str)
+{
+ g_autoptr(XbBuilderNode) category = NULL;
+ g_autoptr(XbBuilderNode) categories = NULL;
+
+ g_return_if_fail (XB_IS_BUILDER_NODE (component));
+ g_return_if_fail (str != NULL);
+
+ /* create <categories> if it does not already exist */
+ categories = xb_builder_node_get_child (component, "categories", NULL);
+ if (categories == NULL)
+ categories = xb_builder_node_insert (component, "categories", NULL);
+
+ /* create <category>str</category> if it does not already exist */
+ category = xb_builder_node_get_child (categories, "category", str);
+ if (category == NULL) {
+ category = xb_builder_node_insert (categories, "category", NULL);
+ xb_builder_node_set_text (category, str, -1);
+ }
+}
+
+void
+gs_appstream_component_add_icon (XbBuilderNode *component, const gchar *str)
+{
+ g_autoptr(XbBuilderNode) icon = NULL;
+
+ g_return_if_fail (XB_IS_BUILDER_NODE (component));
+ g_return_if_fail (str != NULL);
+
+ /* create <icon>str</icon> if it does not already exist */
+ icon = xb_builder_node_get_child (component, "icon", NULL);
+ if (icon == NULL) {
+ icon = xb_builder_node_insert (component, "icon",
+ "type", "stock",
+ NULL);
+ xb_builder_node_set_text (icon, str, -1);
+ }
+}
+
+void
+gs_appstream_component_add_extra_info (XbBuilderNode *component)
+{
+ const gchar *kind;
+
+ g_return_if_fail (XB_IS_BUILDER_NODE (component));
+
+ kind = xb_builder_node_get_attr (component, "type");
+
+ /* add the gnome-software-specific 'Addon' group and ensure they
+ * all have an icon set */
+ switch (as_component_kind_from_string (kind)) {
+ case AS_COMPONENT_KIND_WEB_APP:
+ gs_appstream_component_add_keyword (component, kind);
+ break;
+ case AS_COMPONENT_KIND_FONT:
+ gs_appstream_component_add_category (component, "Addon");
+ gs_appstream_component_add_category (component, "Font");
+ break;
+ case AS_COMPONENT_KIND_DRIVER:
+ gs_appstream_component_add_category (component, "Addon");
+ gs_appstream_component_add_category (component, "Driver");
+ gs_appstream_component_add_icon (component, "system-component-driver");
+ break;
+ case AS_COMPONENT_KIND_LOCALIZATION:
+ gs_appstream_component_add_category (component, "Addon");
+ gs_appstream_component_add_category (component, "Localization");
+ gs_appstream_component_add_icon (component, "system-component-language");
+ break;
+ case AS_COMPONENT_KIND_CODEC:
+ gs_appstream_component_add_category (component, "Addon");
+ gs_appstream_component_add_category (component, "Codec");
+ gs_appstream_component_add_icon (component, "system-component-codecs");
+ break;
+ case AS_COMPONENT_KIND_INPUT_METHOD:
+ gs_appstream_component_add_keyword (component, kind);
+ gs_appstream_component_add_category (component, "Addon");
+ gs_appstream_component_add_category (component, "InputSource");
+ gs_appstream_component_add_icon (component, "system-component-input-sources");
+ break;
+ case AS_COMPONENT_KIND_FIRMWARE:
+ gs_appstream_component_add_icon (component, "system-component-firmware");
+ break;
+ default:
+ break;
+ }
+}
+
+/* Resolve any media URIs which are actually relative
+ * paths against the media_baseurl property */
+void
+gs_appstream_component_fix_url (XbBuilderNode *component, const gchar *baseurl)
+{
+ const gchar *text;
+ g_autofree gchar *url = NULL;
+
+ g_return_if_fail (XB_IS_BUILDER_NODE (component));
+ g_return_if_fail (baseurl != NULL);
+
+ text = xb_builder_node_get_text (component);
+
+ if (text == NULL)
+ return;
+
+ if (g_str_has_prefix (text, "http:") ||
+ g_str_has_prefix (text, "https:"))
+ return;
+
+ url = g_strconcat (baseurl, "/", text, NULL);
+ xb_builder_node_set_text (component, url , -1);
+}