diff options
Diffstat (limited to 'plugins/core')
-rw-r--r-- | plugins/core/gs-appstream.c | 1581 | ||||
-rw-r--r-- | plugins/core/gs-appstream.h | 76 | ||||
-rw-r--r-- | plugins/core/gs-desktop-common.c | 333 | ||||
-rw-r--r-- | plugins/core/gs-desktop-common.h | 31 | ||||
-rw-r--r-- | plugins/core/gs-plugin-appstream.c | 1092 | ||||
-rw-r--r-- | plugins/core/gs-plugin-desktop-categories.c | 106 | ||||
-rw-r--r-- | plugins/core/gs-plugin-desktop-menu-path.c | 111 | ||||
-rw-r--r-- | plugins/core/gs-plugin-generic-updates.c | 106 | ||||
-rw-r--r-- | plugins/core/gs-plugin-hardcoded-blocklist.c | 78 | ||||
-rw-r--r-- | plugins/core/gs-plugin-hardcoded-popular.c | 66 | ||||
-rw-r--r-- | plugins/core/gs-plugin-icons.c | 348 | ||||
-rw-r--r-- | plugins/core/gs-plugin-key-colors-metadata.c | 89 | ||||
-rw-r--r-- | plugins/core/gs-plugin-key-colors.c | 195 | ||||
-rw-r--r-- | plugins/core/gs-plugin-os-release.c | 112 | ||||
-rw-r--r-- | plugins/core/gs-plugin-provenance-license.c | 152 | ||||
-rw-r--r-- | plugins/core/gs-plugin-provenance.c | 141 | ||||
-rw-r--r-- | plugins/core/gs-plugin-rewrite-resource.c | 73 | ||||
-rw-r--r-- | plugins/core/gs-self-test.c | 277 | ||||
-rw-r--r-- | plugins/core/meson.build | 250 | ||||
l--------- | plugins/core/tests/os-release | 1 |
20 files changed, 5218 insertions, 0 deletions
diff --git a/plugins/core/gs-appstream.c b/plugins/core/gs-appstream.c new file mode 100644 index 0000000..d514cbf --- /dev/null +++ b/plugins/core/gs-appstream.c @@ -0,0 +1,1581 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <gnome-software.h> + +#include "gs-appstream.h" + +#define GS_APPSTREAM_MAX_SCREENSHOTS 5 + +GsApp * +gs_appstream_create_app (GsPlugin *plugin, XbSilo *silo, XbNode *component, GError **error) +{ + GsApp *app; + g_autoptr(GsApp) app_new = 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_DEFAULT, + error)) + return NULL; + + /* never add wildcard apps to the plugin cache */ + if (gs_app_has_quirk (app_new, GS_APP_QUIRK_IS_WILDCARD)) + return g_steal_pointer (&app_new); + + /* no longer supported */ + if (gs_app_get_kind (app_new) == AS_APP_KIND_SHELL_EXTENSION) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "shell extensions no longer supported"); + return NULL; + } + + /* 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); +} + +static gchar * +gs_appstream_format_description (XbNode *root, GError **error) +{ + g_autoptr(GString) str = g_string_new (NULL); + g_autoptr(XbNode) n = xb_node_get_child (root); + + while (n != NULL) { + g_autoptr(XbNode) n2 = NULL; + + /* support <p>, <ul>, <ol> and <li>, ignore all else */ + if (g_strcmp0 (xb_node_get_element (n), "p") == 0) { + g_string_append_printf (str, "%s\n\n", xb_node_get_text (n)); + } else if (g_strcmp0 (xb_node_get_element (n), "ul") == 0) { + g_autoptr(GPtrArray) children = xb_node_get_children (n); + for (guint i = 0; i < children->len; i++) { + XbNode *nc = g_ptr_array_index (children, i); + if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) { + g_string_append_printf (str, " • %s\n", + xb_node_get_text (nc)); + } + } + g_string_append (str, "\n"); + } else if (g_strcmp0 (xb_node_get_element (n), "ol") == 0) { + g_autoptr(GPtrArray) children = xb_node_get_children (n); + for (guint i = 0; i < children->len; i++) { + XbNode *nc = g_ptr_array_index (children, i); + if (g_strcmp0 (xb_node_get_element (nc), "li") == 0) { + g_string_append_printf (str, " %u. %s\n", + i + 1, + xb_node_get_text (nc)); + } + } + g_string_append (str, "\n"); + } + + n2 = xb_node_get_next (n); + g_set_object (&n, n2); + } + + /* remove extra newlines */ + while (str->len > 0 && str->str[str->len - 1] == '\n') + g_string_truncate (str, str->len - 1); + + /* success */ + return g_string_free (g_steal_pointer (&str), FALSE); +} + +static gchar * +gs_appstream_build_icon_prefix (XbNode *component) +{ + const gchar *origin; + const gchar *tmp; + gint npath; + g_auto(GStrv) path = NULL; + g_autoptr(XbNode) components = NULL; + + /* no parent, e.g. AppData */ + components = xb_node_get_parent (component); + if (components == NULL) + return NULL; + + /* set explicitly */ + tmp = xb_node_query_text (components, "info/icon-prefix", NULL); + if (tmp != NULL) + return g_strdup (tmp); + + /* fall back to origin */ + origin = xb_node_get_attr (components, "origin"); + if (origin == NULL) + return NULL; + + /* no metadata */ + tmp = xb_node_query_text (components, "info/filename", NULL); + if (tmp == NULL) + return NULL; + + /* check format */ + path = g_strsplit (tmp, "/", -1); + npath = g_strv_length (path); + if (npath < 3 || !(g_strcmp0 (path[npath-2], "xmls") == 0 || g_strcmp0 (path[npath-2], "yaml") == 0)) + 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); +} + +static AsIcon * +gs_appstream_new_icon (XbNode *component, XbNode *n, AsIconKind icon_kind, guint sz) +{ + AsIcon *icon = as_icon_new (); + g_autofree gchar *icon_path = NULL; + as_icon_set_kind (icon, icon_kind); + switch (icon_kind) { + case AS_ICON_KIND_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) + sz = xb_node_get_attr_as_uint (n, "width"); + if (sz > 0) { + as_icon_set_width (icon, sz); + as_icon_set_height (icon, sz); + } + icon_path = gs_appstream_build_icon_prefix (component); + if (icon_path != NULL) + as_icon_set_prefix (icon, icon_path); + return icon; +} + +static AsIcon * +gs_appstream_get_icon_by_kind (XbNode *component, AsIconKind icon_kind) +{ + g_autofree gchar *xpath = NULL; + g_autoptr(XbNode) icon = NULL; + + xpath = g_strdup_printf ("icon[@type='%s']", + as_icon_kind_to_string (icon_kind)); + icon = xb_node_query_first (component, xpath, NULL); + if (icon == NULL) + return NULL; + return gs_appstream_new_icon (component, icon, icon_kind, 0); +} + +static AsIcon * +gs_appstream_get_icon_by_kind_and_size (XbNode *component, AsIconKind icon_kind, guint sz) +{ + g_autofree gchar *xpath = NULL; + g_autoptr(XbNode) icon = NULL; + + xpath = g_strdup_printf ("icon[@type='%s'][@height='%u'][@width='%u']", + as_icon_kind_to_string (icon_kind), sz, sz); + icon = xb_node_query_first (component, xpath, NULL); + if (icon == NULL) + return NULL; + return gs_appstream_new_icon (component, icon, icon_kind, sz); +} + +static void +gs_appstream_refine_icon (GsPlugin *plugin, GsApp *app, XbNode *component) +{ + g_autoptr(AsIcon) icon = NULL; + g_autoptr(XbNode) n = NULL; + + /* try a stock icon first */ + icon = gs_appstream_get_icon_by_kind (component, AS_ICON_KIND_STOCK); + if (icon != NULL) { + /* the stock icon referenced by the AppStream data may not be present in the current + * theme (usually more stock icon entries are added to permit huge themes like Papirus + * to style all apps in the software center). Since we can not rely on the icon's presence, + * we also add other icons to the list and do not return here. */ + gs_app_add_icon (app, icon); + } + + /* if HiDPI get a 128px cached icon */ + if (gs_plugin_get_scale (plugin) == 2) { + icon = gs_appstream_get_icon_by_kind_and_size (component, + AS_ICON_KIND_CACHED, + 128); + if (icon != NULL) { + gs_app_add_icon (app, icon); + return; + } + } + + /* non-HiDPI cached icon */ + icon = gs_appstream_get_icon_by_kind_and_size (component, + AS_ICON_KIND_CACHED, + 64); + if (icon != NULL) { + gs_app_add_icon (app, icon); + return; + } + + /* prefer local */ + icon = gs_appstream_get_icon_by_kind (component, AS_ICON_KIND_LOCAL); + if (icon != NULL) { + /* does not exist, so try to find using the icon theme */ + if (as_icon_get_kind (icon) == AS_ICON_KIND_LOCAL && + as_icon_get_filename (icon) == NULL) { + g_debug ("converting missing LOCAL icon %s to STOCK", + as_icon_get_name (icon)); + as_icon_set_kind (icon, AS_ICON_KIND_STOCK); + } + gs_app_add_icon (app, icon); + return; + } + + /* remote URL */ + icon = gs_appstream_get_icon_by_kind (component, AS_ICON_KIND_REMOTE); + if (icon != NULL) { + gs_app_add_icon (app, icon); + return; + } + + /* assume a stock icon */ + n = xb_node_query_first (component, "icon", NULL); + if (n != NULL) { + icon = gs_appstream_new_icon (component, n, AS_ICON_KIND_STOCK, 0); + gs_app_add_icon (app, icon); + } +} + +static gboolean +gs_appstream_refine_add_addons (GsPlugin *plugin, + GsApp *app, + XbSilo *silo, + GError **error) +{ + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) addons = NULL; + + /* get all components */ + xpath = g_strdup_printf ("components/component/extends[text()='%s']/..", + gs_app_get_id (app)); + addons = xb_silo_query (silo, xpath, 0, &error_local); + if (addons == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < addons->len; i++) { + XbNode *addon = g_ptr_array_index (addons, i); + g_autoptr(GsApp) app2 = NULL; + app2 = gs_appstream_create_app (plugin, silo, addon, error); + if (app2 == NULL) + return FALSE; + gs_app_add_addon (app, app2); + } + return TRUE; +} + +static gboolean +gs_appstream_refine_add_images (GsApp *app, AsScreenshot *ss, XbNode *screenshot, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) images = NULL; + + /* get all components */ + images = xb_node_query (screenshot, "image", 0, &error_local); + if (images == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < images->len; i++) { + XbNode *image = g_ptr_array_index (images, i); + g_autoptr(AsImage) im = as_image_new (); + as_image_set_height (im, xb_node_get_attr_as_uint (image, "height")); + as_image_set_width (im, xb_node_get_attr_as_uint (image, "width")); + as_image_set_kind (im, as_image_kind_from_string (xb_node_get_attr (image, "type"))); + as_image_set_url (im, xb_node_get_text (image)); + as_screenshot_add_image (ss, im); + } + + /* success */ + return TRUE; +} + +static gboolean +gs_appstream_refine_add_screenshots (GsApp *app, XbNode *component, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) screenshots = NULL; + + /* get all components */ + screenshots = xb_node_query (component, "screenshots/screenshot", 0, &error_local); + if (screenshots == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < screenshots->len; i++) { + XbNode *screenshot = g_ptr_array_index (screenshots, i); + g_autoptr(AsScreenshot) ss = as_screenshot_new (); + if (!gs_appstream_refine_add_images (app, ss, screenshot, error)) + return FALSE; + gs_app_add_screenshot (app, ss); + } + + /* FIXME: move into no refine flags section? */ + if (screenshots ->len > 0) + gs_app_add_kudo (app, GS_APP_KUDO_HAS_SCREENSHOTS); + + /* success */ + return TRUE; +} + +static gboolean +gs_appstream_refine_add_provides (GsApp *app, XbNode *component, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) provides = NULL; + + /* get all components */ + provides = xb_node_query (component, "provides/*", 0, &error_local); + if (provides == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < provides->len; i++) { + XbNode *provide = g_ptr_array_index (provides, i); + g_autoptr(AsProvide) pr = as_provide_new (); + as_provide_set_kind (pr, as_provide_kind_from_string (xb_node_get_element (provide))); + as_provide_set_value (pr, xb_node_get_text (provide)); + gs_app_add_provide (app, pr); + } + + /* success */ + return TRUE; +} + +static gboolean +gs_appstream_is_recent_release (XbNode *component) +{ + guint64 ts; + guint64 secs; + + /* get newest release */ + ts = xb_node_query_attr_as_uint (component, "releases/release", "timestamp", NULL); + if (ts == G_MAXUINT64) + return FALSE; + + /* is last build less than one year ago? */ + secs = ((guint64) 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; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < values->len; i++) { + XbNode *value = g_ptr_array_index (values, i); + const gchar *key = xb_node_get_attr (value, "key"); + if (key == NULL) + continue; + if (gs_app_get_metadata_item (app, key) != NULL) + continue; + gs_app_set_metadata (app, key, xb_node_get_text (value)); + } + return TRUE; +} + +static gboolean +gs_appstream_refine_app_updates (GsPlugin *plugin, + GsApp *app, + XbSilo *silo, + XbNode *component, + GError **error) +{ + AsUrgencyKind urgency_best = AS_URGENCY_KIND_UNKNOWN; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GHashTable) installed = g_hash_table_new (g_str_hash, g_str_equal); + g_autoptr(GPtrArray) releases_inst = NULL; + g_autoptr(GPtrArray) releases = NULL; + g_autoptr(GPtrArray) updates_list = g_ptr_array_new (); + + /* only for UPDATABLE apps */ + if (!gs_app_is_updatable (app)) + return TRUE; + + /* find out which releases are already installed */ + xpath = g_strdup_printf ("component/id[text()='%s']/../releases/*[@version]", + gs_app_get_id (app)); + releases_inst = xb_silo_query (silo, xpath, 0, &error_local); + if (releases_inst == NULL) { + if (!g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && + !g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + } else { + for (guint i = 0; i < releases_inst->len; i++) { + XbNode *release = g_ptr_array_index (releases_inst, i); + g_hash_table_insert (installed, + (gpointer) xb_node_get_attr (release, "version"), + (gpointer) release); + } + } + g_clear_error (&error_local); + + /* get all components */ + releases = xb_node_query (component, "releases/*", 0, &error_local); + if (releases == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < releases->len; i++) { + XbNode *release = g_ptr_array_index (releases, i); + const gchar *version = xb_node_get_attr (release, "version"); + g_autoptr(XbNode) description = NULL; + AsUrgencyKind urgency_tmp; + + /* ignore releases with no version */ + if (version == NULL) + continue; + + /* already installed */ + if (g_hash_table_lookup (installed, version) != NULL) + continue; + + /* limit this to three versions backwards if there has never + * been a detected installed version */ + if (g_hash_table_size (installed) == 0 && i >= 3) + break; + + /* use the 'worst' urgency, e.g. critical over enhancement */ + urgency_tmp = as_urgency_kind_from_string (xb_node_get_attr (release, "urgency")); + if (urgency_tmp > urgency_best) + urgency_best = urgency_tmp; + + /* add updates with a description */ + description = xb_node_query_first (release, "description", NULL); + if (description == NULL) + continue; + g_ptr_array_add (updates_list, release); + } + + /* only set if known */ + if (urgency_best != AS_URGENCY_KIND_UNKNOWN) + gs_app_set_update_urgency (app, urgency_best); + + /* no prefix on each release */ + if (updates_list->len == 1) { + XbNode *release = g_ptr_array_index (updates_list, 0); + g_autoptr(XbNode) n = NULL; + g_autofree gchar *desc = NULL; + n = xb_node_query_first (release, "description", NULL); + desc = gs_appstream_format_description (n, NULL); + gs_app_set_update_details (app, desc); + + /* get the descriptions with a version prefix */ + } else if (updates_list->len > 1) { + g_autoptr(GString) update_desc = g_string_new (""); + for (guint i = 0; i < updates_list->len; i++) { + XbNode *release = g_ptr_array_index (updates_list, i); + g_autofree gchar *desc = NULL; + g_autoptr(XbNode) n = NULL; + + n = xb_node_query_first (release, "description", NULL); + desc = gs_appstream_format_description (n, NULL); + g_string_append_printf (update_desc, + "Version %s:\n%s\n\n", + xb_node_get_attr (release, "version"), + desc); + } + + /* remove trailing newlines */ + if (update_desc->len > 2) + g_string_truncate (update_desc, update_desc->len - 2); + gs_app_set_update_details (app, update_desc->str); + } + + /* if there is no already set update version use the newest */ + if (gs_app_get_update_version (app) == NULL && + updates_list->len > 0) { + XbNode *release = g_ptr_array_index (updates_list, 0); + gs_app_set_update_version (app, xb_node_get_attr (release, "version")); + } + + /* success */ + return TRUE; +} + +/** + * _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 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; + if (g_strcmp0 (locale_copy, "en_US") == 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_environment_id (project_group); +} + +static gboolean +gs_appstream_refine_app_content_rating (GsPlugin *plugin, + GsApp *app, + XbNode *content_rating, + GError **error) +{ + g_autoptr(AsContentRating) cr = as_content_rating_new (); + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) content_attributes = NULL; + const gchar *content_rating_kind = NULL; + + /* get kind */ + content_rating_kind = xb_node_get_attr (content_rating, "type"); + /* we only really expect/support OARS 1.0 and 1.1 */ + if (content_rating_kind == NULL || + (g_strcmp0 (content_rating_kind, "oars-1.0") != 0 && + g_strcmp0 (content_rating_kind, "oars-1.1") != 0)) { + return TRUE; + } + + as_content_rating_set_kind (cr, content_rating_kind); + + /* get attributes; no attributes being found (i.e. + * `<content_rating type="*"/>`) is OK: it means that all attributes have + * value `none`, as per the + * [OARS semantics](https://github.com/hughsie/oars/blob/master/specification/oars-1.1.md) */ + content_attributes = xb_node_query (content_rating, "content_attribute", 0, &error_local); + if (content_attributes == NULL && + g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_clear_error (&error_local); + } else if (content_attributes == NULL && + g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { + return TRUE; + } else if (content_attributes == NULL) { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + for (guint i = 0; content_attributes != NULL && i < content_attributes->len; i++) { + XbNode *content_attribute = g_ptr_array_index (content_attributes, i); + as_content_rating_add_attribute (cr, + xb_node_get_attr (content_attribute, "id"), + as_content_rating_value_from_string (xb_node_get_text (content_attribute))); + } + + gs_app_set_content_rating (app, cr); + return TRUE; +} + +static gboolean +gs_appstream_refine_app_content_ratings (GsPlugin *plugin, + GsApp *app, + XbNode *component, + GError **error) +{ + g_autoptr(GPtrArray) content_ratings = NULL; + g_autoptr(GError) error_local = NULL; + + /* find any content ratings */ + content_ratings = xb_node_query (component, "content_rating", 0, &error_local); + if (content_ratings == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < content_ratings->len; i++) { + XbNode *content_rating = g_ptr_array_index (content_ratings, i); + if (!gs_appstream_refine_app_content_rating (plugin, app, content_rating, error)) + return FALSE; + } + return TRUE; +} + +gboolean +gs_appstream_refine_app (GsPlugin *plugin, + GsApp *app, + XbSilo *silo, + XbNode *component, + GsPluginRefineFlags refine_flags, + GError **error) +{ + const gchar *tmp; + g_autoptr(GPtrArray) bundles = NULL; + g_autoptr(GPtrArray) launchables = NULL; + g_autoptr(XbNode) req = NULL; + + /* is compatible */ + req = xb_node_query_first (component, + "requires/id[@type='id']" + "[text()='org.gnome.Software.desktop']", NULL); + if (req != NULL) { +#if AS_CHECK_VERSION(0,7,15) + gint rc = as_utils_vercmp_full (xb_node_get_attr (req, "version"), + PACKAGE_VERSION, + AS_VERSION_COMPARE_FLAG_NONE); +#else + gint rc = as_utils_vercmp (xb_node_get_attr (req, "version"), + PACKAGE_VERSION); +#endif + if (rc > 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "not for this gnome-software"); + return FALSE; + } + } + + /* types we can never launch */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_ADDON: + case AS_APP_KIND_CODEC: + case AS_APP_KIND_DRIVER: + case AS_APP_KIND_FIRMWARE: + case AS_APP_KIND_FONT: + case AS_APP_KIND_GENERIC: + case AS_APP_KIND_INPUT_METHOD: + case AS_APP_KIND_LOCALIZATION: + case AS_APP_KIND_OS_UPDATE: + case AS_APP_KIND_OS_UPGRADE: + case AS_APP_KIND_RUNTIME: + case AS_APP_KIND_SOURCE: + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + break; + default: + break; + } + + /* check if the special metadata affects the not-launchable quirk */ + tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::not-launchable"); + if (tmp != NULL) { + if (g_strcmp0 (tmp, "true") == 0) + gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + else if (g_strcmp0 (tmp, "false") == 0) + gs_app_remove_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE); + } + + tmp = gs_app_get_metadata_item (app, "GnomeSoftware::quirks::hide-everywhere"); + if (tmp != NULL) { + if (g_strcmp0 (tmp, "true") == 0) + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + else if (g_strcmp0 (tmp, "false") == 0) + gs_app_remove_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + } + + /* try to detect old-style AppStream 'override' + * files without the merge attribute */ + if (xb_node_query_text (component, "name", NULL) == NULL && + xb_node_query_text (component, "metadata_license", NULL) == NULL) { + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + } + + /* set id */ + tmp = xb_node_query_text (component, "id", NULL); + if (tmp != NULL && gs_app_get_id (app) == NULL) + gs_app_set_id (app, tmp); + + /* set source */ + tmp = xb_node_query_text (component, "../info/filename", NULL); + if (tmp != NULL && gs_app_get_metadata_item (app, "appstream::source-file") == NULL) { + gs_app_set_metadata (app, "appstream::source-file", tmp); + } + + /* set scope */ + tmp = xb_node_query_text (component, "../info/scope", NULL); + if (tmp != NULL) + gs_app_set_scope (app, as_app_scope_from_string (tmp)); + + /* set content rating */ + if (TRUE) { + if (!gs_appstream_refine_app_content_ratings (plugin, app, component, error)) + return FALSE; + } + + /* set name */ + tmp = xb_node_query_text (component, "name", NULL); + if (tmp != NULL) + gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, tmp); + + /* set summary */ + tmp = xb_node_query_text (component, "summary", NULL); + if (tmp != NULL) + gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, tmp); + + /* add urls */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) { + g_autoptr(GPtrArray) urls = NULL; + urls = xb_node_query (component, "url", 0, NULL); + if (urls != NULL) { + for (guint i = 0; i < urls->len; i++) { + XbNode *url = g_ptr_array_index (urls, i); + const gchar *kind = xb_node_get_attr (url, "type"); + if (kind == NULL) + continue; + gs_app_set_url (app, + as_url_kind_from_string (kind), + xb_node_get_text (url)); + } + } + } + + /* add launchables */ + launchables = xb_node_query (component, "launchable", 0, NULL); + if (launchables != NULL) { + for (guint i = 0; i < launchables->len; i++) { + XbNode *launchable = g_ptr_array_index (launchables, i); + const gchar *kind = xb_node_get_attr (launchable, "type"); + if (g_strcmp0 (kind, "desktop-id") == 0) { + gs_app_set_launchable (app, + AS_LAUNCHABLE_KIND_DESKTOP_ID, + xb_node_get_text (launchable)); + break; + } else if (g_strcmp0 (kind, "url") == 0) { + gs_app_set_launchable (app, + AS_LAUNCHABLE_KIND_URL, + xb_node_get_text (launchable)); + } + } + } + + /* set license */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) > 0 && + gs_app_get_license (app) == NULL) { + tmp = xb_node_query_text (component, "project_license", NULL); + if (tmp != NULL) + gs_app_set_license (app, GS_APP_QUALITY_HIGHEST, tmp); + } + + /* set description */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) { + g_autofree gchar *description = NULL; + g_autoptr(XbNode) n = xb_node_query_first (component, "description", NULL); + if (n != NULL) + description = gs_appstream_format_description (n, NULL); + if (description != NULL) + gs_app_set_description (app, GS_APP_QUALITY_HIGHEST, description); + } + + /* set icon */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) > 0 && + gs_app_get_icons(app)->len == 0) + gs_appstream_refine_icon (plugin, app, component); + + /* set categories */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES) { + g_autoptr(GPtrArray) categories = NULL; + categories = xb_node_query (component, "categories/category", 0, NULL); + if (categories != NULL) { + for (guint i = 0; i < categories->len; i++) { + XbNode *category = g_ptr_array_index (categories, i); + gs_app_add_category (app, xb_node_get_text (category)); + + /* Special case: We used to use the `Blacklisted` + * category to hide apps from their .desktop + * file or appdata. We now use a quirk for that. + * This special case can be removed when all + * appstream files no longer use the `Blacklisted` + * category (including external-appstream files + * put together by distributions). */ + if (g_strcmp0 (xb_node_get_text (category), "Blacklisted") == 0) + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + } + } + } + + /* set project group */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP) > 0 && + gs_app_get_project_group (app) == NULL) { + tmp = xb_node_query_text (component, "project_group", NULL); + if (tmp != NULL && gs_appstream_is_valid_project_group (tmp)) + gs_app_set_project_group (app, tmp); + } + + /* set developer name */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) > 0 && + gs_app_get_developer_name (app) == NULL) { + tmp = xb_node_query_text (component, "developer_name", NULL); + if (tmp != NULL) + gs_app_set_developer_name (app, tmp); + } + + /* set id kind */ + if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN || + gs_app_get_kind (app) == AS_APP_KIND_GENERIC) { + tmp = xb_node_get_attr (component, "type"); + gs_app_set_kind (app, as_app_kind_from_string (tmp)); + } + + /* copy all the metadata */ + if (!gs_appstream_copy_metadata (app, component, error)) + return FALSE; + + /* add bundles */ + bundles = xb_node_query (component, "bundle", 0, NULL); + if (bundles != NULL && gs_app_get_sources(app)->len == 0) { + for (guint i = 0; i < bundles->len; i++) { + XbNode *bundle = g_ptr_array_index (bundles, i); + const gchar *kind = xb_node_get_attr (bundle, "type"); + gs_app_add_source (app, xb_node_get_text (bundle)); + 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 (xb_node_get_text (bundle), "/", -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", + xb_node_get_text (bundle)); + return FALSE; + } + + /* we only need the branch for the unique ID */ + gs_app_set_branch (app, split[3]); + } + } + } + + /* add legacy package names */ + if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN) { + g_autoptr(GPtrArray) pkgnames = NULL; + pkgnames = xb_node_query (component, "pkgname", 0, NULL); + if (pkgnames != NULL && gs_app_get_sources(app)->len == 0) { + for (guint i = 0; i < pkgnames->len; i++) { + XbNode *pkgname = g_ptr_array_index (pkgnames, i); + tmp = xb_node_get_text (pkgname); + if (tmp != NULL && tmp[0] != '\0') + gs_app_add_source (app, tmp); + } + gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE); + } + } + + /* set origin for flatpaks */ + if (gs_app_get_origin (app) == NULL && + gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_FLATPAK) { + g_autoptr(XbNode) parent = xb_node_get_parent (component); + if (parent != NULL) { + tmp = xb_node_get_attr (parent, "origin"); + gs_app_set_origin (app, tmp); + } + } + + /* set addons */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) { + if (!gs_appstream_refine_add_addons (plugin, app, silo, error)) + return FALSE; + } + + /* set screenshots */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) > 0 && + gs_app_get_screenshots(app)->len == 0) { + if (!gs_appstream_refine_add_screenshots (app, component, error)) + return FALSE; + } + + /* set provides */ + if (!gs_appstream_refine_add_provides (app, component, error)) + return FALSE; + + /* add kudos */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) { + g_autoptr(GPtrArray) kudos = NULL; + tmp = gs_plugin_get_locale (plugin); + if (!_gs_utils_locale_has_translations (tmp)) { + gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE); + } else { + + g_autoptr(GString) xpath = g_string_new (NULL); + g_auto(GStrv) variants = g_get_locale_variants (tmp); + + /* @variants includes @tmp */ + for (gsize i = 0; variants[i] != NULL; i++) + xb_string_append_union (xpath, "languages/lang[text()='%s'][@percentage>50]", variants[i]); + + if (xb_node_query_text (component, xpath->str, NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_MY_LANGUAGE); + } + + /* any keywords */ + if (xb_node_query_text (component, "keywords/keyword", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_HAS_KEYWORDS); + + /* HiDPI icon */ + if (xb_node_query_text (component, "icon[@width='128']", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON); + + /* was this application released recently */ + if (gs_appstream_is_recent_release (component)) + gs_app_add_kudo (app, GS_APP_KUDO_RECENT_RELEASE); + + /* add a kudo to featured and popular apps */ + if (xb_node_query_text (component, "kudos/kudo[text()='GnomeSoftware::popular']", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED); + if (xb_node_query_text (component, "categories/category[text()='Featured']", NULL) != NULL) + gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED); + + /* add new-style kudos */ + kudos = xb_node_query (component, "kudos/kudo", 0, NULL); + for (guint i = 0; kudos != NULL && i < kudos->len; i++) { + XbNode *kudo = g_ptr_array_index (kudos, i); + switch (as_kudo_kind_from_string (xb_node_get_text (kudo))) { + case AS_KUDO_KIND_SEARCH_PROVIDER: + gs_app_add_kudo (app, GS_APP_KUDO_SEARCH_PROVIDER); + break; + case AS_KUDO_KIND_USER_DOCS: + gs_app_add_kudo (app, GS_APP_KUDO_INSTALLS_USER_DOCS); + break; + case AS_KUDO_KIND_MODERN_TOOLKIT: + gs_app_add_kudo (app, GS_APP_KUDO_MODERN_TOOLKIT); + break; + case AS_KUDO_KIND_NOTIFICATIONS: + gs_app_add_kudo (app, GS_APP_KUDO_USES_NOTIFICATIONS); + break; + case AS_KUDO_KIND_HIGH_CONTRAST: + gs_app_add_kudo (app, GS_APP_KUDO_HIGH_CONTRAST); + break; + case AS_KUDO_KIND_HI_DPI_ICON: + gs_app_add_kudo (app, GS_APP_KUDO_HI_DPI_ICON); + break; + default: + break; + } + } + } + + /* we have an origin in the XML */ + if ((refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) > 0 && + gs_app_get_origin_appstream (app) == NULL) { + g_autoptr(XbNode) parent = xb_node_get_parent (component); + if (parent != NULL) { + tmp = xb_node_get_attr (parent, "origin"); + if (gs_appstream_origin_valid (tmp)) + gs_app_set_origin_appstream (app, tmp); + } + } + + /* is there any update information */ + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) { + if (!gs_appstream_refine_app_updates (plugin, + app, + silo, + component, + error)) + return FALSE; + } + + return TRUE; +} + +typedef struct { + AsAppSearchMatch 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); + xb_query_bind_str (helper->query, 0, search, NULL); + n = xb_node_query_full (component, helper->query, 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; +} + +gboolean +gs_appstream_search (GsPlugin *plugin, + XbSilo *silo, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func ((GDestroyNotify) gs_appstream_search_helper_free); + g_autoptr(GPtrArray) components = NULL; + g_autoptr(GTimer) timer = g_timer_new (); + const struct { + AsAppSearchMatch match_value; + const gchar *xpath; + } queries[] = { + { AS_APP_SEARCH_MATCH_MIMETYPE, "mimetypes/mimetype[text()~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_PKGNAME, "pkgname[text()~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_COMMENT, "summary[text()~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_NAME, "name[text()~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_KEYWORD, "keywords/keyword[text()~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_ID, "id[text()~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_ID, "launchable[text()~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_ORIGIN, "../components[@origin~=stem(?)]" }, + { AS_APP_SEARCH_MATCH_NONE, NULL } + }; + + /* 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; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + guint16 match_value = gs_appstream_silo_search_component (array, component, values); + if (match_value != 0) { + g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error); + if (app == NULL) + return FALSE; + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + g_debug ("not returning wildcard %s", + gs_app_get_unique_id (app)); + continue; + } + g_debug ("add %s", gs_app_get_unique_id (app)); + gs_app_set_match_value (app, match_value); + gs_app_list_add (list, app); + } + } + g_debug ("search took %fms", g_timer_elapsed (timer, NULL) * 1000); + return TRUE; +} + +gboolean +gs_appstream_add_category_apps (GsPlugin *plugin, + XbSilo *silo, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *desktop_groups; + g_autoptr(GError) error_local = NULL; + + 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; + + /* generate query */ + if (g_strv_length (split) == 1) { + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../..", + split[0]); + } else if (g_strv_length (split) == 2) { + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../" + "category[text()='%s']/../..", + split[0], split[1]); + } + components = xb_silo_query (silo, xpath, 0, &error_local); + if (components == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + 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_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + } + + } + return TRUE; +} + +static guint +gs_appstream_count_component_for_groups (GsPlugin *plugin, XbSilo *silo, const gchar *desktop_group) +{ + guint limit = 10; + g_autofree gchar *xpath = NULL; + g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1); + g_autoptr(GPtrArray) array = NULL; + g_autoptr(GError) error_local = NULL; + + if (g_strv_length (split) == 1) { /* "all" group for a parent category */ + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../..", + split[0]); + } else if (g_strv_length (split) == 2) { + xpath = g_strdup_printf ("components/component/categories/" + "category[text()='%s']/../" + "category[text()='%s']/../..", + split[0], split[1]); + } else { + return 0; + } + + array = xb_silo_query (silo, xpath, limit, &error_local); + if (array == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return 0; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return 0; + g_warning ("%s", error_local->message); + return 0; + } + return array->len; +} + +/* we're not actually adding categories here, we're just setting the number of + * applications available in each category */ +gboolean +gs_appstream_add_categories (GsPlugin *plugin, + XbSilo *silo, + GPtrArray *list, + GCancellable *cancellable, + GError **error) +{ + 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 (plugin, silo, group); + for (guint l = 0; l < cnt; l++) { + gs_category_increment_size (parent); + if (children->len > 1) { + /* Parent category has multiple groups, so increment + * each group's size too */ + gs_category_increment_size (cat); + } + } + } + } + continue; + } + return TRUE; +} + +gboolean +gs_appstream_add_popular (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = NULL; + + /* 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; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < array->len; i++) { + g_autoptr(GsApp) app = NULL; + XbNode *component = g_ptr_array_index (array, i); + const gchar *component_id = xb_node_query_text (component, "id", NULL); + if (component_id == NULL) + continue; + app = gs_app_new (component_id); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_appstream_add_recent (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error) +{ + guint64 now = (guint64) g_get_real_time () / G_USEC_PER_SEC; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = NULL; + + /* use predicate conditions to the max */ + xpath = g_strdup_printf ("components/component/releases/" + "release[@timestamp>%" G_GUINT64_FORMAT "]/../..", + now - (30 * 24 * 60 * 60)); + 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; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < array->len; i++) { + XbNode *component = g_ptr_array_index (array, i); + g_autoptr(GsApp) app = gs_appstream_create_app (plugin, silo, component, error); + if (app == NULL) + return FALSE; + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_appstream_add_alternates (GsPlugin *plugin, + 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); + + /* 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; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + 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; + app2 = gs_app_new (xb_node_get_text (n)); + gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app2); + } + return TRUE; +} + +gboolean +gs_appstream_add_featured (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = NULL; + + /* find out how many packages are in each category */ + array = xb_silo_query (silo, + "components/component/custom/" + "value[@key='GnomeSoftware::FeatureTile-css']/../..", + 0, &error_local); + if (array == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + 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; +} + +void +gs_appstream_component_add_keyword (XbBuilderNode *component, const gchar *str) +{ + g_autoptr(XbBuilderNode) keyword = NULL; + g_autoptr(XbBuilderNode) keywords = 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; + + /* 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; + + /* 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; + + /* 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 (GsPlugin *plugin, XbBuilderNode *component) +{ + const gchar *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_app_kind_from_string (kind)) { + case AS_APP_KIND_WEB_APP: + gs_appstream_component_add_keyword (component, kind); + break; + case AS_APP_KIND_FONT: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Font"); + break; + case AS_APP_KIND_DRIVER: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Driver"); + gs_appstream_component_add_icon (component, "application-x-firmware-symbolic"); + break; + case AS_APP_KIND_LOCALIZATION: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Localization"); + gs_appstream_component_add_icon (component, "accessories-dictionary-symbolic"); + break; + case AS_APP_KIND_CODEC: + gs_appstream_component_add_category (component, "Addon"); + gs_appstream_component_add_category (component, "Codec"); + gs_appstream_component_add_icon (component, "application-x-addon"); + break; + case AS_APP_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-run-symbolic"); + break; + case AS_APP_KIND_FIRMWARE: + gs_appstream_component_add_icon (component, "system-run-symbolic"); + break; + default: + break; + } +} diff --git a/plugins/core/gs-appstream.h b/plugins/core/gs-appstream.h new file mode 100644 index 0000000..d6e9a0b --- /dev/null +++ b/plugins/core/gs-appstream.h @@ -0,0 +1,76 @@ +/* -*- 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> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gnome-software.h> +#include <xmlb.h> + +G_BEGIN_DECLS + +GsApp *gs_appstream_create_app (GsPlugin *plugin, + XbSilo *silo, + XbNode *component, + GError **error); +gboolean gs_appstream_refine_app (GsPlugin *plugin, + GsApp *app, + XbSilo *silo, + XbNode *component, + GsPluginRefineFlags flags, + GError **error); +gboolean gs_appstream_search (GsPlugin *plugin, + XbSilo *silo, + const gchar * const *values, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_categories (GsPlugin *plugin, + XbSilo *silo, + GPtrArray *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_category_apps (GsPlugin *plugin, + XbSilo *silo, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_popular (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_featured (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_alternates (GsPlugin *plugin, + XbSilo *silo, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error); +gboolean gs_appstream_add_recent (GsPlugin *plugin, + XbSilo *silo, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error); +void gs_appstream_component_add_extra_info (GsPlugin *plugin, + XbBuilderNode *component); +void gs_appstream_component_add_keyword (XbBuilderNode *component, + const gchar *str); +void gs_appstream_component_add_category (XbBuilderNode *component, + const gchar *str); +void gs_appstream_component_add_icon (XbBuilderNode *component, + const gchar *str); +void gs_appstream_component_add_provide (XbBuilderNode *component, + const gchar *str); + +G_END_DECLS diff --git a/plugins/core/gs-desktop-common.c b/plugins/core/gs-desktop-common.c new file mode 100644 index 0000000..27e11bd --- /dev/null +++ b/plugins/core/gs-desktop-common.c @@ -0,0 +1,333 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-desktop-common.h" + +/* AudioVideo */ +static const GsDesktopMap map_audiovisual[] = { + { "all", NC_("Menu of Audio & Video", "All"), + { "AudioVideo", + NULL } }, + { "featured", NC_("Menu of Audio & Video", "Featured"), + { "AudioVideo::Featured", + NULL} }, + { "creation-editing", NC_("Menu of Audio & Video", "Audio Creation & Editing"), + { "AudioVideo::AudioVideoEditing", + "AudioVideo::Midi", + "AudioVideo::DiscBurning", + "AudioVideo::Sequencer", + NULL} }, + { "music-players", NC_("Menu of Audio & Video", "Music Players"), + { "AudioVideo::Music", + "AudioVideo::Player", + NULL} }, + { NULL } +}; + +/* Development */ +static const GsDesktopMap map_developertools[] = { + { "all", NC_("Menu of Developer Tools", "All"), + { "Development", + NULL } }, + { "featured", NC_("Menu of Developer Tools", "Featured"), + { "Development::Featured", + NULL} }, + { "debuggers", NC_("Menu of Developer Tools", "Debuggers"), + { "Development::Debugger", + NULL} }, + { "ide", NC_("Menu of Developer Tools", "IDEs"), + { "Development::IDE", + "Development::GUIDesigner", + NULL} }, + { NULL } +}; + +/* Education & Science */ +static const GsDesktopMap map_education_science[] = { + { "all", NC_("Menu of Education & Science", "All"), + { "Education", + "Science", + NULL } }, + { "featured", NC_("Menu of Education & Science", "Featured"), + { "Education::Featured", + "Science::Featured", + NULL} }, + { "artificial-intelligence", NC_("Menu of Education & Science", "Artificial Intelligence"), + { "Science::ArtificialIntelligence", + NULL} }, + { "astronomy", NC_("Menu of Education & Science", "Astronomy"), + { "Education::Astronomy", + "Science::Astronomy", + NULL} }, + { "chemistry", NC_("Menu of Education & Science", "Chemistry"), + { "Education::Chemistry", + "Science::Chemistry", + NULL} }, + { "languages", NC_("Menu of Education & Science", "Languages"), + { "Education::Languages", + "Education::Literature", + NULL} }, + { "math", NC_("Menu of Education & Science", "Math"), + { "Education::Math", + "Education::NumericalAnalysis", + "Science::Math", + "Science::Physics", + "Science::NumericalAnalysis", + NULL} }, + { "robotics", NC_("Menu of Education & Science", "Robotics"), + { "Science::Robotics", + NULL} }, + + { NULL } +}; + +/* Games */ +static const GsDesktopMap map_games[] = { + { "all", NC_("Menu of Games", "All"), + { "Game", + NULL } }, + { "featured", NC_("Menu of Games", "Featured"), + { "Game::Featured", + NULL} }, + { "action", NC_("Menu of Games", "Action"), + { "Game::ActionGame", + NULL} }, + { "adventure", NC_("Menu of Games", "Adventure"), + { "Game::AdventureGame", + NULL} }, + { "arcade", NC_("Menu of Games", "Arcade"), + { "Game::ArcadeGame", + NULL} }, + { "blocks", NC_("Menu of Games", "Blocks"), + { "Game::BlocksGame", + NULL} }, + { "board", NC_("Menu of Games", "Board"), + { "Game::BoardGame", + NULL} }, + { "card", NC_("Menu of Games", "Card"), + { "Game::CardGame", + NULL} }, + { "emulator", NC_("Menu of Games", "Emulators"), + { "Game::Emulator", + NULL} }, + { "kids", NC_("Menu of Games", "Kids"), + { "Game::KidsGame", + NULL} }, + { "logic", NC_("Menu of Games", "Logic"), + { "Game::LogicGame", + NULL} }, + { "role-playing", NC_("Menu of Games", "Role Playing"), + { "Game::RolePlaying", + NULL} }, + { "sports", NC_("Menu of Games", "Sports"), + { "Game::SportsGame", + "Game::Simulation", + NULL} }, + { "strategy", NC_("Menu of Games", "Strategy"), + { "Game::StrategyGame", + NULL} }, + { NULL } +}; + +/* Graphics */ +static const GsDesktopMap map_graphics[] = { + { "all", NC_("Menu of Graphics & Photography", "All"), + { "Graphics", + NULL } }, + { "featured", NC_("Menu of Graphics & Photography", "Featured"), + { "Graphics::Featured", + NULL} }, + { "3d", NC_("Menu of Graphics & Photography", "3D Graphics"), + { "Graphics::3DGraphics", + NULL} }, + { "photography", NC_("Menu of Graphics & Photography", "Photography"), + { "Graphics::Photography", + NULL} }, + { "scanning", NC_("Menu of Graphics & Photography", "Scanning"), + { "Graphics::Scanning", + NULL} }, + { "vector", NC_("Menu of Graphics & Photography", "Vector Graphics"), + { "Graphics::VectorGraphics", + NULL} }, + { "viewers", NC_("Menu of Graphics & Photography", "Viewers"), + { "Graphics::Viewer", + NULL} }, + { NULL } +}; + +/* Office */ +static const GsDesktopMap map_productivity[] = { + { "all", NC_("Menu of Productivity", "All"), + { "Office", + NULL } }, + { "featured", NC_("Menu of Productivity", "Featured"), + { "Office::Featured", + NULL} }, + { "calendar", NC_("Menu of Productivity", "Calendar"), + { "Office::Calendar", + "Office::ProjectManagement", + NULL} }, + { "database", NC_("Menu of Productivity", "Database"), + { "Office::Database", + NULL} }, + { "finance", NC_("Menu of Productivity", "Finance"), + { "Office::Finance", + "Office::Spreadsheet", + NULL} }, + { "word-processor", NC_("Menu of Productivity", "Word Processor"), + { "Office::WordProcessor", + "Office::Dictionary", + NULL} }, + { NULL } +}; + +/* Addons */ +static const GsDesktopMap map_addons[] = { + { "fonts", NC_("Menu of Add-ons", "Fonts"), + { "Addon::Font", + NULL} }, + { "codecs", NC_("Menu of Add-ons", "Codecs"), + { "Addon::Codec", + NULL} }, + { "input-sources", NC_("Menu of Add-ons", "Input Sources"), + { "Addon::InputSource", + NULL} }, + { "language-packs", NC_("Menu of Add-ons", "Language Packs"), + { "Addon::LanguagePack", + NULL} }, + { "localization", NC_("Menu of Add-ons", "Localization"), + { "Addon::Localization", + NULL} }, + { "drivers", NC_("Menu of Add-ons", "Hardware Drivers"), + { "Addon::Driver", + NULL} }, + { NULL } +}; + +/* Communication */ +static const GsDesktopMap map_communication[] = { + { "all", NC_("Menu of Communication & News", "All"), + { "Network", + NULL } }, + { "featured", NC_("Menu of Communication & News", "Featured"), + { "Network::Featured", + NULL} }, + { "chat", NC_("Menu of Communication & News", "Chat"), + { "Network::Chat", + "Network::IRCClient", + "Network::Telephony", + "Network::VideoConference", + "Network::Email", + NULL} }, + { "news", NC_("Menu of Communication & News", "News"), + { "Network::Feed", + "Network::News", + NULL} }, + { "web-browsers", NC_("Menu of Communication & News", "Web Browsers"), + { "Network::WebBrowser", + NULL} }, + { NULL } +}; + +/* Utility */ +static const GsDesktopMap map_utilities[] = { + { "all", NC_("Menu of Utilities", "All"), + { "Utility", + NULL } }, + { "featured", NC_("Menu of Utilities", "Featured"), + { "Utility::Featured", + NULL} }, + { "text-editors", NC_("Menu of Utilities", "Text Editors"), + { "Utility::TextEditor", + NULL} }, + { NULL } +}; + +/* Reference */ +static const GsDesktopMap map_reference[] = { + { "all", NC_("Menu of Reference", "All"), + { "Reference", + NULL } }, + { "featured", NC_("Menu of Reference", "Featured"), + { "Reference::Featured", + NULL} }, + { "art", NC_("Menu of Art", "Art"), + { "Reference::Art", + NULL} }, + { "biography", NC_("Menu of Reference", "Biography"), + { "Reference::Biography", + NULL} }, + { "comics", NC_("Menu of Reference", "Comics"), + { "Reference::Comics", + NULL} }, + { "fiction", NC_("Menu of Reference", "Fiction"), + { "Reference::Fiction", + NULL} }, + { "health", NC_("Menu of Reference", "Health"), + { "Reference::Health", + NULL} }, + { "history", NC_("Menu of Reference", "History"), + { "Reference::History", + NULL} }, + { "lifestyle", NC_("Menu of Reference", "Lifestyle"), + { "Reference::Lifestyle", + NULL} }, + { "politics", NC_("Menu of Reference", "Politics"), + { "Reference::Politics", + NULL} }, + { "sports", NC_("Menu of Reference", "Sports"), + { "Reference::Sports", + NULL} }, + { NULL } +}; + +/* main categories */ +/* Please keep category name and subcategory context synchronized!!! */ +static const GsDesktopData msdata[] = { + /* TRANSLATORS: this is the menu spec main category for Audio & Video */ + { "audio-video", map_audiovisual, N_("Audio & Video"), + "folder-music-symbolic", 100 }, + /* TRANSLATORS: this is the menu spec main category for Development */ + { "developer-tools", map_developertools, N_("Developer Tools"), + "applications-engineering-symbolic", 40 }, + /* TRANSLATORS: this is the menu spec main category for Education & Science */ + { "education-science", map_education_science, N_("Education & Science"), + "system-help-symbolic", 30 }, + /* TRANSLATORS: this is the menu spec main category for Game */ + { "games", map_games, N_("Games"), + "applications-games-symbolic", 70 }, + /* TRANSLATORS: this is the menu spec main category for Graphics */ + { "graphics", map_graphics, N_("Graphics & Photography"), + "applications-graphics-symbolic", 60 }, + /* TRANSLATORS: this is the menu spec main category for Office */ + { "productivity", map_productivity, N_("Productivity"), + "text-editor-symbolic", 80 }, + /* TRANSLATORS: this is the menu spec main category for Add-ons */ + { "addons", map_addons, N_("Add-ons"), + "application-x-addon-symbolic", 50 }, + /* TRANSLATORS: this is the menu spec main category for Communication */ + { "communication", map_communication, N_("Communication & News"), + "user-available-symbolic", 90 }, + /* TRANSLATORS: this is the menu spec main category for Reference */ + { "reference", map_reference, N_("Reference"), + "view-dual-symbolic", 0 }, + /* TRANSLATORS: this is the menu spec main category for Utilities */ + { "utilities", map_utilities, N_("Utilities"), + "applications-utilities-symbolic", 10 }, + { NULL } +}; + +const GsDesktopData * +gs_desktop_get_data (void) +{ + return msdata; +} diff --git a/plugins/core/gs-desktop-common.h b/plugins/core/gs-desktop-common.h new file mode 100644 index 0000000..3f5d378 --- /dev/null +++ b/plugins/core/gs-desktop-common.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2011-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct { + const gchar *id; + const gchar *name; + const gchar *fdo_cats[16]; +} GsDesktopMap; + +typedef struct { + const gchar *id; + const GsDesktopMap *mapping; + const gchar *name; + const gchar *icon; + gint score; +} GsDesktopData; + +const GsDesktopData *gs_desktop_get_data (void); + +G_END_DECLS diff --git a/plugins/core/gs-plugin-appstream.c b/plugins/core/gs-plugin-appstream.c new file mode 100644 index 0000000..6a28577 --- /dev/null +++ b/plugins/core/gs-plugin-appstream.c @@ -0,0 +1,1092 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2014 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> +#include <xmlb.h> + +#include "gs-appstream.h" + +/* + * SECTION: + * Uses offline AppStream data to populate and refine package results. + * + * This plugin calls UpdatesChanged() if any of the AppStream stores are + * changed in any way. + * + * Methods: | AddCategory + * Refines: | [source]->[name,summary,pixbuf,id,kind] + */ + +struct GsPluginData { + XbSilo *silo; + GRWLock silo_lock; + GSettings *settings; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + + /* XbSilo needs external locking as we destroy the silo and build a new + * one when something changes */ + g_rw_lock_init (&priv->silo_lock); + + /* need package name */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "dpkg"); + + /* require settings */ + priv->settings = g_settings_new ("org.gnome.software"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_object_unref (priv->silo); + g_object_unref (priv->settings); + g_rw_lock_clear (&priv->silo_lock); +} + +static const gchar * +gs_plugin_appstream_convert_component_kind (const gchar *kind) +{ + if (g_strcmp0 (kind, "web-application") == 0) + return "webapp"; + if (g_strcmp0 (kind, "console-application") == 0) + return "console"; + return kind; +} + +static gboolean +gs_plugin_appstream_upgrade_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "application") == 0) { + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + g_autofree gchar *kind = NULL; + if (id != NULL) { + kind = g_strdup (xb_builder_node_get_attr (id, "type")); + xb_builder_node_remove_attr (id, "type"); + } + if (kind != NULL) + xb_builder_node_set_attr (bn, "type", kind); + xb_builder_node_set_element (bn, "component"); + } else if (g_strcmp0 (xb_builder_node_get_element (bn), "metadata") == 0) { + xb_builder_node_set_element (bn, "custom"); + } else if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + const gchar *type_old = xb_builder_node_get_attr (bn, "type"); + const gchar *type_new = gs_plugin_appstream_convert_component_kind (type_old); + if (type_old != type_new) + xb_builder_node_set_attr (bn, "type", type_new); + } + return TRUE; +} + +static gboolean +gs_plugin_appstream_add_icons_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") != 0) + return TRUE; + gs_appstream_component_add_extra_info (plugin, bn); + return TRUE; +} + +static gboolean +gs_plugin_appstream_add_origin_keyword_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) { + const gchar *origin = xb_builder_node_get_attr (bn, "origin"); + GPtrArray *components = xb_builder_node_get_children (bn); + if (origin == NULL || origin[0] == '\0') + return TRUE; + g_debug ("origin %s has %u components", origin, components->len); + if (components->len < 200) { + for (guint i = 0; i < components->len; i++) { + XbBuilderNode *component = g_ptr_array_index (components, i); + gs_appstream_component_add_keyword (component, origin); + } + } + } + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_appdata_fn (GsPlugin *plugin, + XbBuilder *builder, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderFixup) fixup = NULL; + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* fix up any legacy installed files */ + fixup = xb_builder_fixup_new ("AppStreamUpgrade2", + gs_plugin_appstream_upgrade_cb, + plugin, NULL); + xb_builder_fixup_set_max_depth (fixup, 3); + xb_builder_source_add_fixup (source, fixup); + + /* add metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "filename", filename, NULL); + xb_builder_source_set_info (source, info); + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_appdata (GsPlugin *plugin, + XbBuilder *builder, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GDir) dir = NULL; + g_autoptr(GFile) parent = g_file_new_for_path (path); + if (!g_file_query_exists (parent, cancellable)) + return TRUE; + + dir = g_dir_open (path, 0, error); + if (dir == NULL) + return FALSE; + + while ((fn = g_dir_read_name (dir)) != NULL) { + if (g_str_has_suffix (fn, ".appdata.xml") || + g_str_has_suffix (fn, ".metainfo.xml")) { + g_autofree gchar *filename = g_build_filename (path, fn, NULL); + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_appstream_load_appdata_fn (plugin, + builder, + filename, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } + } + + /* success */ + return TRUE; +} + +static GInputStream * +gs_plugin_appstream_load_desktop_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + GString *xml; + g_autoptr(AsApp) app = as_app_new (); + g_autoptr(GBytes) bytes = NULL; + bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error); + if (bytes == NULL) + return NULL; + as_app_set_id (app, xb_builder_source_ctx_get_filename (ctx)); + if (!as_app_parse_data (app, bytes, AS_APP_PARSE_FLAG_USE_FALLBACKS, error)) + return NULL; + xml = as_app_to_xml (app, error); + if (xml == NULL) + return NULL; + g_string_prepend (xml, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + return g_memory_input_stream_new_from_data (g_string_free (xml, FALSE), -1, g_free); +} + +static gboolean +gs_plugin_appstream_load_desktop_fn (GsPlugin *plugin, + XbBuilder *builder, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderFixup) fixup = NULL; + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + /* add support for desktop files */ + xb_builder_source_add_adapter (source, "application/x-desktop", + gs_plugin_appstream_load_desktop_cb, NULL, NULL); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* add metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "filename", filename, NULL); + xb_builder_source_set_info (source, info); + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_desktop (GsPlugin *plugin, + XbBuilder *builder, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GDir) dir = NULL; + g_autoptr(GFile) parent = g_file_new_for_path (path); + if (!g_file_query_exists (parent, cancellable)) + return TRUE; + + dir = g_dir_open (path, 0, error); + if (dir == NULL) + return FALSE; + + while ((fn = g_dir_read_name (dir)) != NULL) { + if (g_str_has_suffix (fn, ".desktop")) { + g_autofree gchar *filename = g_build_filename (path, fn, NULL); + g_autoptr(GError) error_local = NULL; + if (g_strcmp0 (fn, "mimeinfo.cache") == 0) + continue; + if (!gs_plugin_appstream_load_desktop_fn (plugin, + builder, + filename, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } + } + + /* success */ + return TRUE; +} + +static GInputStream * +gs_plugin_appstream_load_dep11_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + GString *xml; + g_autoptr(AsStore) store = as_store_new (); + g_autoptr(GBytes) bytes = NULL; + bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error); + if (bytes == NULL) + return NULL; + if (!as_store_from_bytes (store, bytes, cancellable, error)) + return FALSE; + xml = as_store_to_xml (store, AS_NODE_INSERT_FLAG_NONE); + if (xml == NULL) + return NULL; + g_string_prepend (xml, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + return g_memory_input_stream_new_from_data (g_string_free (xml, FALSE), -1, g_free); +} + +static gboolean +gs_plugin_appstream_load_appstream_fn (GsPlugin *plugin, + XbBuilder *builder, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) file = g_file_new_for_path (filename); + g_autoptr(XbBuilderNode) info = NULL; + g_autoptr(XbBuilderFixup) fixup1 = NULL; + g_autoptr(XbBuilderFixup) fixup2 = NULL; + g_autoptr(XbBuilderFixup) fixup3 = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + /* add support for DEP-11 files */ + xb_builder_source_add_adapter (source, + "application/x-yaml", + gs_plugin_appstream_load_dep11_cb, + NULL, NULL); + + /* add source */ + if (!xb_builder_source_load_file (source, file, +#if LIBXMLB_CHECK_VERSION(0, 2, 0) + XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, +#else + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, +#endif + cancellable, + error)) { + return FALSE; + } + + /* add metadata */ + info = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info, "scope", "system", NULL); + xb_builder_node_insert_text (info, "filename", filename, NULL); + xb_builder_source_set_info (source, info); + + /* add missing icons as required */ + fixup1 = xb_builder_fixup_new ("AddIcons", + gs_plugin_appstream_add_icons_cb, + plugin, NULL); + xb_builder_fixup_set_max_depth (fixup1, 2); + xb_builder_source_add_fixup (source, fixup1); + + /* fix up any legacy installed files */ + fixup2 = xb_builder_fixup_new ("AppStreamUpgrade2", + gs_plugin_appstream_upgrade_cb, + plugin, NULL); + xb_builder_fixup_set_max_depth (fixup2, 3); + xb_builder_source_add_fixup (source, fixup2); + + /* add the origin as a search keyword for small repos */ + fixup3 = xb_builder_fixup_new ("AddOriginKeyword", + gs_plugin_appstream_add_origin_keyword_cb, + plugin, NULL); + xb_builder_fixup_set_max_depth (fixup3, 1); + xb_builder_source_add_fixup (source, fixup3); + + /* success */ + xb_builder_import_source (builder, source); + return TRUE; +} + +static gboolean +gs_plugin_appstream_load_appstream (GsPlugin *plugin, + XbBuilder *builder, + const gchar *path, + GCancellable *cancellable, + GError **error) +{ + const gchar *fn; + g_autoptr(GDir) dir = NULL; + g_autoptr(GFile) parent = g_file_new_for_path (path); + + /* parent patch does not exist */ + if (!g_file_query_exists (parent, cancellable)) + return TRUE; + dir = g_dir_open (path, 0, error); + if (dir == NULL) + return FALSE; + while ((fn = g_dir_read_name (dir)) != NULL) { + if (g_str_has_suffix (fn, ".xml") || + g_str_has_suffix (fn, ".yml") || + g_str_has_suffix (fn, ".yml.gz") || + g_str_has_suffix (fn, ".xml.gz")) { + g_autofree gchar *filename = g_build_filename (path, fn, NULL); + g_autoptr(GError) error_local = NULL; + if (!gs_plugin_appstream_load_appstream_fn (plugin, + builder, + filename, + cancellable, + &error_local)) { + g_debug ("ignoring %s: %s", filename, error_local->message); + continue; + } + } + } + + /* success */ + return TRUE; +} + +static gboolean +gs_plugin_appstream_check_silo (GsPlugin *plugin, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *locale; + const gchar *test_xml; + g_autofree gchar *blobfn = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbNode) n = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GRWLockReaderLocker) reader_locker = NULL; + g_autoptr(GRWLockWriterLocker) writer_locker = NULL; + g_autoptr(GPtrArray) parent_appdata = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GPtrArray) parent_appstream = g_ptr_array_new_with_free_func (g_free); + + + reader_locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + /* everything is okay */ + if (priv->silo != NULL && xb_silo_is_valid (priv->silo)) + return TRUE; + g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free); + + /* drat! silo needs regenerating */ + writer_locker = g_rw_lock_writer_locker_new (&priv->silo_lock); + g_clear_object (&priv->silo); + + /* verbose profiling */ + if (g_getenv ("GS_XMLB_VERBOSE") != NULL) { + xb_builder_set_profile_flags (builder, + XB_SILO_PROFILE_FLAG_XPATH | + XB_SILO_PROFILE_FLAG_DEBUG); + } + + /* add current locales */ + locale = g_getenv ("GS_SELF_TEST_LOCALE"); + if (locale == NULL) { + const gchar *const *locales = g_get_language_names (); + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + } else { + xb_builder_add_locale (builder, locale); + } + + /* only when in self test */ + test_xml = g_getenv ("GS_SELF_TEST_APPSTREAM_XML"); + if (test_xml != NULL) { + g_autoptr(XbBuilderFixup) fixup1 = NULL; + g_autoptr(XbBuilderFixup) fixup2 = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + if (!xb_builder_source_load_xml (source, test_xml, + XB_BUILDER_SOURCE_FLAG_NONE, + error)) + return FALSE; + fixup1 = xb_builder_fixup_new ("AddOriginKeywords", + gs_plugin_appstream_add_origin_keyword_cb, + plugin, NULL); + xb_builder_fixup_set_max_depth (fixup1, 1); + xb_builder_source_add_fixup (source, fixup1); + fixup2 = xb_builder_fixup_new ("AddIcons", + gs_plugin_appstream_add_icons_cb, + plugin, NULL); + xb_builder_fixup_set_max_depth (fixup2, 2); + xb_builder_source_add_fixup (source, fixup2); + xb_builder_import_source (builder, source); + } else { + /* add search paths */ + g_ptr_array_add (parent_appstream, + g_build_filename (DATADIR, "app-info", "xmls", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename (DATADIR, "app-info", "yaml", NULL)); + g_ptr_array_add (parent_appdata, + g_build_filename (DATADIR, "appdata", NULL)); + g_ptr_array_add (parent_appdata, + g_build_filename (DATADIR, "metainfo", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename (LOCALSTATEDIR, "cache", "app-info", "xmls", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename (LOCALSTATEDIR, "cache", "app-info", "yaml", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename (LOCALSTATEDIR, "lib", "app-info", "xmls", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename (LOCALSTATEDIR, "lib", "app-info", "yaml", NULL)); + + /* 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) { + g_ptr_array_add (parent_appstream, + g_build_filename ("/usr/share", "app-info", "xmls", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename ("/usr/share", "app-info", "yaml", NULL)); + g_ptr_array_add (parent_appdata, + g_build_filename ("/usr/share", "appdata", NULL)); + g_ptr_array_add (parent_appdata, + g_build_filename ("/usr/share", "metainfo", NULL)); + } + if (g_strcmp0 (LOCALSTATEDIR, "/var") != 0) { + g_ptr_array_add (parent_appstream, + g_build_filename ("/var", "cache", "app-info", "xmls", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename ("/var", "cache", "app-info", "yaml", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename ("/var", "lib", "app-info", "xmls", NULL)); + g_ptr_array_add (parent_appstream, + g_build_filename ("/var", "lib", "app-info", "yaml", NULL)); + } + + /* import all files */ + for (guint i = 0; i < parent_appstream->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appstream, i); + if (!gs_plugin_appstream_load_appstream (plugin, builder, fn, + cancellable, error)) + return FALSE; + } + for (guint i = 0; i < parent_appdata->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appdata, i); + if (!gs_plugin_appstream_load_appdata (plugin, builder, fn, + cancellable, error)) + return FALSE; + } + if (!gs_plugin_appstream_load_desktop (plugin, builder, + DATADIR "/applications", + cancellable, error)) { + return FALSE; + } + if (g_strcmp0 (DATADIR, "/usr/share") != 0 && + !gs_plugin_appstream_load_desktop (plugin, builder, + "/usr/share/applications", + cancellable, error)) { + return FALSE; + } + } + + /* regenerate with each minor release */ + xb_builder_append_guid (builder, PACKAGE_VERSION); + + /* create per-user cache */ + blobfn = gs_utils_get_cache_filename ("appstream", "components.xmlb", + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (blobfn == NULL) + return FALSE; + file = g_file_new_for_path (blobfn); + g_debug ("ensuring %s", blobfn); + priv->silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID | + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + NULL, error); + if (priv->silo == NULL) + return FALSE; + + /* watch all directories too */ + for (guint i = 0; i < parent_appstream->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appstream, i); + g_autoptr(GFile) file_tmp = g_file_new_for_path (fn); + if (!xb_silo_watch_file (priv->silo, file_tmp, cancellable, error)) + return FALSE; + } + for (guint i = 0; i < parent_appdata->len; i++) { + const gchar *fn = g_ptr_array_index (parent_appdata, i); + g_autoptr(GFile) file_tmp = g_file_new_for_path (fn); + if (!xb_silo_watch_file (priv->silo, file_tmp, cancellable, error)) + return FALSE; + } + + /* test we found something */ + n = xb_silo_query_first (priv->silo, "components/component", NULL); + if (n == NULL) { + g_warning ("No AppStream data, try 'make install-sample-data' in data/"); + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "No AppStream data found"); + return FALSE; + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + /* set up silo, compiling if required */ + return gs_plugin_appstream_check_silo (plugin, cancellable, error); +} + +gboolean +gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *path = NULL; + g_autofree gchar *scheme = NULL; + g_autofree gchar *xpath = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(XbNode) component = NULL; + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + /* not us */ + scheme = gs_utils_get_url_scheme (url); + if (g_strcmp0 (scheme, "appstream") != 0) + return TRUE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + + /* create app */ + path = gs_utils_get_url_path (url); + xpath = g_strdup_printf ("components/component/id[text()='%s']", path); + component = xb_silo_query_first (priv->silo, xpath, NULL); + if (component == NULL) + return TRUE; + app = gs_appstream_create_app (plugin, priv->silo, component, error); + if (app == NULL) + return FALSE; + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_list_add (list, app); + return TRUE; +} + +static void +gs_plugin_appstream_set_compulsory_quirk (GsApp *app, XbNode *component) +{ + g_autoptr(GPtrArray) array = NULL; + const gchar *current_desktop; + + /* + * Set the core applications for the current desktop that cannot be + * removed. + * + * If XDG_CURRENT_DESKTOP contains ":", indicating that it is made up + * of multiple components per the Desktop Entry Specification, an app + * is compulsory if any of the components in XDG_CURRENT_DESKTOP match + * any value in <compulsory_for_desktops />. In that way, + * "GNOME-Classic:GNOME" shares compulsory apps with GNOME. + * + * As a special case, if the <compulsory_for_desktop /> value contains + * a ":", we match the entire XDG_CURRENT_DESKTOP. This lets people set + * compulsory apps for such compound desktops if they want. + * + */ + array = xb_node_query (component, "compulsory_for_desktop", 0, NULL); + if (array == NULL) + return; + current_desktop = g_getenv ("XDG_CURRENT_DESKTOP"); + if (current_desktop != NULL) { + g_auto(GStrv) xdg_current_desktops = g_strsplit (current_desktop, ":", 0); + for (guint i = 0; i < array->len; i++) { + XbNode *n = g_ptr_array_index (array, i); + const gchar *tmp = xb_node_get_text (n); + /* if the value has a :, check the whole string */ + if (g_strstr_len (tmp, -1, ":")) { + if (g_strcmp0 (current_desktop, tmp) == 0) { + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); + break; + } + /* otherwise check if any element matches this one */ + } else if (g_strv_contains ((const gchar * const *) xdg_current_desktops, tmp)) { + gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY); + break; + } + } + } +} + +static gboolean +gs_plugin_appstream_refine_state (GsPlugin *plugin, GsApp *app, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(XbNode) component = NULL; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + + xpath = g_strdup_printf ("component/id[text()='%s']", gs_app_get_id (app)); + component = xb_silo_query_first (priv->silo, xpath, &error_local); + if (component == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + return TRUE; +} + +static gboolean +gs_plugin_refine_from_id (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + gboolean *found, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *id; + g_autoptr(GError) error_local = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GString) xpath = g_string_new (NULL); + g_autoptr(GPtrArray) components = NULL; + + /* not enough info to find */ + id = gs_app_get_id (app); + if (id == NULL) + return TRUE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + + /* look in AppStream then fall back to AppData */ + xb_string_append_union (xpath, "components/component/id[text()='%s']/../pkgname/..", id); + xb_string_append_union (xpath, "components/component[@type='webapp']/id[text()='%s']/..", id); + xb_string_append_union (xpath, "component/id[text()='%s']/..", id); + components = xb_silo_query (priv->silo, xpath->str, 0, &error_local); + if (components == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + if (!gs_appstream_refine_app (plugin, app, priv->silo, + component, flags, error)) + return FALSE; + gs_plugin_appstream_set_compulsory_quirk (app, component); + } + + /* if an installed desktop or appdata file exists set to installed */ + if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) { + if (!gs_plugin_appstream_refine_state (plugin, app, error)) + return FALSE; + } + + /* success */ + *found = TRUE; + return TRUE; +} + +static gboolean +gs_plugin_refine_from_pkgname (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GPtrArray *sources = gs_app_get_sources (app); + g_autoptr(GError) error_local = NULL; + + /* not enough info to find */ + if (sources->len == 0) + return TRUE; + + /* find all apps when matching any prefixes */ + for (guint j = 0; j < sources->len; j++) { + const gchar *pkgname = g_ptr_array_index (sources, j); + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GString) xpath = g_string_new (NULL); + g_autoptr(XbNode) component = NULL; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + + /* prefer actual apps and then fallback to anything else */ + xb_string_append_union (xpath, "components/component[@type='desktop']/pkgname[text()='%s']/..", pkgname); + xb_string_append_union (xpath, "components/component[@type='console']/pkgname[text()='%s']/..", pkgname); + xb_string_append_union (xpath, "components/component[@type='webapp']/pkgname[text()='%s']/..", pkgname); + xb_string_append_union (xpath, "components/component/pkgname[text()='%s']/..", pkgname); + component = xb_silo_query_first (priv->silo, xpath->str, &error_local); + if (component == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + continue; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + continue; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + if (!gs_appstream_refine_app (plugin, app, priv->silo, component, flags, error)) + return FALSE; + gs_plugin_appstream_set_compulsory_quirk (app, component); + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + gboolean found = FALSE; + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + + /* not us */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE && + gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_UNKNOWN) + return TRUE; + + /* find by ID then fall back to package name */ + if (!gs_plugin_refine_from_id (plugin, app, flags, &found, error)) + return FALSE; + if (!found) { + if (!gs_plugin_refine_from_pkgname (plugin, app, flags, error)) + return FALSE; + } + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_refine_wildcard (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags refine_flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *id; + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GPtrArray) components = NULL; + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + /* not enough info to find */ + id = gs_app_get_id (app); + if (id == NULL) + return TRUE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + + /* find all app with package names when matching any prefixes */ + xpath = g_strdup_printf ("components/component/id[text()='%s']/../pkgname/..", id); + components = xb_silo_query (priv->silo, xpath, 0, &error_local); + if (components == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + for (guint i = 0; i < components->len; i++) { + XbNode *component = g_ptr_array_index (components, i); + g_autoptr(GsApp) new = NULL; + + /* new app */ + new = gs_appstream_create_app (plugin, priv->silo, component, error); + if (new == NULL) + return FALSE; + gs_app_set_scope (new, AS_APP_SCOPE_SYSTEM); + gs_app_subsume_metadata (new, app); + if (!gs_appstream_refine_app (plugin, new, priv->silo, component, + refine_flags, error)) + return FALSE; + gs_app_list_add (list, new); + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_add_category_apps (GsPlugin *plugin, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + return gs_appstream_add_category_apps (plugin, + priv->silo, + category, + list, + cancellable, + error); +} + +gboolean +gs_plugin_add_search (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + return gs_appstream_search (plugin, + priv->silo, + (const gchar * const *) values, + list, + cancellable, + error); +} + +gboolean +gs_plugin_add_installed (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + g_autoptr(GPtrArray) components = NULL; + + /* check silo is valid */ + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + + /* get all installed appdata files (notice no 'components/' prefix...) */ + components = xb_silo_query (priv->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, priv->silo, component, error); + if (app == NULL) + return FALSE; + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM); + gs_app_list_add (list, app); + } + return TRUE; +} + +gboolean +gs_plugin_add_categories (GsPlugin *plugin, + GPtrArray *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + return gs_appstream_add_categories (plugin, priv->silo, list, + cancellable, error); +} + +gboolean +gs_plugin_add_popular (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + return gs_appstream_add_popular (plugin, priv->silo, list, cancellable, error); +} + +gboolean +gs_plugin_add_featured (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + return gs_appstream_add_featured (plugin, priv->silo, list, cancellable, error); +} + +gboolean +gs_plugin_add_recent (GsPlugin *plugin, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + return gs_appstream_add_recent (plugin, priv->silo, list, age, + cancellable, error); +} + +gboolean +gs_plugin_add_alternates (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GRWLockReaderLocker) locker = NULL; + + if (!gs_plugin_appstream_check_silo (plugin, cancellable, error)) + return FALSE; + + locker = g_rw_lock_reader_locker_new (&priv->silo_lock); + return gs_appstream_add_alternates (plugin, priv->silo, app, list, + cancellable, error); +} + +gboolean +gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error) +{ + return gs_plugin_appstream_check_silo (plugin, cancellable, error); +} diff --git a/plugins/core/gs-plugin-desktop-categories.c b/plugins/core/gs-plugin-desktop-categories.c new file mode 100644 index 0000000..f90788d --- /dev/null +++ b/plugins/core/gs-plugin-desktop-categories.c @@ -0,0 +1,106 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2011-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> +#include <glib/gi18n.h> + +#include "gs-desktop-common.h" + +/* + * SECTION: + * Adds categories from a hardcoded list based on the the desktop menu + * specification. + */ + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + /* need categories */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "appstream"); +} + +gboolean +gs_plugin_add_categories (GsPlugin *plugin, + GPtrArray *list, + GCancellable *cancellable, + GError **error) +{ + const GsDesktopData *msdata; + guint i, j, k; + + msdata = gs_desktop_get_data (); + for (i = 0; msdata[i].id != NULL; i++) { + GsCategory *category; + g_autofree gchar *msgctxt = NULL; + + /* add parent category */ + category = gs_category_new (msdata[i].id); + gs_category_set_icon (category, msdata[i].icon); + gs_category_set_name (category, gettext (msdata[i].name)); + gs_category_set_score (category, msdata[i].score); + g_ptr_array_add (list, category); + msgctxt = g_strdup_printf ("Menu of %s", msdata[i].name); + + /* add subcategories */ + for (j = 0; msdata[i].mapping[j].id != NULL; j++) { + const GsDesktopMap *map = &msdata[i].mapping[j]; + g_autoptr(GsCategory) sub = gs_category_new (map->id); + for (k = 0; map->fdo_cats[k] != NULL; k++) + gs_category_add_desktop_group (sub, map->fdo_cats[k]); + gs_category_set_name (sub, g_dpgettext2 (GETTEXT_PACKAGE, + msgctxt, + map->name)); + gs_category_add_child (category, sub); + } + } + return TRUE; +} + +/* most of this time this won't be required, unless the user creates a + * GsCategory manually and uses it to get results, for instance in the + * overview page or `gnome-software-cmd get-category-apps games/featured` */ +gboolean +gs_plugin_add_category_apps (GsPlugin *plugin, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *desktop_groups; + GsCategory *parent; + const GsDesktopData *msdata; + guint i, j, k; + + /* already set */ + desktop_groups = gs_category_get_desktop_groups (category); + if (desktop_groups->len > 0) + return TRUE; + + /* not valid */ + parent = gs_category_get_parent (category); + if (parent == NULL) + return TRUE; + + /* find desktop_groups for a parent::child category */ + msdata = gs_desktop_get_data (); + for (i = 0; msdata[i].id != NULL; i++) { + if (g_strcmp0 (gs_category_get_id (parent), msdata[i].id) != 0) + continue; + for (j = 0; msdata[i].mapping[j].id != NULL; j++) { + const GsDesktopMap *map = &msdata[i].mapping[j]; + if (g_strcmp0 (gs_category_get_id (category), map->id) != 0) + continue; + for (k = 0; map->fdo_cats[k] != NULL; k++) + gs_category_add_desktop_group (category, map->fdo_cats[k]); + } + } + return TRUE; +} diff --git a/plugins/core/gs-plugin-desktop-menu-path.c b/plugins/core/gs-plugin-desktop-menu-path.c new file mode 100644 index 0000000..2ec73b5 --- /dev/null +++ b/plugins/core/gs-plugin-desktop-menu-path.c @@ -0,0 +1,111 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2011-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> + +#include <gnome-software.h> + +#include "gs-desktop-common.h" + +/* + * SECTION: + * Adds categories from a hardcoded list based on the the desktop menu + * specification. + */ + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + /* need categories */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +static gboolean +_gs_app_has_desktop_group (GsApp *app, const gchar *desktop_group) +{ + guint i; + g_auto(GStrv) split = g_strsplit (desktop_group, "::", -1); + for (i = 0; split[i] != NULL; i++) { + if (!gs_app_has_category (app, split[i])) + return FALSE; + } + return TRUE; +} + +/* adds the menu-path for applications */ +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *strv[] = { "", NULL, NULL }; + const GsDesktopData *msdata; + gboolean found = FALSE; + guint i, j, k; + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH) == 0) + return TRUE; + if (gs_app_get_menu_path (app) != NULL) + return TRUE; + + /* find a top level category the app has */ + msdata = gs_desktop_get_data (); + for (i = 0; !found && msdata[i].id != NULL; i++) { + const GsDesktopData *data = &msdata[i]; + for (j = 0; !found && data->mapping[j].id != NULL; j++) { + const GsDesktopMap *map = &data->mapping[j]; + g_autofree gchar *msgctxt = NULL; + + if (g_strcmp0 (map->id, "all") == 0) + continue; + if (g_strcmp0 (map->id, "featured") == 0) + continue; + msgctxt = g_strdup_printf ("Menu of %s", data->name); + for (k = 0; !found && map->fdo_cats[k] != NULL; k++) { + const gchar *tmp = msdata[i].mapping[j].fdo_cats[k]; + if (_gs_app_has_desktop_group (app, tmp)) { + strv[0] = g_dgettext (GETTEXT_PACKAGE, msdata[i].name); + strv[1] = g_dpgettext2 (GETTEXT_PACKAGE, msgctxt, + msdata[i].mapping[j].name); + found = TRUE; + break; + } + } + } + } + + /* always set something to avoid keep searching for this */ + gs_app_set_menu_path (app, (gchar **) strv); + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH) == 0) + return TRUE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-plugin-generic-updates.c b/plugins/core/gs-plugin-generic-updates.c new file mode 100644 index 0000000..8ca56e1 --- /dev/null +++ b/plugins/core/gs-plugin-generic-updates.c @@ -0,0 +1,106 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit-refine"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "rpm-ostree"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); +} + +static gboolean +gs_plugin_generic_updates_merge_os_update (GsApp *app) +{ + /* this is only for grouping system-installed packages */ + if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE || + gs_app_get_scope (app) != AS_APP_SCOPE_SYSTEM) + return FALSE; + + if (gs_app_get_kind (app) == AS_APP_KIND_GENERIC) + return TRUE; + if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) + return TRUE; + + return FALSE; +} + +static GsApp * +gs_plugin_generic_updates_get_os_update (GsPlugin *plugin) +{ + GsApp *app; + const gchar *id = "org.gnome.Software.OsUpdate"; + g_autoptr(AsIcon) ic = NULL; + + /* create new */ + app = gs_app_new (id); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_PROXY); + gs_app_set_management_plugin (app, ""); + gs_app_set_kind (app, AS_APP_KIND_OS_UPDATE); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_name (app, + GS_APP_QUALITY_NORMAL, + /* TRANSLATORS: this is a group of updates that are not + * packages and are not shown in the main list */ + _("OS Updates")); + gs_app_set_summary (app, + GS_APP_QUALITY_NORMAL, + /* TRANSLATORS: this is a longer description of the + * "OS Updates" string */ + _("Includes performance, stability and security improvements.")); + gs_app_set_description (app, + GS_APP_QUALITY_NORMAL, + gs_app_get_summary (app)); + ic = as_icon_new (); + as_icon_set_kind (ic, AS_ICON_KIND_STOCK); + as_icon_set_name (ic, "software-update-available-symbolic"); + gs_app_add_icon (app, ic); + return app; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) os_updates = gs_app_list_new (); + + /* not from get_updates() */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) == 0) + return TRUE; + + /* do we have any packages left that are not apps? */ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app_tmp = gs_app_list_index (list, i); + if (gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD)) + continue; + if (gs_plugin_generic_updates_merge_os_update (app_tmp)) + gs_app_list_add (os_updates, app_tmp); + } + if (gs_app_list_length (os_updates) == 0) + return TRUE; + + /* create new meta object */ + app = gs_plugin_generic_updates_get_os_update (plugin); + for (guint i = 0; i < gs_app_list_length (os_updates); i++) { + GsApp *app_tmp = gs_app_list_index (os_updates, i); + gs_app_add_related (app, app_tmp); + gs_app_list_remove (list, app_tmp); + } + gs_app_list_add (list, app); + return TRUE; +} diff --git a/plugins/core/gs-plugin-hardcoded-blocklist.c b/plugins/core/gs-plugin-hardcoded-blocklist.c new file mode 100644 index 0000000..f993535 --- /dev/null +++ b/plugins/core/gs-plugin-hardcoded-blocklist.c @@ -0,0 +1,78 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <fnmatch.h> +#include <gnome-software.h> + +/* + * SECTION: + * Blocklists some applications based on a hardcoded list. + */ + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + /* need ID */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + guint i; + const gchar *app_globs[] = { + "freeciv-server.desktop", + "links.desktop", + "nm-connection-editor.desktop", + "plank.desktop", + "*release-notes*.desktop", + "*Release_Notes*.desktop", + "Rodent-*.desktop", + "rygel-preferences.desktop", + "system-config-keyboard.desktop", + "tracker-preferences.desktop", + "Uninstall*.desktop", + "wine-*.desktop", + NULL }; + + /* not set yet */ + if (gs_app_get_id (app) == NULL) + return TRUE; + + /* search */ + for (i = 0; app_globs[i] != NULL; i++) { + if (fnmatch (app_globs[i], gs_app_get_id (app), 0) == 0) { + gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE); + break; + } + } + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-plugin-hardcoded-popular.c b/plugins/core/gs-plugin-hardcoded-popular.c new file mode 100644 index 0000000..2431274 --- /dev/null +++ b/plugins/core/gs-plugin-hardcoded-popular.c @@ -0,0 +1,66 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + /* let appstream add applications first */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +gboolean +gs_plugin_add_popular (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error) +{ + guint i; + const gchar *apps[] = { + "org.gnome.Builder.desktop", + "org.gnome.Calculator.desktop", + "org.gnome.clocks.desktop", + "org.gnome.Dictionary.desktop", + "org.gnome.Documents.desktop", + "org.gnome.Evince", + "org.gnome.gedit.desktop", + "org.gnome.Maps.desktop", + "org.gnome.Weather", + NULL }; + + /* we've already got enough popular apps */ + if (gs_app_list_length (list) >= 9) + return TRUE; + + /* just add all */ + g_debug ("using hardcoded as only %u apps", gs_app_list_length (list)); + for (i = 0; apps[i] != NULL; i++) { + g_autoptr(GsApp) app = NULL; + + /* look in the cache */ + app = gs_plugin_cache_lookup (plugin, apps[i]); + if (app != NULL) { + gs_app_list_add (list, app); + continue; + } + + /* create new */ + app = gs_app_new (apps[i]); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_metadata (app, "GnomeSoftware::Creator", + gs_plugin_get_name (plugin)); + gs_app_list_add (list, app); + + /* save in the cache */ + gs_plugin_cache_add (plugin, apps[i], app); + } + return TRUE; +} diff --git a/plugins/core/gs-plugin-icons.c b/plugins/core/gs-plugin-icons.c new file mode 100644 index 0000000..112f1d2 --- /dev/null +++ b/plugins/core/gs-plugin-icons.c @@ -0,0 +1,348 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2014 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <string.h> + +#include <gnome-software.h> + +/* + * SECTION: + * Loads remote icons and converts them into local cached ones. + * + * It is provided so that each plugin handling icons does not + * have to handle the download and caching functionality. + */ + +struct GsPluginData { + GtkIconTheme *icon_theme; + GMutex icon_theme_lock; + GHashTable *icon_theme_paths; +}; + +static void gs_plugin_icons_add_theme_path (GsPlugin *plugin, const gchar *path); + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + const gchar *test_search_path; + + priv->icon_theme = gtk_icon_theme_new (); + gtk_icon_theme_set_screen (priv->icon_theme, gdk_screen_get_default ()); + priv->icon_theme_paths = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_mutex_init (&priv->icon_theme_lock); + + test_search_path = g_getenv ("GS_SELF_TEST_ICON_THEME_PATH"); + if (test_search_path != NULL) { + g_auto(GStrv) dirs = g_strsplit (test_search_path, ":", -1); + + /* add_theme_path() prepends, so we have to iterate in reverse to preserve order */ + for (gsize i = g_strv_length (dirs); i > 0; i--) + gs_plugin_icons_add_theme_path (plugin, dirs[i - 1]); + } + + /* needs remote icons downloaded */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "epiphany"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_object_unref (priv->icon_theme); + g_hash_table_unref (priv->icon_theme_paths); + g_mutex_clear (&priv->icon_theme_lock); +} + +static gboolean +gs_plugin_icons_download (GsPlugin *plugin, + const gchar *uri, + const gchar *filename, + GError **error) +{ + guint status_code; + g_autoptr(GdkPixbuf) pixbuf_new = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(SoupMessage) msg = NULL; + + /* create the GET data */ + msg = soup_message_new (SOUP_METHOD_GET, uri); + if (msg == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "%s is not a valid URL", uri); + return FALSE; + } + + /* set sync request */ + status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg); + if (status_code != SOUP_STATUS_OK) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "Failed to download icon %s: %s", + uri, soup_status_get_phrase (status_code)); + return FALSE; + } + + /* we're assuming this is a 64x64 png file, resize if not */ + stream = g_memory_input_stream_new_from_data (msg->response_body->data, + msg->response_body->length, + NULL); + pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, error); + if (pixbuf == NULL) { + gs_utils_error_convert_gdk_pixbuf (error); + return FALSE; + } + if (gdk_pixbuf_get_height (pixbuf) == 64 && + gdk_pixbuf_get_width (pixbuf) == 64) { + pixbuf_new = g_object_ref (pixbuf); + } else { + pixbuf_new = gdk_pixbuf_scale_simple (pixbuf, 64, 64, + GDK_INTERP_BILINEAR); + } + + /* write file */ + if (!gdk_pixbuf_save (pixbuf_new, filename, "png", error, NULL)) { + gs_utils_error_convert_gdk_pixbuf (error); + return FALSE; + } + return TRUE; +} + +static GdkPixbuf * +gs_plugin_icons_load_local (GsPlugin *plugin, AsIcon *icon, GError **error) +{ + GdkPixbuf *pixbuf; + gint size; + if (as_icon_get_filename (icon) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "icon has no filename"); + return NULL; + } + size = (gint) (64 * gs_plugin_get_scale (plugin)); + pixbuf = gdk_pixbuf_new_from_file_at_size (as_icon_get_filename (icon), + size, size, error); + if (pixbuf == NULL) { + gs_utils_error_convert_gdk_pixbuf (error); + return NULL; + } + return pixbuf; +} + +static gchar * +gs_plugin_icons_get_cache_fn (AsIcon *icon) +{ + g_autofree gchar *basename = NULL; + g_autofree gchar *checksum = NULL; + checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA1, + as_icon_get_url (icon), + -1); + basename = g_path_get_basename (as_icon_get_url (icon)); + return g_strdup_printf ("%s-%s", checksum, basename); +} + +static GdkPixbuf * +gs_plugin_icons_load_remote (GsPlugin *plugin, AsIcon *icon, GError **error) +{ + const gchar *fn; + gchar *found; + + /* not applicable for remote */ + if (as_icon_get_url (icon) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "icon has no URL"); + return NULL; + } + + /* set cache filename if not already set */ + if (as_icon_get_filename (icon) == NULL) { + g_autofree gchar *fn_cache = NULL; + g_autofree gchar *fn_basename = NULL; + + /* use a hash-prefixed filename to avoid cache clashes */ + fn_basename = gs_plugin_icons_get_cache_fn (icon); + fn_cache = gs_utils_get_cache_filename ("icons", + fn_basename, + GS_UTILS_CACHE_FLAG_WRITEABLE, + error); + if (fn_cache == NULL) + return NULL; + as_icon_set_filename (icon, fn_cache); + } + + /* already in cache */ + if (g_file_test (as_icon_get_filename (icon), G_FILE_TEST_EXISTS)) + return gs_plugin_icons_load_local (plugin, icon, error); + + /* a REMOTE that's really LOCAL */ + if (g_str_has_prefix (as_icon_get_url (icon), "file://")) { + as_icon_set_filename (icon, as_icon_get_url (icon) + 7); + as_icon_set_kind (icon, AS_ICON_KIND_LOCAL); + return gs_plugin_icons_load_local (plugin, icon, error); + } + + /* convert filename from jpg to png */ + fn = as_icon_get_filename (icon); + found = g_strstr_len (fn, -1, ".jpg"); + if (found != NULL) + memcpy (found, ".png", 4); + + /* create runtime dir and download */ + if (!gs_mkdir_parent (fn, error)) + return NULL; + if (!gs_plugin_icons_download (plugin, as_icon_get_url (icon), fn, error)) + return NULL; + as_icon_set_kind (icon, AS_ICON_KIND_LOCAL); + return gs_plugin_icons_load_local (plugin, icon, error); +} + +static void +gs_plugin_icons_add_theme_path (GsPlugin *plugin, const gchar *path) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (path == NULL) + return; + if (!g_hash_table_contains (priv->icon_theme_paths, path)) { + gtk_icon_theme_prepend_search_path (priv->icon_theme, path); + g_hash_table_add (priv->icon_theme_paths, g_strdup (path)); + } +} + +static GdkPixbuf * +gs_plugin_icons_load_stock (GsPlugin *plugin, AsIcon *icon, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + GdkPixbuf *pixbuf; + gint size; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->icon_theme_lock); + + /* required */ + if (as_icon_get_name (icon) == NULL) { + g_set_error_literal (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "icon has no name"); + return NULL; + } + gs_plugin_icons_add_theme_path (plugin, as_icon_get_prefix (icon)); + size = (gint) (64 * gs_plugin_get_scale (plugin)); + pixbuf = gtk_icon_theme_load_icon (priv->icon_theme, + as_icon_get_name (icon), + size, + GTK_ICON_LOOKUP_USE_BUILTIN | + GTK_ICON_LOOKUP_FORCE_SIZE, + error); + if (pixbuf == NULL) { + gs_utils_error_convert_gdk_pixbuf (error); + return NULL; + } + return pixbuf; +} + +static GdkPixbuf * +gs_plugin_icons_load_cached (GsPlugin *plugin, AsIcon *icon, GError **error) +{ + if (!as_icon_load (icon, AS_ICON_LOAD_FLAG_SEARCH_SIZE, error)) { + gs_utils_error_convert_gdk_pixbuf (error); + gs_utils_error_convert_appstream (error); + return NULL; + } + return g_object_ref (as_icon_get_pixbuf (icon)); +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *icons; + guint i; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) == 0) + return TRUE; + + /* invalid */ + if (gs_app_get_pixbuf (app) != NULL) + return TRUE; + + /* process all icons */ + icons = gs_app_get_icons (app); + for (i = 0; i < icons->len; i++) { + AsIcon *icon = g_ptr_array_index (icons, i); + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GError) error_local = NULL; + + /* handle different icon types */ + switch (as_icon_get_kind (icon)) { + case AS_ICON_KIND_LOCAL: + pixbuf = gs_plugin_icons_load_local (plugin, icon, &error_local); + break; + case AS_ICON_KIND_STOCK: + pixbuf = gs_plugin_icons_load_stock (plugin, icon, &error_local); + break; + case AS_ICON_KIND_REMOTE: + pixbuf = gs_plugin_icons_load_remote (plugin, icon, &error_local); + break; + case AS_ICON_KIND_CACHED: + pixbuf = gs_plugin_icons_load_cached (plugin, icon, &error_local); + break; + default: + g_set_error (&error_local, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "icon kind '%s' unknown", + as_icon_kind_to_string (as_icon_get_kind (icon))); + break; + } + if (pixbuf != NULL) { + gs_app_set_pixbuf (app, pixbuf); + break; + } + + /* we failed, but keep going */ + g_debug ("failed to load icon for %s: %s", + gs_app_get_id (app), + error_local->message); + } + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) == 0) + return TRUE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-plugin-key-colors-metadata.c b/plugins/core/gs-plugin-key-colors-metadata.c new file mode 100644 index 0000000..2b8fa21 --- /dev/null +++ b/plugins/core/gs-plugin-key-colors-metadata.c @@ -0,0 +1,89 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "key-colors"); +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *key_colors; + const gchar *keys[] = { + "GnomeSoftware::AppTile-css", + "GnomeSoftware::FeatureTile-css", + "GnomeSoftware::UpgradeBanner-css", + NULL }; + + /* not set */ + key_colors = gs_app_get_key_colors (app); + if (key_colors->len == 0) + return TRUE; + + /* rewrite URIs */ + for (guint i = 0; keys[i] != NULL; i++) { + const gchar *css; + g_autoptr(GString) css_new = NULL; + + /* metadata is not set */ + css = gs_app_get_metadata_item (app, keys[i]); + if (css == NULL) + continue; + if (g_strstr_len (css, -1, "@keycolor") == NULL) + continue; + + /* replace key color values */ + css_new = g_string_new (css); + for (guint j = 0; j < key_colors->len; j++) { + GdkRGBA *color = g_ptr_array_index (key_colors, j); + g_autofree gchar *key = NULL; + g_autofree gchar *value = NULL; + key = g_strdup_printf ("@keycolor-%02u@", j); + value = g_strdup_printf ("rgb(%.0f,%.0f,%.0f)", + color->red * 255.f, + color->green * 255.f, + color->blue * 255.f); + as_utils_string_replace (css_new, key, value); + } + + /* only replace if it's different */ + if (g_strcmp0 (css, css_new->str) != 0) { + gs_app_set_metadata (app, keys[i], NULL); + gs_app_set_metadata (app, keys[i], css_new->str); + } + + } + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-plugin-key-colors.c b/plugins/core/gs-plugin-key-colors.c new file mode 100644 index 0000000..08bf7a8 --- /dev/null +++ b/plugins/core/gs-plugin-key-colors.c @@ -0,0 +1,195 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + /* need icon */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "icons"); +} + +typedef struct { + guint8 R; + guint8 G; + guint8 B; +} CdColorRGB8; + +static guint32 +cd_color_rgb8_to_uint32 (CdColorRGB8 *rgb) +{ + return (guint32) rgb->R | + (guint32) rgb->G << 8 | + (guint32) rgb->B << 16; +} + +typedef struct { + GdkRGBA color; + guint cnt; +} GsColorBin; + +static gint +gs_color_bin_sort_cb (gconstpointer a, gconstpointer b) +{ + GsColorBin *s1 = (GsColorBin *) a; + GsColorBin *s2 = (GsColorBin *) b; + if (s1->cnt < s2->cnt) + return 1; + if (s1->cnt > s2->cnt) + return -1; + return 0; +} + +/* convert range of 0..255 to 0..1 */ +static inline gdouble +_convert_from_rgb8 (guchar val) +{ + return (gdouble) val / 255.f; +} + +static void +gs_plugin_key_colors_set_for_pixbuf (GsApp *app, GdkPixbuf *pb, guint number) +{ + gint rowstride, n_channels; + gint x, y, width, height; + guchar *pixels, *p; + guint bin_size = 200; + guint i; + guint number_of_bins; + + /* go through each pixel */ + n_channels = gdk_pixbuf_get_n_channels (pb); + rowstride = gdk_pixbuf_get_rowstride (pb); + pixels = gdk_pixbuf_get_pixels (pb); + width = gdk_pixbuf_get_width (pb); + height = gdk_pixbuf_get_height (pb); + + for (bin_size = 250; bin_size > 0; bin_size -= 2) { + g_autoptr(GHashTable) hash = NULL; + hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, g_free); + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + CdColorRGB8 tmp; + GsColorBin *s; + gpointer key; + + /* disregard any with alpha */ + p = pixels + y * rowstride + x * n_channels; + if (p[3] != 255) + continue; + + /* find in cache */ + tmp.R = (guint8) (p[0] / bin_size); + tmp.G = (guint8) (p[1] / bin_size); + tmp.B = (guint8) (p[2] / bin_size); + key = GUINT_TO_POINTER (cd_color_rgb8_to_uint32 (&tmp)); + s = g_hash_table_lookup (hash, key); + if (s != NULL) { + s->color.red += _convert_from_rgb8 (p[0]); + s->color.green += _convert_from_rgb8 (p[1]); + s->color.blue += _convert_from_rgb8 (p[2]); + s->cnt++; + continue; + } + + /* add to hash table */ + s = g_new0 (GsColorBin, 1); + s->color.red = _convert_from_rgb8 (p[0]); + s->color.green = _convert_from_rgb8 (p[1]); + s->color.blue = _convert_from_rgb8 (p[2]); + s->color.alpha = 1.0; + s->cnt = 1; + g_hash_table_insert (hash, key, s); + } + } + + number_of_bins = g_hash_table_size (hash); +// g_debug ("number of colors: %i", number_of_bins); + if (number_of_bins >= number) { + g_autoptr(GList) values = NULL; + + /* order by most popular */ + values = g_hash_table_get_values (hash); + values = g_list_sort (values, gs_color_bin_sort_cb); + for (GList *l = values; l != NULL; l = l->next) { + GsColorBin *s = l->data; + g_autofree GdkRGBA *color = g_new0 (GdkRGBA, 1); + color->red = s->color.red / s->cnt; + color->green = s->color.green / s->cnt; + color->blue = s->color.blue / s->cnt; + gs_app_add_key_color (app, color); + } + return; + } + } + + /* the algorithm failed, so just return a monochrome ramp */ + for (i = 0; i < 3; i++) { + g_autofree GdkRGBA *color = g_new0 (GdkRGBA, 1); + color->red = (gdouble) i / 3.f; + color->green = color->red; + color->blue = color->red; + color->alpha = 1.0f; + gs_app_add_key_color (app, color); + } +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GdkPixbuf *pb; + g_autoptr(GdkPixbuf) pb_small = NULL; + + /* add a rating */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS) == 0) + return TRUE; + + /* already set */ + if (gs_app_get_key_colors(app)->len > 0) + return TRUE; + + /* no pixbuf */ + pb = gs_app_get_pixbuf (app); + if (pb == NULL) { + g_debug ("no pixbuf, so no key colors"); + return TRUE; + } + + /* get a list of key colors */ + pb_small = gdk_pixbuf_scale_simple (pb, 32, 32, GDK_INTERP_BILINEAR); + gs_plugin_key_colors_set_for_pixbuf (app, pb_small, 10); + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS) == 0) + return TRUE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-plugin-os-release.c b/plugins/core/gs-plugin-os-release.c new file mode 100644 index 0000000..3ae902f --- /dev/null +++ b/plugins/core/gs-plugin-os-release.c @@ -0,0 +1,112 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +struct GsPluginData { + GsApp *app_system; +}; + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + priv->app_system = gs_app_new ("system"); + gs_app_set_kind (priv->app_system, AS_APP_KIND_OS_UPGRADE); + gs_app_set_state (priv->app_system, AS_APP_STATE_INSTALLED); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_object_unref (priv->app_system); +} + +gboolean +gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *cpe_name; + const gchar *home_url; + const gchar *name; + const gchar *version; + g_autoptr(GsOsRelease) os_release = NULL; + + /* parse os-release, wherever it may be */ + os_release = gs_os_release_new (error); + if (os_release == NULL) + return FALSE; + cpe_name = gs_os_release_get_cpe_name (os_release); + if (cpe_name != NULL) + gs_app_set_metadata (priv->app_system, "GnomeSoftware::CpeName", cpe_name); + name = gs_os_release_get_name (os_release); + if (name != NULL) + gs_app_set_name (priv->app_system, GS_APP_QUALITY_LOWEST, name); + version = gs_os_release_get_version_id (os_release); + if (version != NULL) + gs_app_set_version (priv->app_system, version); + + /* use libsoup to convert a URL */ + home_url = gs_os_release_get_home_url (os_release); + if (home_url != NULL) { + g_autoptr(SoupURI) uri = NULL; + + /* homepage */ + gs_app_set_url (priv->app_system, AS_URL_KIND_HOMEPAGE, home_url); + + /* build ID from the reverse-DNS URL and the name version */ + uri = soup_uri_new (home_url); + if (uri != NULL) { + g_auto(GStrv) split = NULL; + const gchar *home_host = soup_uri_get_host (uri); + split = g_strsplit_set (home_host, ".", -1); + if (g_strv_length (split) >= 2) { + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s.%s.%s-%s", + split[1], + split[0], + name, + version); + gs_app_set_id (priv->app_system, id); + } + } + } + + /* success */ + return TRUE; +} + +gboolean +gs_plugin_refine_wildcard (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* match meta-id */ + if (g_strcmp0 (gs_app_get_id (app), "system") == 0) { + /* copy over interesting metadata */ + if (gs_app_get_install_date (app) != 0 && + gs_app_get_install_date (priv->app_system) == 0) { + gs_app_set_install_date (priv->app_system, + gs_app_get_install_date (app)); + } + + gs_app_list_add (list, priv->app_system); + return TRUE; + } + + /* success */ + return TRUE; +} diff --git a/plugins/core/gs-plugin-provenance-license.c b/plugins/core/gs-plugin-provenance-license.c new file mode 100644 index 0000000..1103a6d --- /dev/null +++ b/plugins/core/gs-plugin-provenance-license.c @@ -0,0 +1,152 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016 Matthias Klumpp <mak@debian.org> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +/* + * SECTION: + * Marks the application as Free Software if it comes from an origin + * that is recognized as being DFSGish-free. + */ + +struct GsPluginData { + GSettings *settings; + gchar **sources; + gchar *license_id; +}; + +static gchar ** +gs_plugin_provenance_license_get_sources (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *tmp; + + tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_LICENSE_SOURCES"); + if (tmp != NULL) { + g_debug ("using custom provenance_license sources of %s", tmp); + return g_strsplit (tmp, ",", -1); + } + return g_settings_get_strv (priv->settings, "free-repos"); +} + +static gchar * +gs_plugin_provenance_license_get_id (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *tmp; + g_autofree gchar *url = NULL; + + tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_LICENSE_URL"); + if (tmp != NULL) { + g_debug ("using custom license generic sources of %s", tmp); + url = g_strdup (tmp); + } else { + url = g_settings_get_string (priv->settings, "free-repos-url"); + if (url == NULL) + return g_strdup ("LicenseRef-free"); + } + return g_strdup_printf ("LicenseRef-free=%s", url); +} + +static void +gs_plugin_provenance_license_changed_cb (GSettings *settings, + const gchar *key, + GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (g_strcmp0 (key, "free-repos") == 0) { + g_strfreev (priv->sources); + priv->sources = gs_plugin_provenance_license_get_sources (plugin); + } + if (g_strcmp0 (key, "free-repos-url") == 0) { + g_free (priv->license_id); + priv->license_id = gs_plugin_provenance_license_get_id (plugin); + } +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + priv->settings = g_settings_new ("org.gnome.software"); + g_signal_connect (priv->settings, "changed", + G_CALLBACK (gs_plugin_provenance_license_changed_cb), plugin); + priv->sources = gs_plugin_provenance_license_get_sources (plugin); + priv->license_id = gs_plugin_provenance_license_get_id (plugin); + + /* need this set */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "provenance"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_strfreev (priv->sources); + g_free (priv->license_id); + g_object_unref (priv->settings); +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *origin; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0) + return TRUE; + + /* no provenance */ + if (!gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) + return TRUE; + + /* nothing to search */ + if (priv->sources == NULL || priv->sources[0] == NULL) + return TRUE; + + /* simple case */ + origin = gs_app_get_origin (app); + if (origin != NULL && gs_utils_strv_fnmatch (priv->sources, origin)) + gs_app_set_license (app, GS_APP_QUALITY_NORMAL, priv->license_id); + + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0) + return TRUE; + /* nothing to search */ + if (priv->sources == NULL || priv->sources[0] == NULL) + return TRUE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-plugin-provenance.c b/plugins/core/gs-plugin-provenance.c new file mode 100644 index 0000000..03a854b --- /dev/null +++ b/plugins/core/gs-plugin-provenance.c @@ -0,0 +1,141 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <gnome-software.h> + +/* + * SECTION: + * Sets the package provenance to TRUE if installed by an official + * software source. + */ + +struct GsPluginData { + GSettings *settings; + gchar **sources; +}; + +static gchar ** +gs_plugin_provenance_get_sources (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *tmp; + tmp = g_getenv ("GS_SELF_TEST_PROVENANCE_SOURCES"); + if (tmp != NULL) { + g_debug ("using custom provenance sources of %s", tmp); + return g_strsplit (tmp, ",", -1); + } + return g_settings_get_strv (priv->settings, "official-repos"); +} + +static void +gs_plugin_provenance_settings_changed_cb (GSettings *settings, + const gchar *key, + GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + if (g_strcmp0 (key, "official-repos") == 0) { + g_strfreev (priv->sources); + priv->sources = gs_plugin_provenance_get_sources (plugin); + } +} + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData)); + priv->settings = g_settings_new ("org.gnome.software"); + g_signal_connect (priv->settings, "changed", + G_CALLBACK (gs_plugin_provenance_settings_changed_cb), plugin); + priv->sources = gs_plugin_provenance_get_sources (plugin); + + /* after the package source is set */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "dummy"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit-refine"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "rpm-ostree"); +} + +void +gs_plugin_destroy (GsPlugin *plugin) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + g_strfreev (priv->sources); + g_object_unref (priv->settings); +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + const gchar *origin; + gchar **sources; + + /* not required */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) == 0) + return TRUE; + if (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) + return TRUE; + + /* nothing to search */ + sources = priv->sources; + if (sources == NULL || sources[0] == NULL) + return TRUE; + + /* simple case */ + origin = gs_app_get_origin (app); + if (origin != NULL && gs_utils_strv_fnmatch (sources, origin)) { + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + return TRUE; + } + + /* this only works for packages */ + origin = gs_app_get_source_id_default (app); + if (origin == NULL) + return TRUE; + origin = g_strrstr (origin, ";"); + if (origin == NULL) + return TRUE; + if (g_str_has_prefix (origin + 1, "installed:")) + origin += 10; + if (gs_utils_strv_fnmatch (sources, origin + 1)) { + gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE); + return TRUE; + } + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + GsPluginData *priv = gs_plugin_get_data (plugin); + + /* nothing to do here */ + if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) == 0) + return TRUE; + /* nothing to search */ + if (priv->sources == NULL || priv->sources[0] == NULL) + return TRUE; + + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-plugin-rewrite-resource.c b/plugins/core/gs-plugin-rewrite-resource.c new file mode 100644 index 0000000..2856a3e --- /dev/null +++ b/plugins/core/gs-plugin-rewrite-resource.c @@ -0,0 +1,73 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <config.h> + +#include <glib/gi18n.h> +#include <gnome-software.h> + +void +gs_plugin_initialize (GsPlugin *plugin) +{ + /* let appstream add metadata first */ + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); +} + +static gboolean +refine_app (GsPlugin *plugin, + GsApp *app, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *keys[] = { + "GnomeSoftware::AppTile-css", + "GnomeSoftware::FeatureTile-css", + "GnomeSoftware::UpgradeBanner-css", + NULL }; + + /* rewrite URIs */ + for (guint i = 0; keys[i] != NULL; i++) { + const gchar *css = gs_app_get_metadata_item (app, keys[i]); + if (css != NULL) { + g_autofree gchar *css_new = NULL; + g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin)); + gs_app_set_summary_missing (app_dl, + /* TRANSLATORS: status text when downloading */ + _("Downloading featured images…")); + css_new = gs_plugin_download_rewrite_resource (plugin, + app, + css, + cancellable, + error); + if (css_new == NULL) + return FALSE; + if (g_strcmp0 (css, css_new) != 0) { + gs_app_set_metadata (app, keys[i], NULL); + gs_app_set_metadata (app, keys[i], css_new); + } + } + } + return TRUE; +} + +gboolean +gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error) +{ + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + if (!refine_app (plugin, app, flags, cancellable, error)) + return FALSE; + } + + return TRUE; +} diff --git a/plugins/core/gs-self-test.c b/plugins/core/gs-self-test.c new file mode 100644 index 0000000..5aabbdf --- /dev/null +++ b/plugins/core/gs-self-test.c @@ -0,0 +1,277 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Joaquim Rocha <jrocha@endlessm.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gstdio.h> + +#include "gnome-software-private.h" + +#include "gs-appstream.h" +#include "gs-test.h" + +static void +gs_plugins_core_search_repo_name_func (GsPluginLoader *plugin_loader) +{ + GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app_tmp = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* force this app to be installed */ + app_tmp = gs_plugin_loader_app_create (plugin_loader, "*/*/yellow/desktop/arachne.desktop/*"); + gs_app_set_state (app_tmp, AS_APP_STATE_INSTALLED); + + /* get search result based on addon keyword */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", "yellow", + NULL); + gs_plugin_job_set_refine_flags (plugin_job, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON); + list = gs_plugin_loader_job_process (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (list != NULL); + + /* make sure there is one entry, the parent app */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + app = gs_app_list_index (list, 0); + g_assert_cmpstr (gs_app_get_id (app), ==, "arachne.desktop"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); +} + +static void +gs_plugins_core_os_release_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + g_autoptr(GsApp) app = NULL; + g_autoptr(GsApp) app3 = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GError) error = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* refine system application */ + app = gs_plugin_loader_get_system_app (plugin_loader); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + /* make sure there is valid content */ + g_assert_cmpstr (gs_app_get_id (app), ==, "org.fedoraproject.Fedora-25"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_OS_UPGRADE); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + g_assert_cmpstr (gs_app_get_name (app), ==, "Fedora"); + g_assert_cmpstr (gs_app_get_version (app), ==, "25"); + g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, + "https://fedoraproject.org/"); + g_assert_cmpstr (gs_app_get_metadata_item (app, "GnomeSoftware::CpeName"), ==, + "cpe:/o:fedoraproject:fedora:25"); + + /* this comes from appstream */ + g_assert_cmpstr (gs_app_get_summary (app), ==, "Fedora Workstation"); + + /* check we can get this by the old name too */ + app3 = gs_plugin_loader_get_system_app (plugin_loader); + g_assert (app3 != NULL); + g_assert (app3 == app); +} + +static void +gs_plugins_core_generic_updates_func (GsPluginLoader *plugin_loader) +{ + gboolean ret; + GsApp *os_update; + GsAppList *related; + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsPluginJob) plugin_job2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app1 = NULL; + g_autoptr(GsApp) app2 = NULL; + g_autoptr(GsApp) app_wildcard = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_wildcard = NULL; + + /* drop all caches */ + gs_utils_rmtree (g_getenv ("GS_SELF_TEST_CACHEDIR"), NULL); + gs_plugin_loader_setup_again (plugin_loader); + + /* create a list with generic apps */ + list = gs_app_list_new (); + app1 = gs_app_new ("package1"); + app2 = gs_app_new ("package2"); + gs_app_set_kind (app1, AS_APP_KIND_GENERIC); + gs_app_set_kind (app2, AS_APP_KIND_GENERIC); + gs_app_set_bundle_kind (app1, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_bundle_kind (app2, AS_BUNDLE_KIND_PACKAGE); + gs_app_set_scope (app1, AS_APP_SCOPE_SYSTEM); + gs_app_set_scope (app2, AS_APP_SCOPE_SYSTEM); + gs_app_set_state (app1, AS_APP_STATE_UPDATABLE); + gs_app_set_state (app2, AS_APP_STATE_UPDATABLE); + gs_app_add_source (app1, "package1"); + gs_app_add_source (app2, "package2"); + gs_app_list_add (list, app1); + gs_app_list_add (list, app2); + + /* refine to make the generic-updates plugin merge them into a single OsUpdate item */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "list", list, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + /* make sure there is one entry, the os update */ + g_assert_cmpint (gs_app_list_length (list), ==, 1); + os_update = gs_app_list_index (list, 0); + + /* make sure the os update is valid */ + g_assert_cmpstr (gs_app_get_id (os_update), ==, "org.gnome.Software.OsUpdate"); + g_assert_cmpint (gs_app_get_kind (os_update), ==, AS_APP_KIND_OS_UPDATE); + g_assert (gs_app_has_quirk (os_update, GS_APP_QUIRK_IS_PROXY)); + + /* must have two related apps, the ones we added earlier */ + related = gs_app_get_related (os_update); + g_assert_cmpint (gs_app_list_length (related), ==, 2); + + /* another test to make sure that we don't get an OsUpdate item created for wildcard apps */ + list_wildcard = gs_app_list_new (); + app_wildcard = gs_app_new ("nosuchapp.desktop"); + gs_app_add_quirk (app_wildcard, GS_APP_QUIRK_IS_WILDCARD); + gs_app_set_kind (app_wildcard, AS_APP_KIND_GENERIC); + gs_app_list_add (list_wildcard, app_wildcard); + plugin_job2 = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "list", list_wildcard, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS, + NULL); + ret = gs_plugin_loader_job_action (plugin_loader, plugin_job2, NULL, &error); + gs_test_flush_main_context (); + g_assert_no_error (error); + g_assert (ret); + + /* no OsUpdate item created */ + for (guint i = 0; i < gs_app_list_length (list_wildcard); i++) { + GsApp *app_tmp = gs_app_list_index (list_wildcard, i); + g_assert_cmpint (gs_app_get_kind (app_tmp), !=, AS_APP_KIND_OS_UPDATE); + g_assert (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_PROXY)); + } +} + +int +main (int argc, char **argv) +{ + g_autofree gchar *tmp_root = NULL; + gboolean ret; + int retval; + g_autofree gchar *os_release_filename = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPluginLoader) plugin_loader = NULL; + const gchar *xml; + const gchar *allowlist[] = { + "appstream", + "generic-updates", + "icons", + "os-release", + NULL + }; + + /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories + * for each of the tests, we want to use the system MIME registry, assuming + * that it exists and correctly has shared-mime-info installed. */ +#if GLIB_CHECK_VERSION(2, 60, 0) + g_content_type_set_mime_dirs (NULL); +#endif + + /* Similarly, add the system-wide icon theme path before it’s + * overwritten by %G_TEST_OPTION_ISOLATE_DIRS. */ + gs_test_expose_icon_theme_paths (); + + g_test_init (&argc, &argv, +#if GLIB_CHECK_VERSION(2, 60, 0) + G_TEST_OPTION_ISOLATE_DIRS, +#endif + NULL); + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + /* Use a common cache directory for all tests, since the appstream + * plugin uses it and cannot be reinitialised for each test. */ + tmp_root = g_dir_make_tmp ("gnome-software-core-test-XXXXXX", NULL); + g_assert (tmp_root != NULL); + g_setenv ("GS_SELF_TEST_CACHEDIR", tmp_root, TRUE); + + os_release_filename = gs_test_get_filename (TESTDATADIR, "os-release"); + g_assert (os_release_filename != NULL); + g_setenv ("GS_SELF_TEST_OS_RELEASE_FILENAME", os_release_filename, TRUE); + + /* fake some data */ + xml = "<?xml version=\"1.0\"?>\n" + "<components origin=\"yellow\" version=\"0.9\">\n" + " <component type=\"desktop\">\n" + " <id>arachne.desktop</id>\n" + " <name>test</name>\n" + " <summary>Test</summary>\n" + " <icon type=\"stock\">system-file-manager</icon>\n" + " <pkgname>arachne</pkgname>\n" + " </component>\n" + " <component type=\"os-upgrade\">\n" + " <id>org.fedoraproject.Fedora-25</id>\n" + " <name>Fedora</name>\n" + " <summary>Fedora Workstation</summary>\n" + " <pkgname>fedora-release</pkgname>\n" + " </component>\n" + " <info>\n" + " <scope>user</scope>\n" + " </info>\n" + "</components>\n"; + g_setenv ("GS_SELF_TEST_APPSTREAM_XML", xml, TRUE); + + /* only critical and error are fatal */ + g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); + + /* we can only load this once per process */ + plugin_loader = gs_plugin_loader_new (); + gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR); + ret = gs_plugin_loader_setup (plugin_loader, + (gchar**) allowlist, + NULL, + NULL, + &error); + g_assert_no_error (error); + g_assert (ret); + + /* plugin tests go here */ + g_test_add_data_func ("/gnome-software/plugins/core/search-repo-name", + plugin_loader, + (GTestDataFunc) gs_plugins_core_search_repo_name_func); + g_test_add_data_func ("/gnome-software/plugins/core/os-release", + plugin_loader, + (GTestDataFunc) gs_plugins_core_os_release_func); + g_test_add_data_func ("/gnome-software/plugins/core/generic-updates", + plugin_loader, + (GTestDataFunc) gs_plugins_core_generic_updates_func); + retval = g_test_run (); + + /* Clean up. */ + gs_utils_rmtree (tmp_root, NULL); + + return retval; +} diff --git a/plugins/core/meson.build b/plugins/core/meson.build new file mode 100644 index 0000000..10c43f3 --- /dev/null +++ b/plugins/core/meson.build @@ -0,0 +1,250 @@ +cargs = ['-DG_LOG_DOMAIN="GsPlugin"'] + +shared_module( + 'gs_plugin_generic-updates', + sources : 'gs-plugin-generic-updates.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_key-colors', + sources : 'gs-plugin-key-colors.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_key-colors-metadata', + sources : 'gs-plugin-key-colors-metadata.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_provenance', + sources : 'gs-plugin-provenance.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_provenance-license', + sources : 'gs-plugin-provenance-license.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + + +shared_module( + 'gs_plugin_icons', + sources : 'gs-plugin-icons.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_appstream', + sources : [ + 'gs-appstream.c', + 'gs-plugin-appstream.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : [ + plugin_libs, + libxmlb, + ], + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_desktop-categories', + sources : [ + 'gs-plugin-desktop-categories.c', + 'gs-desktop-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_desktop-menu-path', + sources : [ + 'gs-plugin-desktop-menu-path.c', + 'gs-desktop-common.c', + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_hardcoded-blocklist', + sources : 'gs-plugin-hardcoded-blocklist.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +if get_option('hardcoded_popular') + shared_module( + 'gs_plugin_hardcoded-popular', + sources : 'gs-plugin-hardcoded-popular.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] + ) +endif + +shared_module( + 'gs_plugin_rewrite-resource', + sources : 'gs-plugin-rewrite-resource.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +shared_module( + 'gs_plugin_os-release', + sources : 'gs-plugin-os-release.c', + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + install : true, + install_dir: plugin_dir, + c_args : cargs, + dependencies : plugin_libs, + link_with : [ + libgnomesoftware + ] +) + +if get_option('tests') + cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"'] + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"'] + e = executable( + 'gs-self-test-core', + compiled_schemas, + sources : [ + 'gs-self-test.c', + 'gs-appstream.c' + ], + include_directories : [ + include_directories('../..'), + include_directories('../../lib'), + ], + dependencies : [ + plugin_libs, + libxmlb, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + ) + test('gs-self-test-core', e, suite: ['plugins', 'core'], env: test_env) +endif diff --git a/plugins/core/tests/os-release b/plugins/core/tests/os-release new file mode 120000 index 0000000..1efe264 --- /dev/null +++ b/plugins/core/tests/os-release @@ -0,0 +1 @@ +../../../data/tests/os-release
\ No newline at end of file |