3341 lines
114 KiB
C
3341 lines
114 KiB
C
/* -*- 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-or-later
|
||
*/
|
||
|
||
#include "config.h"
|
||
|
||
#include <glib/gstdio.h>
|
||
#include <gnome-software.h>
|
||
#include <locale.h>
|
||
#include <malloc.h>
|
||
|
||
#include "gs-external-appstream-utils.h"
|
||
#include "gs-appstream.h"
|
||
|
||
#define GS_APPSTREAM_MAX_SCREENSHOTS 5
|
||
|
||
/* This requires changes for https://github.com/hughsie/libxmlb/issues/120
|
||
* The libxmlb crashes when all nodes are marked for a removal in the fixup-s
|
||
*/
|
||
#if LIBXMLB_CHECK_VERSION(0, 3, 9)
|
||
#define HAVE_FIXED_LIBXMLB 1
|
||
#endif
|
||
|
||
GsApp *
|
||
gs_appstream_create_app (GsPlugin *plugin,
|
||
XbSilo *silo,
|
||
XbNode *component,
|
||
const gchar *appstream_source_file,
|
||
AsComponentScope default_scope,
|
||
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,
|
||
NULL, appstream_source_file, default_scope, 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);
|
||
gchar *r_ptr = escaped, *w_ptr = escaped;
|
||
gboolean has_space;
|
||
/* skip leading spaces */
|
||
while (g_ascii_isspace (*r_ptr))
|
||
r_ptr++;
|
||
/* replace consecutive white-spaces with a single space */
|
||
for (has_space = FALSE; *r_ptr != '\0'; r_ptr++) {
|
||
if (g_ascii_isspace (*r_ptr)) {
|
||
has_space = TRUE;
|
||
} else {
|
||
if (has_space) {
|
||
*w_ptr = ' ';
|
||
w_ptr++;
|
||
has_space = FALSE;
|
||
}
|
||
if (w_ptr != r_ptr)
|
||
*w_ptr = *r_ptr;
|
||
w_ptr++;
|
||
}
|
||
}
|
||
if (has_space) {
|
||
*w_ptr = ' ';
|
||
w_ptr++;
|
||
}
|
||
if (w_ptr != r_ptr)
|
||
*w_ptr = '\0';
|
||
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 void
|
||
format_issue_link (GString *str,
|
||
const gchar *issue_content,
|
||
AsIssueKind kind,
|
||
const gchar *url)
|
||
{
|
||
g_autofree gchar *escaped_text = NULL;
|
||
|
||
if (url != NULL) {
|
||
escaped_text = g_markup_printf_escaped ("<a href=\"%s\" title=\"%s\">%s</a>",
|
||
url, url, issue_content);
|
||
g_string_append (str, escaped_text);
|
||
return;
|
||
}
|
||
|
||
switch (kind) {
|
||
case AS_ISSUE_KIND_CVE:
|
||
#define CVE_URL "https://cve.mitre.org/cgi-bin/cvename.cgi?name="
|
||
/* @issue_content is expected to be in the form ‘CVE-2023-12345’ */
|
||
escaped_text = g_markup_printf_escaped ("<a href=\"" CVE_URL "%s\" title=\"" CVE_URL "%s\">%s</a>",
|
||
issue_content, issue_content, issue_content);
|
||
#undef CVE_URL
|
||
break;
|
||
case AS_ISSUE_KIND_GENERIC:
|
||
case AS_ISSUE_KIND_UNKNOWN:
|
||
default:
|
||
escaped_text = g_markup_escape_text (issue_content, -1);
|
||
break;
|
||
}
|
||
|
||
g_string_append (str, escaped_text);
|
||
}
|
||
|
||
static gchar *
|
||
gs_appstream_format_description (XbNode *description_node,
|
||
XbNode *issues_node)
|
||
{
|
||
g_autoptr(GString) str = g_string_new (NULL);
|
||
|
||
for (g_autoptr(XbNode) n = description_node ? xb_node_get_child (description_node) : NULL; 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(XbNode) child = NULL;
|
||
g_autoptr(XbNode) next = NULL;
|
||
for (child = xb_node_get_child (n); child != NULL; g_object_unref (child), child = g_steal_pointer (&next)) {
|
||
next = xb_node_get_next (child);
|
||
if (g_strcmp0 (xb_node_get_element (child), "li") == 0) {
|
||
g_autofree gchar *escaped = gs_appstream_format_description_text (child);
|
||
|
||
/* 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(XbNode) child = NULL;
|
||
g_autoptr(XbNode) next = NULL;
|
||
guint i = 0;
|
||
for (child = xb_node_get_child (n); child != NULL; i++, g_object_unref (child), child = g_steal_pointer (&next)) {
|
||
next = xb_node_get_next (child);
|
||
if (g_strcmp0 (xb_node_get_element (child), "li") == 0) {
|
||
g_autofree gchar *escaped = gs_appstream_format_description_text (child);
|
||
|
||
/* 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);
|
||
|
||
if (issues_node) {
|
||
/* Add a single new line to delimit the description node's text from the issues */
|
||
if (str->len)
|
||
g_string_append_c (str, '\n');
|
||
|
||
for (g_autoptr(XbNode) n = xb_node_get_child (issues_node); n != NULL; node_set_to_next (&n)) {
|
||
if (g_strcmp0 (xb_node_get_element (n), "issue") == 0) {
|
||
const gchar *node_text = xb_node_get_text (n);
|
||
AsIssueKind issue_kind = as_issue_kind_from_string (xb_node_get_attr (n, "type"));
|
||
const gchar *issue_url = xb_node_get_attr (n, "url");
|
||
|
||
if (node_text != NULL && *node_text != '\0') {
|
||
if (str->len > 0 && str->str[str->len - 1] != '\n')
|
||
g_string_append_c (str, '\n');
|
||
g_string_append (str, " • ");
|
||
format_issue_link (str, node_text, issue_kind, issue_url);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* remove extra newlines, in case there was no text for the issues */
|
||
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;
|
||
guint64 scale = 0;
|
||
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);
|
||
}
|
||
|
||
scale = xb_node_get_attr_as_uint (n, "scale");
|
||
if (scale > 0 && scale < G_MAXUINT)
|
||
as_icon_set_scale (icon, (guint) scale);
|
||
|
||
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
|
||
traverse_components_for_icons (GsApp *app,
|
||
GPtrArray *components)
|
||
{
|
||
if (components == NULL)
|
||
return;
|
||
|
||
for (guint i = 0; i < components->len; i++) {
|
||
XbNode *component = g_ptr_array_index (components, i);
|
||
g_autoptr(XbNode) child = NULL;
|
||
g_autoptr(XbNode) next = NULL;
|
||
for (child = xb_node_get_child (component); child != NULL; g_object_unref (child), child = g_steal_pointer (&next)) {
|
||
next = xb_node_get_next (child);
|
||
if (g_strcmp0 (xb_node_get_element (child), "icon") == 0) {
|
||
/* 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. */
|
||
g_autoptr(AsIcon) as_icon = NULL;
|
||
g_autoptr(GIcon) gicon = NULL;
|
||
const gchar *icon_kind_str = xb_node_get_attr (child, "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;
|
||
}
|
||
|
||
as_icon = gs_appstream_new_icon (component, child, icon_kind, 0);
|
||
gicon = gs_icon_new_for_appstream_icon (as_icon);
|
||
if (gicon != NULL)
|
||
gs_app_add_icon (app, gicon);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
gs_appstream_refine_add_addons (GsPlugin *plugin,
|
||
GsApp *app,
|
||
XbSilo *silo,
|
||
const gchar *appstream_source_file,
|
||
AsComponentScope default_scope,
|
||
GError **error)
|
||
{
|
||
g_autofree gchar *xpath = NULL;
|
||
g_autoptr(GError) error_local = NULL;
|
||
g_autoptr(GPtrArray) addons = NULL;
|
||
g_autoptr(GsAppList) addons_list = NULL;
|
||
AsProvided *provided;
|
||
|
||
/* get all components */
|
||
xpath = g_strdup_printf ("components/component/extends[text()='%s']/..",
|
||
gs_app_get_id (app));
|
||
provided = gs_app_get_provided_for_kind (app, AS_PROVIDED_KIND_ID);
|
||
if (provided != NULL) {
|
||
GString *extended_xpath = g_string_new (xpath);
|
||
GPtrArray *items = as_provided_get_items (provided);
|
||
for (guint i = 0; i < items->len; i++) {
|
||
const gchar *id = g_ptr_array_index (items, i);
|
||
g_string_append_printf (extended_xpath, "|components/component/extends[text()='%s']", id);
|
||
}
|
||
g_free (xpath);
|
||
xpath = g_string_free (extended_xpath, FALSE);
|
||
}
|
||
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, appstream_source_file, default_scope, 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 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;
|
||
}
|
||
|
||
/**
|
||
* _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_relation (GsApp *app,
|
||
XbNode *relation_node,
|
||
AsRelationKind kind,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GPtrArray) relations = NULL;
|
||
|
||
/* 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")) {
|
||
const gchar *compare;
|
||
const gchar *side;
|
||
#if !AS_CHECK_VERSION(1, 0, 0)
|
||
AsDisplayLengthKind display_length_kind;
|
||
#endif
|
||
|
||
/* 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);
|
||
|
||
#if AS_CHECK_VERSION(1, 0, 0)
|
||
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
|
||
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 {
|
||
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));
|
||
}
|
||
#endif
|
||
} else if (g_str_equal (item_kind, "id")) {
|
||
if (kind == AS_RELATION_KIND_REQUIRES &&
|
||
g_strcmp0 (xb_node_get_attr (child, "type"), "id") == 0 &&
|
||
g_strcmp0 (xb_node_get_text (child), "org.gnome.Software.desktop") == 0) {
|
||
/* is compatible */
|
||
gint rc = gs_utils_compare_versions (xb_node_get_attr (child, "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;
|
||
}
|
||
}
|
||
} else {
|
||
g_debug ("Relation type ‘%s’ not currently supported for %s; ignoring",
|
||
item_kind, gs_app_get_id (app));
|
||
continue;
|
||
}
|
||
|
||
if (relations == NULL)
|
||
relations = g_ptr_array_new_with_free_func (g_object_unref);
|
||
g_ptr_array_add (relations, g_steal_pointer (&relation));
|
||
}
|
||
|
||
gs_app_set_relations (app, relations);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static void
|
||
gs_appstream_find_description_and_issues_nodes (XbNode *release_node,
|
||
XbNode **out_description_node, /* (out) (transfer full) */
|
||
XbNode **out_issues_node) /* (out) (transfer full) */
|
||
{
|
||
g_autoptr(XbNode) child = NULL;
|
||
g_autoptr(XbNode) next = NULL;
|
||
g_autoptr(XbNode) description_node = NULL;
|
||
g_autoptr(XbNode) issues_node = NULL;
|
||
|
||
for (child = xb_node_get_child (release_node);
|
||
child != NULL && (description_node == NULL || issues_node == NULL);
|
||
g_object_unref (child), child = g_steal_pointer (&next)) {
|
||
next = xb_node_get_next (child);
|
||
if (description_node == NULL && g_strcmp0 (xb_node_get_element (child), "description") == 0) {
|
||
description_node = g_object_ref (child);
|
||
} else if (issues_node == NULL && g_strcmp0 (xb_node_get_element (child), "issues") == 0) {
|
||
issues_node = g_object_ref (child);
|
||
}
|
||
}
|
||
|
||
if (out_description_node)
|
||
*out_description_node = g_steal_pointer (&description_node);
|
||
if (out_issues_node)
|
||
*out_issues_node = g_steal_pointer (&issues_node);
|
||
}
|
||
|
||
typedef enum {
|
||
ELEMENT_KIND_UNKNOWN = -1,
|
||
ELEMENT_KIND_BRANDING,
|
||
ELEMENT_KIND_BUNDLE,
|
||
ELEMENT_KIND_CATEGORIES,
|
||
ELEMENT_KIND_CONTENT_RATING,
|
||
ELEMENT_KIND_CUSTOM,
|
||
ELEMENT_KIND_DESCRIPTION,
|
||
ELEMENT_KIND_DEVELOPER_NAME,
|
||
ELEMENT_KIND_DEVELOPER,
|
||
ELEMENT_KIND_ICON,
|
||
ELEMENT_KIND_ID,
|
||
ELEMENT_KIND_INFO,
|
||
ELEMENT_KIND_KEYWORDS,
|
||
ELEMENT_KIND_KUDOS,
|
||
ELEMENT_KIND_LANGUAGES,
|
||
ELEMENT_KIND_LAUNCHABLE,
|
||
ELEMENT_KIND_METADATA_LICENSE,
|
||
ELEMENT_KIND_NAME,
|
||
ELEMENT_KIND_PKGNAME,
|
||
ELEMENT_KIND_PROJECT_GROUP,
|
||
ELEMENT_KIND_PROJECT_LICENSE,
|
||
ELEMENT_KIND_PROVIDES,
|
||
ELEMENT_KIND_RECOMMENDS,
|
||
ELEMENT_KIND_RELEASES,
|
||
ELEMENT_KIND_REQUIRES,
|
||
ELEMENT_KIND_SCREENSHOTS,
|
||
ELEMENT_KIND_SUMMARY,
|
||
ELEMENT_KIND_SUPPORTS,
|
||
ELEMENT_KIND_URL
|
||
} ElementKind;
|
||
|
||
/* This is not for speed, but to not accidentally have checked for the element name twice in the block */
|
||
static ElementKind
|
||
gs_appstream_get_element_kind (const gchar *element_name)
|
||
{
|
||
struct {
|
||
const gchar *name;
|
||
ElementKind kind;
|
||
} kinds[] = {
|
||
{ "branding", ELEMENT_KIND_BRANDING },
|
||
{ "bundle", ELEMENT_KIND_BUNDLE },
|
||
{ "categories", ELEMENT_KIND_CATEGORIES },
|
||
{ "content_rating", ELEMENT_KIND_CONTENT_RATING },
|
||
{ "custom", ELEMENT_KIND_CUSTOM },
|
||
{ "description", ELEMENT_KIND_DESCRIPTION },
|
||
{ "developer", ELEMENT_KIND_DEVELOPER },
|
||
{ "developer_name", ELEMENT_KIND_DEVELOPER_NAME },
|
||
{ "icon", ELEMENT_KIND_ICON },
|
||
{ "id", ELEMENT_KIND_ID },
|
||
{ "info", ELEMENT_KIND_INFO },
|
||
{ "keywords", ELEMENT_KIND_KEYWORDS },
|
||
{ "kudos", ELEMENT_KIND_KUDOS },
|
||
{ "languages", ELEMENT_KIND_LANGUAGES },
|
||
{ "launchable", ELEMENT_KIND_LAUNCHABLE },
|
||
{ "metadata_license", ELEMENT_KIND_METADATA_LICENSE },
|
||
{ "name", ELEMENT_KIND_NAME },
|
||
{ "pkgname", ELEMENT_KIND_PKGNAME },
|
||
{ "project_group", ELEMENT_KIND_PROJECT_GROUP },
|
||
{ "project_license", ELEMENT_KIND_PROJECT_LICENSE },
|
||
{ "provides", ELEMENT_KIND_PROVIDES },
|
||
{ "recommends", ELEMENT_KIND_RECOMMENDS },
|
||
{ "releases", ELEMENT_KIND_RELEASES },
|
||
{ "requires", ELEMENT_KIND_REQUIRES },
|
||
{ "screenshots", ELEMENT_KIND_SCREENSHOTS },
|
||
{ "summary", ELEMENT_KIND_SUMMARY },
|
||
{ "supports", ELEMENT_KIND_SUPPORTS },
|
||
{ "url", ELEMENT_KIND_URL }
|
||
};
|
||
for (guint i = 0; i < G_N_ELEMENTS (kinds); i++) {
|
||
if (g_strcmp0 (element_name, kinds[i].name) == 0)
|
||
return kinds[i].kind;
|
||
}
|
||
return ELEMENT_KIND_UNKNOWN;
|
||
}
|
||
|
||
gboolean
|
||
gs_appstream_refine_app (GsPlugin *plugin,
|
||
GsApp *app,
|
||
XbSilo *silo,
|
||
XbNode *component,
|
||
GsPluginRefineFlags refine_flags,
|
||
GHashTable *installed_by_desktopid,
|
||
const gchar *appstream_source_file,
|
||
AsComponentScope default_scope,
|
||
GError **error)
|
||
{
|
||
GsAppQuality name_quality = GS_APP_QUALITY_HIGHEST;
|
||
const gchar *tmp;
|
||
const gchar *developer_name_fallback = NULL;
|
||
gboolean has_name = FALSE, has_metadata_license = FALSE;
|
||
gboolean had_icons, had_sources;
|
||
gboolean locale_has_translations = FALSE;
|
||
g_autoptr(GPtrArray) legacy_pkgnames = NULL;
|
||
g_autoptr(XbNode) launchable_desktop_id = NULL;
|
||
g_autoptr(XbNode) child = NULL;
|
||
g_autoptr(XbNode) next = 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);
|
||
|
||
had_icons = gs_app_has_icons (app);
|
||
had_sources = gs_app_get_sources (app)->len > 0;
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) != 0) {
|
||
tmp = setlocale (LC_MESSAGES, NULL);
|
||
locale_has_translations = _gs_utils_locale_has_translations (tmp);
|
||
}
|
||
|
||
/* 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_REPOSITORY:
|
||
/* plugins may know better name, than what there's set in the appstream for the repos */
|
||
name_quality = GS_APP_QUALITY_NORMAL;
|
||
/* fall-through */
|
||
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:
|
||
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);
|
||
}
|
||
|
||
tmp = gs_app_get_metadata_item (app, "flathub::verification::verified");
|
||
if (g_strcmp0 (tmp, "true") == 0)
|
||
gs_app_add_quirk (app, GS_APP_QUIRK_DEVELOPER_VERIFIED);
|
||
else
|
||
gs_app_remove_quirk (app, GS_APP_QUIRK_DEVELOPER_VERIFIED);
|
||
|
||
legacy_pkgnames = g_ptr_array_new_with_free_func (g_object_unref);
|
||
|
||
for (child = xb_node_get_child (component); child != NULL; g_object_unref (child), child = g_steal_pointer (&next)) {
|
||
next = xb_node_get_next (child);
|
||
|
||
switch (gs_appstream_get_element_kind (xb_node_get_element (child))) {
|
||
default:
|
||
case ELEMENT_KIND_UNKNOWN:
|
||
break;
|
||
case ELEMENT_KIND_BRANDING:
|
||
{
|
||
g_autoptr(XbNode) branding_child = NULL;
|
||
g_autoptr(XbNode) branding_next = NULL;
|
||
for (branding_child = xb_node_get_child (child);
|
||
branding_child != NULL;
|
||
g_object_unref (branding_child), branding_child = g_steal_pointer (&branding_next)) {
|
||
branding_next = xb_node_get_next (branding_child);
|
||
if (g_strcmp0 (xb_node_get_element (branding_child), "color") == 0) {
|
||
const gchar *type = xb_node_get_attr (branding_child, "type");
|
||
if (g_strcmp0 (type, "primary") == 0) {
|
||
const gchar *color = xb_node_get_text (branding_child);
|
||
GdkRGBA rgba;
|
||
if (color != NULL && gdk_rgba_parse (&rgba, color)) {
|
||
const gchar *scheme_preference = xb_node_get_attr (branding_child, "scheme_preference");
|
||
GsColorScheme color_scheme = GS_COLOR_SCHEME_ANY;
|
||
|
||
if (g_strcmp0 (scheme_preference, "light") == 0)
|
||
color_scheme = GS_COLOR_SCHEME_LIGHT;
|
||
else if (g_strcmp0 (scheme_preference, "dark") == 0)
|
||
color_scheme = GS_COLOR_SCHEME_DARK;
|
||
|
||
gs_app_set_key_color_for_color_scheme (app, color_scheme, &rgba);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_BUNDLE:
|
||
if (!had_sources) {
|
||
const gchar *kind = xb_node_get_attr (child, "type");
|
||
const gchar *bundle_id = xb_node_get_text (child);
|
||
|
||
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]);
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_CATEGORIES:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES) != 0) {
|
||
g_autoptr(XbNode) cat_child = NULL;
|
||
g_autoptr(XbNode) cat_next = NULL;
|
||
for (cat_child = xb_node_get_child (child); cat_child != NULL; g_object_unref (cat_child), cat_child = g_steal_pointer (&cat_next)) {
|
||
cat_next = xb_node_get_next (cat_child);
|
||
if (g_strcmp0 (xb_node_get_element (cat_child), "category") == 0) {
|
||
tmp = xb_node_get_text (cat_child);
|
||
if (tmp != NULL) {
|
||
gs_app_add_category (app, tmp);
|
||
|
||
/* 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 (tmp, "Blacklisted") == 0)
|
||
gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE);
|
||
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) != 0 &&
|
||
!gs_app_has_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED) &&
|
||
g_strcmp0 (tmp, "Featured") == 0)
|
||
gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) != 0 &&
|
||
!gs_app_has_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED)) {
|
||
g_autoptr(XbNode) cat_child = NULL;
|
||
g_autoptr(XbNode) cat_next = NULL;
|
||
for (cat_child = xb_node_get_child (child); cat_child != NULL; g_object_unref (cat_child), cat_child = g_steal_pointer (&cat_next)) {
|
||
cat_next = xb_node_get_next (cat_child);
|
||
if (g_strcmp0 (xb_node_get_element (cat_child), "category") == 0) {
|
||
tmp = xb_node_get_text (cat_child);
|
||
if (g_strcmp0 (tmp, "Featured") == 0) {
|
||
gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_CONTENT_RATING: {
|
||
g_autoptr(AsContentRating) content_rating = gs_app_dup_content_rating (app);
|
||
if (content_rating == NULL) {
|
||
const gchar *content_rating_kind = NULL;
|
||
|
||
/* get kind */
|
||
content_rating_kind = xb_node_get_attr (child, "type");
|
||
/* we only really expect/support OARS 1.0 and 1.1 */
|
||
if (g_strcmp0 (content_rating_kind, "oars-1.0") == 0 ||
|
||
g_strcmp0 (content_rating_kind, "oars-1.1") == 0) {
|
||
g_autoptr(AsContentRating) cr = as_content_rating_new ();
|
||
g_autoptr(XbNode) cr_child = NULL;
|
||
g_autoptr(XbNode) cr_next = NULL;
|
||
|
||
as_content_rating_set_kind (cr, content_rating_kind);
|
||
for (cr_child = xb_node_get_child (child); cr_child != NULL; g_object_unref (cr_child), cr_child = g_steal_pointer (&cr_next)) {
|
||
cr_next = xb_node_get_next (cr_child);
|
||
if (g_strcmp0 (xb_node_get_element (cr_child), "content_attribute") == 0) {
|
||
as_content_rating_add_attribute (cr,
|
||
xb_node_get_attr (cr_child, "id"),
|
||
as_content_rating_value_from_string (xb_node_get_text (cr_child)));
|
||
}
|
||
}
|
||
if (cr != NULL)
|
||
gs_app_set_content_rating (app, cr);
|
||
}
|
||
}
|
||
} break;
|
||
case ELEMENT_KIND_CUSTOM: {
|
||
g_autoptr(XbNode) cus_child = NULL;
|
||
g_autoptr(XbNode) cus_next = NULL;
|
||
for (cus_child = xb_node_get_child (child); cus_child != NULL; g_object_unref (cus_child), cus_child = g_steal_pointer (&cus_next)) {
|
||
const gchar *key = xb_node_get_attr (cus_child, "key");
|
||
cus_next = xb_node_get_next (cus_child);
|
||
if (key == NULL)
|
||
continue;
|
||
if (gs_app_get_metadata_item (app, key) != NULL)
|
||
continue;
|
||
gs_app_set_metadata (app, key, xb_node_get_text (cus_child));
|
||
}
|
||
} break;
|
||
case ELEMENT_KIND_DESCRIPTION:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) != 0) {
|
||
g_autofree gchar *description = gs_appstream_format_description (child, NULL);
|
||
if (description != NULL)
|
||
gs_app_set_description (app, GS_APP_QUALITY_HIGHEST, description);
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_DEVELOPER:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) > 0 &&
|
||
gs_app_get_developer_name (app) == NULL) {
|
||
g_autoptr(XbNode) developer_child = NULL;
|
||
g_autoptr(XbNode) developer_next = NULL;
|
||
for (developer_child = xb_node_get_child (child);
|
||
developer_child != NULL;
|
||
g_object_unref (developer_child), developer_child = g_steal_pointer (&developer_next)) {
|
||
developer_next = xb_node_get_next (developer_child);
|
||
if (g_strcmp0 (xb_node_get_element (developer_child), "name") == 0) {
|
||
tmp = xb_node_get_text (developer_child);
|
||
if (tmp != NULL) {
|
||
gs_app_set_developer_name (app, tmp);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_DEVELOPER_NAME:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) > 0 &&
|
||
developer_name_fallback == NULL) {
|
||
developer_name_fallback = xb_node_get_text (child);
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_ICON:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) != 0 &&
|
||
!had_icons) {
|
||
/* 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. */
|
||
const gchar *icon_kind_str = xb_node_get_attr (child, "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);
|
||
} else {
|
||
g_autoptr(GIcon) gicon = NULL;
|
||
g_autoptr(AsIcon) as_icon = NULL;
|
||
as_icon = gs_appstream_new_icon (component, child, icon_kind, 0);
|
||
gicon = gs_icon_new_for_appstream_icon (as_icon);
|
||
if (gicon != NULL)
|
||
gs_app_add_icon (app, gicon);
|
||
}
|
||
}
|
||
/* HiDPI icon */
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) != 0 &&
|
||
!gs_app_has_kudo (app, GS_APP_KUDO_HI_DPI_ICON) &&
|
||
xb_node_get_attr_as_uint (child, "width") == 128) {
|
||
gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON);
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_ID:
|
||
if (gs_app_get_id (app) == NULL) {
|
||
tmp = xb_node_get_text (child);
|
||
if (tmp != NULL)
|
||
gs_app_set_id (app, tmp);
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_INFO:
|
||
if (gs_app_get_metadata_item (app, "appstream::source-file") == NULL) {
|
||
g_autoptr(XbNode) info_child = NULL;
|
||
g_autoptr(XbNode) info_next = NULL;
|
||
for (info_child = xb_node_get_child (child); info_child != NULL; g_object_unref (info_child), info_child = g_steal_pointer (&info_next)) {
|
||
info_next = xb_node_get_next (info_child);
|
||
if (g_strcmp0 (xb_node_get_element (info_child), "filename") == 0) {
|
||
tmp = xb_node_get_text (info_child);
|
||
if (tmp != NULL)
|
||
gs_app_set_metadata (app, "appstream::source-file", tmp);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_KEYWORDS:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) != 0 &&
|
||
!gs_app_has_kudo (app, GS_APP_KUDO_HAS_KEYWORDS)) {
|
||
g_autoptr(XbNode) kw_child = NULL;
|
||
g_autoptr(XbNode) kw_next = NULL;
|
||
for (kw_child = xb_node_get_child (child); kw_child != NULL; g_object_unref (kw_child), kw_child = g_steal_pointer (&kw_next)) {
|
||
kw_next = xb_node_get_next (kw_child);
|
||
if (g_strcmp0 (xb_node_get_element (kw_child), "keyword") == 0) {
|
||
gs_app_add_kudo (app, GS_APP_KUDO_HAS_KEYWORDS);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_KUDOS:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) != 0 &&
|
||
!gs_app_has_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED)) {
|
||
g_autoptr(XbNode) kudos_child = NULL;
|
||
g_autoptr(XbNode) kudos_next = NULL;
|
||
for (kudos_child = xb_node_get_child (child); kudos_child != NULL; g_object_unref (kudos_child), kudos_child = g_steal_pointer (&kudos_next)) {
|
||
kudos_next = xb_node_get_next (kudos_child);
|
||
if (g_strcmp0 (xb_node_get_element (kudos_child), "GnomeSoftware::popular") == 0) {
|
||
gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_LANGUAGES:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) != 0) {
|
||
if (!locale_has_translations)
|
||
gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE);
|
||
|
||
if (!gs_app_get_has_translations (app) &&
|
||
!gs_app_has_kudo (app, GS_APP_KUDO_MY_LANGUAGE)) {
|
||
g_autoptr(XbNode) langs_child = NULL;
|
||
g_autoptr(XbNode) langs_next = NULL;
|
||
g_auto(GStrv) variants = g_get_locale_variants (setlocale (LC_MESSAGES, NULL));
|
||
|
||
for (langs_child = xb_node_get_child (child); langs_child != NULL; g_object_unref (langs_child), langs_child = g_steal_pointer (&langs_next)) {
|
||
langs_next = xb_node_get_next (langs_child);
|
||
if (g_strcmp0 (xb_node_get_element (langs_child), "lang") == 0) {
|
||
tmp = xb_node_get_text (langs_child);
|
||
if (tmp != NULL) {
|
||
gboolean is_variant = FALSE;
|
||
|
||
/* Set this under the FLAGS_REQUIRE_KUDOS flag because it’s
|
||
* only useful in combination with KUDO_MY_LANGUAGE */
|
||
gs_app_set_has_translations (app, TRUE);
|
||
|
||
for (gsize j = 0; variants[j]; j++) {
|
||
if (g_strcmp0 (tmp, variants[j]) == 0) {
|
||
is_variant = TRUE;
|
||
break;
|
||
}
|
||
}
|
||
if (is_variant && xb_node_get_attr_as_uint (langs_child, "percentage") > 50) {
|
||
gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_LAUNCHABLE: {
|
||
const gchar *kind = xb_node_get_attr (child, "type");
|
||
if (g_strcmp0 (kind, "desktop-id") == 0) {
|
||
gs_app_set_launchable (app,
|
||
AS_LAUNCHABLE_KIND_DESKTOP_ID,
|
||
xb_node_get_text (child));
|
||
g_set_object (&launchable_desktop_id, child);
|
||
} else if (g_strcmp0 (kind, "url") == 0) {
|
||
gs_app_set_launchable (app,
|
||
AS_LAUNCHABLE_KIND_URL,
|
||
xb_node_get_text (child));
|
||
}
|
||
} break;
|
||
case ELEMENT_KIND_METADATA_LICENSE:
|
||
has_metadata_license = TRUE;
|
||
break;
|
||
case ELEMENT_KIND_NAME:
|
||
tmp = xb_node_get_text (child);
|
||
if (tmp != NULL) {
|
||
gs_app_set_name (app, name_quality, tmp);
|
||
has_name = TRUE;
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_PKGNAME:
|
||
g_ptr_array_add (legacy_pkgnames, g_object_ref (child));
|
||
break;
|
||
case ELEMENT_KIND_PROJECT_GROUP:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP) > 0 &&
|
||
gs_app_get_project_group (app) == NULL) {
|
||
tmp = xb_node_get_text (child);
|
||
if (tmp != NULL && gs_appstream_is_valid_project_group (tmp))
|
||
gs_app_set_project_group (app, tmp);
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_PROJECT_LICENSE:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) != 0 &&
|
||
gs_app_get_license (app) == NULL) {
|
||
tmp = xb_node_get_text (child);
|
||
if (tmp != NULL)
|
||
gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, tmp);
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_PROVIDES: {
|
||
g_autoptr(XbNode) prov_child = NULL;
|
||
g_autoptr(XbNode) prov_next = NULL;
|
||
for (prov_child = xb_node_get_child (child); prov_child != NULL; g_object_unref (prov_child), prov_child = g_steal_pointer (&prov_next)) {
|
||
AsProvidedKind kind;
|
||
const gchar *element_name = xb_node_get_element (prov_child);
|
||
prov_next = xb_node_get_next (prov_child);
|
||
|
||
/* 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 (prov_child, "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, "python3") == 0) {
|
||
kind = AS_PROVIDED_KIND_PYTHON;
|
||
} else if (g_strcmp0 (element_name, "dbus") == 0) {
|
||
const gchar *dbus_type = xb_node_get_attr (prov_child, "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 (prov_child) == NULL) {
|
||
/* give up */
|
||
g_debug ("ignoring unknown or empty provided item type:'%s' value:'%s'", element_name, xb_node_get_text (prov_child));
|
||
continue;
|
||
}
|
||
|
||
gs_app_add_provided_item (app, kind, xb_node_get_text (prov_child));
|
||
}
|
||
} break;
|
||
case ELEMENT_KIND_RECOMMENDS:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) != 0) {
|
||
if (!gs_appstream_refine_app_relation (app, child, AS_RELATION_KIND_RECOMMENDS, error))
|
||
return FALSE;
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_RELEASES: {
|
||
g_autoptr(GPtrArray) current_version_history = gs_app_get_version_history (app);
|
||
gboolean needs_version_history = current_version_history == NULL || current_version_history->len == 0;
|
||
gboolean needs_update_details = (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) != 0 &&
|
||
silo != NULL && gs_app_is_updatable (app);
|
||
/* set the release date */
|
||
if (gs_app_get_release_date (app) == 0) {
|
||
g_autoptr(XbNode) release = xb_node_get_child (child);
|
||
if (release != NULL && g_strcmp0 (xb_node_get_element (release), "release") == 0) {
|
||
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_get_attr_as_uint (release, "timestamp");
|
||
date_str = xb_node_get_attr (release, "date");
|
||
|
||
if (timestamp != G_MAXUINT64) {
|
||
gs_app_set_release_date (app, timestamp);
|
||
} else if (date_str != NULL) {
|
||
g_autoptr(GDateTime) date = g_date_time_new_from_iso8601 (date_str, NULL);
|
||
if (date != NULL)
|
||
gs_app_set_release_date (app, g_date_time_to_unix (date));
|
||
}
|
||
}
|
||
}
|
||
if (needs_version_history || needs_update_details) {
|
||
g_autoptr(GPtrArray) version_history = NULL; /* (element-type AsRelease) */
|
||
g_autoptr(GHashTable) installed = NULL;
|
||
g_autoptr(GPtrArray) updates_list = NULL;
|
||
g_autoptr(XbNode) rels_child = NULL;
|
||
g_autoptr(XbNode) rels_next = NULL;
|
||
AsUrgencyKind urgency_best = AS_URGENCY_KIND_UNKNOWN;
|
||
guint i;
|
||
|
||
if (needs_update_details) {
|
||
g_autofree gchar *xpath = NULL;
|
||
g_autoptr(GPtrArray) releases_inst = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
installed = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);
|
||
updates_list = g_ptr_array_new_with_free_func (g_object_unref);
|
||
|
||
/* 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, &local_error);
|
||
if (releases_inst == NULL) {
|
||
if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
|
||
g_propagate_error (error, g_steal_pointer (&local_error));
|
||
return FALSE;
|
||
}
|
||
} else {
|
||
for (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"),
|
||
g_object_ref (release));
|
||
}
|
||
}
|
||
g_clear_error (&local_error);
|
||
}
|
||
|
||
if (needs_version_history)
|
||
version_history = g_ptr_array_new_with_free_func (g_object_unref);
|
||
|
||
for (i = 0, rels_child = xb_node_get_child (child); rels_child != NULL;
|
||
i++, g_object_unref (rels_child), rels_child = g_steal_pointer (&rels_next)) {
|
||
g_autoptr(XbNode) description_node = NULL;
|
||
g_autoptr(XbNode) issues_node = NULL;
|
||
const gchar *version;
|
||
|
||
rels_next = xb_node_get_next (rels_child);
|
||
if (g_strcmp0 (xb_node_get_element (rels_child), "release") != 0)
|
||
continue;
|
||
|
||
version = xb_node_get_attr (rels_child, "version");
|
||
/* ignore releases with no version */
|
||
if (version == NULL)
|
||
continue;
|
||
|
||
gs_appstream_find_description_and_issues_nodes (rels_child, &description_node, &issues_node);
|
||
|
||
if (version_history != NULL) {
|
||
g_autoptr(AsRelease) release = NULL;
|
||
g_autofree gchar *description = NULL;
|
||
guint64 timestamp;
|
||
const gchar *date_str;
|
||
|
||
timestamp = xb_node_get_attr_as_uint (rels_child, "timestamp");
|
||
date_str = xb_node_get_attr (rels_child, "date");
|
||
|
||
/* include updates with or without a description */
|
||
if (description_node != NULL || issues_node != NULL)
|
||
description = gs_appstream_format_description (description_node, issues_node);
|
||
|
||
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 (needs_update_details) {
|
||
AsUrgencyKind urgency_tmp;
|
||
|
||
/* 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)
|
||
continue;
|
||
|
||
/* use the 'worst' urgency, e.g. critical over enhancement */
|
||
urgency_tmp = as_urgency_kind_from_string (xb_node_get_attr (rels_child, "urgency"));
|
||
if (urgency_tmp > urgency_best)
|
||
urgency_best = urgency_tmp;
|
||
|
||
/* add updates with a description */
|
||
if (description_node != NULL || issues_node != NULL)
|
||
g_ptr_array_add (updates_list, g_object_ref (rels_child));
|
||
}
|
||
}
|
||
|
||
if (version_history != NULL && version_history->len > 0)
|
||
gs_app_set_version_history (app, version_history);
|
||
|
||
if (needs_update_details) {
|
||
/* 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) description_node = NULL;
|
||
g_autoptr(XbNode) issues_node = NULL;
|
||
g_autofree gchar *desc = NULL;
|
||
gs_appstream_find_description_and_issues_nodes (release, &description_node, &issues_node);
|
||
desc = gs_appstream_format_description (description_node, issues_node);
|
||
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 j = 0; j < updates_list->len; j++) {
|
||
XbNode *release = g_ptr_array_index (updates_list, j);
|
||
const gchar *release_version = xb_node_get_attr (release, "version");
|
||
g_autofree gchar *desc = NULL;
|
||
g_autoptr(XbNode) description_node = NULL;
|
||
g_autoptr(XbNode) issues_node = NULL;
|
||
|
||
/* use the first release description, then skip the currently installed version and all below it */
|
||
if (i != 0 && version != NULL && gs_utils_compare_versions (version, release_version) >= 0)
|
||
continue;
|
||
|
||
gs_appstream_find_description_and_issues_nodes (release, &description_node, &issues_node);
|
||
desc = gs_appstream_format_description (description_node, issues_node);
|
||
|
||
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"));
|
||
}
|
||
}
|
||
}
|
||
} break;
|
||
case ELEMENT_KIND_REQUIRES:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) != 0) {
|
||
if (!gs_appstream_refine_app_relation (app, child, AS_RELATION_KIND_REQUIRES, error))
|
||
return FALSE;
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_SCREENSHOTS:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) != 0 &&
|
||
gs_app_get_screenshots (app)->len == 0) {
|
||
g_autoptr(XbNode) scrs_child = NULL;
|
||
g_autoptr(XbNode) scrs_next = NULL;
|
||
for (scrs_child = xb_node_get_child (child); scrs_child != NULL; g_object_unref (scrs_child), scrs_child = g_steal_pointer (&scrs_next)) {
|
||
scrs_next = xb_node_get_next (scrs_child);
|
||
if (g_strcmp0 (xb_node_get_element (scrs_child), "screenshot") == 0) {
|
||
g_autoptr(AsScreenshot) scr = as_screenshot_new ();
|
||
g_autoptr(XbNode) scr_child = NULL;
|
||
g_autoptr(XbNode) scr_next = NULL;
|
||
gboolean any_added = FALSE;
|
||
for (scr_child = xb_node_get_child (scrs_child); scr_child != NULL; g_object_unref (scr_child), scr_child = g_steal_pointer (&scr_next)) {
|
||
scr_next = xb_node_get_next (scr_child);
|
||
if (g_strcmp0 (xb_node_get_element (scr_child), "image") == 0) {
|
||
g_autoptr(AsImage) im = as_image_new ();
|
||
as_image_set_height (im, xb_node_get_attr_as_uint (scr_child, "height"));
|
||
as_image_set_width (im, xb_node_get_attr_as_uint (scr_child, "width"));
|
||
as_image_set_kind (im, as_image_kind_from_string (xb_node_get_attr (scr_child, "type")));
|
||
as_image_set_url (im, xb_node_get_text (scr_child));
|
||
as_screenshot_add_image (scr, im);
|
||
any_added = TRUE;
|
||
} else if (g_strcmp0 (xb_node_get_element (scr_child), "video") == 0) {
|
||
g_autoptr(AsVideo) vid = as_video_new ();
|
||
as_video_set_height (vid, xb_node_get_attr_as_uint (scr_child, "height"));
|
||
as_video_set_width (vid, xb_node_get_attr_as_uint (scr_child, "width"));
|
||
as_video_set_codec_kind (vid, as_video_codec_kind_from_string (xb_node_get_attr (scr_child, "codec")));
|
||
as_video_set_container_kind (vid, as_video_container_kind_from_string (xb_node_get_attr (scr_child, "container")));
|
||
as_video_set_url (vid, xb_node_get_text (scr_child));
|
||
as_screenshot_add_video (scr, vid);
|
||
any_added = TRUE;
|
||
}
|
||
}
|
||
if (any_added)
|
||
gs_app_add_screenshot (app, scr);
|
||
}
|
||
}
|
||
/* FIXME: move into no refine flags section? */
|
||
if (gs_app_get_screenshots (app)->len)
|
||
gs_app_add_kudo (app, GS_APP_KUDO_HAS_SCREENSHOTS);
|
||
}
|
||
break;
|
||
case ELEMENT_KIND_SUMMARY:
|
||
tmp = xb_node_get_text (child);
|
||
if (tmp != NULL)
|
||
gs_app_set_summary (app, name_quality, tmp);
|
||
break;
|
||
case ELEMENT_KIND_SUPPORTS:
|
||
#if AS_CHECK_VERSION(0, 15, 0)
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) != 0) {
|
||
if (!gs_appstream_refine_app_relation (app, child, AS_RELATION_KIND_SUPPORTS, error))
|
||
return FALSE;
|
||
}
|
||
#endif
|
||
break;
|
||
case ELEMENT_KIND_URL:
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) != 0) {
|
||
const gchar *kind = xb_node_get_attr (child, "type");
|
||
if (kind != NULL) {
|
||
gs_app_set_url (app,
|
||
as_url_kind_from_string (kind),
|
||
xb_node_get_text (child));
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (developer_name_fallback != NULL &&
|
||
gs_app_get_developer_name (app) == NULL) {
|
||
gs_app_set_developer_name (app, developer_name_fallback);
|
||
}
|
||
|
||
/* try to detect old-style AppStream 'override'
|
||
* files without the merge attribute */
|
||
if (!has_name && !has_metadata_license)
|
||
gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
|
||
|
||
if (gs_app_get_metadata_item (app, "appstream::source-file") == NULL) {
|
||
if (appstream_source_file != NULL) {
|
||
/* empty string means the node was not found by the caller */
|
||
if (*appstream_source_file != '\0')
|
||
gs_app_set_metadata (app, "appstream::source-file", appstream_source_file);
|
||
} else {
|
||
tmp = xb_node_query_text (component, "../info/filename", NULL);
|
||
if (tmp != NULL)
|
||
gs_app_set_metadata (app, "appstream::source-file", tmp);
|
||
}
|
||
}
|
||
|
||
/* set scope */
|
||
if (gs_app_get_scope (app) == AS_COMPONENT_SCOPE_UNKNOWN) {
|
||
/* all callers should provide both appstream_source_file and default_scope, thus
|
||
when the appstream_source_file the "unknown" scope means "not found in the silo" */
|
||
if (appstream_source_file != NULL) {
|
||
if (default_scope != AS_COMPONENT_SCOPE_UNKNOWN)
|
||
gs_app_set_scope (app, default_scope);
|
||
} else {
|
||
tmp = xb_node_query_text (component, "../info/scope", NULL);
|
||
if (tmp != NULL)
|
||
gs_app_set_scope (app, as_component_scope_from_string (tmp));
|
||
}
|
||
}
|
||
|
||
if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) != 0 &&
|
||
!had_icons && !gs_app_has_icons (app)) {
|
||
/* If no icon found, try to inherit the icon from the .desktop file */
|
||
g_autofree gchar *xpath = NULL;
|
||
if (launchable_desktop_id != NULL) {
|
||
const gchar *launchable_id = xb_node_get_text (launchable_desktop_id);
|
||
if (launchable_id != NULL) {
|
||
if (installed_by_desktopid != NULL) {
|
||
GPtrArray *components = g_hash_table_lookup (installed_by_desktopid, launchable_id);
|
||
traverse_components_for_icons (app, components);
|
||
} else {
|
||
g_autoptr(GPtrArray) components = NULL;
|
||
xpath = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..",
|
||
launchable_id);
|
||
components = xb_silo_query (silo, xpath, 0, NULL);
|
||
traverse_components_for_icons (app, components);
|
||
g_clear_pointer (&xpath, g_free);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (installed_by_desktopid != NULL) {
|
||
GPtrArray *components = g_hash_table_lookup (installed_by_desktopid, gs_app_get_id (app));
|
||
traverse_components_for_icons (app, components);
|
||
} else {
|
||
g_autoptr(GPtrArray) components = NULL;
|
||
xpath = g_strdup_printf ("/component[@type='desktop-application']/launchable[@type='desktop-id'][text()='%s']/..",
|
||
gs_app_get_id (app));
|
||
components = xb_silo_query (silo, xpath, 0, NULL);
|
||
traverse_components_for_icons (app, components);
|
||
}
|
||
}
|
||
|
||
/* add legacy package names */
|
||
if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN &&
|
||
legacy_pkgnames->len > 0 && gs_app_get_sources (app)->len == 0) {
|
||
for (guint i = 0; i < legacy_pkgnames->len; i++) {
|
||
XbNode *pkgname = g_ptr_array_index (legacy_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 */
|
||
if (gs_app_get_origin_appstream (app) == NULL || (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))) {
|
||
g_autoptr(XbNode) parent = xb_node_get_parent (component);
|
||
tmp = NULL;
|
||
if (parent != NULL) {
|
||
tmp = xb_node_get_attr (parent, "origin");
|
||
if (gs_appstream_origin_valid (tmp)) {
|
||
if (gs_app_get_origin_appstream (app) == NULL)
|
||
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, appstream_source_file, default_scope, error))
|
||
return FALSE;
|
||
}
|
||
|
||
if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) {
|
||
if (!locale_has_translations)
|
||
gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE);
|
||
|
||
/* was this app released recently */
|
||
if (gs_appstream_is_recent_release (component))
|
||
gs_app_add_kudo (app, GS_APP_KUDO_RECENT_RELEASE);
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static void
|
||
gs_appstream_read_silo_info_from_component (XbNode *component,
|
||
gchar **out_silo_filename,
|
||
AsComponentScope *out_scope)
|
||
{
|
||
const gchar *tmp;
|
||
|
||
g_return_if_fail (component != NULL);
|
||
if (out_silo_filename != NULL) {
|
||
*out_silo_filename = NULL;
|
||
|
||
tmp = xb_node_query_text (component, "info/filename", NULL);
|
||
if (tmp == NULL)
|
||
tmp = xb_node_query_text (component, "../info/filename", NULL);
|
||
if (tmp != NULL)
|
||
*out_silo_filename = g_strdup (tmp);
|
||
}
|
||
|
||
if (out_scope) {
|
||
tmp = xb_node_query_text (component, "../info/scope", NULL);
|
||
if (tmp != NULL)
|
||
*out_scope = as_component_scope_from_string (tmp);
|
||
else
|
||
*out_scope = AS_COMPONENT_SCOPE_UNKNOWN;
|
||
}
|
||
}
|
||
|
||
typedef struct {
|
||
guint16 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);
|
||
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);
|
||
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 {
|
||
guint16 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)
|
||
{
|
||
AsComponentScope default_scope = AS_COMPONENT_SCOPE_UNKNOWN;
|
||
g_autofree gchar *silo_filename = NULL;
|
||
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 ();
|
||
#if AS_CHECK_VERSION(1, 0, 0)
|
||
const guint16 component_id_weight = as_utils_get_tag_search_weight ("id");
|
||
#else
|
||
const guint16 component_id_weight = AS_SEARCH_TOKEN_MATCH_ID;
|
||
#endif
|
||
|
||
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;
|
||
}
|
||
if (components->len > 0)
|
||
gs_appstream_read_silo_info_from_component (g_ptr_array_index (components, 0), &silo_filename, &default_scope);
|
||
|
||
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, silo_filename ? silo_filename : "", default_scope, 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 & (~component_id_weight));
|
||
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)
|
||
{
|
||
#if AS_CHECK_VERSION(1, 0, 0)
|
||
guint16 pkgname_weight = as_utils_get_tag_search_weight ("pkgname");
|
||
guint16 name_weight = as_utils_get_tag_search_weight ("name");
|
||
guint16 id_weight = as_utils_get_tag_search_weight ("id");
|
||
const Query queries[] = {
|
||
{ as_utils_get_tag_search_weight ("mediatype"), "provides/mediatype[text()~=stem(?)]" },
|
||
/* Search once with a tokenize-and-casefold operator (`~=`) to support casefolded
|
||
* full-text search, then again using substring matching (`contains()`), to
|
||
* support prefix matching. Only do the prefix matches on a few fields, and at a
|
||
* lower priority, otherwise things will get confusing.
|
||
*
|
||
* See https://gitlab.gnome.org/GNOME/gnome-software/-/issues/2277 */
|
||
{ pkgname_weight, "pkgname[text()~=stem(?)]" },
|
||
{ pkgname_weight / 2, "pkgname[contains(text(),stem(?))]" },
|
||
{ as_utils_get_tag_search_weight ("summary"), "summary[text()~=stem(?)]" },
|
||
{ name_weight, "name[text()~=stem(?)]" },
|
||
{ name_weight / 2, "name[contains(text(),stem(?))]" },
|
||
{ as_utils_get_tag_search_weight ("keyword"), "keywords/keyword[text()~=stem(?)]" },
|
||
{ id_weight, "id[text()~=stem(?)]" },
|
||
{ id_weight, "launchable[text()~=stem(?)]" },
|
||
{ as_utils_get_tag_search_weight ("origin"), "../components[@origin~=stem(?)]" },
|
||
{ 0, NULL }
|
||
};
|
||
#else
|
||
const Query queries[] = {
|
||
{ AS_SEARCH_TOKEN_MATCH_MEDIATYPE, "mimetypes/mimetype[text()~=stem(?)]" },
|
||
{ AS_SEARCH_TOKEN_MATCH_PKGNAME, "pkgname[text()~=stem(?)]" },
|
||
{ AS_SEARCH_TOKEN_MATCH_PKGNAME / 2, "pkgname[contains(text(),stem(?))]" },
|
||
{ AS_SEARCH_TOKEN_MATCH_SUMMARY, "summary[text()~=stem(?)]" },
|
||
{ AS_SEARCH_TOKEN_MATCH_NAME, "name[text()~=stem(?)]" },
|
||
{ AS_SEARCH_TOKEN_MATCH_NAME / 2, "name[contains(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 }
|
||
};
|
||
#endif
|
||
|
||
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)
|
||
{
|
||
#if AS_CHECK_VERSION(1, 0, 0)
|
||
const Query queries[] = {
|
||
{ as_utils_get_tag_search_weight ("pkgname"), "developer/name[text()~=stem(?)]" },
|
||
{ as_utils_get_tag_search_weight ("summary"), "project_group[text()~=stem(?)]" },
|
||
/* for legacy support */
|
||
{ as_utils_get_tag_search_weight ("pkgname"), "developer_name[text()~=stem(?)]" },
|
||
{ 0, NULL }
|
||
};
|
||
#else
|
||
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 }
|
||
};
|
||
#endif
|
||
|
||
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[not(@merge)]/categories/"
|
||
"category[text()='%s']/../..",
|
||
split[0]);
|
||
} else if (g_strv_length (split) == 2) {
|
||
xpath = g_strdup_printf ("components/component[not(@merge)]/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)
|
||
{
|
||
/* the overview page checks for 100 apps, then try to get them */
|
||
const guint limit = 100;
|
||
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[not(@merge)]/categories/"
|
||
"category[text()='%s']/../..",
|
||
split[0]);
|
||
} else if (g_strv_length (split) == 2) {
|
||
xpath = g_strdup_printf ("components/component[not(@merge)]/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
|
||
* apps 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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
gs_appstream_add_installed (GsPlugin *plugin,
|
||
XbSilo *silo,
|
||
GsAppList *list,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
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);
|
||
|
||
/* 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, NULL, AS_COMPONENT_SCOPE_UNKNOWN, 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)
|
||
{
|
||
AsComponentScope default_scope = AS_COMPONENT_SCOPE_UNKNOWN;
|
||
guint64 now = (guint64) g_get_real_time () / G_USEC_PER_SEC, max_future_timestamp;
|
||
g_autofree gchar *xpath = NULL;
|
||
g_autofree gchar *silo_filename = 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;
|
||
}
|
||
if (array->len > 0)
|
||
gs_appstream_read_silo_info_from_component (g_ptr_array_index (array, 0), &silo_filename, &default_scope);
|
||
|
||
/* This is to cover mistakes when the release date is set in the future,
|
||
to not have it picked for too long. */
|
||
max_future_timestamp = now + (3 * 24 * 60 * 60);
|
||
for (guint i = 0; i < array->len; i++) {
|
||
XbNode *component = g_ptr_array_index (array, i);
|
||
g_autoptr(GsApp) app = NULL;
|
||
guint64 timestamp = component_get_release_timestamp (component);
|
||
/* set the release date */
|
||
if (timestamp != G_MAXUINT64 && timestamp < max_future_timestamp) {
|
||
app = gs_appstream_create_app (plugin, silo, component, silo_filename ? silo_filename : "", default_scope, error);
|
||
if (app == NULL)
|
||
return FALSE;
|
||
|
||
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, NULL, AS_COMPONENT_SCOPE_UNKNOWN, error);
|
||
if (app == NULL)
|
||
return FALSE;
|
||
gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM);
|
||
gs_app_list_add (list, app);
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static GInputStream *
|
||
gs_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_appstream_load_desktop_fn (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_simple_adapter (source, "application/x-desktop",
|
||
gs_appstream_load_desktop_cb, NULL, NULL);
|
||
|
||
/* add source */
|
||
if (!xb_builder_source_load_file (source, file, 0, 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;
|
||
}
|
||
|
||
gboolean
|
||
gs_appstream_load_desktop_files (XbBuilder *builder,
|
||
const gchar *path,
|
||
gboolean *out_any_loaded,
|
||
GFileMonitor **out_file_monitor,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
const gchar *fn;
|
||
g_autoptr(GDir) dir = NULL;
|
||
g_autoptr(GFile) parent = g_file_new_for_path (path);
|
||
if (out_any_loaded)
|
||
*out_any_loaded = FALSE;
|
||
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;
|
||
|
||
if (out_file_monitor != NULL) {
|
||
g_autoptr(GError) error_local = NULL;
|
||
*out_file_monitor = g_file_monitor (parent, G_FILE_MONITOR_NONE, cancellable, &error_local);
|
||
if (error_local)
|
||
g_debug ("appstream: Failed to create file monitor for '%s': %s", path, error_local->message);
|
||
}
|
||
|
||
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_appstream_load_desktop_fn (builder,
|
||
filename,
|
||
cancellable,
|
||
&error_local)) {
|
||
g_debug ("ignoring %s: %s", filename, error_local->message);
|
||
continue;
|
||
}
|
||
if (out_any_loaded)
|
||
*out_any_loaded = TRUE;
|
||
}
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
/* Add the current paths _after_ the legacy, that way the data stored in the current
|
||
paths has precedence over the (possibly stale) data in the legacy paths. */
|
||
g_ptr_array_add (locations,
|
||
g_build_filename (catalog_path, "xml", NULL));
|
||
g_ptr_array_add (locations,
|
||
g_build_filename (catalog_path, "yaml", NULL));
|
||
}
|
||
|
||
GPtrArray *
|
||
gs_appstream_get_appstream_data_dirs (void)
|
||
{
|
||
GPtrArray *appstream_data_dirs = g_ptr_array_new_with_free_func (g_free);
|
||
#ifdef ENABLE_EXTERNAL_APPSTREAM
|
||
g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software");
|
||
#endif
|
||
g_autofree gchar *state_cache_dir = NULL;
|
||
g_autofree gchar *state_lib_dir = NULL;
|
||
|
||
/* add search paths */
|
||
gs_add_appstream_catalog_location (appstream_data_dirs, DATADIR);
|
||
|
||
state_cache_dir = g_build_filename (LOCALSTATEDIR, "cache", NULL);
|
||
gs_add_appstream_catalog_location (appstream_data_dirs, state_cache_dir);
|
||
state_lib_dir = g_build_filename (LOCALSTATEDIR, "lib", NULL);
|
||
gs_add_appstream_catalog_location (appstream_data_dirs, state_lib_dir);
|
||
|
||
#ifdef ENABLE_EXTERNAL_APPSTREAM
|
||
/* check for the corresponding setting */
|
||
if (!g_settings_get_boolean (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 (appstream_data_dirs,
|
||
g_build_filename (user_catalog_path, "xml", NULL));
|
||
g_ptr_array_add (appstream_data_dirs,
|
||
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 (appstream_data_dirs, "/usr/share");
|
||
if (g_strcmp0 (LOCALSTATEDIR, "/var") != 0) {
|
||
gs_add_appstream_catalog_location (appstream_data_dirs, "/var/cache");
|
||
gs_add_appstream_catalog_location (appstream_data_dirs, "/var/lib");
|
||
}
|
||
|
||
return appstream_data_dirs;
|
||
}
|
||
|
||
void
|
||
gs_appstream_add_current_locales (XbBuilder *builder)
|
||
{
|
||
const gchar *const *locales = g_get_language_names ();
|
||
for (guint i = 0; locales[i] != NULL; i++)
|
||
xb_builder_add_locale (builder, locales[i]);
|
||
}
|
||
|
||
static gboolean
|
||
gs_appstream_is_merge_node (XbBuilderNode *bn)
|
||
{
|
||
const gchar *merge = xb_builder_node_get_attr (bn, "merge");
|
||
if (merge != NULL) {
|
||
AsMergeKind kind = as_merge_kind_from_string (merge);
|
||
return kind != AS_MERGE_KIND_NONE;
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
#ifdef HAVE_FIXED_LIBXMLB
|
||
static gboolean
|
||
gs_appstream_remove_merge_components_cb (XbBuilderFixup *self,
|
||
XbBuilderNode *bn,
|
||
gpointer user_data,
|
||
GError **error)
|
||
{
|
||
if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0 &&
|
||
gs_appstream_is_merge_node (bn))
|
||
xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
gs_appstream_remove_nonmerge_components_cb (XbBuilderFixup *self,
|
||
XbBuilderNode *bn,
|
||
gpointer user_data,
|
||
GError **error)
|
||
{
|
||
if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0 &&
|
||
!gs_appstream_is_merge_node (bn))
|
||
xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
|
||
return TRUE;
|
||
}
|
||
#endif
|
||
|
||
static GInputStream *
|
||
gs_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_CATALOG);
|
||
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_catalog (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);
|
||
}
|
||
|
||
static gboolean
|
||
gs_appstream_load_appstream_file (XbBuilder *builder,
|
||
const gchar *filename,
|
||
GCancellable *cancellable)
|
||
{
|
||
g_autoptr(GFile) file = g_file_new_for_path (filename);
|
||
g_autoptr(GError) local_error = NULL;
|
||
g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
|
||
g_autoptr(XbBuilderNode) info = NULL;
|
||
g_autoptr(XbBuilderFixup) fixup = NULL;
|
||
|
||
if (g_cancellable_is_cancelled (cancellable))
|
||
return FALSE;
|
||
|
||
/* add support for DEP-11 files */
|
||
xb_builder_source_add_adapter (source,
|
||
"application/yaml",
|
||
gs_appstream_load_dep11_cb,
|
||
NULL, NULL);
|
||
xb_builder_source_add_adapter (source,
|
||
"application/x-yaml",
|
||
gs_appstream_load_dep11_cb,
|
||
NULL, NULL);
|
||
|
||
/* add source */
|
||
if (!xb_builder_source_load_file (source, file, XB_BUILDER_SOURCE_FLAG_NONE, cancellable, &local_error)) {
|
||
g_debug ("Failed to load appstream file '%s': %s", filename, local_error->message);
|
||
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);
|
||
|
||
#ifdef HAVE_FIXED_LIBXMLB
|
||
fixup = xb_builder_fixup_new ("RemoveNonMergeComponents",
|
||
gs_appstream_remove_nonmerge_components_cb,
|
||
NULL, NULL);
|
||
xb_builder_fixup_set_max_depth (fixup, 2);
|
||
xb_builder_source_add_fixup (source, fixup);
|
||
#endif
|
||
|
||
xb_builder_import_source (builder, source);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
gs_appstream_load_appstream_dir (XbBuilder *builder,
|
||
const gchar *path,
|
||
GCancellable *cancellable)
|
||
{
|
||
const gchar *fn;
|
||
gboolean any_loaded = FALSE;
|
||
g_autoptr(GDir) dir = NULL;
|
||
#ifdef ENABLE_EXTERNAL_APPSTREAM
|
||
g_autoptr(GSettings) settings = g_settings_new ("org.gnome.software");
|
||
gboolean external_appstream_system_wide = g_settings_get_boolean (settings, "external-appstream-system-wide");
|
||
#endif
|
||
|
||
dir = g_dir_open (path, 0, NULL);
|
||
if (dir == NULL)
|
||
return FALSE;
|
||
while ((fn = g_dir_read_name (dir)) != NULL && !g_cancellable_is_cancelled (cancellable)) {
|
||
#ifdef ENABLE_EXTERNAL_APPSTREAM
|
||
/* Ignore our own system-installed files when
|
||
external-appstream-system-wide is FALSE */
|
||
if (!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);
|
||
any_loaded = gs_appstream_load_appstream_file (builder, filename, cancellable) || any_loaded;
|
||
}
|
||
}
|
||
|
||
return any_loaded;
|
||
}
|
||
|
||
typedef struct {
|
||
GSList *components; /* XbNode * */
|
||
} SiloIndexData;
|
||
|
||
static SiloIndexData *
|
||
silo_index_data_new (XbNode *node)
|
||
{
|
||
SiloIndexData *sid = g_new0 (SiloIndexData, 1);
|
||
sid->components = g_slist_prepend (sid->components, g_object_ref (node));
|
||
return sid;
|
||
}
|
||
|
||
static void
|
||
silo_index_data_free (SiloIndexData *sid)
|
||
{
|
||
if (sid != NULL) {
|
||
g_slist_free_full (sid->components, g_object_unref);
|
||
g_free (sid);
|
||
}
|
||
}
|
||
|
||
typedef struct {
|
||
XbSilo *appstream_silo;
|
||
XbSilo *desktop_silo;
|
||
GHashTable *appstream_index; /* gchar *id ~> SiloIndexData * */
|
||
GHashTable *desktop_index; /* gchar *id ~> SiloIndexData * */
|
||
} MergeData;
|
||
|
||
static MergeData *
|
||
merge_data_new (void)
|
||
{
|
||
MergeData *md = g_new0 (MergeData, 1);
|
||
return md;
|
||
}
|
||
|
||
static void
|
||
merge_data_free (MergeData *md)
|
||
{
|
||
if (md == NULL)
|
||
return;
|
||
|
||
g_clear_pointer (&md->appstream_index, g_hash_table_unref);
|
||
g_clear_pointer (&md->desktop_index, g_hash_table_unref);
|
||
g_clear_object (&md->appstream_silo);
|
||
g_clear_object (&md->desktop_silo);
|
||
g_free (md);
|
||
}
|
||
|
||
static void
|
||
gs_appstream_add_node_to_silo_index (GHashTable *index, /* gchar *id ~> SiloIndexData * */
|
||
const gchar *id,
|
||
XbNode *node)
|
||
{
|
||
SiloIndexData *sid;
|
||
if (id == NULL)
|
||
return;
|
||
sid = g_hash_table_lookup (index, id);
|
||
if (sid != NULL) {
|
||
sid->components = g_slist_prepend (sid->components, g_object_ref (node));
|
||
} else {
|
||
sid = silo_index_data_new (node);
|
||
g_hash_table_insert (index, g_strdup (id), sid);
|
||
}
|
||
}
|
||
|
||
static void
|
||
gs_appstream_traverse_silo_for_index (XbNode *node,
|
||
GHashTable *index,
|
||
gboolean only_merges,
|
||
gint depth)
|
||
{
|
||
if (g_strcmp0 (xb_node_get_element (node), "component") == 0) {
|
||
g_autoptr(XbNode) child = NULL;
|
||
g_autoptr(XbNode) next = NULL;
|
||
gboolean need_id = TRUE, need_provides = !only_merges, need_info = need_provides;
|
||
if (only_merges) {
|
||
gboolean is_merge = FALSE;
|
||
const gchar *merge = xb_node_get_attr (node, "merge");
|
||
if (merge != NULL) {
|
||
AsMergeKind kind = as_merge_kind_from_string (merge);
|
||
is_merge = kind != AS_MERGE_KIND_NONE;
|
||
}
|
||
if (!is_merge)
|
||
return;
|
||
}
|
||
for (child = xb_node_get_child (node);
|
||
child != NULL && (need_id || need_provides || need_info);
|
||
g_object_unref (child), child = g_steal_pointer (&next)) {
|
||
const gchar *element = xb_node_get_element (child);
|
||
next = xb_node_get_next (child);
|
||
if (need_id && g_strcmp0 (element, "id") == 0) {
|
||
gs_appstream_add_node_to_silo_index (index, xb_node_get_text (child), node);
|
||
need_id = FALSE;
|
||
} else if (need_provides && g_strcmp0 (element, "provides") == 0) {
|
||
g_autoptr(XbNode) provides_child = NULL;
|
||
g_autoptr(XbNode) provides_next = NULL;
|
||
for (provides_child = xb_node_get_child (child);
|
||
provides_child != NULL;
|
||
g_object_unref (provides_child), provides_child = g_steal_pointer (&provides_next)) {
|
||
provides_next = xb_node_get_next (provides_child);
|
||
if (g_strcmp0 (xb_node_get_element (provides_child), "id") == 0)
|
||
gs_appstream_add_node_to_silo_index (index, xb_node_get_text (provides_child), node);
|
||
}
|
||
|
||
need_provides = FALSE;
|
||
} else if (need_info && g_strcmp0 (element, "info") == 0) {
|
||
/* In case it's a .desktop file and the node is not there yet, then add it.
|
||
It's because the <id/> from the desktop file may not match the <launchable/>,
|
||
which is the file name. */
|
||
g_autoptr(XbNode) info_child = NULL;
|
||
g_autoptr(XbNode) info_next = NULL;
|
||
for (info_child = xb_node_get_child (child);
|
||
info_child != NULL;
|
||
g_object_unref (info_child), info_child = g_steal_pointer (&info_next)) {
|
||
info_next = xb_node_get_next (info_child);
|
||
if (g_strcmp0 (xb_node_get_element (info_child), "filename") == 0) {
|
||
const gchar *filename = xb_node_get_text (info_child);
|
||
if (filename != NULL && g_str_has_suffix (filename, ".desktop")) {
|
||
filename = strrchr (filename, G_DIR_SEPARATOR);
|
||
if (filename != NULL) {
|
||
SiloIndexData *sid;
|
||
filename++;
|
||
sid = g_hash_table_lookup (index, filename);
|
||
if (sid != NULL) {
|
||
if (!g_slist_find (sid->components, node))
|
||
sid->components = g_slist_prepend (sid->components, g_object_ref (node));
|
||
} else {
|
||
sid = silo_index_data_new (node);
|
||
g_hash_table_insert (index, g_strdup (filename), sid);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
need_info = FALSE;
|
||
}
|
||
}
|
||
} else if (depth < 2) {
|
||
XbNodeChildIter iter;
|
||
XbNode *child = NULL;
|
||
xb_node_child_iter_init (&iter, node);
|
||
while (xb_node_child_iter_loop (&iter, &child)) {
|
||
gs_appstream_traverse_silo_for_index (child, index, only_merges, depth + 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
static GHashTable * /* gchar *id ~> SiloIndexData * */
|
||
gs_appstream_create_silo_index (XbSilo *silo,
|
||
gboolean only_merges)
|
||
{
|
||
GHashTable *index = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) silo_index_data_free);
|
||
for (g_autoptr(XbNode) node = xb_silo_get_root (silo); node != NULL; node_set_to_next (&node)) {
|
||
gs_appstream_traverse_silo_for_index (node, index, only_merges, 0);
|
||
}
|
||
return index;
|
||
}
|
||
|
||
static MergeData *
|
||
gs_appstream_gather_merge_data (GPtrArray *appstream_paths,
|
||
GPtrArray *desktop_paths,
|
||
GCancellable *cancellable)
|
||
{
|
||
MergeData *md = merge_data_new ();
|
||
g_autoptr(GPtrArray) common_appstream_paths = gs_appstream_get_appstream_data_dirs ();
|
||
if (appstream_paths != NULL) {
|
||
g_autoptr(GError) local_error = NULL;
|
||
g_autoptr(XbBuilder) builder = xb_builder_new ();
|
||
gboolean any_loaded = FALSE;
|
||
gs_appstream_add_current_locales (builder);
|
||
for (guint i = 0; i < appstream_paths->len && !g_cancellable_is_cancelled (cancellable); i++) {
|
||
const gchar *path = g_ptr_array_index (appstream_paths, i);
|
||
if (g_file_test (path, G_FILE_TEST_IS_DIR))
|
||
any_loaded = gs_appstream_load_appstream_dir (builder, path, cancellable) || any_loaded;
|
||
else
|
||
any_loaded = gs_appstream_load_appstream_file (builder, path, cancellable) || any_loaded;
|
||
for (guint j = 0; j < common_appstream_paths->len; j++) {
|
||
if (g_strcmp0 (g_ptr_array_index (common_appstream_paths, j), path) == 0) {
|
||
g_ptr_array_remove_index (common_appstream_paths, j);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
for (guint i = 0; i < common_appstream_paths->len; i++) {
|
||
const gchar *path = g_ptr_array_index (common_appstream_paths, i);
|
||
any_loaded = gs_appstream_load_appstream_dir (builder, path, cancellable) || any_loaded;
|
||
}
|
||
if (any_loaded && !g_cancellable_is_cancelled (cancellable)) {
|
||
md->appstream_silo = xb_builder_compile (builder,
|
||
XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
|
||
XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
|
||
cancellable, &local_error);
|
||
#ifdef __GLIBC__
|
||
/* https://gitlab.gnome.org/GNOME/gnome-software/-/issues/941
|
||
* libxmlb <= 0.3.22 makes lots of temporary heap allocations parsing large XMLs
|
||
* trim the heap after parsing to control RSS growth. */
|
||
malloc_trim (0);
|
||
#endif
|
||
if (md->appstream_silo != NULL)
|
||
md->appstream_index = gs_appstream_create_silo_index (md->appstream_silo, TRUE);
|
||
else
|
||
g_warning ("Failed to compile appstream silo: %s", local_error->message);
|
||
}
|
||
} else {
|
||
g_autoptr(GError) local_error = NULL;
|
||
g_autoptr(XbBuilder) builder = xb_builder_new ();
|
||
gboolean any_loaded = FALSE;
|
||
gs_appstream_add_current_locales (builder);
|
||
for (guint i = 0; i < common_appstream_paths->len && !g_cancellable_is_cancelled (cancellable); i++) {
|
||
const gchar *path = g_ptr_array_index (common_appstream_paths, i);
|
||
any_loaded = gs_appstream_load_appstream_dir (builder, path, cancellable) || any_loaded;
|
||
}
|
||
if (any_loaded && !g_cancellable_is_cancelled (cancellable)) {
|
||
md->appstream_silo = xb_builder_compile (builder,
|
||
XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
|
||
XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
|
||
cancellable, &local_error);
|
||
if (md->appstream_silo != NULL)
|
||
md->appstream_index = gs_appstream_create_silo_index (md->appstream_silo, TRUE);
|
||
else
|
||
g_warning ("Failed to compile common paths appstream silo: %s", local_error->message);
|
||
}
|
||
}
|
||
if (desktop_paths != NULL) {
|
||
g_autoptr(GError) local_error = NULL;
|
||
g_autoptr(XbBuilder) builder = xb_builder_new ();
|
||
gboolean any_loaded = FALSE;
|
||
gs_appstream_add_current_locales (builder);
|
||
for (guint i = 0; i < desktop_paths->len && !g_cancellable_is_cancelled (cancellable); i++) {
|
||
const gchar *path = g_ptr_array_index (desktop_paths, i);
|
||
gboolean this_loaded = FALSE;
|
||
gs_appstream_load_desktop_files (builder, path, &this_loaded, NULL, cancellable, NULL);
|
||
any_loaded = any_loaded || this_loaded;
|
||
}
|
||
if (any_loaded && !g_cancellable_is_cancelled (cancellable)) {
|
||
md->desktop_silo = xb_builder_compile (builder,
|
||
XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
|
||
XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
|
||
cancellable, &local_error);
|
||
if (md->desktop_silo != NULL)
|
||
md->desktop_index = gs_appstream_create_silo_index (md->desktop_silo, FALSE);
|
||
else
|
||
g_warning ("Failed to compile desktop silo: %s", local_error->message);
|
||
}
|
||
}
|
||
return md;
|
||
}
|
||
|
||
static void
|
||
gs_appstream_copy_attrs (XbBuilderNode *des_node,
|
||
XbNode *src_node)
|
||
{
|
||
XbNodeAttrIter iter;
|
||
const gchar *attr_name, *attr_value;
|
||
|
||
xb_node_attr_iter_init (&iter, src_node);
|
||
while (xb_node_attr_iter_next (&iter, &attr_name, &attr_value)) {
|
||
xb_builder_node_set_attr (des_node, attr_name, attr_value);
|
||
}
|
||
}
|
||
|
||
static void
|
||
gs_appstream_copy_node (XbBuilderNode *des_parent,
|
||
XbNode *src_node,
|
||
gint level)
|
||
{
|
||
g_autoptr(XbBuilderNode) new_node = NULL;
|
||
g_autoptr(GPtrArray) children = NULL;
|
||
const gchar *text, *element_name;
|
||
gboolean merge_into_existing = FALSE;
|
||
element_name = xb_node_get_element (src_node);
|
||
text = xb_node_get_text (src_node);
|
||
if (level == 1 && (
|
||
g_strcmp0 (element_name, "categories") == 0 ||
|
||
g_strcmp0 (element_name, "custom") == 0 ||
|
||
g_strcmp0 (element_name, "kudos") == 0 ||
|
||
g_strcmp0 (element_name, "provides") == 0)) {
|
||
new_node = xb_builder_node_get_child (des_parent, element_name, text);
|
||
merge_into_existing = new_node != NULL;
|
||
} else if (level == 2 && (
|
||
g_strcmp0 (element_name, "category") == 0 ||
|
||
g_strcmp0 (element_name, "kudo") == 0)) {
|
||
/* Such category/kudo already exists */
|
||
new_node = xb_builder_node_get_child (des_parent, element_name, text);
|
||
if (new_node != NULL)
|
||
return;
|
||
}
|
||
if (new_node == NULL) {
|
||
new_node = xb_builder_node_new (element_name);
|
||
if (text != NULL)
|
||
xb_builder_node_set_text (new_node, text, -1);
|
||
xb_builder_node_add_child (des_parent, new_node);
|
||
gs_appstream_copy_attrs (new_node, src_node);
|
||
}
|
||
children = xb_node_get_children (src_node);
|
||
for (guint i = 0; children && i < children->len; i++) {
|
||
XbNode *child = g_ptr_array_index (children, i);
|
||
gs_appstream_copy_node (new_node, child, level + 1);
|
||
}
|
||
if (!merge_into_existing) {
|
||
text = xb_node_get_tail (src_node);
|
||
if (text != NULL)
|
||
xb_builder_node_set_tail (new_node, text, -1);
|
||
}
|
||
}
|
||
|
||
static void
|
||
gs_appstream_merge_component_children (XbBuilderNode *bn,
|
||
XbNode *node,
|
||
gboolean is_replace)
|
||
{
|
||
g_autoptr(GHashTable) checked_elems = g_hash_table_new (g_str_hash, g_str_equal); /* gchar *name ~> NULL*/
|
||
g_autoptr(GHashTable) existing_elems = NULL;
|
||
g_autoptr(GPtrArray) node_children = xb_node_get_children (node);
|
||
if (!is_replace) {
|
||
GPtrArray *bn_children = xb_builder_node_get_children (bn);
|
||
existing_elems = g_hash_table_new (g_str_hash, g_str_equal); /* gchar *name ~> NULL*/
|
||
for (guint i = 0; bn_children && i < bn_children->len; i++) {
|
||
XbBuilderNode *bn_child = g_ptr_array_index (bn_children, i);
|
||
const gchar *elem_name = xb_builder_node_get_element (bn_child);
|
||
if (elem_name)
|
||
g_hash_table_add (existing_elems, (gpointer) elem_name);
|
||
}
|
||
}
|
||
for (guint i = 0; node_children != NULL && i < node_children->len; i++) {
|
||
XbNode *child = g_ptr_array_index (node_children, i);
|
||
const gchar *elem_name = xb_node_get_element (child);
|
||
if (g_strcmp0 (elem_name, "id") == 0 ||
|
||
g_strcmp0 (elem_name, "info") == 0)
|
||
continue;
|
||
if (is_replace && g_hash_table_add (checked_elems, (gpointer) elem_name)) {
|
||
GPtrArray *bn_children = xb_builder_node_get_children (bn);
|
||
for (guint j = 0; bn_children && j < bn_children->len; j++) {
|
||
XbBuilderNode *bn_child = g_ptr_array_index (bn_children, j);
|
||
if (g_strcmp0 (xb_builder_node_get_element (bn_child), elem_name) == 0)
|
||
xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
|
||
}
|
||
} else if (!is_replace && g_hash_table_contains (existing_elems, elem_name)) {
|
||
/* list of those to skip if already exist */
|
||
if (g_strcmp0 (elem_name, "name") == 0 ||
|
||
g_strcmp0 (elem_name, "summary") == 0 ||
|
||
g_strcmp0 (elem_name, "description") == 0 ||
|
||
g_strcmp0 (elem_name, "launchable") == 0)
|
||
continue;
|
||
}
|
||
gs_appstream_copy_node (bn, child, 1);
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
gs_appstream_apply_merges_for_id (MergeData *md,
|
||
XbBuilderNode *bn,
|
||
const gchar *id)
|
||
{
|
||
SiloIndexData *sid;
|
||
|
||
if (id == NULL || md->appstream_index == NULL)
|
||
return FALSE;
|
||
|
||
sid = g_hash_table_lookup (md->appstream_index, id);
|
||
if (sid != NULL) {
|
||
for (GSList *link = sid->components; link != NULL; link = g_slist_next (link)) {
|
||
XbNode *node = link->data;
|
||
if (node != NULL) {
|
||
const gchar *merge = xb_node_get_attr (node, "merge");
|
||
if (merge != NULL) {
|
||
AsMergeKind kind = as_merge_kind_from_string (merge);
|
||
if (kind == AS_MERGE_KIND_REMOVE_COMPONENT) {
|
||
xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE);
|
||
return TRUE;
|
||
} else if (kind == AS_MERGE_KIND_APPEND ||
|
||
kind == AS_MERGE_KIND_REPLACE) {
|
||
gs_appstream_merge_component_children (bn, node, kind == AS_MERGE_KIND_REPLACE);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static gboolean
|
||
gs_appstream_apply_merges_cb (XbBuilderFixup *self,
|
||
XbBuilderNode *bn,
|
||
gpointer user_data,
|
||
GError **error)
|
||
{
|
||
MergeData *md = user_data;
|
||
if (!xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE) &&
|
||
g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0 &&
|
||
!gs_appstream_is_merge_node (bn)) {
|
||
if (md->appstream_index != NULL) {
|
||
g_autoptr(XbBuilderNode) id_node = xb_builder_node_get_child (bn, "id", NULL);
|
||
if (id_node != NULL) {
|
||
g_autoptr(XbBuilderNode) provides_node = NULL;
|
||
const gchar *id = xb_builder_node_get_text (id_node);
|
||
gboolean skip_node = gs_appstream_apply_merges_for_id (md, bn, id);
|
||
if (skip_node)
|
||
return TRUE;
|
||
provides_node = xb_builder_node_get_child (bn, "provides", NULL);
|
||
if (provides_node != NULL) {
|
||
GPtrArray *children = xb_builder_node_get_children (provides_node);
|
||
for (guint i = 0; children != NULL && i < children->len; i++) {
|
||
XbBuilderNode *child = g_ptr_array_index (children, i);
|
||
if (g_strcmp0 (xb_builder_node_get_element (child), "id") == 0) {
|
||
id = xb_builder_node_get_text (child);
|
||
skip_node = gs_appstream_apply_merges_for_id (md, bn, id);
|
||
if (skip_node)
|
||
return TRUE;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (md->desktop_index) {
|
||
GPtrArray *children = xb_builder_node_get_children (bn);
|
||
const gchar *desktop_id = NULL;
|
||
for (guint i = 0; children != NULL && i < children->len; i++) {
|
||
XbBuilderNode *child = g_ptr_array_index (children, i);
|
||
if (g_strcmp0 (xb_builder_node_get_element (child), "launchable") == 0 &&
|
||
g_strcmp0 (xb_builder_node_get_attr (child, "type"), "desktop-id") == 0) {
|
||
/* Can merge, only if just one desktop-id launchable is present:
|
||
https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Application.html#tag-dapp-launchable */
|
||
if (desktop_id != NULL) {
|
||
desktop_id = NULL;
|
||
break;
|
||
}
|
||
desktop_id = xb_builder_node_get_text (child);
|
||
if (desktop_id != NULL && *desktop_id == '\0')
|
||
desktop_id = NULL;
|
||
} else if (g_strcmp0 (xb_builder_node_get_element (child), "info") == 0) {
|
||
/* Make sure it'll not update itself, aka skip updating data
|
||
from .desktop files into .desktop files */
|
||
g_autoptr(XbBuilderNode) filename_node = xb_builder_node_get_child (child, "filename", NULL);
|
||
if (filename_node) {
|
||
const gchar *filename = xb_builder_node_get_text (filename_node);
|
||
if (filename != NULL && g_str_has_suffix (filename, ".desktop")) {
|
||
desktop_id = NULL;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (desktop_id != NULL) {
|
||
SiloIndexData *sid = g_hash_table_lookup (md->desktop_index, desktop_id);
|
||
if (sid != NULL) {
|
||
for (GSList *link = sid->components; link != NULL; link = g_slist_next (link)) {
|
||
XbNode *node = link->data;
|
||
/* Add data from the corresponding .desktop file */
|
||
if (node != NULL)
|
||
gs_appstream_merge_component_children (bn, node, FALSE);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
void
|
||
gs_appstream_add_data_merge_fixup (XbBuilder *builder,
|
||
GPtrArray *appstream_paths,
|
||
GPtrArray *desktop_paths,
|
||
GCancellable *cancellable)
|
||
{
|
||
#ifdef HAVE_FIXED_LIBXMLB
|
||
g_autoptr(XbBuilderFixup) fixup1 = NULL;
|
||
#endif
|
||
g_autoptr(XbBuilderFixup) fixup2 = NULL;
|
||
MergeData *md;
|
||
|
||
/* First read all of the merge components and .desktop files (which will be merged as well) */
|
||
md = gs_appstream_gather_merge_data (appstream_paths, desktop_paths, cancellable);
|
||
|
||
#ifdef HAVE_FIXED_LIBXMLB
|
||
/* Then drop all the merge components from the result, because they are useless when being merged */
|
||
fixup1 = xb_builder_fixup_new ("RemoveMergeComponents",
|
||
gs_appstream_remove_merge_components_cb,
|
||
NULL, NULL);
|
||
xb_builder_fixup_set_max_depth (fixup1, 2);
|
||
xb_builder_add_fixup (builder, fixup1);
|
||
#endif
|
||
|
||
/* Then apply merge data to the components */
|
||
fixup2 = xb_builder_fixup_new ("ApplyMerges",
|
||
gs_appstream_apply_merges_cb,
|
||
md, (GDestroyNotify) merge_data_free);
|
||
xb_builder_fixup_set_max_depth (fixup2, 2);
|
||
xb_builder_add_fixup (builder, fixup2);
|
||
}
|
||
|
||
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);
|
||
}
|